#!/usr/bin/env python3
# Copyright (c) 2026 Huawei Technologies Co., Ltd.
# openUBMC is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#         http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.

"""比较 libmcpp 实现、API 文档和 mdb_interface 定义并生成报告。

功能:从 libmcpp 生产源码自动发现资源协作接口,检查接口、属性、方法、方法签名
以及可静态识别的属性读写权限;同时检查 API 文档是否由当前实现和 MDB 元数据生成。
输入:mdb_interface 仓库路径和待检查的 API 文档。
输出:docs/api/mdb_interface_comparison_report.md。
退出码:普通模式在文档漂移、接口差异、未校验项或扫描警告存在时返回 1;
--check 模式只检查已提交报告是否需要重新生成,报告过期、无法读取或扫描存在警告时返回 1。
执行:在 libmcpp 根目录运行 ``python scripts/compare_mdb_api_docs.py
--mdb-root ../mdb_interface``。
"""

from __future__ import annotations

import argparse
import difflib
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path

from generate_mdb_api_docs import DEFAULT_OUTPUT, partition_interfaces, render_document
from mdb_api_common import (
    Interface,
    load_implementation_interfaces,
    load_mdb_interfaces,
    markdown_text,
)


ROOT = Path(__file__).resolve().parent.parent
DEFAULT_REPORT = ROOT / "docs" / "api" / "mdb_interface_comparison_report.md"


@dataclass
class Difference:
    """单个实现差异。"""

    interface: str
    subject: str
    expected: str
    actual: str


@dataclass
class Unchecked:
    """无法由源码静态确认的一致性检查项。"""

    interface: str
    subject: str
    reason: str


@dataclass
class MemberMatch:
    """实现成员与 MDB 成员的一组名称匹配。"""

    source_name: str
    expected_name: str

    @property
    def name_mismatch(self) -> bool:
        return self.source_name != self.expected_name


def _git_revision(repository: Path) -> str:
    try:
        result = subprocess.run(
            ["git", "-C", str(repository), "rev-parse", "HEAD"],
            check=True,
            capture_output=True,
            text=True,
        )
    except (OSError, subprocess.CalledProcessError):
        return "unknown"
    return result.stdout.strip()


def _display_types(parameters: list) -> str:
    values = [parameter.type_name for parameter in parameters]
    return ", ".join(values) if values else "NA"


def _match_members(
    source_mapping: dict, expected_mapping: dict
) -> tuple[list[MemberMatch], list[str], list[str]]:
    """统一匹配属性和方法;优先精确匹配,再做无歧义的 casefold 匹配。"""

    source_names = set(source_mapping)
    expected_names = set(expected_mapping)
    matches = [MemberMatch(name, name) for name in sorted(source_names & expected_names)]
    source_remaining = source_names - expected_names
    expected_remaining = expected_names - source_names
    matched_source: set[str] = set()
    matched_expected: set[str] = set()

    for expected_name in sorted(expected_remaining):
        candidates = [
            source_name
            for source_name in source_remaining - matched_source
            if source_name.casefold() == expected_name.casefold()
        ]
        if len(candidates) != 1:
            continue
        source_name = candidates[0]
        matches.append(MemberMatch(source_name, expected_name))
        matched_source.add(source_name)
        matched_expected.add(expected_name)

    return (
        matches,
        sorted(source_remaining - matched_source),
        sorted(expected_remaining - matched_expected),
    )


def _property_pairs(
    source: Interface, expected: Interface, differences: list[Difference]
) -> list[MemberMatch]:
    matches, source_only, expected_only = _match_members(
        source.properties, expected.properties
    )
    for match in matches:
        if match.name_mismatch:
            differences.append(
                Difference(
                    source.name,
                    "属性名称",
                    f"`{match.expected_name}`",
                    f"`{match.source_name}`",
                )
            )
    for name in expected_only:
        differences.append(
            Difference(
                source.name,
                f"缺少属性 `{name}`",
                expected.properties[name].type_name,
                "不存在",
            )
        )
    for name in source_only:
        differences.append(
            Difference(
                source.name,
                f"多余属性 `{name}`",
                "不存在",
                source.properties[name].type_name,
            )
        )
    return matches


