from __future__ import annotations

from concurrent.futures import ThreadPoolExecutor, as_completed
import json
import time
from typing import Any

from fileatlas.llm import extract_json_content, extract_json_content_ollama, load_llm_from_env
from moduleatlas import DISPLAY_NAME

try:
    from tqdm import tqdm  # type: ignore
except Exception:  # pragma: no cover
    tqdm = None  # type: ignore[assignment]


def _default_module_summary(module: dict[str, Any]) -> tuple[str, str]:
    """
    Rule-based fallback: (purpose, summary)
    """
    mid = str(module.get("module_id") or "")
    name = str(module.get("name") or mid)
    files = module.get("files", []) if isinstance(module.get("files", []), list) else []
    file_count = len(files)

    purpose = ""
    lowered = mid.lower()
    if any(k in lowered.split("/") for k in ["tests", "test"]):
        purpose = "Automated tests"
    elif any(k in lowered.split("/") for k in ["docs", "doc"]):
        purpose = "Documentation"
    elif any(k in lowered.split("/") for k in ["scripts", "tools"]):
        purpose = "Utilities and scripts"
    elif any(k in lowered.split("/") for k in ["configs", "config"]):
        purpose = "Configuration"
    elif any(k in lowered.split("/") for k in ["utils", "common", "shared"]):
        purpose = "Shared utilities"
    else:
        purpose = "Module"

    summary = f"{name}: {purpose} ({file_count} file{'s' if file_count != 1 else ''})."
    return purpose, summary


def _analysis_digest(project: dict[str, Any], path: str) -> dict[str, Any]:
    files = project.get("files", {}) if isinstance(project.get("files", {}), dict) else {}
    entry = files.get(path, {}) if isinstance(files.get(path, {}), dict) else {}
    facts = entry.get("facts", {}) if isinstance(entry.get("facts", {}), dict) else {}
    analysis = entry.get("analysis", {}) if isinstance(entry.get("analysis", {}), dict) else {}
    imports = facts.get("imports", []) if isinstance(facts.get("imports", []), list) else []
    import_modules: list[str] = []
    for it in imports[:50]:
        if isinstance(it, dict):
            mod = it.get("module")
            if isinstance(mod, str) and mod.strip():
                import_modules.append(mod.strip())
    return {
        "path": path,
        "language": facts.get("language", ""),
        "size_bytes": facts.get("size_bytes", 0),
        "imports_count": len(imports),
        "imports": import_modules[:30],
        "calls_count": len(facts.get("calls", []) if isinstance(facts.get("calls", []), list) else []),
        "symbols_count": len(facts.get("symbols", []) if isinstance(facts.get("symbols", []), list) else []),
        "summary": analysis.get("summary", ""),
        "responsibilities": analysis.get("responsibilities", []) or [],
        "key_interfaces": analysis.get("key_interfaces", []) or [],
        "external_dependencies": analysis.get("external_dependencies", []) or [],
        "risks": analysis.get("risks", []) or [],
        "analysis_error": analysis.get("error", ""),
    }


def _select_representative_files(project: dict[str, Any], files: list[str], *, limit: int = 40) -> list[str]:
    """
    Pick representative files for a module to feed into LLM.
    Heuristics: prioritize likely entrypoints + large python files, then fill.
    """
    files_map = project.get("files", {}) if isinstance(project.get("files", {}), dict) else {}

    def facts_for(p: str) -> dict[str, Any]:
        entry = files_map.get(p, {}) if isinstance(files_map.get(p, {}), dict) else {}
        facts = entry.get("facts", {}) if isinstance(entry.get("facts", {}), dict) else {}
        return facts

    def score(p: str) -> float:
        lp = (p or "").lower()
        s = 0.0
        # likely entrypoints / orchestrators
        for kw, w in [
            ("__main__.py", 50),
            ("main.py", 45),
            ("runner.py", 40),
            ("train", 30),
            ("trainer", 30),
            ("worker", 25),
            ("server", 20),
            ("api", 15),
            ("config", 12),
            ("protocol", 12),
        ]:
            if kw in lp:
                s += float(w)
        facts = facts_for(p)
        if str(facts.get("language") or "") == "python":
            s += 10.0
        try:
            s += min(float(int(facts.get("size_bytes") or 0)) / 2000.0, 20.0)
        except Exception:
            pass
        return s

    uniq: list[str] = []
    seen: set[str] = set()
    for p in files:
        ps = str(p)
        if ps and ps not in seen:
            seen.add(ps)
            uniq.append(ps)

    ranked = sorted(uniq, key=lambda p: (-score(p), p))
    return ranked[: max(1, int(limit))]


