#!/usr/bin/env python3
"""窗译源码的一站式导航、影响分析、检查和快照入口。"""

from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import plistlib
import re
import subprocess
import sys
from typing import Any, Iterable


REPO_ROOT = Path(__file__).resolve().parent.parent
MAP_PATH = REPO_ROOT / "Developer" / "source-map.json"
KNOWN_TEST_SUITES = {
    "RealtimeCoreTests",
    "TranslationProviderTests",
    "TutorialContentTests",
    "LicenseCoreTests",
    "AppLoggerTests",
    "OCRCoreTests",
    "TranslationCoreTests",
    "CacheCoreTests",
    "LocalModelDownloadPolicyTests",
}


def load_map() -> dict[str, Any]:
    with MAP_PATH.open("r", encoding="utf-8") as handle:
        return json.load(handle)


def searchable_text(item: dict[str, Any]) -> str:
    values: list[str] = []
    for key, value in item.items():
        if isinstance(value, str):
            values.append(value)
        elif isinstance(value, list):
            values.extend(str(entry) for entry in value)
    return " ".join(values).lower()


def matching_feature(data: dict[str, Any], selector: str) -> dict[str, Any] | None:
    normalized = selector.lower()
    exact = [
        feature
        for feature in data["features"]
        if normalized in {feature["id"].lower(), feature["name"].lower()}
    ]
    if exact:
        return exact[0]
    partial = [
        feature for feature in data["features"] if normalized in searchable_text(feature)
    ]
    return partial[0] if len(partial) == 1 else None


def print_paths(paths: Iterable[str], indent: str = "  - ") -> None:
    for path in paths:
        print(f"{indent}{path}")


def print_feature(feature: dict[str, Any]) -> None:
    print(f"[{feature['id']}] {feature['name']}")
    print(f"用途：{feature['description']}")
    print("直接查看：")
    print_paths(feature["paths"])
    if feature.get("protectedRules"):
        print("不可破坏的规则：")
        print_paths(feature["protectedRules"])
    suites = feature.get("testSuites", [])
    print("对应检查：" + ("、".join(suites) if suites else "类型检查与源码索引检查"))
    print(f"影响分析：./scripts/dev impact <文件路径>")
    print(f"功能快照：./scripts/dev snapshot {feature['id']} <功能版本>")


def command_list(data: dict[str, Any], _args: argparse.Namespace) -> int:
    print(f"窗译 {data['appVersion']}（Build {data['build']}）源码功能目录")
    for feature in data["features"]:
        print(f"  {feature['id']:<24} {feature['name']}")
    return 0


def command_feature(data: dict[str, Any], args: argparse.Namespace) -> int:
    feature = matching_feature(data, args.selector)
    if feature is None:
        print(f"没有唯一匹配的功能：{args.selector}", file=sys.stderr)
        print("先运行 ./scripts/dev find <关键词> 查看候选项。", file=sys.stderr)
        return 2
    print_feature(feature)
    return 0


def command_find(data: dict[str, Any], args: argparse.Namespace) -> int:
    query = " ".join(args.query).strip().lower()
    if not query:
        print("请输入要找的功能或问题。", file=sys.stderr)
        return 2
    found = False
    for feature in data["features"]:
        if query in searchable_text(feature):
            if not found:
                print("功能定位")
            found = True
            print(f"  [{feature['id']}] {feature['name']}")
            print_paths(feature["paths"], indent="    - ")
    for route in data["bugRoutes"]:
        if query in searchable_text(route):
            print(f"故障路线：{route['symptom']}")
            print("  先查看：")
            print_paths(route["firstLook"], indent="    - ")
            print("  相关功能：" + "、".join(route["featureIds"]))
            found = True
    for module in data["modules"]:
        if query in searchable_text(module):
            print(f"模块目录：[{module['id']}] {module['name']} -> {module['path']}")
            found = True
    if not found:
        print(f"索引中暂时没有“{query}”。可运行 ./scripts/dev list 查看全部功能。")
        return 1
    return 0


def path_matches(mapped: str, requested: str) -> bool:
    mapped_path = Path(mapped)
    requested_path = Path(requested)
    return requested_path == mapped_path or mapped_path in requested_path.parents