def _compare_properties(
    source: Interface,
    expected: Interface,
    differences: list[Difference],
    unchecked: list[Unchecked],
) -> None:
    for match in _property_pairs(source, expected, differences):
        actual_property = source.properties[match.source_name]
        expected_property = expected.properties[match.expected_name]
        actual_type = actual_property.type_name
        expected_type = expected_property.type_name
        if actual_type == "unknown" or expected_type == "unknown":
            reason = (
                "MDB 属性缺少可静态比较的 baseType"
                if expected_type == "unknown"
                else f"MDB 期望 `{expected_type}`,源码类型无法识别"
            )
            unchecked.append(
                Unchecked(source.name, f"属性 `{match.expected_name}` 类型", reason)
            )
        elif actual_type != expected_type:
            differences.append(
                Difference(
                    source.name,
                    f"属性 `{match.expected_name}` 类型",
                    expected_type,
                    actual_type,
                )
            )
        if actual_property.access == "NA":
            unchecked.append(
                Unchecked(
                    source.name,
                    f"属性 `{match.expected_name}` 读写",
                    f"MDB 期望 `{expected_property.access}`,源码包含无法静态解析的只读设置",
                )
            )
        elif actual_property.access != expected_property.access:
            differences.append(
                Difference(
                    source.name,
                    f"属性 `{match.expected_name}` 读写",
                    expected_property.access,
                    actual_property.access,
                )
            )


def _method_pairs(
    source: Interface, expected: Interface, differences: list[Difference]
) -> list[MemberMatch]:
    matches, source_only, expected_only = _match_members(source.methods, expected.methods)
    for match in matches:
        if match.name_mismatch:
            differences.append(
                Difference(
                    source.name,
                    "方法名称",
                    f"`{match.expected_name}`",
                    f"`{match.source_name}`",
                )
            )
    for name in expected_only:
        differences.append(Difference(source.name, f"缺少方法 `{name}`", "存在", "不存在"))
    for name in source_only:
        differences.append(Difference(source.name, f"多余方法 `{name}`", "不存在", "存在"))
    return matches


def _compare_parameter_types(
    interface_name: str,
    method_name: str,
    direction: str,
    actual_parameters: list,
    expected_parameters: list,
    differences: list[Difference],
    unchecked: list[Unchecked],
) -> None:
    """比较方法参数类型;无法识别的类型单独列为未校验。"""

    subject = f"方法 `{method_name}` {direction}类型及顺序"
    if len(actual_parameters) != len(expected_parameters):
        differences.append(
            Difference(
                interface_name,
                subject,
                _display_types(expected_parameters),
                _display_types(actual_parameters),
            )
        )
        return

    has_known_mismatch = False
    pending_unchecked = []
    for index, (actual, expected) in enumerate(
        zip(actual_parameters, expected_parameters), start=1
    ):
        if expected.type_name == "unknown":
            reference = f" `{expected.reference}`" if expected.reference else ""
            pending_unchecked.append(
                Unchecked(
                    interface_name,
                    f"方法 `{method_name}` {direction}第 {index} 个参数类型",
                    f"MDB 参考类型{reference}缺少 baseType,无法静态比较",
                )
            )
        elif actual.type_name == "unknown":
            pending_unchecked.append(
                Unchecked(
                    interface_name,
                    f"方法 `{method_name}` {direction}第 {index} 个参数类型",
                    f"MDB 期望 `{expected.type_name}`,源码类型无法识别",
                )
            )
        elif actual.type_name != expected.type_name:
            has_known_mismatch = True
    if has_known_mismatch:
        differences.append(
            Difference(
                interface_name,
                subject,
                _display_types(expected_parameters),
                _display_types(actual_parameters),
            )
        )
    else:
        unchecked.extend(pending_unchecked)


