"""
可选「代码生成树」：从源码构建**符号索引树**（节点 ≈ 模块 / 类 / 函数），供 AI 定位与多轮修改后重新同步。

- **Python**：标准库 ``ast``（精确层次，接近 AST 的声明子集）。
- **Go / JavaScript / TypeScript（含 jsx/tsx）**：行级**启发式**轮廓（非完整 AST，快但可能误报）。
- 与「完整 AST」的关系：本树是**面向导航的裁剪视图**；完整 AST/CST 可用 Tree-sitter 等另建（可选依赖 ``aicd[code-tree]`` 预留）。
"""

from __future__ import annotations

import ast
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from aicd.tools.code_gen_tree_outline import go_file_outline, javascript_file_outline


SCHEMA = "aicd_code_gen_tree"
SCHEMA_VERSION = 1

# 扫描这些后缀（polyglot / auto）；python_only 仅 .py
POLYGLOT_SUFFIXES = frozenset(
    {".py", ".go", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"}
)


def _doc_preview(node: ast.AST, max_len: int = 240) -> str | None:
    raw = ast.get_docstring(node)
    if not raw:
        return None
    line = raw.strip().split("\n", 1)[0].strip()
    if len(line) > max_len:
        return line[: max_len - 1] + "…"
    return line or None


def _end_line(node: ast.AST) -> int:
    end = getattr(node, "end_lineno", None)
    if isinstance(end, int) and end > 0:
        return end
    return int(getattr(node, "lineno", 1) or 1)


@dataclass
class _Builder:
    file_abs: str
    file_key: str
    module_qname: str
    nodes: list[dict[str, Any]] = field(default_factory=list)
    _next: int = 0

    def _new_id(self) -> str:
        i = self._next
        self._next += 1
        return f"{self.file_key}-{i}"

    def walk_body(
        self, body: list[ast.stmt], parent_id: str | None, parent_qname: str
    ) -> None:
        for node in body:
            if isinstance(node, ast.ClassDef):
                name = node.name
                qn = f"{parent_qname}.{name}" if parent_qname else name
                nid = self._new_id()
                self.nodes.append(
                    {
                        "id": nid,
                        "parent_id": parent_id,
                        "kind": "class",
                        "name": name,
                        "qname": qn,
                        "file": self.file_abs,
                        "line_start": int(node.lineno),
                        "line_end": _end_line(node),
                        "docstring_preview": _doc_preview(node),
                        "ai_description_slot": None,
                        "source_lang": "python",
                        "parser": "ast",
                    }
                )
                self.walk_body(node.body, nid, qn)
            elif isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
                kind = (
                    "async_function"
                    if isinstance(node, ast.AsyncFunctionDef)
                    else "function"
                )
                name = node.name
                qn = f"{parent_qname}.{name}" if parent_qname else name
                nid = self._new_id()
                self.nodes.append(
                    {
                        "id": nid,
                        "parent_id": parent_id,
                        "kind": kind,
                        "name": name,
                        "qname": qn,
                        "file": self.file_abs,
                        "line_start": int(node.lineno),
                        "line_end": _end_line(node),
                        "docstring_preview": _doc_preview(node),
                        "ai_description_slot": None,
                        "source_lang": "python",
                        "parser": "ast",
                    }
                )
                self.walk_body(node.body, nid, qn)


def python_file_gen_tree(path: Path, *, module_qname: str | None = None) -> dict[str, Any]:
    """解析单个 ``.py`` 文件，返回 ``ok`` / ``error`` 或节点列表。"""
    p = path.resolve()
    if not p.is_file():
        return {"ok": False, "error": "not a file"}
    if p.suffix.lower() != ".py":
        return {"ok": False, "error": "not a Python file (.py)"}
    try:
        src = p.read_text(encoding="utf-8")
    except OSError as e:
        return {"ok": False, "error": str(e)}
    try:
        tree = ast.parse(src, filename=str(p))
    except SyntaxError as e:
        return {"ok": False, "error": str(e), "path": str(p)}

    mod_name = module_qname if module_qname else p.stem
    fk = p.stem[:48] + "-" + hex(hash(str(p)) & 0xFFFF)[2:]
    b = _Builder(file_abs=str(p), file_key=fk, module_qname=mod_name)
    root_id = b._new_id()
    b.nodes.insert(
        0,
        {
            "id": root_id,
            "parent_id": None,
            "kind": "module",
            "name": mod_name,
            "qname": mod_name,
            "file": str(p),
            "line_start": 1,
            "line_end": max(_end_line(tree) if tree.body else 1, 1),
            "docstring_preview": _doc_preview(tree),
            "ai_description_slot": None,
            "source_lang": "python",
            "parser": "ast",
        },
    )
    b.walk_body(tree.body, root_id, mod_name)
    return {
        "ok": True,
        "path": str(p),
        "module_qname": mod_name,
        "nodes": b.nodes,
    }


def _module_qname_for_source(root: Path, file_path: Path) -> str:
    """相对 ``root`` 的路径 → 近似逻辑名（``pkg.sub.mod``），去掉常见后缀。"""
    try:
        rel = file_path.resolve().relative_to(root.resolve())
    except ValueError:
        return file_path.stem
    parts = list(rel.parts)
    if not parts:
        return file_path.stem
    last = parts[-1]
    for ext in (
        ".py",
        ".go",
        ".ts",
        ".tsx",
        ".jsx",
        ".js",
        ".mjs",
        ".cjs",
    ):
        if last.lower().endswith(ext):
            parts[-1] = last[: -len(ext)]
            break
    if parts[-1] == "__init__":
        parts = parts[:-1]
    return ".".join(parts) if parts else file_path.stem


def _iter_source_paths(root: Path, *, recursive: bool) -> list[Path]:
    root = root.resolve()
    if root.is_file():
        return [root] if root.suffix.lower() in POLYGLOT_SUFFIXES else []
    if not root.is_dir():
        return []
    out: list[Path] = []
    if recursive:
        for p in root.rglob("*"):
            if p.is_file() and p.suffix.lower() in POLYGLOT_SUFFIXES:
                out.append(p)
    else:
        for p in root.iterdir():
            if p.is_file() and p.suffix.lower() in POLYGLOT_SUFFIXES:
                out.append(p)
    out.sort(key=lambda x: str(x).lower())
    return out


def _count_source_files(root: Path, *, recursive: bool) -> int:
    return len(_iter_source_paths(root, recursive=recursive))


def _scan_one_source(
    root: Path,
    fp: Path,
    *,
    scan_profile: str,
    root_is_dir: bool,
) -> dict[str, Any] | None:
    mq = _module_qname_for_source(root, fp) if root_is_dir else None
    suf = fp.suffix.lower()
    if suf == ".py":
        return python_file_gen_tree(fp, module_qname=mq)
    if scan_profile == "python_only":
        return None
    if suf == ".go":
        return go_file_outline(fp, module_qname=mq)
    if suf in (".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"):
        return javascript_file_outline(fp, module_qname=mq)
    return None


def build_scan_payload(
    root: Path,
    *,
    recursive: bool = True,
    max_files: int = 120,
    scan_profile: str = "auto",
) -> dict[str, Any]:
    """
    扫描 ``root``（文件或目录），合并多语言节点。

    ``scan_profile``: ``auto`` | ``polyglot``（.py/.go/.js/…）| ``python_only``。
    """
    root = root.resolve()
    prof = (scan_profile or "auto").strip().lower()
    if prof not in ("auto", "polyglot", "python_only"):
        prof = "auto"
    if prof == "auto":
        prof = "polyglot"

    all_paths = _iter_source_paths(root, recursive=recursive)
    if prof == "python_only":
        all_paths = [p for p in all_paths if p.suffix.lower() == ".py"]
    files = all_paths[:max(1, min(5000, max_files))]

    langs_impl: dict[str, str] = {
        "python": "ast",
        "go": "heuristic",
        "javascript": "heuristic",
        "typescript": "heuristic",
        "tsx": "heuristic",
        "jsx": "heuristic",
    }

    if not files:
        return {
            "ok": True,
            "version": SCHEMA_VERSION,
            "schema": SCHEMA,
            "scan_profile": prof,
            "languages": [],
            "language_implementations": {},
            "root": str(root),
            "files_scanned": 0,
            "nodes": [],
            "truncated": False,
            "what_is_this": (
                "代码生成树：面向 AI 导航的符号层次索引；Python 基于 AST，Go/JS/TS 为启发式轮廓，"
                "不等价于完整语法树。"
            ),
            "open_source_notes": (
                "精确多语言 CST/AST：Tree-sitter（tree-sitter-language-pack 等）；"
                "C/C++ 语义需 clang/compile_commands；Java/Kotlin 可用 javac/IDE 索引。"
            ),
        }

    truncated = _count_source_files(root, recursive=recursive) > len(files)
    root_is_dir = root.is_dir()

    all_nodes: list[dict[str, Any]] = []
    errors: list[dict[str, Any]] = []
    seen_lang: set[str] = set()
    for fp in files:
        one = _scan_one_source(
            root, fp, scan_profile=prof, root_is_dir=root_is_dir
        )
        if one is None:
            continue
        if not one.get("ok"):
            errors.append({"path": str(fp), "error": one.get("error", "unknown")})
            continue
        for n in one["nodes"]:
            all_nodes.append(dict(n))
            sl = str(n.get("source_lang") or "python")
            seen_lang.add(sl)

    out: dict[str, Any] = {
        "ok": True,
        "version": SCHEMA_VERSION,
        "schema": SCHEMA,
        "scan_profile": prof,
        "languages": sorted(seen_lang),
        "language_implementations": {k: langs_impl.get(k, "?") for k in sorted(seen_lang)},
        "root": str(root),
        "files_scanned": len(files) - len(errors),
        "nodes": all_nodes,
        "truncated": truncated,
        "truncation_reason": (
            f"capped at max_files={max_files}" if truncated else None
        ),
        "parse_errors": errors or None,
        "what_is_this": (
            "代码生成树：符号索引树（模块/类/函数节点 + 文件行号），用于快速定位与增量分析；"
            "非完整 AST。大仓库请用 code_gen_tree_query 做子树/过滤/统计。"
        ),
        "open_source_notes": (
            "Python: ast。Go/JS/TS: 行级启发式。更稳的多语言解析：Tree-sitter；"
            "C/C++/Java 等可逐步加 grammar 或语言服务器。"
        ),
        "usage_for_agents": (
            "定位：nodes[].qname + file + line_start/line_end。版本化落盘：code_gen_tree_sync "
            "use_versioned_store=true + topic_slug → 每次同步 index_revision 自增；接续工作务必读 "
            "code_gen_tree_manifest 取 latest。大库：code_gen_tree_query（stats / filter / "
            "children / subtree / regex），勿一次性把整份 tree JSON 读入聊天。"
        ),
    }
    return out


def write_gen_tree_json(payload: dict[str, Any], dest: Path) -> dict[str, Any]:
    dest.parent.mkdir(parents=True, exist_ok=True)
    body = {
        **payload,
        "generated_at": datetime.now(timezone.utc).isoformat(),
    }
    dest.write_text(
        json.dumps(body, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    return {"ok": True, "output_path": str(dest.resolve())}
