"""Phase 1 project initialization and framework generation."""

from __future__ import annotations

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

from scripts.runtime.artifacts import PROJECTS_DIR, ensure_phase_dirs, load_manifest, write_manifest
from scripts.runtime.materials import ingest_input_materials, render_material_inventory
from scripts.runtime.methods import ResearchMethod, ResearchMethodRegistry
from scripts.runtime.tasks import AXIS_ROUTES


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


def slugify_topic(topic: str) -> str:
    slug = re.sub(r"[^a-zA-Z0-9]+", "-", topic.lower()).strip("-")
    if slug:
        return slug[:80]
    digest = hashlib.sha1(topic.encode("utf-8")).hexdigest()[:8]
    return f"research-{digest}"


def create_project(
    *,
    topic: str,
    slug: str | None = None,
    projects_dir: Path = PROJECTS_DIR,
    method_key: str | None = None,
    report_type: str = "research",
    model_profile: str = "medium",
    target_words: int = 30000,
    input_materials: list[str] | None = None,
) -> Path:
    method = ResearchMethodRegistry().get(method_key)
    project_slug = slug or slugify_topic(topic)
    project_root = projects_dir / project_slug
    if project_root.exists():
        raise FileExistsError(f"project already exists: {project_root}")
    ensure_phase_dirs(project_root)
    (project_root / "phase0" / "inputs").mkdir(parents=True, exist_ok=True)
    (project_root / "phase0" / "extracted").mkdir(parents=True, exist_ok=True)
    material_inventory = ingest_input_materials(project_root, input_materials)
    now = utc_now_iso()
    manifest: dict[str, Any] = {
        "version": "0.20.0",
        "runtime": "python-core-v0.20",
        "topic": topic,
        "slug": project_slug,
        "report_title": topic,
        "type": report_type,
        "work_language": "zh",
        "model_profile": model_profile,
        "research_method": method.key,
        "target_words": target_words,
        "input_materials": input_materials or [],
        "material_inventory": material_inventory,
        "created_at": now,
        "updated_at": now,
        "phase1": {
            "status": "initialized",
            "approved": False,
            "requires_user_interview": True,
            "material_brief_path": "phase1/material_brief.md",
        },
        "phase2": {"status": "pending"},
        "phase3": {"status": "pending"},
        "phase4": {"status": "pending"},
    }
    write_manifest(project_root, manifest)
    _write_interview_seed(project_root, manifest, method)
    write_material_brief(project_root, manifest, method)
    return project_root


def _write_interview_seed(project_root: Path, manifest: dict[str, Any], method: ResearchMethod) -> None:
    material_text = render_material_inventory(manifest.get("material_inventory") or [])
    text = (
        f"# Phase 1 访谈记录\n\n"
        f"- 主题：{manifest['topic']}\n"
        f"- 研究方法：{method.key} - {method.name}\n"
        f"- 报告类型：{manifest['type']}\n"
        f"- 目标字数：{manifest['target_words']}\n"
        f"- 工作语言：中文主写作；检索关键词、证据摘录和来源笔记可保留英文。\n\n"
        f"## 已提供材料\n\n{material_text}\n\n"
        "## 后续访谈问题\n\n"
        "1. 本报告最重要的决策用途是什么？\n"
        "2. 是否有必须覆盖或必须排除的公司、产品、工艺、市场或法规范围？\n"
        "3. 结论偏好是战略建议、风险清单、投资判断，还是执行路线图？\n"
    )
    (project_root / "phase1" / "interview.md").write_text(text, encoding="utf-8")


def _material_excerpt(project_root: Path, rel_path: str, *, max_chars: int = 1200) -> str:
    path = project_root / rel_path
    if not path.exists():
        return "（未找到抽取文本）"
    text = path.read_text(encoding="utf-8")
    compact = "\n".join(line.rstrip() for line in text.splitlines() if line.strip())
    return compact[:max_chars] + ("..." if len(compact) > max_chars else "")


def _derive_material_observations(project_root: Path, inventory: list[dict[str, Any]]) -> list[str]:
    combined_parts: list[str] = []
    for item in inventory:
        rel = item.get("ocr_extracted_to") or item.get("extracted_to")
        if rel and (project_root / rel).exists():
            combined_parts.append((project_root / rel).read_text(encoding="utf-8"))
    text = "\n".join(combined_parts)
    checks = [
        ("审计范围覆盖生产管理、原液、制剂和无菌相关模块，报告需要同时处理 GMP 合规、工艺转移和运营协同，而不是只写质量体系。", ["生产管理", "原液", "制剂", "无菌"]),
        ("材料显示高风险项为 0、中风险项为 1，适合采用“商业化 readiness 与系统成熟度差距”而非“体系失控”作为初始假设。", ["高风险 0", "中风险1", "低风险7"]),
        ("商业化经验、无菌保障细节、文件要求与执行一致性是需要访谈确认的主线风险。", ["商业化经验不足", "无菌保障", "文件要求与执行一致性"]),
        ("工艺规程、批记录、CPP/CQA、VMPR/VMP、验证主计划等内容反复出现，说明工艺验证和商业化文件体系可能是 Phase 2 的重点证据轴。", ["CPP", "CQA", "VMPR", "VMP"]),
        ("温度、压差、WFI、冷却段微生物、RABS/ORABS、first air、APS 等无菌和设施细节需要映射到 EU Annex 1、NMPA GMP 和企业 SOP。", ["温度", "压差", "WFI", "APS"]),
        ("复盘材料包含责任人和局部答复，后续整改路线图应尽量回填 owner、期限、关闭证据和复核机制。", ["填写人", "是否已经回答完整", "整改"]),
    ]
    observations = [message for message, needles in checks if any(needle in text for needle in needles)]
    return observations or ["材料已导入但尚未形成足够结构化判断；需要先访谈确认研究用途、范围和优先级。"]