def _compare_methods(
    source: Interface,
    expected: Interface,
    differences: list[Difference],
    unchecked: list[Unchecked],
) -> None:
    for match in _method_pairs(source, expected, differences):
        source_method = source.methods[match.source_name]
        expected_method = expected.methods[match.expected_name]
        _compare_parameter_types(
            source.name,
            match.expected_name,
            "入参",
            source_method.inputs,
            expected_method.inputs,
            differences,
            unchecked,
        )
        _compare_parameter_types(
            source.name,
            match.expected_name,
            "返回值",
            source_method.outputs,
            expected_method.outputs,
            differences,
            unchecked,
        )


def compare_implementation(
    implementation: dict[str, Interface],
    mdb: dict[str, Interface],
) -> tuple[list[Difference], list[Unchecked]]:
    """比较 libmcpp 已实现接口和 MDB 定义。"""

    differences: list[Difference] = []
    unchecked: list[Unchecked] = []
    for name, source in implementation.items():
        expected = mdb.get(name)
        if expected is None:
            differences.append(Difference(name, "接口定义", "MDB 中存在", "MDB 中不存在"))
            continue
        _compare_properties(source, expected, differences, unchecked)
        _compare_methods(source, expected, differences, unchecked)
    return differences, unchecked


def _document_diff(actual: str, expected: str, document_path: Path) -> list[str]:
    return list(
        difflib.unified_diff(
            actual.splitlines(),
            expected.splitlines(),
            fromfile=str(document_path),
            tofile="由当前 libmcpp 实现和 MDB 元数据重新生成的文档",
            lineterm="",
        )
    )


def _read_document(document_path: Path) -> tuple[str | None, str]:
    """读取待比较文档,并返回内容或明确的失败原因。"""

    if not document_path.is_file():
        return None, "文档文件不存在"
    try:
        return document_path.read_text(encoding="utf-8"), ""
    except (OSError, UnicodeDecodeError) as error:
        return None, f"文档读取失败:{error}"


def _read_report(report_path: Path) -> tuple[str | None, str]:
    """读取已提交的比较报告,并返回内容或明确的失败原因。"""

    if not report_path.is_file():
        return None, "报告文件不存在"
    try:
        return report_path.read_text(encoding="utf-8"), ""
    except (OSError, UnicodeDecodeError) as error:
        return None, f"报告读取失败:{error}"


def _report_diff(actual: str, expected: str, report_path: Path) -> list[str]:
    return list(
        difflib.unified_diff(
            actual.splitlines(),
            expected.splitlines(),
            fromfile=str(report_path),
            tofile="由当前 libmcpp 实现、API 文档和 MDB 定义重新生成的报告",
            lineterm="",
        )
    )


def _marked(value: str, mismatch: bool, unchecked: bool = False) -> str:
    text = markdown_text(value)
    if mismatch:
        return f"{text} ❌"
    if unchecked:
        return f"{text} ⚠️"
    return text


def _parameter_comparison(
    actual_parameters: list, expected_parameters: list
) -> tuple[bool, bool]:
    """返回(确定不一致、无法静态确认)。"""

    if len(actual_parameters) != len(expected_parameters):
        return True, False
    unknown = False
    for actual, expected in zip(actual_parameters, expected_parameters):
        if actual.type_name == "unknown" or expected.type_name == "unknown":
            unknown = True
        elif actual.type_name != expected.type_name:
            return True, False
    return False, unknown


def _member_name(match: MemberMatch) -> str:
    if match.name_mismatch:
        return f"{markdown_text(match.expected_name)} / {_marked(match.source_name, True)}"
    return markdown_text(match.source_name)