def _derive_module_details(module: dict[str, Any], project: dict[str, Any]) -> dict[str, Any]:
    """
    Deterministic enrichment from file-level facts/analysis (no LLM).
    Returns fields like responsibilities/key_files/external_dependencies/risks/top_imports.
    """
    files = module.get("files", []) if isinstance(module.get("files", []), list) else []
    digests = [_analysis_digest(project, str(p)) for p in files]

    # Aggregate responsibilities / risks / external deps (dedupe preserving frequency)
    def _top_strings(values: list[str], *, limit: int) -> list[str]:
        freq: dict[str, int] = {}
        order: list[str] = []
        for v in values:
            s = str(v).strip()
            if not s:
                continue
            if s not in freq:
                order.append(s)
                freq[s] = 0
            freq[s] += 1
        order.sort(key=lambda s: (-freq.get(s, 0), s))
        return order[:limit]

    all_resp: list[str] = []
    all_risks: list[str] = []
    all_deps: list[str] = []
    all_imports: list[str] = []
    for d in digests:
        if isinstance(d, dict):
            all_resp.extend(d.get("responsibilities", []) if isinstance(d.get("responsibilities", []), list) else [])
            all_risks.extend(d.get("risks", []) if isinstance(d.get("risks", []), list) else [])
            all_deps.extend(d.get("external_dependencies", []) if isinstance(d.get("external_dependencies", []), list) else [])
            all_imports.extend(d.get("imports", []) if isinstance(d.get("imports", []), list) else [])

    responsibilities = _top_strings([str(x) for x in all_resp], limit=8)
    risks = _top_strings([str(x) for x in all_risks], limit=6)
    external_dependencies = _top_strings([str(x) for x in all_deps], limit=12)

    # Top imports (excluding empty; keep frequency)
    top_imports = _top_strings([str(x) for x in all_imports], limit=15)

    # Key files: representative subset with short reasons (heuristic)
    rep = _select_representative_files(project, [str(p) for p in files], limit=10)
    key_files: list[dict[str, str]] = []
    for p in rep:
        lp = p.lower()
        why = "representative module file"
        if "__main__.py" in lp:
            why = "module entrypoint"
        elif "main" in lp:
            why = "main/orchestrator"
        elif "runner" in lp:
            why = "runner/orchestrator"
        elif "trainer" in lp or "train" in lp:
            why = "training loop / algorithm driver"
        elif "worker" in lp:
            why = "distributed worker / runtime component"
        elif "config" in lp or lp.endswith((".yaml", ".yml", ".toml", ".json")):
            why = "configuration"
        elif "protocol" in lp:
            why = "data protocol / interface boundary"
        key_files.append({"path": p, "why": why})

    return {
        "responsibilities": responsibilities,
        "key_files": key_files,
        "external_dependencies": external_dependencies,
        "risks": risks,
        "top_imports": top_imports,
    }