def write_material_brief(
    project_root: Path,
    manifest: dict[str, Any] | None = None,
    method: ResearchMethod | None = None,
) -> Path:
    """Write a Phase 0/1 material brief that must be reviewed before Phase 2."""
    manifest = manifest or load_manifest(project_root)
    method = method or ResearchMethodRegistry().get(manifest.get("research_method"))
    inventory = manifest.get("material_inventory") or []
    lines = [
        f"# Phase 0 材料简报：{manifest.get('topic', project_root.name)}",
        "",
        "status: 待用户确认",
        f"research_method: {method.key}",
        "",
        "## 已导入材料",
        "",
        render_material_inventory(inventory),
        "",
        "## 材料初步解读",
        "",
        "以下内容由 Python core 从已落盘材料抽样生成，只作为访谈起点；不得直接视为最终结论。",
        "",
    ]
    lines.extend(["## 初步问题聚类（待访谈确认）", ""])
    for observation in _derive_material_observations(project_root, inventory):
        lines.append(f"- {observation}")
    lines.append("")
    lines.append("## 材料摘录")
    lines.append("")
    for item in inventory:
        rel = item.get("ocr_extracted_to") or item.get("extracted_to")
        if not rel:
            continue
        lines.extend(
            [
                f"### {Path(rel).name}",
                "",
                _material_excerpt(project_root, rel),
                "",
            ]
        )
    lines.extend(
        [
            "## 建议访谈确认点",
            "",
            "1. 本报告的最重要用途是什么：内部整改、客户沟通、董事会决策，还是外部审计准备？",
            "2. 哪些审计发现最需要优先展开：无菌保障、工艺验证、数据完整性、质量体系闭环，还是运营协同？",
            "3. 是否存在必须排除或脱敏的项目、人员、客户、产品或工艺信息？",
            "4. 短中长期整改的时间边界如何定义，例如 30/90/180 天，还是按临床/商业化里程碑划分？",
            "5. 是否需要把 NMPA、FDA、EMA、ICH、WHO 的法规基线分别映射到整改责任人和证据包？",
            "",
            "## Gate",
            "",
            "请用户确认本材料简报与访谈问题后，再生成或批准 `phase1/framework.md` 并进入 Phase 2。",
            "",
        ]
    )
    out = project_root / "phase1" / "material_brief.md"
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text("\n".join(lines), encoding="utf-8")
    return out


def _axis_prompt_brief(axis: str, method: ResearchMethod) -> str:
    prompts = {
        "input_material_findings": "从用户材料中提取现场事实、审计发现、复盘记录和内部答复，并标注原始材料位置。",
        "nmpa_fda_ema_ich_who_baseline": "把 NMPA、FDA、EMA、ICH、WHO、药典或 Annex 1 等要求转化为可核验的法规基线，并纳入 FDA warning letters 与会议材料作为执法尺度参照。",
        "quality_system_gap": "把现场发现映射到质量体系流程缺口，覆盖偏差、变更、CAPA、文件、培训和数据完整性；优先检索 FDA warning letters 中同类缺陷的执法表述。",
        "manufacturing_process_risk": "围绕生产工艺、设施、公用系统、CPP/CQA、验证和无菌保障识别系统性风险，并用 FDA warning letters / inspection enforcement examples 校准严重度。",
        "operations_management_gap": "诊断运营管理、跨部门协同、会议机制、指标体系和交付节奏的结构性问题，并参考 FDA 会议纪要/meeting materials 中对质量治理的关注点。",
        "team_capability": "识别人员能力、岗位职责、质量文化和管理梯队方面的缺口与建设路径。",
        "capa_roadmap": "把差距转化为短中长期 CAPA 组合，要求绑定 owner、期限、优先级、关闭证据和复核机制。",
        "verification_evidence": "定义整改完成后可被审计接受的验证证据，包括记录、报告、趋势和管理评审输入。",
        "counter": "主动寻找反方证据、限制条件和可能降低严重度或改变优先级的解释，避免单向论证。",
    }
    return prompts.get(axis, f"按照 `{method.key}` 方法，对 {axis} 轴进行证据收集、证伪和结构化归纳。")


def _material_paths(manifest: dict[str, Any]) -> list[dict[str, str]]:
    materials: list[dict[str, str]] = []
    for item in manifest.get("material_inventory") or []:
        rel = item.get("ocr_extracted_to") or item.get("extracted_to") or item.get("copied_to")
        if rel:
            materials.append({"path": rel, "role": "input_material"})
    return materials