def _render_property_comparison(
    source: Interface, expected: Interface | None
) -> list[str]:
    expected_properties = expected.properties if expected is not None else {}
    matches, source_only, expected_only = _match_members(
        source.properties, expected_properties
    )
    if not matches and not source_only and not expected_only:
        return []

    lines = [
        "#### 属性",
        "",
        "| 属性名(定义 / 实现) | 定义类型 | 定义读写 | 实现类型 | 实现读写 |",
        "| ---------------------- | -------- | -------- | -------- | -------- |",
    ]
    for match in matches:
        actual = source.properties[match.source_name]
        definition = expected_properties[match.expected_name]
        type_unchecked = actual.type_name == "unknown" or definition.type_name == "unknown"
        type_mismatch = not type_unchecked and actual.type_name != definition.type_name
        access_unchecked = actual.access == "NA"
        access_mismatch = not access_unchecked and actual.access != definition.access
        lines.append(
            f"| {_member_name(match)} | {markdown_text(definition.type_name)} | "
            f"{markdown_text(definition.access)} | "
            f"{_marked(actual.type_name, type_mismatch, type_unchecked)} | "
            f"{_marked(actual.access, access_mismatch, access_unchecked)} |"
        )
    for name in expected_only:
        definition = expected_properties[name]
        lines.append(
            f"| {markdown_text(name)} / 不存在 ❌ | {markdown_text(definition.type_name)} | "
            f"{markdown_text(definition.access)} | 不存在 ❌ | 不存在 ❌ |"
        )
    for name in source_only:
        actual = source.properties[name]
        lines.append(
            f"| 不存在 / {markdown_text(name)} ❌ | NA | NA | "
            f"{_marked(actual.type_name, True)} | {_marked(actual.access, True)} |"
        )
    return lines


def _render_method_comparison(
    source: Interface, expected: Interface | None
) -> list[str]:
    expected_methods = expected.methods if expected is not None else {}
    matches, source_only, expected_only = _match_members(source.methods, expected_methods)
    if not matches and not source_only and not expected_only:
        return []

    lines = [
        "#### 方法",
        "",
        "| 方法名(定义 / 实现) | 定义入参 | 实现入参 | 定义返回值 | 实现返回值 |",
        "| ---------------------- | -------- | -------- | ---------- | ---------- |",
    ]
    for match in matches:
        actual = source.methods[match.source_name]
        definition = expected_methods[match.expected_name]
        input_mismatch, input_unchecked = _parameter_comparison(
            actual.inputs, definition.inputs
        )
        output_mismatch, output_unchecked = _parameter_comparison(
            actual.outputs, definition.outputs
        )
        lines.append(
            f"| {_member_name(match)} | {markdown_text(_display_types(definition.inputs))} | "
            f"{_marked(_display_types(actual.inputs), input_mismatch, input_unchecked)} | "
            f"{markdown_text(_display_types(definition.outputs))} | "
            f"{_marked(_display_types(actual.outputs), output_mismatch, output_unchecked)} |"
        )
    for name in expected_only:
        definition = expected_methods[name]
        lines.append(
            f"| {markdown_text(name)} / 不存在 ❌ | "
            f"{markdown_text(_display_types(definition.inputs))} | 不存在 ❌ | "
            f"{markdown_text(_display_types(definition.outputs))} | 不存在 ❌ |"
        )
    for name in source_only:
        actual = source.methods[name]
        lines.append(
            f"| 不存在 / {markdown_text(name)} ❌ | NA | "
            f"{_marked(_display_types(actual.inputs), True)} | NA | "
            f"{_marked(_display_types(actual.outputs), True)} |"
        )
    return lines


