"""Task-card and evidence-packet primitives for v0.20 Phase 2."""

from __future__ import annotations

import json
import re
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any

from scripts.runtime.methods import ResearchMethod


VALID_ROUTES = {"general", "evidence", "scholar", "patents", "news", "fda"}
DEFAULT_AXES = ["literature", "regulatory", "patents", "market", "counter"]
AXIS_ROUTES = {
    "literature": ["scholar", "evidence", "general"],
    "clinical": ["scholar", "evidence", "general"],
    "regulatory": ["fda", "evidence", "general", "news"],
    "patents": ["patents", "evidence", "general"],
    "market": ["news", "general"],
    "china": ["news", "general"],
    "counter": ["fda", "scholar", "evidence", "general"],
    "regulatory_gap": ["fda", "evidence", "general", "news"],
    "risk_classification": ["evidence", "general", "scholar"],
    "capa_design": ["evidence", "general", "news"],
    "ownership_timeline": ["general"],
    "verification_evidence": ["fda", "evidence", "general", "scholar"],
    "process_flow": ["scholar", "evidence", "general"],
    "cqa_cpp": ["scholar", "evidence", "general"],
    "scale_up_risk": ["scholar", "evidence", "general"],
    "control_strategy": ["scholar", "evidence", "general"],
    "supply_chain": ["news", "general"],
    "scientific_rationale": ["scholar", "evidence", "general"],
    "poc_evidence": ["scholar", "evidence", "general"],
    "ip_fto": ["patents", "evidence", "general"],
    "development_path": ["scholar", "evidence", "general"],
    "commercial_window": ["news", "general"],
    "current_state": ["evidence", "general"],
    "capability_gap": ["evidence", "general"],
    "operating_model": ["evidence", "general"],
    "governance": ["evidence", "general"],
    "implementation_roadmap": ["evidence", "general"],
    "nmpa_fda_ema_ich_who_baseline": ["fda", "evidence", "general", "news"],
    "quality_system_gap": ["fda", "evidence", "general"],
    "manufacturing_process_risk": ["fda", "scholar", "evidence", "general"],
    "operations_management_gap": ["fda", "evidence", "general"],
    "team_capability": ["evidence", "general", "news"],
    "capa_roadmap": ["fda", "evidence", "general"],
    "input_material_findings": ["evidence", "general"],
    "fda_enforcement_precedents": ["fda"],
    "chapter_integrated": ["fda", "scholar", "evidence", "general"],
}


@dataclass
class Chapter:
    chapter_id: str
    index: int
    title: str
    notes: str = ""


@dataclass
class TaskCard:
    task_id: str
    chapter_ids: list[str]
    topic_axis: str
    questions: list[str]
    search_routes: list[str]
    output_packet: str
    chapter_title: str = ""
    preferred_model_role: str = "dr_analyst"
    status: str = "pending"
    dependencies: list[str] = field(default_factory=list)
    research_goal: str = ""
    research_method: str = ""
    prompt_brief: str = ""
    required_skills: list[str] = field(default_factory=list)
    allowed_materials: list[str] = field(default_factory=list)
    expected_evidence: dict[str, Any] = field(default_factory=dict)
    stop_conditions: list[str] = field(default_factory=list)
    model_hint: str = ""

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


def parse_framework_chapters(framework_text: str) -> list[Chapter]:
    """Extract Chinese or English chapter headings from a framework markdown."""
    lines = framework_text.splitlines()
    chapters: list[Chapter] = []
    current: Chapter | None = None
    note_lines: list[str] = []
    heading_re = re.compile(
        r"^#{1,3}\s*(?:第\s*)?(\d{1,2})\s*(?:章|[.)、:-])?\s*(.+?)\s*$",
        re.IGNORECASE,
    )
    english_re = re.compile(r"^#{1,3}\s*chapter\s+(\d{1,2})[:.)\s-]+(.+?)\s*$", re.IGNORECASE)
    for line in lines:
        match = heading_re.match(line.strip()) or english_re.match(line.strip())
        if match:
            if current:
                current.notes = "\n".join(note_lines).strip()
                chapters.append(current)
            index = int(match.group(1))
            title = match.group(2).strip(" #")
            current = Chapter(chapter_id=f"ch{index:02d}", index=index, title=title)
            note_lines = []
        elif current:
            note_lines.append(line)
    if current:
        current.notes = "\n".join(note_lines).strip()
        chapters.append(current)
    return chapters


