"""Phase 3 review checks for the Python core."""

from __future__ import annotations

import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from scripts.runtime.artifacts import load_manifest, write_manifest
from scripts.runtime.tasks import validate_packet


def utc_now_iso() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat()


def _source_ids_from_jsonl(path: Path) -> set[str]:
    ids: set[str] = set()
    if not path.exists():
        return ids
    for line in path.read_text(encoding="utf-8").splitlines():
        if not line.strip():
            continue
        try:
            obj = json.loads(line)
        except json.JSONDecodeError:
            continue
        source_id = obj.get("id") or obj.get("source_id")
        if source_id:
            ids.add(str(source_id))
    return ids


def _draft_citations(drafts: list[Path]) -> set[str]:
    cited: set[str] = set()
    for draft in drafts:
        cited.update(re.findall(r"\[(src_[A-Za-z0-9_-]+)\]", draft.read_text(encoding="utf-8")))
    return cited


def _ready_packet_stems(project_root: Path) -> set[str]:
    ready: set[str] = set()
    for path in sorted((project_root / "phase2" / "packets").glob("*.json")):
        try:
            packet = json.loads(path.read_text(encoding="utf-8"))
            validate_packet(packet)
        except Exception:
            continue
        ready.add(path.stem)
    return ready


def _read_text_if_exists(path: Path, *, max_chars: int | None = None) -> str:
    if not path.exists():
        return ""
    text = path.read_text(encoding="utf-8", errors="ignore")
    return text[:max_chars] if max_chars is not None else text


def _json_if_exists(path: Path, *, max_chars: int | None = None) -> str:
    if not path.exists():
        return ""
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
        text = json.dumps(data, ensure_ascii=False, indent=2)
    except Exception:
        text = path.read_text(encoding="utf-8", errors="ignore")
    return text[:max_chars] if max_chars is not None else text


def _draft_quality_findings(drafts: list[Path]) -> list[dict[str, Any]]:
    findings: list[dict[str, Any]] = []
    generic_markers = [
        "需要进一步完善",
        "应当加强",
        "持续改进",
        "系统性",
        "闭环管理",
        "质量文化",
    ]
    for draft in drafts:
        text = draft.read_text(encoding="utf-8")
        zh_chars = sum(1 for char in text if "\u4e00" <= char <= "\u9fff")
        citations = re.findall(r"\[(src_[A-Za-z0-9_-]+)\]", text)
        evidence_table_present = "证据落点" in text and "待补证据" in text
        if zh_chars >= 1200 and len(set(citations)) < 5:
            findings.append({"severity": "P1", "message": f"{draft.name} 引用来源过少，可能未充分使用 evidence packet。"})
        if zh_chars >= 1200 and not evidence_table_present:
            findings.append({"severity": "P1", "message": f"{draft.name} 缺少“证据落点与待补证据”小节，难以判断 evidence 是否真正落到纸面。"})
        generic_count = sum(text.count(marker) for marker in generic_markers)
        if zh_chars >= 1200 and generic_count >= 18:
            findings.append({"severity": "P1", "message": f"{draft.name} 泛化管理表述过多，需要回炉为具体审计发现、风险影响和整改动作。"})
    return findings