def command_impact(data: dict[str, Any], args: argparse.Namespace) -> int:
    requested = Path(args.path)
    if requested.is_absolute():
        try:
            requested = requested.resolve().relative_to(REPO_ROOT)
        except ValueError:
            print("文件不在窗译源码目录内。", file=sys.stderr)
            return 2
    requested_text = requested.as_posix().lstrip("./")
    matches = [
        feature
        for feature in data["features"]
        if any(path_matches(path, requested_text) for path in feature["paths"])
    ]
    if not matches:
        print(f"没有为 {requested_text} 登记功能影响；请补充 Developer/source-map.json。")
        return 1
    print(f"{requested_text} 影响以下功能：")
    for feature in matches:
        print(f"  [{feature['id']}] {feature['name']}")
        for rule in feature.get("protectedRules", []):
            print(f"    保护规则：{rule}")
    suites = sorted({suite for feature in matches for suite in feature.get("testSuites", [])})
    if suites:
        print("修改后应检查：" + "、".join(suites))
    return 0


def validate_map(data: dict[str, Any], verbose: bool = True) -> list[str]:
    errors: list[str] = []
    if data.get("schemaVersion") != 1:
        errors.append("source-map.json schemaVersion 必须为 1")
    feature_ids = [feature["id"] for feature in data.get("features", [])]
    if len(feature_ids) != len(set(feature_ids)):
        errors.append("功能 id 存在重复")
    module_ids = [module["id"] for module in data.get("modules", [])]
    if len(module_ids) != len(set(module_ids)):
        errors.append("模块 id 存在重复")
    for path in data.get("entrypoints", []):
        if not (REPO_ROOT / path).exists():
            errors.append(f"启动入口不存在：{path}")
    for module in data.get("modules", []):
        if not (REPO_ROOT / module["path"]).is_dir():
            errors.append(f"模块目录不存在：{module['path']}")
    for feature in data.get("features", []):
        for path in feature.get("paths", []):
            candidate = Path(path)
            if candidate.is_absolute() or ".." in candidate.parts:
                errors.append(f"功能 {feature['id']} 包含不安全路径：{path}")
            elif not (REPO_ROOT / candidate).exists():
                errors.append(f"功能 {feature['id']} 的路径不存在：{path}")
        for suite in feature.get("testSuites", []):
            if suite not in KNOWN_TEST_SUITES:
                errors.append(f"功能 {feature['id']} 使用未知检查：{suite}")
    known_ids = set(feature_ids)
    for route in data.get("bugRoutes", []):
        for feature_id in route.get("featureIds", []):
            if feature_id not in known_ids:
                errors.append(f"故障路线引用未知功能：{feature_id}")
        for path in route.get("firstLook", []):
            if not (REPO_ROOT / path).exists():
                errors.append(f"故障路线文件不存在：{path}")
    source_root = REPO_ROOT / "mado-improved" / "Sources"
    for swift_file in source_root.rglob("*.swift"):
        relative = swift_file.relative_to(REPO_ROOT).as_posix()
        if not any(path_matches(module["path"], relative) for module in data["modules"]):
            errors.append(f"Swift 文件未归入模块：{relative}")
    versions = read_version_values()
    if len({versions["swiftVersion"], versions["plistVersion"], data["appVersion"]}) != 1:
        errors.append(
            "版本号不同步：BuildInfo=" + versions["swiftVersion"]
            + " Info.plist=" + versions["plistVersion"]
            + " source-map=" + data["appVersion"]
        )
    if len({versions["swiftBuild"], versions["plistBuild"], data["build"]}) != 1:
        errors.append(
            "Build 不同步：BuildInfo=" + versions["swiftBuild"]
            + " Info.plist=" + versions["plistBuild"]
            + " source-map=" + data["build"]
        )
    if verbose:
        if errors:
            for error in errors:
                print("错误：" + error, file=sys.stderr)
        else:
            print(
                f"源码索引有效：{len(data['modules'])} 个模块、"
                f"{len(data['features'])} 项功能、{len(data['bugRoutes'])} 条故障路线。"
            )
    return errors


def command_validate(data: dict[str, Any], _args: argparse.Namespace) -> int:
    return 1 if validate_map(data) else 0


def read_version_values() -> dict[str, str]:
    build_info = (REPO_ROOT / "mado-improved/Sources/App/BuildInfo.swift").read_text(
        encoding="utf-8"
    )
    version_match = re.search(r'marketingVersion = "([^"]+)"', build_info)
    build_match = re.search(r'buildNumber = "([^"]+)"', build_info)
    if not version_match or not build_match:
        raise ValueError("无法从 BuildInfo.swift 读取版本")
    with (REPO_ROOT / "mado-improved/Info.plist").open("rb") as handle:
        info = plistlib.load(handle)
    return {
        "swiftVersion": version_match.group(1),
        "swiftBuild": build_match.group(1),
        "plistVersion": str(info["CFBundleShortVersionString"]),
        "plistBuild": str(info["CFBundleVersion"]),
    }