def _questions_for_axis(chapter: Chapter, axis: str, method: ResearchMethod | None = None) -> list[str]:
    if axis == "chapter_integrated":
        lanes = "；".join(method.integrated_lanes if method else [])
        return [
            f"围绕《{chapter.title}》形成章节级综合证据包，不再拆成孤立小轴。",
            f"必须按当前 research_method 的 evidence lanes 组织证据：{lanes or '本地材料、权威来源、反方证据、可执行建议'}。",
            "若项目有用户材料，必须先读取本地材料证据并提取原文；再用本方法适用的权威来源交叉验证。",
            "必须形成：材料/事实基线、外部权威证据、差距或机会判断、反方/限制条件、可执行建议和待补证据。",
        ]
    questions = [
        f"围绕《{chapter.title}》从 {axis} 角度提炼可证伪的核心结论。",
        "至少寻找两个 Tier 1-2 来源支撑主要结论；不足时标注待验证。",
        "主动检索反方证据、限制条件或失败案例。",
    ]
    if axis in {
        "nmpa_fda_ema_ich_who_baseline",
        "quality_system_gap",
        "manufacturing_process_risk",
        "operations_management_gap",
        "capa_roadmap",
        "verification_evidence",
        "counter",
        "fda_enforcement_precedents",
    }:
        questions.append(
            "必须检索并优先评估 FDA Warning Letters、inspection/enforcement 页面、会议纪要或 meeting materials，作为 GMP 缺陷严重度和整改优先级的佐证。"
        )
    return questions


def _default_required_skills(axis: str) -> list[str]:
    skills = ["search-gateway", "search-strategy", "source-quality", "evidence-table"]
    if axis == "counter":
        skills.append("mckinsey-method")
    return skills


def _default_expected_evidence(axis: str) -> dict[str, Any]:
    expected = {
        "min_tier_1_2_sources": 2,
        "must_include_counter_evidence": True,
        "must_include_source_metadata": True,
        "preferred_evidence_types": [
            "regulatory_or_best_practice_requirement",
            "fda_warning_letter_or_meeting_record",
            "site_or_material_finding",
            "quantitative_fact_or_record",
            "implementation_or_verification_evidence",
        ],
        "axis": axis,
    }
    if axis == "chapter_integrated":
        expected.update(
            {
                "min_local_material_evidence": 2,
                "min_official_sources": 2,
                "min_fda_or_regulatory_precedents": 1,
                "min_capa_actions": 3,
                "preferred_evidence_types": [
                    "local_audit_or_recap_quote",
                    "official_regulatory_requirement",
                    "fda_warning_letter_or_meeting_record",
                    "gap_analysis",
                    "capa_action_with_owner_and_verification",
                    "counter_evidence_or_boundary_condition",
                ],
            }
        )
    return expected


def _default_stop_conditions() -> list[str]:
    return [
        "已形成至少 3 条可追溯 evidence_items，且每条关键 claim 有 source_id。",
        "已主动记录 counter_evidence 或明确说明未找到反方证据的检索路径。",
        "candidate_sources 不足以支撑结论时停止写作，并把缺口写入 open_questions。",
    ]


def _integrated_prompt_brief(chapter: Chapter, method: ResearchMethod | None) -> str:
    lanes = "；".join(method.integrated_lanes if method else [])
    return (
        f"本任务是《{chapter.title}》的章节级综合证据包。不要把多条窄轴 packet 机械拼贴；"
        f"必须围绕当前研究方法的 lanes 一次性收束主线：{lanes or '事实材料、权威证据、反方证据、行动建议'}。"
        "输出必须让章节作者能直接写出判断、证据落点和可执行建议。"
    )


def _task_card_for_chapter_axis(
    *,
    chapter: Chapter,
    axis: str,
    routes: list[str],
    method_key: str,
    required_skills: list[str] | None = None,
    allowed_materials: list[str] | None = None,
    prompt_brief: str | None = None,
    questions: list[str] | None = None,
    research_goal: str | None = None,
    expected_evidence: dict[str, Any] | None = None,
    stop_conditions: list[str] | None = None,
    method: ResearchMethod | None = None,
) -> TaskCard:
    return TaskCard(
        task_id=f"{chapter.chapter_id}-{axis}",
        chapter_ids=[chapter.chapter_id],
        topic_axis=axis,
        questions=questions or _questions_for_axis(chapter, axis, method),
        search_routes=routes,
        output_packet=f"phase2/packets/{chapter.chapter_id}-{axis}.json",
        chapter_title=chapter.title,
        preferred_model_role="dr_verifier" if axis == "counter" else "dr_analyst",
        research_goal=research_goal or f"为《{chapter.title}》收集并验证 {axis} 轴证据，形成可写入章节的具体判断与证据落点。",
        research_method=method_key,
        prompt_brief=prompt_brief or (_integrated_prompt_brief(chapter, method) if axis == "chapter_integrated" else f"围绕《{chapter.title}》的 {axis} 轴，优先形成可证伪、可引用、可落地的证据包。"),
        required_skills=required_skills or _default_required_skills(axis),
        allowed_materials=allowed_materials or [],
        expected_evidence=expected_evidence or _default_expected_evidence(axis),
        stop_conditions=stop_conditions or _default_stop_conditions(),
        model_hint="use_cross_model_verifier" if axis == "counter" else "use_cost_effective_research_worker",
    )