def build_phase3_model_review_context(project_root: Path, *, max_chars: int = 650_000) -> str:
    """Build a structured, bounded context packet for an independent model review."""
    deterministic_path = build_phase3_critique(project_root)
    deterministic_copy = project_root / "phase3" / "critique_deterministic.md"
    deterministic_copy.write_text(deterministic_path.read_text(encoding="utf-8"), encoding="utf-8")

    manifest = load_manifest(project_root)
    parts: list[str] = [
        f"# Phase 3 Model Review Context: {manifest.get('topic', project_root.name)}",
        "",
        "## Review Contract",
        "",
        "- 这是给非 Codex 模型的独立总编审校上下文，不要求重写正文。",
        "- 请判断 Phase2 草稿能否进入 Phase4，或必须回炉补证据/重写。",
        "- 重点关注：证据是否落纸面、并发 packet 是否造成碎片化、法规/最佳实践覆盖是否足够、整改建议是否具体可执行。",
        "",
        "## Manifest",
        "",
        "```json",
        json.dumps(manifest, ensure_ascii=False, indent=2),
        "```",
        "",
        "## Deterministic Review Baseline",
        "",
        _read_text_if_exists(deterministic_copy),
        "",
        "## Phase 1 Framework",
        "",
        _read_text_if_exists(project_root / "phase1" / "framework.md", max_chars=50_000),
        "",
        "## Phase 1 Research Brief",
        "",
        _read_text_if_exists(project_root / "phase1" / "research_brief.md", max_chars=30_000),
        "",
        "## Phase 2 Brief Warnings",
        "",
        _json_if_exists(project_root / "phase2" / "brief_warnings.json", max_chars=30_000) or "无",
        "",
        "## Phase 2 Packet Errors",
        "",
    ]
    errors = sorted((project_root / "phase2" / "packet_errors").glob("*.json"))
    if errors:
        for path in errors[:40]:
            parts.extend([f"### {path.name}", "", _json_if_exists(path, max_chars=2_000), ""])
    else:
        parts.append("无")

    parts.extend(["", "## Source Registry Summary", ""])
    source_lines = []
    sources_path = project_root / "phase2" / "sources.jsonl"
    if sources_path.exists():
        for line in sources_path.read_text(encoding="utf-8").splitlines()[:260]:
            if not line.strip():
                continue
            try:
                source = json.loads(line)
            except json.JSONDecodeError:
                continue
            source_lines.append(
                "- {id} | {tier} | {title} | {url} | cached={cached}".format(
                    id=source.get("id", ""),
                    tier=source.get("tier", ""),
                    title=str(source.get("title", ""))[:120],
                    url=source.get("url", ""),
                    cached=source.get("cached_text_path", ""),
                )
            )
    parts.append("\n".join(source_lines) or "无")

    parts.extend(["", "## Compressed Findings", ""])
    for path in sorted((project_root / "phase2" / "compressed_findings").glob("ch*.json")):
        parts.extend([f"### {path.name}", "", "```json", _json_if_exists(path, max_chars=35_000), "```", ""])

    parts.extend(["", "## Chapter Drafts", ""])
    for path in sorted((project_root / "phase2" / "drafts").glob("ch*.md")):
        parts.extend([f"### {path.name}", "", _read_text_if_exists(path, max_chars=55_000), ""])

    context = "\n".join(parts)
    if len(context) > max_chars:
        context = context[:max_chars] + "\n\n[Context truncated by max_chars; review should flag if truncation limits confidence.]\n"

    out = project_root / "phase3" / "review_context_opus_4_7.md"
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text(context, encoding="utf-8")
    return context


def phase3_model_review_system_prompt() -> str:
    return (
        "你是 Deep Research Phase 3 的独立总编审校模型，本次由 ZenMux Claude Opus 4.7 执行，用于避免 Codex/OpenAI 模型偏见。\n"
        "你的任务是审校，不是润色或重写。必须用中文输出，英文仅可保留 source title、URL、法规缩写和原文短摘录。\n"
        "请严格检查：1) 研究目标与 Phase1 框架是否契合；2) Phase2 并发 evidence packets 是否被章节真正吸收，还是造成碎片化；"
        "3) FDA/NMPA/EMA/ICH/WHO/EU GMP 等权威来源是否足以支撑关键判断；4) 用户材料是否被正确作为起点且被权威来源交叉验证；"
        "5) 运营管理与团队能力章节是否具体，不得泛泛咨询腔；6) CAPA 建议是否包含 owner、期限、关闭证据、QA verification、复核窗口和升级阈值；"
        "7) 引用链和 source_id 是否可追踪；8) 是否仍有明显 AI 味、中英文混杂或空泛表达。\n\n"
        "输出必须使用以下 Markdown 结构：\n"
        "# Phase 3 Opus 4.7 独立审校\n"
        "## 总体判定\n"
        "给出：通过 / 有条件通过 / 回炉 Phase2，并说明最核心理由。\n"
        "## P0/P1 阻断问题\n"
        "列出必须修复的问题；每条写明章节/文件、问题、为什么阻断、建议动作。\n"
        "## 章节级审校表\n"
        "用表格覆盖 ch01-ch11：主线质量、证据密度、法规覆盖、整改可执行性、是否需要回炉。\n"
        "## 证据与信源质量\n"
        "单独评价 FDA warning letters、ICH Q9/Q10、EU GMP Annex 1、本地缓存信源、第三方低质信源的使用情况。\n"
        "## 碎片化与叙事连贯性\n"
        "判断并发研究是否造成割裂，并给出具体整合建议。\n"
        "## Phase2 回炉任务清单\n"
        "如果需要回炉，列出可执行任务卡级别的补证据/重写要求。\n"
        "## Phase4 准入条件\n"
        "明确进入 final 前必须满足的条件。\n"
    )