def _llm_summarize_module(module: dict[str, Any], project: dict[str, Any]) -> dict[str, Any]:
    provider, client, model = load_llm_from_env()

    files = module.get("files", []) if isinstance(module.get("files", []), list) else []
    rep_files = _select_representative_files(project, [str(p) for p in files], limit=45)
    digests = [_analysis_digest(project, str(p)) for p in rep_files]
    metrics = module.get("metrics", {}) if isinstance(module.get("metrics", {}), dict) else {}

    user_payload = {
        "module_id": module.get("module_id", ""),
        "name": module.get("name", ""),
        "paths": module.get("paths", []),
        "file_count": len(files),
        "fan_in": metrics.get("fan_in", 0),
        "fan_out": metrics.get("fan_out", 0),
        "files_sampled": digests,
        "derived": _derive_module_details(module, project),
    }

    system = (
        "你是资深软件架构分析专家。"
        "你只能根据输入的文件摘要/职责/接口做推断，不要编造不存在的模块或行为。"
        "输出必须是 JSON object。"
    )
    prompt = (
        "请为这个模块生成“可读性强”的模块级架构说明（用于架构文档）。\n\n"
        "要求输出 JSON，字段：\n"
        "- purpose: 6-18 个字/词，说明模块定位（不要太泛）\n"
        "- summary: 3-8 句，讲清楚模块做什么、典型使用场景、主要依赖/交互（可提 fan-in/out）\n"
        "- responsibilities: 3-8 条要点（每条一句话），描述模块承担的核心职责\n"
        "- key_files: 4-10 个对象数组，每个包含 path 与 why（说明该文件在模块中的角色）\n"
        "- external_dependencies: 0-12 条字符串（只列 INPUT 里出现过/可推断的外部依赖）\n"
        "- risks: 0-8 条字符串（边界条件/容易踩坑点/运行风险；若无则空）\n"
        "\n约束：\n"
        "- 只能基于 INPUT 推断，不要编造不存在的函数/类/子模块\n"
        "- 尽量引用 INPUT 提供的文件 summary/responsibilities/key_interfaces/imports 来支撑结论\n"
    )
    messages = [
        {"role": "system", "content": system},
        {"role": "user", "content": prompt + "\n\nINPUT:\n" + json.dumps(user_payload, ensure_ascii=False, indent=2)},
    ]

    if provider == "ollama":
        resp = client.chat(model=model, messages=messages)  # type: ignore[attr-defined]
        obj = extract_json_content_ollama(resp)
    else:
        resp = client.chat_completions(model=model, messages=messages)  # type: ignore[attr-defined]
        obj = extract_json_content(resp)
    if not isinstance(obj, dict):
        return {"error": "bad_llm_json", "raw": obj}
    if "error" in obj and not any(k in obj for k in ["purpose", "summary"]):
        return {"error": str(obj.get("error")), "raw": obj.get("raw", obj)}
    out: dict[str, Any] = {}
    if "purpose" in obj:
        out["purpose"] = obj.get("purpose")
    if "summary" in obj:
        out["summary"] = obj.get("summary")
    # Optional enrich fields (best-effort)
    for k in ["responsibilities", "key_files", "external_dependencies", "risks", "top_imports"]:
        if k in obj:
            out[k] = obj.get(k)
    return out


def _is_retryable_error(err: str) -> bool:
    if not err:
        return False
    if "Missing OPENAI_API_KEY" in err:
        return False
    if err in {"bad_llm_json", "bad_response", "non_json_content"}:
        return True
    if err.startswith("LLMError:") or err.startswith("LLM HTTPError"):
        return True
    if err.startswith("Ollama HTTPError") or err.startswith("Ollama request failed"):
        return True
    if err.startswith("LLM request failed"):
        return True
    return False


def _llm_summarize_module_with_retry(module: dict[str, Any], project: dict[str, Any], *, max_attempts: int = 3) -> dict[str, Any]:
    attempts = max(1, int(max_attempts))
    last: dict[str, Any] = {"error": "LLMError: unknown", "raw": None}
    for i in range(attempts):
        try:
            out = _llm_summarize_module(module, project)
        except Exception as e:
            out = {"error": f"LLMError: {e}", "raw": None}
        last = out
        err = str(out.get("error") or "")
        if i >= attempts - 1 or not _is_retryable_error(err):
            return out
        time.sleep(0.5 * (2**i))
    return last