def _keywords_from_title(title: str) -> list[str]:
    english = re.findall(r"[A-Za-z][A-Za-z0-9/+-]{1,}", title)
    chinese_parts = re.split(r"[，,、；;：:\s]+|和|与|及|的|在|为|从|来自|集中|决定|需要|形成|成为|不是|而是", title)
    domain_terms = [
        "审计",
        "商业化",
        "阶段门",
        "风险",
        "法规",
        "欧盟",
        "NMPA",
        "GMP",
        "ICH",
        "无菌",
        "RABS",
        "First Air",
        "APS",
        "灯检",
        "隧道",
        "原液",
        "WFI",
        "SCADA",
        "EMS",
        "CPP",
        "CQA",
        "PPQ",
        "清洁验证",
        "偏差",
        "变更",
        "CAPA",
        "数据完整性",
        "人员",
        "培训",
        "质量文化",
        "运营",
        "跨部门",
        "指标",
        "团队",
        "CDMO",
        "整改",
        "owner",
    ]
    title_terms = [term for term in domain_terms if term in title]
    keywords = [item.strip() for item in [*english, *title_terms, *chinese_parts] if len(item.strip()) >= 2]
    seen: set[str] = set()
    unique: list[str] = []
    for keyword in keywords:
        if keyword not in seen:
            seen.add(keyword)
            unique.append(keyword)
    return unique[:12]


def _material_lines_for_chapter(project_root: Path, manifest: dict[str, Any], title: str, *, limit: int = 4) -> list[str]:
    keywords = _keywords_from_title(title)
    candidates: list[tuple[int, int, str]] = []
    order = 0
    for item in manifest.get("material_inventory") or []:
        rel = item.get("ocr_extracted_to") or item.get("extracted_to")
        if not rel:
            continue
        path = project_root / rel
        if not path.exists():
            continue
        for raw in path.read_text(encoding="utf-8").splitlines():
            line = raw.strip()
            if len(line) < 8 or len(line) > 220:
                continue
            if line.startswith("#") or line.startswith("- source_path:") or line.startswith("- extracted_at:"):
                continue
            if "OCR Material:" in line:
                continue
            if re.match(r"^(审计对象|审计执行方|审计执行人|审计时间)[:：]", line):
                continue
            score = sum(1 for keyword in keywords if keyword and keyword in line)
            if score:
                order += 1
                candidates.append((score, order, f"{rel}：{line}"))
    candidates.sort(key=lambda item: (-item[0], item[1]))
    return [line for _, _, line in candidates[:limit]]


def _minimum_evidence_for_method(method: ResearchMethod) -> dict[str, Any]:
    if method.key == "gmp_quality_operations_diagnosis":
        return {
            "local_material_quotes": 2,
            "official_regulatory_or_guideline_sources": 2,
            "enforcement_or_best_practice_precedents": 1,
            "counter_evidence_or_boundary_conditions": 1,
            "actionable_remediation_items": 3,
        }
    return {
        "high_quality_sources": 4,
        "tier_1_2_sources": 2,
        "counter_evidence_or_boundary_conditions": 1,
        "decision_relevant_implications": 2,
    }


def _central_thesis(manifest: dict[str, Any], method: ResearchMethod) -> str:
    topic = manifest.get("topic") or manifest.get("report_title") or "本研究主题"
    if method.key == "gmp_quality_operations_diagnosis":
        return (
            f"初始主判断：{topic} 不应只按审计风险项数量来评价，而应从商业化 readiness、"
            "质量体系运行成熟度、生产工艺证据链和运营协同能力四条线同时诊断。Phase 2 必须用"
            "现场材料原文、官方法规/指南、执法案例或标杆实践来证明、修正或推翻这一判断。"
        )
    return (
        f"初始主判断：{topic} 需要先形成可被证据推翻的观点型框架，再由 Phase 2 按方法论证据线"
        "逐项求证；不能把并发检索结果直接堆砌成报告。"
    )