def build_phase3_model_critique(
    project_root: Path,
    *,
    client: Any,
    model: str = "zenmux-anthropic/claude-opus-4-7",
    max_context_chars: int = 650_000,
) -> Path:
    context = build_phase3_model_review_context(project_root, max_chars=max_context_chars)
    content = client.chat_complete(
        model=model,
        system=phase3_model_review_system_prompt(),
        user=context,
        temperature=0.2,
        max_tokens=20_000,
        tag="phase3:opus-review",
    )
    out = project_root / "phase3" / "critique.md"
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text(content.rstrip() + "\n", encoding="utf-8")

    manifest = load_manifest(project_root)
    phase3 = manifest.setdefault("phase3", {})
    phase3.update(
        {
            "status": "completed",
            "review_mode": "model",
            "review_model": model,
            "critique_path": "phase3/critique.md",
            "context_path": "phase3/review_context_opus_4_7.md",
            "deterministic_critique_path": "phase3/critique_deterministic.md",
            "updated_at": utc_now_iso(),
        }
    )
    manifest["updated_at"] = utc_now_iso()
    write_manifest(project_root, manifest)
    return out


def build_phase3_critique(project_root: Path) -> Path:
    manifest = load_manifest(project_root)
    drafts = sorted((project_root / "phase2" / "drafts").glob("ch*.md"))
    packets = sorted((project_root / "phase2" / "packets").glob("*.json"))
    ready_stems = _ready_packet_stems(project_root)
    packet_errors = [
        path for path in sorted((project_root / "phase2" / "packet_errors").glob("*.json"))
        if path.stem not in ready_stems
    ]
    chapter_errors = sorted((project_root / "phase2" / "chapter_errors").glob("*.json"))
    sources = _source_ids_from_jsonl(project_root / "phase2" / "sources.jsonl")
    cited = _draft_citations(drafts)
    missing_sources = sorted(cited - sources) if sources else sorted(cited)
    uncited_sources = sorted(sources - cited) if cited else sorted(sources)

    findings: list[dict[str, Any]] = []
    if not drafts:
        findings.append({"severity": "P1", "message": "Phase 2 drafts 缺失，尚不能进入 Phase 4 成稿。"})
    if packet_errors:
        findings.append({"severity": "P1", "message": f"存在 {len(packet_errors)} 个 packet 失败，需要回炉补证据。"})
    if chapter_errors:
        findings.append({"severity": "P1", "message": f"存在 {len(chapter_errors)} 个章节组装失败，需要修复引用或重写该章。"})
    quality_holds = manifest.get("quality_holds") or []
    if quality_holds:
        findings.append({"severity": "P1", "message": "存在质量暂停标记：" + ", ".join(quality_holds)})
    findings.extend(_draft_quality_findings(drafts))
    if missing_sources:
        findings.append({"severity": "P1", "message": f"正文引用未在 sources.jsonl 中登记：{', '.join(missing_sources)}"})
    if not findings:
        findings.append({"severity": "P2", "message": "基础产物完整；仍需人工或大上下文模型审校逻辑链、反方证据和章节叙事。"})

    lines = [
        "# Phase 3 审校 critique",
        "",
        f"- 项目：{manifest.get('topic', project_root.name)}",
        f"- 运行时：python-core-v0.20",
        f"- drafts：{len(drafts)}",
        f"- packets：{len(packets)}",
        f"- sources：{len(sources)}",
        f"- cited_source_ids：{', '.join(sorted(cited)) if cited else '无'}",
        "",
        "## Findings",
        "",
    ]
    for item in findings:
        lines.append(f"- [{item['severity']}] {item['message']}")
    lines.extend(
        [
            "",
            "## Residual Risks",
            "",
            "- 本 deterministic review 只做结构、引用和错误包检查；深层逻辑审校仍建议交给 `phase3_review` 角色执行。",
            "- 若 sources 为空，本审校会把所有正文引用视为待登记来源。",
            "",
            "## Next",
            "",
            "- 若存在 P1，先回到 Phase 2 修复 packet/chapter 错误。",
            "- 若仅有 P2，可进入 `dr.py finalize` 的中文原生成稿路径。",
            "",
        ]
    )
    out = project_root / "phase3" / "critique.md"
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text("\n".join(lines), encoding="utf-8")
    phase3 = manifest.setdefault("phase3", {})
    phase3.update(
        {
            "status": "completed",
            "critique_path": "phase3/critique.md",
            "findings_total": len(findings),
            "missing_sources": missing_sources,
            "uncited_sources": uncited_sources,
            "updated_at": utc_now_iso(),
        }
    )
    manifest["updated_at"] = utc_now_iso()
    write_manifest(project_root, manifest)
    return out