def command_version(data: dict[str, Any], args: argparse.Namespace) -> int:
    values = read_version_values()
    print(
        "当前版本：BuildInfo " + values["swiftVersion"] + " / " + values["swiftBuild"]
        + "；Info.plist " + values["plistVersion"] + " / " + values["plistBuild"]
        + "；源码索引 " + data["appVersion"] + " / " + data["build"]
    )
    if validate_map(data, verbose=False):
        print("当前版本资料不同步，升级前必须先修正。", file=sys.stderr)
        return 1
    releases_root = REPO_ROOT / "Archive/releases"
    known = sorted(path.name for path in releases_root.iterdir() if path.is_dir())
    print("已使用版本：" + "、".join(known))
    if args.target:
        if args.target == data["appVersion"] or args.target in known:
            print(
                f"版本冲突：{args.target} 已经使用或归档，不能作为新版本继续发布。",
                file=sys.stderr,
            )
            return 3
        print(f"目标版本 {args.target} 未发现历史冲突。")
    return 0


def run_checked(command: list[str], env: dict[str, str] | None = None) -> None:
    print("运行：" + " ".join(command), flush=True)
    subprocess.run(command, cwd=REPO_ROOT, env=env, check=True)


def command_check(data: dict[str, Any], args: argparse.Namespace) -> int:
    if validate_map(data):
        return 1
    if args.selector == "all":
        run_checked(["./scripts/typecheck.sh"])
        run_checked(["./scripts/run_tests.sh"])
        return 0
    feature = matching_feature(data, args.selector)
    if feature is None:
        print(f"没有唯一匹配的功能：{args.selector}", file=sys.stderr)
        return 2
    run_checked(["./scripts/typecheck.sh"])
    suites = feature.get("testSuites", [])
    for suite in suites:
        suite_env = os.environ.copy()
        suite_env["MADO_TEST_FILTER"] = suite
        run_checked(["./scripts/run_tests.sh"], env=suite_env)
    if not suites:
        run_checked(["python3", "scripts/audit_source_structure.py"])
    print(f"{feature['name']} 的登记检查已通过。")
    return 0


def command_snapshot(data: dict[str, Any], args: argparse.Namespace) -> int:
    feature = matching_feature(data, args.selector)
    if feature is None:
        print(f"没有唯一匹配的功能：{args.selector}", file=sys.stderr)
        return 2
    command = [
        sys.executable,
        "scripts/archive_feature_snapshot.py",
        feature["id"],
        args.version,
    ]
    run_checked(command)
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="窗译源码导航和维护入口")
    commands = parser.add_subparsers(dest="command", required=True)
    commands.add_parser("list", help="列出全部功能")
    find_parser = commands.add_parser("find", help="按功能名或故障现象找源码")
    find_parser.add_argument("query", nargs="+")
    feature_parser = commands.add_parser("feature", help="查看一项功能的完整源码入口")
    feature_parser.add_argument("selector")
    impact_parser = commands.add_parser("impact", help="查看修改某文件会影响哪些功能")
    impact_parser.add_argument("path")
    commands.add_parser("validate", help="校验索引中的文件和关系")
    version_parser = commands.add_parser("version", help="检查当前版本同步和目标版本冲突")
    version_parser.add_argument("target", nargs="?")
    check_parser = commands.add_parser("check", help="执行一项功能或全部源码检查")
    check_parser.add_argument("selector")
    snapshot_parser = commands.add_parser("snapshot", help="保存不可覆盖的单项功能源码快照")
    snapshot_parser.add_argument("selector")
    snapshot_parser.add_argument("version")
    return parser


def main() -> int:
    data = load_map()
    args = build_parser().parse_args()
    commands = {
        "list": command_list,
        "find": command_find,
        "feature": command_feature,
        "impact": command_impact,
        "validate": command_validate,
        "version": command_version,
        "check": command_check,
        "snapshot": command_snapshot,
    }
    return commands[args.command](data, args)


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except subprocess.CalledProcessError as error:
        print(f"操作失败，退出码：{error.returncode}", file=sys.stderr)
        raise SystemExit(error.returncode)