def summarize_modules_best_effort(
    modules: list[dict[str, Any]],
    *,
    project: dict[str, Any],
    llm_concurrency: int = 4,
) -> None:
    """
    Mutates modules in-place by filling purpose/summary (+ optional richer fields).
    Even if LLM fails, deterministic derived details are attached.
    """
    max_workers = max(1, int(llm_concurrency))

    def apply_fallback(m: dict[str, Any], err: str | None = None) -> None:
        purpose, summary = _default_module_summary(m)
        if not m.get("purpose"):
            m["purpose"] = purpose
        if not m.get("summary"):
            m["summary"] = summary
        # Always attach deterministic details if missing.
        try:
            derived = _derive_module_details(m, project)
            for k, v in derived.items():
                if not m.get(k):
                    m[k] = v
        except Exception:
            pass
        if err:
            m["error"] = err

    # Try LLM in parallel; fall back per-module if it fails.
    with ThreadPoolExecutor(max_workers=max_workers) as ex:
        futs = {ex.submit(_llm_summarize_module_with_retry, m, project): m for m in modules}
        pbar = None
        if tqdm is not None:
            try:
                pbar = tqdm(total=len(futs), desc=f"{DISPLAY_NAME}: module summaries", unit="module")
            except Exception:
                pbar = None
        try:
            for fut in as_completed(futs):
                m = futs[fut]
                try:
                    out = fut.result()
                except Exception as e:
                    apply_fallback(m, f"LLMError: {e}")
                    if pbar is not None:
                        pbar.update(1)
                    continue
                if isinstance(out, dict) and out.get("purpose"):
                    m["purpose"] = str(out.get("purpose") or "")
                if isinstance(out, dict) and out.get("summary"):
                    m["summary"] = str(out.get("summary") or "")
                # Optional enrich fields
                if isinstance(out, dict):
                    for k in ["responsibilities", "key_files", "external_dependencies", "risks", "top_imports"]:
                        if out.get(k):
                            m[k] = out.get(k)
                err = str(out.get("error") or "")
                if err:
                    apply_fallback(m, err)
                else:
                    apply_fallback(m, None)
                if pbar is not None:
                    pbar.update(1)
        finally:
            if pbar is not None:
                try:
                    pbar.close()
                except Exception:
                    pass


def _render_mermaid(module_edges: list[dict[str, Any]], *, max_edges: int = 40) -> str:
    def _mermaid_id(s: str) -> str:
        """
        Mermaid flowchart node id must be an identifier-like token in many renderers.
        We therefore map module names to a safe id, and keep the human-readable
        module name as the node label.
        """
        import re

        raw = (s or "").strip()
        # Replace non-identifier chars with underscores
        out = re.sub(r"[^A-Za-z0-9_]", "_", raw)
        out = re.sub(r"_+", "_", out).strip("_")
        if not out:
            out = "module"
        if out[0].isdigit():
            out = f"m_{out}"
        return out

    def _mermaid_label(s: str) -> str:
        # Escape double quotes inside label
        return (s or "").replace('"', '\\"')

    edges = sorted(
        (
            (int(e.get("weight") or 0), str(e.get("from_module") or ""), str(e.get("to_module") or ""))
            for e in module_edges
            if str(e.get("from_module") or "") and str(e.get("to_module") or "")
        ),
        key=lambda t: (-t[0], t[1], t[2]),
    )
    lines = ["```mermaid", "flowchart LR"]
    for w, a, b in edges[: max(0, int(max_edges))]:
        label = f" |{w}| " if w else " "
        aid = _mermaid_id(a)
        bid = _mermaid_id(b)
        alabel = _mermaid_label(a)
        blabel = _mermaid_label(b)
        # Use id["label"] to be compatible with more Mermaid parsers.
        lines.append(f'  {aid}["{alabel}"] -->{label}{bid}["{blabel}"]')
    lines.append("```")
    return "\n".join(lines)