def _strategy_for_chapter(title: str, method: ResearchMethod) -> dict[str, Any]:
    """Return non-tautological Phase 1 strategy text for a chapter title."""
    if method.key != "gmp_quality_operations_diagnosis":
        return {
            "core_question": f"本章需要判断：在什么证据条件下“{title}”成立，它会怎样改变最终决策？",
            "bold_hypothesis": f"初始假设不是复述标题，而是预判“{title}”背后存在一个可被验证的因果机制；Phase 2 需要找证据支持、修正或推翻这个机制。",
            "writing_claim": f"本章要把“{title}”写成一个可被证据检验的判断，而不是资料综述。",
            "counter_evidence": [
                "是否存在更简单的替代解释，能削弱本章主判断？",
                "关键证据是否只来自单一来源或利益相关来源？",
                "是否有反例显示本章判断只适用于部分场景？",
            ],
        }

    strategies = [
        (
            ("审计", "阶段门"),
            {
                "core_question": "审计报告的低/中风险项计数，是否低估了白帆从临床/受托生产走向商业化标准时需要跨过的阶段门？",
                "bold_hypothesis": "初始假设：白帆的硬件和文件基础总体可用，但审计材料暴露的是商业化 readiness 缺口，而不是简单的若干孤立缺陷；Phase 2 应验证这些缺口是否集中在无菌保障、工艺验证、质量闭环和运营节奏。",
                "writing_claim": "本章要先把“风险项清单”翻译成管理层可决策的阶段门地图，说明哪些问题影响商业化放行、客户审计和技术转移节奏。",
                "counter_evidence": [
                    "是否已有整改证据证明这些问题只是审计时点的临时缺口？",
                    "低/中风险评级是否足以说明商业化阶段门影响有限？",
                    "审计范围有限是否导致本章不能外推到整体体系成熟度？",
                ],
            },
        ),
        (
            ("法规", "欧盟", "NMPA", "ICH"),
            {
                "core_question": "如果按 EU Annex 1、NMPA GMP、ICH Q9/Q10 以及 FDA 执法尺度校准，哪些现场发现的严重度和整改优先级会发生变化？",
                "bold_hypothesis": "初始假设：白帆按国内 GMP 逻辑已具备基础合规框架，但若以欧盟无菌标准和质量风险管理要求衡量，部分“低风险/建议项”会转化为体系成熟度缺口。",
                "writing_claim": "本章要建立后文共用的法规基线，避免整改优先级只跟随原审计评级，而忽略国际化和商业化标准。",
                "counter_evidence": [
                    "相关国际标准是否并不适用于当前产品阶段或委托生产边界？",
                    "NMPA 与欧盟/美国要求之间是否存在可接受差异？",
                    "是否有企业内部标准已经覆盖但审计材料未呈现？",
                ],
            },
        ),
        (
            ("无菌", "RABS", "First Air", "APS", "灯检"),
            {
                "core_question": "制剂线的主要无菌风险，是硬件布局不足，还是人员干预、首次气流保护、APS 覆盖和灯检标准执行证据不足？",
                "bold_hypothesis": "初始假设：白帆制剂车间硬件基础并非主要短板，真正风险在于关键无菌行为和模拟验证是否能持续证明受控；Phase 2 应重点查 First Air、RABS 干预、APS 场景设计和灯检阳性样品管理。",
                "writing_claim": "本章要把无菌保障从“设施看起来合规”推进到“关键操作和验证证据可被审计接受”。",
                "counter_evidence": [
                    "现场是否已有完整视频复核、APS 覆盖和再培训有效性证据？",
                    "观察到的无菌动作问题是否只是个别人员或单次拍摄偏差？",
                    "灯检和 RABS 风险是否已有 SOP、趋势和复核记录闭环？",
                ],
            },
        ),
        (
            ("原液", "WFI", "SCADA", "EMS"),
            {
                "core_question": "原液和公用系统的风险是否被一次性封闭工艺掩盖，真正缺口在 WFI、SCADA/EMS、离线记录和异常升级证据链？",
                "bold_hypothesis": "初始假设：一次性反应器和封闭转移降低了暴露风险，但不能自动证明系统受控；Phase 2 应验证 WFI 冷却回流、环境/压差报警、SCADA 数据和离线检测记录是否形成完整证据链。",
                "writing_claim": "本章要说明原液与公用系统不是“硬件先进即可”，而是要证明关键状态、报警、数据和异常处理持续受控。",
                "counter_evidence": [
                    "WFI、SCADA/EMS 和离线记录是否已有验证报告与趋势复核？",
                    "一次性系统是否已经充分降低共线和交叉污染风险？",
                    "被指出的公用系统风险是否只是设计建议而非实际偏差？",
                ],
            },
        ),
        (
            ("工艺", "CPP", "CQA", "PPQ", "清洁验证"),
            {
                "core_question": "现有 IND 阶段工艺规程和批记录，距离商业化 PPQ、控制策略和清洁验证所需证据还差在哪里？",
                "bold_hypothesis": "初始假设：白帆目前的工艺文件足以支撑临床阶段执行，但不足以支撑商业化批记录、CPP/CQA 控制、PPQ 和清洁验证闭环；Phase 2 应查明哪些字段、参数和验证证据必须前置补齐。",
                "writing_claim": "本章要把技术转移风险具体化为文件、参数、验证和批记录的硬门槛。",
                "counter_evidence": [
                    "是否已有商业化模板、控制策略或 PPQ 草案未体现在审计材料中？",
                    "当前项目阶段是否尚不需要完整商业化批记录要求？",
                    "清洁验证和工艺验证是否已有主计划覆盖？",
                ],
            },
        ),
        (
            ("偏差", "变更", "CAPA", "数据完整性"),
            {
                "core_question": "白帆的问题是没有质量流程，还是流程之间的事件分类、升级、CAPA 有效性和数据完整性尚未形成运行闭环？",
                "bold_hypothesis": "初始假设：白帆已有偏差、变更和 CAPA 的流程框架，但事件何时启动偏差、何时作为变更、如何证明 CAPA 有效，以及电子/纸质数据如何贯通，仍存在运行机制缺口。",
                "writing_claim": "本章要把质量体系从“有 SOP”推进到“事件能被正确分类、调查、纠正、验证并趋势复核”。",
                "counter_evidence": [
                    "是否有趋势分析、管理评审和 CAPA effectiveness check 证明体系已经闭环？",
                    "个别事件分类问题是否不足以代表体系性缺口？",
                    "电子系统和纸质记录之间是否已有数据完整性控制？",
                ],
            },
        ),
        (
            ("人员", "培训", "质量文化"),
            {
                "core_question": "培训记录齐全是否真的转化为一线无菌行为、偏差判断和质量风险意识？哪些证据能证明培训有效？",
                "bold_hypothesis": "初始假设：白帆不缺培训台账，缺的是把培训结果转化为现场行为的一致性证据；如果 First Air、干预动作、事件判断和灯检执行仍需反复提醒，问题就不是“再培训一次”，而是培训有效性确认和质量文化运行机制不足。",
                "writing_claim": "本章要把人员问题从“有没有培训”改写为“培训是否改变行为、降低风险、形成可复核证据”。",
                "counter_evidence": [
                    "现场抽问、资格确认和再培训记录是否已证明人员理解到位？",
                    "被观察到的行为问题是否只发生在少数岗位或单次演示？",
                    "是否有岗位胜任力矩阵、年度复评和行为观察数据支撑人员能力？",
                ],
            },
        ),
        (
            ("运营", "跨部门", "指标", "review"),
            {
                "core_question": "白帆当前整改和生产准备依赖个人推动，还是已经形成跨部门例会、问题升级、指标看板和管理层复核的运营系统？",
                "bold_hypothesis": "初始假设：运营短板不在于团队不努力，而在于缺少固定节奏和可视化管理系统；如果 owner、关闭证据、升级阈值和管理层 review 不稳定，整改会停留在临时协调，难以支撑商业化节奏。",
                "writing_claim": "本章要说明运营管理是 GMP 风险的放大器：没有节奏、看板和升级机制，技术和质量问题会反复跨部门漂移。",
                "counter_evidence": [
                    "是否已经存在稳定 PMO/例会/看板，只是未进入审计材料？",
                    "短期临时协调是否足以覆盖当前项目阶段，不需要完整运营系统？",
                    "owner、期限和关闭证据是否已经在复盘文件中基本清楚？",
                ],
            },
        ),
        (
            ("团队", "CDMO", "能力矩阵"),
            {
                "core_question": "对标成熟 CDMO，白帆最需要补齐的是人数、岗位能力，还是 QA/MSAT/工程/项目管理之间的角色分工？",
                "bold_hypothesis": "初始假设：白帆的能力缺口不是简单扩编，而是商业化 CDMO 所需的角色矩阵尚未完全成型；Phase 2 应验证 QA 独立性、MSAT 工艺支持、工程保障、生产班组和 PMO 协同能力。",
                "writing_claim": "本章要给出面向商业化的团队能力地图，说明哪些能力必须自建，哪些可外部支持，哪些要通过机制补齐。",
                "counter_evidence": [
                    "现有人员是否已具备商业化经验，只是材料未体现？",
                    "对标 CDMO 是否会高估当前阶段所需组织复杂度？",
                    "是否可通过顾问、外包或客户支持临时补足能力？",
                ],
            },
        ),
        (
            ("整改", "owner", "路线图"),
            {
                "core_question": "哪些整改必须立即完成，哪些属于体系补强，哪些是能力建设？每项如何绑定 owner、关闭证据和复核窗口？",
                "bold_hypothesis": "初始假设：如果整改只按问题清单逐条关闭，会漏掉体系性根因；更有效的路线应分为立即纠偏、90 天体系补强和中长期能力建设三层，并为每层定义关闭证据。",
                "writing_claim": "本章要把诊断转化为可执行 CAPA 组合，而不是泛泛的改进建议。",
                "counter_evidence": [
                    "是否已有整改计划足以覆盖 owner、期限、关闭证据和 QA verification？",
                    "部分整改是否应前移或后移，避免资源过载？",
                    "哪些建议若缺少法规证据，不应被列为强制整改？",
                ],
            },
        ),
        (
            ("管理层", "CAPA", "总表"),
            {
                "core_question": "管理层应通过什么样的 CAPA 总表、法规映射表和复核节奏，持续判断整改是否真正降低风险？",
                "bold_hypothesis": "初始假设：白帆需要的不只是一次性报告，而是一套管理层可追踪的整改仪表盘；否则 CAPA 关闭会变成文件动作，无法证明风险趋势下降和商业化 readiness 提升。",
                "writing_claim": "本章要把报告成果固化成管理层治理工具：CAPA 总表、法规映射、证据包和复核节奏。",
                "counter_evidence": [
                    "现有管理评审或质量例会是否已经能承担这个功能？",
                    "过度表格化是否会增加一线负担而不改善风险？",
                    "哪些指标真正能反映风险降低，而不是制造形式化 KPI？",
                ],
            },
        ),
    ]
    for needles, strategy in strategies:
        if any(needle in title for needle in needles):
            return strategy
    return {
        "core_question": f"本章需要判断“{title}”背后的真实风险、适用边界和整改优先级。",
        "bold_hypothesis": f"初始假设：{title} 不是孤立问题，而是质量体系、工艺证据或运营机制中的一个可验证缺口；Phase 2 必须用材料原文和外部证据判断其严重度。",
        "writing_claim": f"本章要把“{title}”转化为可执行的诊断结论和整改要求。",
        "counter_evidence": [
            "该问题是否已有充分整改或验证证据？",
            "是否只是阶段性限制，而非系统性缺口？",
            "外部标准是否适用于当前业务边界？",
        ],
    }