def _render_report(
    mdb_revision: str,
    implementation: dict[str, Interface],
    comparable_implementation: dict[str, Interface],
    missing_mdb: list[str],
    document_matches: bool,
    document_diff: list[str],
    differences: list[Difference],
    unchecked: list[Unchecked],
    scan_warnings: list[str],
    mdb: dict[str, Interface],
) -> str:
    document_status = "是" if document_matches else "否 ❌"
    properties = [
        prop
        for interface in comparable_implementation.values()
        for prop in interface.properties.values()
    ]
    access_checked = sum(prop.access != "NA" for prop in properties)
    difference_interfaces = {item.interface for item in differences if item.interface}
    unchecked_interfaces = {item.interface for item in unchecked if item.interface}
    issues_by_interface = difference_interfaces | unchecked_interfaces
    lines = [
        "# libmcpp API 与 mdb_interface 一致性比较报告",
        "",
        "<!-- 本文件由 scripts/compare_mdb_api_docs.py 生成,请勿手工修改。 -->",
        "",
        "## 比较基线",
        "",
        f"- mdb_interface 版本:`{mdb_revision}`",
        "- libmcpp 实现发现:生产源码中通过字面量 `MC_INTERFACE` 声明的 "
        "`bmc.kepler.*` 和 `bmc.dev.*` 接口",
        "- API 文档主体:libmcpp 实际暴露的接口、属性、方法、类型和读写权限",
        "- MDB 用途:判定标准接口并补充说明、取值范围和复杂类型定义",
        "- 比较范围:以 libmcpp 生产源码实际实现的接口为边界;mdb_interface 是跨组件全局定义仓,"
        "未被 libmcpp 引用的 MDB 接口不判定为实现缺失",
        "- 方法签名规则:比较入参/返回值类型及顺序,不以参数名称作为一致性条件",
        "",
        "## 整体情况",
        "",
        "| libmcpp 实现接口 | 标准接口(MDB 已定义) | 与定义一致 |",
        "| ----------------- | ---------------------- | ---------- |",
    ]
    for name in sorted(implementation):
        standard = name in mdb
        if not standard or name in difference_interfaces:
            consistency = "否 ❌"
        elif name in unchecked_interfaces:
            consistency = "未确认 ⚠️"
        else:
            consistency = "是"
        lines.append(
            f"| `{markdown_text(name)}` | {'是' if standard else '否'} | "
            f"{consistency} |"
        )

    lines.extend(
        [
            "",
            "### 汇总",
            "",
            "| 检查项 | 结果 |",
            "| ------ | ---- |",
            f"| 已发现的 libmcpp 接口数量 | {len(implementation)} |",
            f"| 标准接口数量 | {len(comparable_implementation)} |",
            f"| 非标准接口数量 | {len(missing_mdb)} |",
            f"| 实现侧属性读写静态校验覆盖 | {access_checked}/{len(properties)} |",
            f"| 扫描警告数量 | {len(scan_warnings)} |",
            f"| 未校验项数量 | {len(unchecked)} |",
            f"| API 文档是否最新 | {document_status} |",
            f"| 接口差异数量 | {len(differences)} |",
        ]
    )

    if not document_matches:
        lines.extend(
            [
                "",
                "## API 文档漂移",
                "",
                "API 文档需要重新生成。以下为差异(最多展示前 200 行):",
                "",
                "```diff",
                *document_diff[:200],
                "```",
            ]
        )

    if scan_warnings:
        lines.extend(["", "## 扫描警告", ""])
        lines.extend(f"- {markdown_text(warning)}" for warning in scan_warnings)

    if unchecked:
        lines.extend(
            [
                "",
                "## 未校验项",
                "",
                "以下项目无法由源码静态确认,不计为确定差异,但会导致检查返回非零退出码:",
                "",
                "| 接口 | 检查项 | 原因 |",
                "| ---- | ------ | ---- |",
            ]
        )
        for item in unchecked:
            lines.append(
                f"| `{markdown_text(item.interface)}` | {markdown_text(item.subject)} | "
                f"{markdown_text(item.reason)} |"
            )

    lines.extend(["", "## 接口差异", ""])
    if not differences and not unchecked:
        lines.append("未发现接口、属性、方法、方法签名或读写权限差异。")
    else:
        lines.append("实现侧不一致或不存在的内容以 ❌ 标记;无法静态确认的内容以 ⚠️ 标记。")
        for name in sorted(issues_by_interface):
            source = implementation.get(name)
            if source is None:
                continue
            expected = mdb.get(name)
            lines.extend(["", f"### {name}", ""])
            if expected is None:
                lines.append("该接口已由 libmcpp 实现,但 mdb_interface 中不存在标准定义。 ❌")
                lines.extend(
                    [
                        "",
                        "该接口的实现成员不单独计为差异,具体能力请查看 API 文档。",
                    ]
                )
                continue
            property_section = _render_property_comparison(source, expected)
            if property_section:
                lines.extend(["", *property_section])
            method_section = _render_method_comparison(source, expected)
            if method_section:
                lines.extend(["", *method_section])

    lines.extend(
        [
            "",
            "## 结论",
            "",
            "API 文档应始终反映 libmcpp 的实际接口能力;接口实现与 mdb_interface 的差异应在本报告中呈现。"
            "扫描警告和未校验项必须清零,接口差异应评估影响后修改实现或标准定义。",
            "",
        ]
    )
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--mdb-root", required=True, type=Path, help="mdb_interface 仓库根目录")
    parser.add_argument("--document", type=Path, default=DEFAULT_OUTPUT, help="待比较的 API Markdown 文档")
    parser.add_argument("--report", type=Path, default=DEFAULT_REPORT, help="比较报告输出文件")
    parser.add_argument("--check", action="store_true", help="只检查现有比较报告是否需要重新生成")
    args = parser.parse_args()

    mdb_root = args.mdb_root.resolve()
    scan_warnings: list[str] = []
    implementation = load_implementation_interfaces(ROOT, scan_warnings)
    try:
        mdb = load_mdb_interfaces(mdb_root, scan_warnings)
    except FileNotFoundError as error:
        print(f"无法加载 MDB 定义:{error}", file=sys.stderr)
        print(
            "请通过 --mdb-root 指定 mdb_interface 仓库根目录。",
            file=sys.stderr,
        )
        return 1
    mdb_revision = _git_revision(mdb_root)

    comparable_implementation, missing_mdb = partition_interfaces(implementation, mdb)
    expected_document = render_document(implementation, mdb)
    document_path = args.document.resolve()
    actual_document, document_error = _read_document(document_path)
    if actual_document is None:
        print(f"无法读取 API 文档 {document_path}:{document_error}", file=sys.stderr)
        print(
            "请先运行 generate_mdb_api_docs.py 生成文档,或通过 --document 指定现有文档。",
            file=sys.stderr,
        )
        return 1
    document_matches = actual_document == expected_document
    document_diff = _document_diff(actual_document, expected_document, document_path)

    differences, unchecked = compare_implementation(implementation, mdb)
    report_content = _render_report(
        mdb_revision,
        implementation,
        comparable_implementation,
        missing_mdb,
        document_matches,
        document_diff,
        differences,
        unchecked,
        scan_warnings,
        mdb,
    )
    report_path = args.report.resolve()
    if args.check:
        actual_report, read_error = _read_report(report_path)
        if actual_report is None:
            print(f"比较报告需要重新生成: {report_path}({read_error})")
            return 1
        if actual_report != report_content:
            print(f"比较报告需要重新生成: {report_path}")
            print("\n".join(_report_diff(actual_report, report_content, report_path)[:200]))
            return 1
        if scan_warnings:
            print(f"比较报告内容是最新的,但源码或 MDB 扫描存在 {len(scan_warnings)} 项警告")
            return 1
        print(f"比较报告已是最新: {report_path}")
        return 0

    report_path.parent.mkdir(parents=True, exist_ok=True)
    report_path.write_text(report_content, encoding="utf-8", newline="\n")

    print(f"已生成比较报告: {report_path}")
    print(f"API 文档: {'已是最新' if document_matches else '需要重新生成'}")
    if differences or unchecked or scan_warnings:
        print(
            "libmcpp 实现与 MDB: "
            f"发现 {len(differences)} 项差异、{len(unchecked)} 项未校验、"
            f"{len(scan_warnings)} 项扫描警告"
        )
    else:
        print("libmcpp 实现与 MDB: 通过")
    return 0 if document_matches and not differences and not unchecked and not scan_warnings else 1


if __name__ == "__main__":
    raise SystemExit(main())