from __future__ import annotations

import ast
import json
import hashlib
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from harbor.core.ddt import DDTScanner, DDTValidator
from harbor.core.utils import find_function_node


class L2Generator:
    def __init__(self, index_path: Optional[Path] = None, meta_path: Optional[Path] = None) -> None:
        self.index_path = index_path or (Path(".harbor") / "cache" / "l3_index.json")
        self.meta_path = meta_path or (Path(".harbor") / "l2_meta.json")
        self.scanner = DDTScanner()
        # validator uses default paths unless overridden
        self.validator = DDTValidator()

    def generate(self, module_path: str) -> str:
        """生成指定模块的 L2 README Markdown 文本。

        功能:
          - 从索引缓存聚合该模块下的 L3 函数（public/internal）。
          - 调用 DDT 校验，生成每个函数的绑定状态。
          - 稳定排序并渲染为 Markdown 文本。

        使用场景:
          - CLI `harbor gen l2`。

        依赖:
          - .harbor/cache/l3_index.json
          - DDTScanner/Validator

        @harbor.scope: public
        @harbor.l3_strictness: strict
        @harbor.idempotency: read-only

        Args:
          module_path (str): 要生成视图的模块路径前缀。

        Returns:
          str: 渲染的 Markdown 文本。
        """
        idx = self._load_index(self.index_path)
        items: List[Dict[str, Any]] = []
        cwd = Path.cwd().resolve()
        module_norm = module_path.replace("\\", "/")
        for fp, meta in idx.get("files", {}).items():
            rel = fp
            try:
                rel = Path(fp).resolve().relative_to(cwd).as_posix()
            except Exception:
                rel = Path(fp).as_posix()
            if f"{module_norm}/" not in rel.replace("\\", "/"):
                continue
            for it in meta.get("items", []):
                it2 = dict(it)
                it2["_file_path"] = fp
                items.append(it2)
        bindings = self.scanner.scan_tests()
        rep = self.validator.validate(bindings)
        bind_ok = {b.func_id for b in rep.valid}
        bind_bad = {}
        for typ, b, msg in rep.violations:
            bind_bad.setdefault(b.func_id, []).append((typ, msg))

        def name_key(it: Dict[str, Any]) -> str:
            return it.get("qualified_name", it["id"]).split(".")[-1]

        def ddt_status(it: Dict[str, Any]) -> str:
            fid = it["id"]
            strictness = it.get("strictness", "standard") or "standard"
            if fid in bind_ok:
                return "✅ Valid"
            if fid in bind_bad:
                return "⚠️ " + "; ".join([t for t, _ in bind_bad[fid]])
            if strictness == "strict":
                return "❌ Missing"
            return "⚪ Missing"

        def summary_for(it: Dict[str, Any]) -> str:
            try:
                src = Path(it["_file_path"]).read_text(encoding="utf-8")
                node = find_function_node(src, it.get("lineno", 0), it.get("name", ""))
                doc = ast.get_docstring(node) if node else None
            except Exception:
                doc = None
            if not doc:
                return "—"
            first = doc.strip().split("\n", 1)[0].strip()
            return (first[:57] + "...") if len(first) > 60 else first

        pub = [it for it in items if (it.get("scope") or "internal") == "public"]
        internal = [it for it in items if (it.get("scope") or "internal") != "public"]
        pub_sorted = sorted(pub, key=name_key)
        int_sorted = sorted(internal, key=name_key)

        ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
        lines: List[str] = []
        lines.append("<!-- AUTO-GENERATED BY HARBOR (v1.0.2). DO NOT EDIT MANUALLY. -->")
        lines.append(f"# Module: {module_path}")
        lines.append("")
        lines.append(f"Generated At: {ts}")
        lines.append("Source: .harbor/cache/l3_index.json")
        lines.append("")
        lines.append("## Public API")
        lines.append("| Function | Summary | Strictness | DDT Status |")
        lines.append("|---|---|---|---|")
        for it in pub_sorted:
            fn = it.get("qualified_name", it["id"])
            sm = summary_for(it)
            st = it.get("strictness", "standard") or "standard"
            ds = ddt_status(it)
            lines.append(f"| {fn} | {sm} | {st} | {ds} |")
        lines.append("")
        if int_sorted:
            lines.append("## Internal Details (optional)")
            lines.append("<details>")
            lines.append("<summary>Internal functions</summary>")
            lines.append("")
            lines.append("| Function | Summary | Strictness | DDT Status |")
            lines.append("|---|---|---|---|")
            for it in int_sorted:
                fn = it.get("qualified_name", it["id"])
                sm = summary_for(it)
                st = it.get("strictness", "standard") or "standard"
                ds = ddt_status(it)
                lines.append(f"| {fn} | {sm} | {st} | {ds} |")
            lines.append("")
            lines.append("</details>")
        lines.append("")
        lines.append("## Dependency (MVP)")
        lines.append("- (TBD) 未来基于 import 简要分析模块依赖。")
        md = "\n".join(lines)
        return md

    def write(self, module_path: str, md: str, force: bool = False) -> Optional[Path]:
        meta = self._load_meta(self.meta_path)
        current_hash = self.compute_meta_hash(md)
        prev_hash = meta.get(module_path)
        if prev_hash == current_hash and not force:
            return None
        target = Path(module_path) / "README.md"
        target.write_text(md, encoding="utf-8")
        meta[module_path] = current_hash
        self._save_meta(self.meta_path, meta)
        return target

    def compute_meta_hash(self, md: str) -> str:
        return hashlib.sha256(md.encode("utf-8")).hexdigest()

    def _load_index(self, path: Path) -> Dict[str, Any]:
        if not path.exists():
            raise IOError("l3_index.json not found")
        return json.loads(path.read_text(encoding="utf-8"))

    def _load_meta(self, path: Path) -> Dict[str, Any]:
        if not path.exists():
            return {}
        try:
            return json.loads(path.read_text(encoding="utf-8"))
        except Exception:
            return {}

    def _save_meta(self, path: Path, meta: Dict[str, Any]) -> None:
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