def build_chapter_planning(
    project_root: Path,
    manifest: dict[str, Any],
    method: ResearchMethod,
    titles: list[str],
    *,
    quota: int,
) -> list[dict[str, Any]]:
    """Build hypothesis-driven chapter plans that become Phase 2 prompt context."""
    lanes = list(method.integrated_lanes or method.task_axes)
    minimum_evidence = _minimum_evidence_for_method(method)
    plans: list[dict[str, Any]] = []
    for idx, title in enumerate(titles, start=1):
        chapter_id = f"ch{idx:02d}"
        material_lines = _material_lines_for_chapter(project_root, manifest, title)
        if not material_lines:
            material_lines = ["未在材料中自动匹配到足够线索；Phase 2 必须先回读全部输入材料并补充原文摘录。"]
        strategy = _strategy_for_chapter(title, method)
        core_question = strategy["core_question"]
        bold_hypothesis = strategy["bold_hypothesis"]
        verification_plan = [
            "先从允许的本地材料提取 2-4 条原文证据，保留出处和上下文。",
            f"再按方法论 evidence lanes 求证：{'；'.join(lanes)}。",
            "每个核心判断至少匹配 2 个独立高质量来源；不足时降级为待验证判断。",
            "主动搜索反方证据、低严重度解释、适用范围限制或替代原因。",
            "输出时把证据、判断、整改/建议和待补证据分开，避免直接写成散文化正文。",
        ]
        counter_evidence = strategy["counter_evidence"]
        writing_claim = strategy["writing_claim"]
        phase2_prompt_context = "\n".join(
            [
                f"章节：{chapter_id} {title}",
                core_question,
                bold_hypothesis,
                "材料起点：",
                *[f"- {line}" for line in material_lines],
                "求证路线：",
                *[f"- {item}" for item in verification_plan],
                "必须寻找的反方/边界：",
                *[f"- {item}" for item in counter_evidence],
                f"写作主张：{writing_claim}",
                f"最低证据要求：{json.dumps(minimum_evidence, ensure_ascii=False)}",
            ]
        )
        plans.append(
            {
                "chapter_id": chapter_id,
                "title": title,
                "suggested_words": quota,
                "core_question": core_question,
                "bold_hypothesis": bold_hypothesis,
                "why_this_matters": "本章用于把 Phase1 的判断转化为 Phase2 可验证命题，并为最终报告保留清晰主线。",
                "material_starting_points": material_lines,
                "evidence_lanes": lanes,
                "verification_plan": verification_plan,
                "counter_evidence_to_seek": counter_evidence,
                "writing_claim": writing_claim,
                "minimum_evidence": minimum_evidence,
                "phase2_prompt_context": phase2_prompt_context,
            }
        )
    return plans