def generate_task_cards(
    slug: str,
    framework_text: str,
    *,
    axes: list[str] | None = None,
    method: ResearchMethod | None = None,
) -> list[TaskCard]:
    del slug  # slug is kept for call-site clarity and future namespacing.
    chapters = parse_framework_chapters(framework_text)
    selected_axes = axes or (method.task_axes if method else DEFAULT_AXES)
    cards: list[TaskCard] = []
    for chapter in chapters:
        for axis in selected_axes:
            routes = AXIS_ROUTES.get(axis, ["general"])
            cards.append(
                _task_card_for_chapter_axis(
                    chapter=chapter,
                    axis=axis,
                    routes=routes,
                    method_key=method.key if method else "",
                    method=method,
                )
            )
    validate_task_cards(cards)
    return cards


def generate_task_cards_from_research_brief(
    slug: str,
    framework_text: str,
    research_brief: dict[str, Any],
    *,
    axes: list[str] | None = None,
    method: ResearchMethod | None = None,
) -> list[TaskCard]:
    del slug
    chapters = parse_framework_chapters(framework_text)
    planning = research_brief.get("task_planning") or {}
    method_key = research_brief.get("research_method") or (method.key if method else "")
    if method is None and method_key:
        from scripts.runtime.methods import ResearchMethodRegistry

        method = ResearchMethodRegistry().get(method_key)
    phase2_mode = planning.get("phase2_mode") or research_brief.get("phase2_mode")
    if axes:
        selected_axes = axes
    elif phase2_mode == "chapter_integrated":
        selected_axes = ["chapter_integrated"]
    else:
        selected_axes = (method.task_axes if method else None) or list(planning.get("search_routes_by_axis") or []) or DEFAULT_AXES
    routes_by_axis = planning.get("search_routes_by_axis") or {}
    prompt_by_axis = planning.get("axis_prompt_briefs") or {}
    base_skills = list(planning.get("required_skills") or [])
    stop_conditions = list(planning.get("stop_conditions") or [])
    allowed_materials = [
        str(item.get("path"))
        for item in research_brief.get("materials", [])
        if item.get("path")
    ]
    if not allowed_materials:
        material_digest = (research_brief.get("phase1_inputs") or {}).get("material_digest")
        if material_digest:
            allowed_materials.append(str(material_digest))
    chapter_plan_by_id = {
        str(item.get("chapter_id")): item
        for item in research_brief.get("chapter_planning", [])
        if item.get("chapter_id")
    }
    cards: list[TaskCard] = []
    for chapter in chapters:
        for axis in selected_axes:
            routes = list(routes_by_axis.get(axis) or AXIS_ROUTES.get(axis, ["general"]))
            skills = base_skills or _default_required_skills(axis)
            if "search-gateway" not in skills:
                skills = ["search-gateway", *skills]
            chapter_plan = chapter_plan_by_id.get(chapter.chapter_id) if axis == "chapter_integrated" else None
            prompt_brief = prompt_by_axis.get(axis)
            questions = None
            research_goal = None
            expected_evidence = None
            card_stop_conditions = stop_conditions or None
            if chapter_plan:
                prompt_brief = chapter_plan.get("phase2_prompt_context") or prompt_brief
                research_goal = chapter_plan.get("core_question")
                questions = [
                    chapter_plan.get("core_question", ""),
                    chapter_plan.get("bold_hypothesis", ""),
                    "按 Phase1 求证计划逐条收集支持证据、反方证据和待补证据。",
                    "不得绕开 Phase1 主基调另起炉灶；若证据推翻假设，必须明确写出修正建议。",
                ]
                questions.extend(str(item) for item in chapter_plan.get("verification_plan", []))
                expected_evidence = _default_expected_evidence(axis)
                expected_evidence.update(
                    {
                        "phase1_minimum_evidence": chapter_plan.get("minimum_evidence") or {},
                        "evidence_lanes": chapter_plan.get("evidence_lanes") or [],
                        "must_address_phase1_hypothesis": True,
                    }
                )
                card_stop_conditions = [
                    *(stop_conditions or _default_stop_conditions()),
                    "已经逐条回应 Phase1 的大胆假设：支持、修正或推翻，并说明依据。",
                    "已经把本地材料原文、外部证据、反方边界和行动建议分开记录。",
                ]
            cards.append(
                _task_card_for_chapter_axis(
                    chapter=chapter,
                    axis=axis,
                    routes=routes,
                    method_key=method_key,
                    required_skills=skills,
                    allowed_materials=allowed_materials,
                    prompt_brief=prompt_brief,
                    questions=questions,
                    research_goal=research_goal,
                    expected_evidence=expected_evidence,
                    stop_conditions=card_stop_conditions,
                    method=method,
                )
            )
    validate_task_cards(cards)
    return cards