def _base_markdown(
    *,
    meta: dict[str, Any],
    modules: list[dict[str, Any]],
    module_edges: list[dict[str, Any]],
    metrics: dict[str, Any],
    suggested_flows: list[dict[str, Any]],
) -> str:
    def _safe_int(x: Any) -> int:
        try:
            return int(x or 0)
        except Exception:
            return 0

    def _fmt_import_ref(import_ref: Any) -> str:
        if not isinstance(import_ref, dict):
            return ""
        # Typical keys from FileAtlas: module, name, level, kind
        mod = str(import_ref.get("module") or "").strip()
        name = str(import_ref.get("name") or "").strip()
        level = _safe_int(import_ref.get("level"))
        if level > 0:
            prefix = "." * level
            if mod:
                mod = prefix + mod
            else:
                mod = prefix
        if mod and name and name != "*":
            return f"from {mod} import {name}"
        if mod:
            return f"import {mod}"
        if name and name != "*":
            return f"import {name}"
        return ""

    out_edges: dict[str, list[dict[str, Any]]] = {}
    in_edges: dict[str, list[dict[str, Any]]] = {}
    for e in module_edges:
        if not isinstance(e, dict):
            continue
        fm = str(e.get("from_module") or "")
        tm = str(e.get("to_module") or "")
        if not fm or not tm:
            continue
        out_edges.setdefault(fm, []).append(e)
        in_edges.setdefault(tm, []).append(e)

    def _sorted_edges(edges: list[dict[str, Any]]) -> list[dict[str, Any]]:
        return sorted(edges, key=lambda x: (-_safe_int(x.get("weight")), str(x.get("from_module") or ""), str(x.get("to_module") or "")))

    lines: list[str] = []
    lines.append(f"# Architecture Overview ({DISPLAY_NAME})")
    lines.append("")
    lines.append(f"- Generated at: `{meta.get('generated_at','')}`")
    lines.append(f"- Source: `{meta.get('project_map_path','project_map.json')}`")
    lines.append("")

    lines.append("## Modules")
    for m in sorted(modules, key=lambda x: str(x.get("module_id") or "")):
        mid = str(m.get("module_id") or "")
        purpose = str(m.get("purpose") or "")
        summary = str(m.get("summary") or "")
        lines.append(f"### `{mid}`")
        if purpose:
            lines.append(f"- **Purpose**: {purpose}")
        if summary:
            lines.append(f"- **Summary**: {summary}")
        mm = m.get("metrics", {}) if isinstance(m.get("metrics", {}), dict) else {}
        if mm:
            lines.append(
                "- **Metrics**: "
                + f"files={mm.get('file_count',0)}, "
                + f"size={mm.get('total_size_bytes',0)}B, "
                + f"fan-in/out={mm.get('fan_in',0)}/{mm.get('fan_out',0)}"
            )

        # Show top inbound/outbound dependencies with evidence (high signal for "not enough detail").
        outs = _sorted_edges(out_edges.get(mid, []))
        ins = _sorted_edges(in_edges.get(mid, []))
        if outs or ins:
            lines.append("- **Dependencies (imports)**:")
            if ins:
                lines.append("  - **Inbound (top)**:")
                for e in ins[:5]:
                    frm = str(e.get("from_module") or "")
                    w = _safe_int(e.get("weight"))
                    lines.append(f"    - `{frm}` → `{mid}` (weight={w})")
                    ev = e.get("evidence", [])
                    if isinstance(ev, list) and ev:
                        for it in ev[:3]:
                            if not isinstance(it, dict):
                                continue
                            fp = str(it.get("from_path") or "")
                            tp = str(it.get("to_path") or "")
                            ref = _fmt_import_ref(it.get("import_ref"))
                            if fp and tp and ref:
                                lines.append(f"      - `{fp}`: `{ref}` ⇒ `{tp}`")
                            elif fp and tp:
                                lines.append(f"      - `{fp}` ⇒ `{tp}`")
            if outs:
                lines.append("  - **Outbound (top)**:")
                for e in outs[:5]:
                    to = str(e.get("to_module") or "")
                    w = _safe_int(e.get("weight"))
                    lines.append(f"    - `{mid}` → `{to}` (weight={w})")
                    ev = e.get("evidence", [])
                    if isinstance(ev, list) and ev:
                        for it in ev[:3]:
                            if not isinstance(it, dict):
                                continue
                            fp = str(it.get("from_path") or "")
                            tp = str(it.get("to_path") or "")
                            ref = _fmt_import_ref(it.get("import_ref"))
                            if fp and tp and ref:
                                lines.append(f"      - `{fp}`: `{ref}` ⇒ `{tp}`")
                            elif fp and tp:
                                lines.append(f"      - `{fp}` ⇒ `{tp}`")

        resp = m.get("responsibilities", [])
        if isinstance(resp, list) and resp:
            lines.append("- **Responsibilities**:")
            for r in resp[:8]:
                lines.append(f"  - {str(r)}")
        kf = m.get("key_files", [])
        if isinstance(kf, list) and kf:
            lines.append("- **Key files**:")
            for it in kf[:10]:
                if isinstance(it, dict):
                    p = str(it.get("path") or "")
                    why = str(it.get("why") or "")
                    if p and why:
                        lines.append(f"  - `{p}`: {why}")
                    elif p:
                        lines.append(f"  - `{p}`")
                else:
                    lines.append(f"  - `{str(it)}`")
        lines.append("")
    lines.append("")

    lines.append("## Module Graph (imports)")
    lines.append(_render_mermaid(module_edges))
    lines.append("")

    # Provide a compact, explicit edge list for auditability.
    lines.append("## Top Module Edges (by import weight)")
    top_edges = _sorted_edges([e for e in module_edges if isinstance(e, dict)])[:30]
    if not top_edges:
        lines.append("- (none)")
    else:
        for e in top_edges:
            fm = str(e.get("from_module") or "")
            tm = str(e.get("to_module") or "")
            w = _safe_int(e.get("weight"))
            lines.append(f"- `{fm}` → `{tm}` (weight={w})")
            ev = e.get("evidence", [])
            if isinstance(ev, list) and ev:
                for it in ev[:3]:
                    if not isinstance(it, dict):
                        continue
                    fp = str(it.get("from_path") or "")
                    tp = str(it.get("to_path") or "")
                    ref = _fmt_import_ref(it.get("import_ref"))
                    if fp and tp and ref:
                        lines.append(f"  - `{fp}`: `{ref}` ⇒ `{tp}`")
                    elif fp and tp:
                        lines.append(f"  - `{fp}` ⇒ `{tp}`")
    lines.append("")

    sccs = metrics.get("sccs", []) if isinstance(metrics.get("sccs", []), list) else []
    cyc = [c for c in sccs if isinstance(c, dict) and int(c.get("size") or 0) > 1]
    lines.append("## Cycles (SCC)")
    if not cyc:
        lines.append("- (none detected)")
    else:
        for c in cyc[:30]:
            lines.append(f"- SCC#{c.get('scc_id')}: {', '.join(c.get('modules') or [])}")
    lines.append("")

    lines.append("## Public Interfaces")
    for m in sorted(modules, key=lambda x: str(x.get("module_id") or "")):
        pis = m.get("public_interfaces", [])
        if not isinstance(pis, list) or not pis:
            continue
        lines.append(f"### `{m.get('module_id','')}`")
        for pi in pis[:20]:
            if not isinstance(pi, dict):
                continue
            qn = pi.get("qualname") or pi.get("name") or ""
            sig = pi.get("signature") or ""
            p = pi.get("path") or ""
            if qn and sig:
                lines.append(f"- `{qn}`: `{sig}` ({p})")
            elif qn:
                lines.append(f"- `{qn}` ({p})")
        lines.append("")

    lines.append("## Suggested Flows (module-level)")
    if not suggested_flows:
        lines.append("- (none)")
    else:
        for f in suggested_flows:
            if not isinstance(f, dict):
                continue
            name = f.get("name") or "flow"
            mods = f.get("modules") or []
            if isinstance(mods, list) and mods:
                lines.append(f"- **{name}**: " + " -> ".join(str(x) for x in mods))
    lines.append("")
    return "\n".join(lines)