def build_research_brief_payload(
    project_root: Path,
    manifest: dict[str, Any],
    method: ResearchMethod,
    chapter_planning: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
    """Create the file-backed Phase 1 research brief used by task-card generation."""
    axes = list(method.task_axes)
    chapter_planning = chapter_planning or []
    return {
        "version": "0.21-alpha",
        "topic": manifest.get("topic", project_root.name),
        "research_method": method.key,
        "method_name": method.name,
        "work_language": "zh",
        "tone": "事实型、整改导向、面向管理层和质量/生产负责人；避免空泛咨询腔。",
        "central_question": f"如何基于已提供材料和权威法规/最佳实践，系统诊断“{manifest.get('topic', project_root.name)}”并形成可执行整改路线图？",
        "central_thesis": _central_thesis(manifest, method),
        "phase_logic": {
            "phase1": "大胆假设：结合输入材料、访谈信息和初步搜索，定下主基调、章节命题和求证路线。",
            "phase2": "小心求证：worker 只围绕 Phase1 命题收集、验证、证伪和补证，不自行重写研究方向。",
            "phase3": "一致性审校：检查 Phase1 假设与 Phase2 证据是否自洽，指出需要回炉的章节或证据缺口。",
        },
        "phase2_mode": "chapter_integrated",
        "success_criteria": [
            "每个核心判断都能回到用户材料、权威法规、最佳实践或反方证据。",
            "短中长期整改建议必须绑定优先级、责任、关闭证据和复核机制。",
            "章节写作必须先收束主线，再使用 evidence packet；不得按 packet 机械拼贴。",
        ],
        "phase2_inputs": {
            "material_brief_path": "phase1/material_brief.md",
            "framework_path": "phase1/framework.md",
            "research_brief_path": "phase1/research_brief.json",
        },
        "materials": _material_paths(manifest),
        "chapter_planning": chapter_planning,
        "task_planning": {
            "chapter_source": "phase1/framework.md",
            "phase2_mode": "chapter_integrated",
            "axes": axes,
            "required_skills": [
                "deep-research",
                "search-gateway",
                "search-strategy",
                "source-quality",
                "evidence-table",
                "citation-manager",
            ],
            "search_routes_by_axis": {axis: AXIS_ROUTES.get(axis, ["general"]) for axis in axes},
            "axis_prompt_briefs": {axis: _axis_prompt_brief(axis, method) for axis in axes},
            "stop_conditions": [
                "每张任务卡至少形成 3 条可追溯 evidence_items，且不得编造 candidate_sources 以外来源。",
                "关键 claim 不足 2 个独立 Tier 1-2 信源时，必须写入 open_questions 和证据缺口。",
                "必须包含 counter_evidence；找不到反方证据时记录检索路径和限制。",
            ],
            "fragmentation_guard": "并发 worker 只生产 evidence packet；章节主线由 compressed_findings 收束，禁止直接把 packet 堆成正文。",
        },
        "clarification_notes": {
            "requires_user_review": True,
            "questions_source": "phase1/material_brief.md",
            "decision_items": [
                "确认报告用途、受众和脱敏边界。",
                "确认研究方法是否适配当前场景；MECE 只是可选方法之一。",
                "确认任务切分和检索策略是否足以让低成本模型独立执行。",
            ],
        },
    }


def write_research_brief(
    project_root: Path,
    manifest: dict[str, Any] | None = None,
    method: ResearchMethod | None = None,
    chapter_planning: list[dict[str, Any]] | None = None,
) -> tuple[Path, Path]:
    manifest = manifest or load_manifest(project_root)
    method = method or ResearchMethodRegistry().get(manifest.get("research_method"))
    payload = build_research_brief_payload(project_root, manifest, method, chapter_planning=chapter_planning)
    json_path = project_root / "phase1" / "research_brief.json"
    md_path = project_root / "phase1" / "research_brief.md"
    json_path.parent.mkdir(parents=True, exist_ok=True)
    hypothesis_path = project_root / "phase1" / "hypothesis_map.json"
    json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    hypothesis_path.write_text(json.dumps(payload.get("chapter_planning") or [], ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    lines = [
        f"# Phase 1 Research Brief：{payload['topic']}",
        "",
        f"- research_method: {payload['research_method']}",
        f"- work_language: {payload['work_language']}",
        f"- tone: {payload['tone']}",
        f"- phase2_mode: {payload['phase2_mode']}",
        "",
        "## 中心问题",
        "",
        payload["central_question"],
        "",
        "## 主基调 / 大胆假设",
        "",
        payload["central_thesis"],
        "",
        "## Phase 逻辑",
        "",
    ]
    for phase_name, phase_text in payload["phase_logic"].items():
        lines.append(f"- `{phase_name}`：{phase_text}")
    lines.extend([
        "",
        "## 成功标准",
        "",
    ])
    lines.extend(f"- {item}" for item in payload["success_criteria"])
    if payload.get("chapter_planning"):
        lines.extend(["", "## 章节命题与求证计划", ""])
        for item in payload["chapter_planning"]:
            lines.extend(
                [
                    f"### {item['chapter_id']} {item['title']}",
                    "",
                    f"- 核心问题：{item['core_question']}",
                    f"- 大胆假设：{item['bold_hypothesis']}",
                    f"- 写作主张：{item['writing_claim']}",
                    f"- 证据线：{'；'.join(item['evidence_lanes'])}",
                    "- 材料起点：",
                ]
            )
            lines.extend(f"  - {line}" for line in item["material_starting_points"])
            lines.extend(["- 求证计划："])
            lines.extend(f"  - {line}" for line in item["verification_plan"])
            lines.extend([""])
    lines.extend(["", "## 任务切分原则", ""])
    planning = payload["task_planning"]
    lines.append(planning["fragmentation_guard"])
    lines.append("")
    for axis in planning["axes"]:
        routes = "、".join(planning["search_routes_by_axis"].get(axis, []))
        prompt = planning["axis_prompt_briefs"].get(axis, "")
        lines.append(f"- `{axis}`：{prompt} 检索路径：{routes}")
    lines.extend(["", "## 必读 Skills", ""])
    lines.extend(f"- {name}" for name in planning["required_skills"])
    lines.extend(["", "## 停止条件", ""])
    lines.extend(f"- {item}" for item in planning["stop_conditions"])
    lines.append("")
    md_path.write_text("\n".join(lines), encoding="utf-8")
    return md_path, json_path


CHAPTER_TEMPLATES: dict[str, list[str]] = {
    "mckinsey_market": [
        "核心结论先行界定市场机会与约束",
        "临床与真实世界证据决定需求天花板",
        "监管路径和支付环境重塑商业化节奏",
        "竞争格局正在从单点产品转向组合能力",
        "专利与技术壁垒决定长期利润池",
        "中国市场的准入和供给能力形成独立变量",
        "资本市场预期与基本面之间存在可验证偏差",
        "反方证据限定结论边界并提示回撤风险",
        "战略选择应围绕资源约束排序",
        "执行路线图需要把证据缺口转化为行动清单",
    ],
    "gmp_gap_assessment": [
        "监管基线决定整改范围而非企业主观偏好",
        "现状差距需要按法规条款和业务流程双重定位",
        "质量风险分级决定 CAPA 优先级",
        "根因分析质量决定整改能否闭环",
        "CAPA 设计必须绑定责任人、证据和期限",
        "验证计划决定整改是否可被审计接受",
        "供应商和外包管理常是系统性缺口放大器",
        "数据完整性风险需要独立成章处理",
        "实施路线图需要平衡停线风险与合规风险",
        "管理层治理机制决定整改能否持续",
    ],
    "cmc_process_risk": [
        "工艺流程图是识别放大风险的起点",
        "CQA 与 CPP 的映射决定控制策略质量",
        "放大过程的失效模式集中在传质、混合和稳定性",
        "分析方法和放行标准决定证据可信度",
        "技术转移风险来自知识隐性化和现场差异",
        "供应链约束会改变工艺控制边界",
        "偏差和变更管理决定商业化后的韧性",
        "监管沟通策略需要提前固化关键假设",
        "反方证据限定平台工艺可复制性",
        "CMC 路线图需要把风险转化为验证实验",
    ],
    "rd_go_no_go": [
        "科学假设强度决定项目是否值得进入下一阶段",
        "POC 证据需要同时证明有效性和可转化性",
        "安全性窗口决定适应症与人群选择",
        "IP 与 FTO 风险决定商业化自由度",
        "开发路径需要把关键不确定性前置验证",
        "竞争窗口决定速度是否仍有战略价值",
        "CMC 与临床运营能力影响真实可行性",
        "反方证据决定 go/no-go 阈值",
        "投资强度应与证据成熟度匹配",
        "决策门槛需要形成可执行检查表",
    ],
    "management_consulting": [
        "现状诊断需要区分症状、根因和约束条件",
        "能力差距决定组织改进优先级",
        "流程断点揭示跨部门协作成本",
        "治理结构决定决策速度和责任清晰度",
        "运营模型需要匹配战略目标而非照搬标杆",
        "数字化工具只有嵌入流程才产生价值",
        "绩效指标需要避免局部最优",
        "变革阻力本身是方案设计输入",
        "路线图需要把 quick wins 与系统建设分层",
        "落地机制决定咨询建议能否转化为成果",
    ],
    "gmp_quality_operations_diagnosis": [
        "从审计清单到商业化阶段门",
        "用法规基线重新校准整改优先级",
        "制剂无菌保障：从硬件合规到行为受控",
        "原液与公用系统：封闭工艺背后的证据缺口",
        "工艺文件与验证：商业化转移的硬门槛",
        "质量系统闭环：偏差、变更、CAPA 与数据完整性",
        "人员能力：培训有效性比培训记录更关键",
        "运营节奏：从临时协调转向管理系统",
        "团队建设：按 CDMO 能力矩阵补齐角色",
        "整改路线图：立即纠偏、体系补强、能力建设",
        "管理层看板：用 CAPA 总表驱动复核",
    ],
}


def _existing_chapter_titles(project_root: Path) -> list[str]:
    framework_path = project_root / "phase1" / "framework.md"
    if not framework_path.exists():
        return []
    from scripts.runtime.tasks import parse_framework_chapters

    chapters = parse_framework_chapters(framework_path.read_text(encoding="utf-8"))
    return [chapter.title for chapter in chapters if chapter.title]


def render_framework(
    project_root: Path,
    *,
    method_key: str | None = None,
    chapter_count: int = 10,
    preserve_existing_outline: bool = False,
) -> Path:
    manifest = load_manifest(project_root)
    registry = ResearchMethodRegistry()
    method = registry.get(method_key or manifest.get("research_method"))
    if method_key:
        manifest["research_method"] = method.key
    existing_titles = _existing_chapter_titles(project_root) if preserve_existing_outline else []
    titles = existing_titles or CHAPTER_TEMPLATES.get(method.key) or CHAPTER_TEMPLATES["mckinsey_market"]
    chapter_count = max(8, min(15, chapter_count))
    selected = titles[:chapter_count] if not existing_titles else titles
    quota = max(800, int(manifest.get("target_words", 30000)) // len(selected))
    chapter_planning = build_chapter_planning(project_root, manifest, method, selected, quota=quota)
    sections = "\n".join(f"- {item}" for item in method.framework_sections)
    axes = "、".join(method.task_axes)
    material_text = render_material_inventory(manifest.get("material_inventory") or [])
    lines = [
        f"# {manifest.get('report_title') or manifest['topic']}：研究框架",
        "",
        f"research_method: {method.key}",
        f"method_name: {method.name}",
        f"work_language: 中文主写作；检索关键词、证据摘录、source title、raw notes 可保留英文。",
        f"target_words: {manifest.get('target_words', 30000)}",
        "",
        "## 方法选择",
        "",
        f"本项目采用 `{method.key}`，因为其结构原则是：{method.structure_principle}",
        "",
        "框架模块：",
        sections,
        "",
        "Phase 2 任务轴：",
        f"- {axes}",
        "",
        "## 输入材料与使用边界",
        "",
        material_text,
        "",
        "这些材料作为现场问题线索和内部事实起点使用；正式结论仍需结合 NMPA、FDA、EMA、ICH、WHO 等权威法规、指南和最佳实践进行验证。",
        "",
        "## 中心假设",
        "",
        _central_thesis(manifest, method),
        "",
        "Phase1 的职责是大胆假设：基于材料、访谈和初步搜索定下主基调、章节命题和求证路线。Phase2 的职责是小心求证：验证、证伪、补证，而不是重新发明报告方向。Phase3 则检查 Phase1 假设与 Phase2 证据是否自洽。",
        "",
    ]
    for item in chapter_planning:
        lines.extend(
            [
                f"## 第{int(item['chapter_id'][2:])}章 {item['title']}",
                "",
                f"建议字数：约 {item['suggested_words']} 字。",
                f"本章要解决的问题：{item['core_question']}",
                f"大胆假设：{item['bold_hypothesis']}",
                f"写作主张：{item['writing_claim']}",
                f"证据线：{'；'.join(item['evidence_lanes'])}",
                "",
                "材料起点：",
                *[f"- {line}" for line in item["material_starting_points"]],
                "",
                "求证计划：",
                *[f"- {line}" for line in item["verification_plan"]],
                "",
                "必须寻找的反方/边界：",
                *[f"- {line}" for line in item["counter_evidence_to_seek"]],
                "",
                f"最低证据要求：`{json.dumps(item['minimum_evidence'], ensure_ascii=False)}`",
                "",
            ]
        )
    lines.extend(
        [
        "## 暂停点",
        "",
            "请先确认 `phase1/material_brief.md` 的材料解读和访谈问题，再确认本框架后进入 Phase 2。若章节逻辑、方法框架或字数配额需要调整，应先修改本文件。",
            "",
            "确认后运行：`uv run python scripts/dr.py approve <project>`；未批准时 `research` 默认会拒绝推进，可用 `--force` 临时覆盖。",
            "",
        ]
    )
    out = project_root / "phase1" / "framework.md"
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text("\n".join(lines), encoding="utf-8")
    research_brief_md, research_brief_json = write_research_brief(project_root, manifest, method, chapter_planning=chapter_planning)
    manifest["phase1"] = {
        "status": "completed",
        "approved": False,
        "framework_path": "phase1/framework.md",
        "research_brief_path": str(research_brief_md.relative_to(project_root)),
        "research_brief_json_path": str(research_brief_json.relative_to(project_root)),
        "requires_user_interview": True,
        "research_method": method.key,
        "updated_at": utc_now_iso(),
    }
    manifest["updated_at"] = utc_now_iso()
    write_manifest(project_root, manifest)
    return out