def detect_dependency_cycles(cards: list[TaskCard]) -> None:
    graph = {card.task_id: card.dependencies for card in cards}
    visiting: set[str] = set()
    visited: set[str] = set()

    def visit(node: str) -> None:
        if node in visiting:
            raise ValueError(f"dependency cycle detected at {node}")
        if node in visited:
            return
        visiting.add(node)
        for dep in graph.get(node, []):
            visit(dep)
        visiting.remove(node)
        visited.add(node)

    for task_id in graph:
        visit(task_id)


def validate_task_cards(cards: list[TaskCard]) -> None:
    seen: set[str] = set()
    for card in cards:
        if not card.research_goal:
            card.research_goal = f"围绕 {card.topic_axis} 轴收集并验证结构化证据。"
        if not card.prompt_brief:
            card.prompt_brief = f"按 {card.topic_axis} 轴形成证据包，避免泛泛结论。"
        if not card.required_skills:
            card.required_skills = _default_required_skills(card.topic_axis)
        if "search-gateway" not in card.required_skills:
            card.required_skills = ["search-gateway", *card.required_skills]
        if not card.expected_evidence:
            card.expected_evidence = _default_expected_evidence(card.topic_axis)
        if not card.stop_conditions:
            card.stop_conditions = _default_stop_conditions()
        if card.task_id in seen:
            raise ValueError(f"duplicate task_id: {card.task_id}")
        seen.add(card.task_id)
        if not card.chapter_ids:
            raise ValueError(f"{card.task_id}: chapter_ids required")
        if not card.chapter_title:
            card.chapter_title = card.chapter_ids[0]
        if not card.questions:
            raise ValueError(f"{card.task_id}: questions required")
        if not card.output_packet.endswith(".json"):
            raise ValueError(f"{card.task_id}: output_packet must be json")
        invalid_routes = sorted(set(card.search_routes) - VALID_ROUTES)
        if invalid_routes:
            raise ValueError(f"{card.task_id}: invalid search_routes {invalid_routes}")
    missing_deps = sorted({dep for card in cards for dep in card.dependencies} - seen)
    if missing_deps:
        raise ValueError(f"unknown dependencies: {missing_deps}")
    detect_dependency_cycles(cards)


def write_task_cards(path: Path, cards: list[TaskCard]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        json.dumps([card.to_dict() for card in cards], ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def load_task_cards(path: Path) -> list[TaskCard]:
    data = json.loads(path.read_text(encoding="utf-8"))
    cards = [TaskCard(**item) for item in data]
    validate_task_cards(cards)
    return cards


def validate_packet(packet: dict[str, Any]) -> None:
    required = {
        "task_id",
        "claims",
        "evidence_items",
        "counter_evidence",
        "source_ids",
        "source_quality_notes",
        "open_questions",
        "raw_quotes_or_notes",
    }
    missing = sorted(required - set(packet))
    if missing:
        raise ValueError(f"packet missing fields: {missing}")
    if not packet["claims"]:
        raise ValueError("packet claims must not be empty")
    if not packet["evidence_items"]:
        raise ValueError("packet evidence_items must not be empty")
    if not packet["counter_evidence"]:
        raise ValueError("packet counter_evidence must not be empty")
    declared = set(packet.get("source_ids") or [])
    referenced: set[str] = set()
    for section in ("claims", "counter_evidence"):
        for item in packet.get(section) or []:
            referenced.update(item.get("source_ids") or [])
    for item in packet.get("evidence_items") or []:
        if item.get("source_id"):
            referenced.add(item["source_id"])
    undeclared = sorted(referenced - declared)
    if undeclared:
        raise ValueError(f"packet source_ids referenced but not declared: {undeclared}")
    packet_sources = packet.get("sources") or []
    if not packet_sources:
        raise ValueError("packet sources must not be empty")
    known_source_ids = {source.get("id") for source in packet_sources}
    missing_sources = sorted(declared - known_source_ids)
    if missing_sources:
        raise ValueError(f"packet source_ids missing source metadata: {missing_sources}")