def render_architecture_md_best_effort(
    *,
    meta: dict[str, Any],
    modules: list[dict[str, Any]],
    module_edges: list[dict[str, Any]],
    metrics: dict[str, Any],
    suggested_flows: list[dict[str, Any]],
) -> str:
    """
    Best-effort LLM "Reduce" stage to produce a nicer architecture.md.
    Falls back to deterministic markdown template.
    """
    base = _base_markdown(meta=meta, modules=modules, module_edges=module_edges, metrics=metrics, suggested_flows=suggested_flows)

    try:
        provider, client, model = load_llm_from_env()
    except Exception:
        return base

    payload = {
        "meta": meta,
        "modules": [
            {
                "module_id": m.get("module_id", ""),
                "purpose": m.get("purpose", ""),
                "summary": m.get("summary", ""),
                "metrics": m.get("metrics", {}),
                "responsibilities": m.get("responsibilities", []),
                "key_files": m.get("key_files", []),
                "external_dependencies": m.get("external_dependencies", []),
                "risks": m.get("risks", []),
                "top_imports": m.get("top_imports", []),
            }
            for m in modules
        ],
        "edges_top": [
            {"from": e.get("from_module", ""), "to": e.get("to_module", ""), "weight": e.get("weight", 0)}
            for e in sorted(module_edges, key=lambda x: -int(x.get("weight") or 0))[:60]
        ],
        "metrics": metrics,
        "suggested_flows": suggested_flows,
        "base_markdown": base,
    }

    system = (
        "你是资深软件架构文档作者。"
        "请在不编造事实的前提下，把输入数据整理为更易读的 Markdown 文档。"
        "输出必须是 JSON object，且包含字段 markdown。"
    )
    prompt = (
        "请基于 INPUT 生成 `architecture.md`。\n\n"
        "关键约束（最重要）：\n"
        "- 你必须把 INPUT.base_markdown 的内容 **完整保留** 在最终输出里（逐段逐行保留，不得删减要点/列表/代码块）。\n"
        "- 允许做的事情：仅对措辞与排版做轻微润色、修复格式、以及在其后 **追加** 更详细的解释/章节。\n"
        "- 禁止做的事情：概括、压缩、合并列表、删章节、删条目、删 Mermaid 代码块、删任何示例。\n\n"
        "约束：\n"
        "- 不要编造未提供的信息\n"
        "- Mermaid 图块必须原样保留（不要修改代码块内容）\n"
        "- 不要在最终文档中新增“外部依赖”或“风险/注意事项”相关小节/条目（即便 INPUT.modules[*] 里包含这些字段）\n"
        "- 使用清晰的二级/三级标题\n"
        "- 每个模块要尽量讲清楚其职责、关键文件与交互边界（可使用 INPUT.modules[*] 的 responsibilities/key_files 等字段）\n"
        "- markdown 字段里只输出 Markdown 文本\n\n"
        "建议追加（可选，但尽量做）：\n"
        "- 在 base_markdown 后追加：按模块列出更详细的 inbound/outbound 依赖 TopN，并引用 edges_top 或 module_edges 的 evidence 作为示例。\n"
    )
    messages = [
        {"role": "system", "content": system},
        {"role": "user", "content": prompt + "\n\nINPUT:\n" + json.dumps(payload, ensure_ascii=False, indent=2)},
    ]

    try:
        if provider == "ollama":
            resp = client.chat(model=model, messages=messages)  # type: ignore[attr-defined]
            obj = extract_json_content_ollama(resp)
        else:
            resp = client.chat_completions(model=model, messages=messages)  # type: ignore[attr-defined]
            obj = extract_json_content(resp)
        if isinstance(obj, dict) and isinstance(obj.get("markdown"), str) and obj["markdown"].strip():
            return str(obj["markdown"])
    except Exception:
        return base

    return base

