#!/usr/bin/env python3
"""根据 glossary 的核查结果，在 final_zh.md 上做精确的文本替换。

原理：
- build_glossary.py 会给每条术语标 `zh`（正确中文）和 `issue`（有发现问题）
- 本脚本扫描所有 `issue` 非空且 `confidence in {high, medium}` 的条目
- 对这些条目，在正文中把「当前错误译名」替换为「正确译名」
- 同时处理英文拼写错误（例如 Maywavee → Mabwell）

规则：
- 安全第一：只做字面替换，不做上下文改写
- 明确可见：每一处替换都打印出来，方便 diff
- 幂等：多次跑结果一致
- 支持 --dry-run 预览

用法：
    uv run python scripts/apply_glossary.py <project_slug>
    uv run python scripts/apply_glossary.py <project_slug> --dry-run
    uv run python scripts/apply_glossary.py <project_slug> --min-confidence high
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))


@dataclass
class Correction:
    """一条要应用的修正。"""

    kind: str            # "en" (英文拼写) / "zh" (中文译名)
    wrong: str           # 当前文中的错误写法
    correct: str         # 正确写法
    term_key: str        # glossary 里这条的 key（人工 debug 用）
    confidence: str      # high / medium / low
    reason: str          # 为什么要改（从 issue 字段提取）


_ISSUE_WRONG_EN_RE = re.compile(
    r"(?:'|\"|term\s*)([A-Z][A-Za-z0-9_\- ]{2,40})['\"]*\s*(?:为|是)[^。]*?"
    r"(?:拼写错误|拼写有误|应为|正确英文名为|正确为|正确拼写为)",
)
_CORRECT_EN_RE = re.compile(
    r"(?:正确英文名为|正确为|正确拼写为|应为|正确写法为)\s*['\"]?([A-Z][A-Za-z0-9_\- ]{2,40})['\"]?",
)
_WRONG_ZH_RE = re.compile(
    r"current_zh\s*['\"]?([\u4e00-\u9fff][^'\"。，；]{1,40})['\"]?\s*(?:为|是|存在)[^。]*?(?:错误|错译|误译|不准确|应为|应该)",
)


def resolve_project(arg: str) -> Path:
    p = Path(arg)
    if p.is_dir():
        return p
    cand = Path.cwd() / "projects" / arg
    if cand.is_dir():
        return cand
    raise SystemExit(f"project not found: {arg}")


def load_glossary(path: Path) -> dict:
    return json.loads(path.read_text(encoding="utf-8"))


# 中文替换的保守性：只对"公司名、机构名、产品名"类这种具体化的词做替换
# 规避常见缩写词在不同上下文有不同含义的情况（如 PDE = 磷酸二酯酶 OR 允许日暴露量）
_AMBIGUOUS_ABBREVS = {
    "PDE", "ASGPR", "BLA", "IND", "NDA", "CMC", "API", "QC", "QA",
    "ADC", "CRO", "CDMO", "CMO", "GMP", "PK", "PD", "TRL",
    "FDA", "EMA", "NMPA", "ICH", "WHO", "CFDA", "CDE",
    "LNP", "RNP", "AAV", "mRNA", "RNAi", "siRNA", "ASO",
    "DNA", "RNA", "cDNA", "dsRNA", "ssRNA",
    "OTP", "HCV", "HBV", "HPV", "HIV",
}


def _looks_like_proper_noun(term_key: str) -> bool:
    """判断这条术语是否是"专有名词"（公司/机构/产品/药物名）。"""
    # 全大写 2-4 字符缩写 → 视作通用缩写，跳过（歧义风险高）
    if term_key.upper() == term_key and 2 <= len(term_key) <= 5:
        return False
    if term_key in _AMBIGUOUS_ABBREVS:
        return False
    # 其他情况：视作专有名词
    return True


def extract_corrections(glossary: dict, min_conf: str) -> list[Correction]:
    """从 glossary 抽出可应用的修正。

    保守策略：
    - 英文拼写错误（term_key 本身错）：高信度直接修
    - 中文译名错误（issue 里说 current_zh 错）：仅对"专有名词"类（公司/产品/机构）修
    - 缩写/通用术语（PDE/ASGPR 等）：不自动修，避免上下文歧义
    """
    rank = {"high": 3, "medium": 2, "low": 1}
    threshold = rank.get(min_conf, 2)
    corrections: list[Correction] = []

    for term_key, entry in glossary.items():
        if not isinstance(entry, dict):
            continue
        conf = entry.get("confidence", "low")
        if rank.get(conf, 0) < threshold:
            continue
        issue = entry.get("issue", "")
        zh = entry.get("zh", "")
        en_full = entry.get("en_full", "")
        if not issue:
            continue

        # 1. 英文拼写错误：key 本身错了，en_full 是对的
        if (
            en_full
            and term_key != en_full
            and re.search(r"拼写错误|拼写有误|spelled|should be", issue, re.IGNORECASE)
            and term_key[0].isupper()  # 通常是公司/产品名
        ):
            # 排除太短的（容易误伤）或包含空格的原始 key
            if len(term_key) >= 4 and len(en_full) >= 4:
                corrections.append(
                    Correction(
                        kind="en",
                        wrong=term_key,
                        correct=en_full.split("(")[0].strip(),  # 去掉括号内的法人全称
                        term_key=term_key,
                        confidence=conf,
                        reason=issue[:200],
                    )
                )

        # 2. 中文译名错误：只对"专有名词"修（排除缩写/通用术语的歧义风险）
        if not _looks_like_proper_noun(term_key):
            continue
        m = _WRONG_ZH_RE.search(issue)
        if m and zh:
            wrong_zh = m.group(1).strip()
            if wrong_zh != zh and len(wrong_zh) >= 2:
                corrections.append(
                    Correction(
                        kind="zh",
                        wrong=wrong_zh,
                        correct=zh,
                        term_key=term_key,
                        confidence=conf,
                        reason=issue[:200],
                    )
                )

    return corrections


def apply_corrections(text: str, corrections: list[Correction]) -> tuple[str, list[tuple[Correction, int]]]:
    """返回 (新文本, [(correction, 替换次数)])。"""
    results: list[tuple[Correction, int]] = []
    new_text = text
    for c in corrections:
        # 英文术语用 \b 边界；中文直接替换
        if c.kind == "en":
            pattern = r"\b" + re.escape(c.wrong) + r"\b"
            new_text, count = re.subn(pattern, c.correct, new_text)
        else:
            count = new_text.count(c.wrong)
            if count:
                new_text = new_text.replace(c.wrong, c.correct)
        results.append((c, count))
    return new_text, results


def main() -> int:
    parser = argparse.ArgumentParser(description="用 glossary 修正正文术语")
    parser.add_argument("project", help="项目 slug 或路径")
    parser.add_argument(
        "--input", default="phase4/final_zh.md",
        help="待修正的 Markdown",
    )
    parser.add_argument(
        "--output", default=None,
        help="输出路径（默认原地覆盖）",
    )
    parser.add_argument(
        "--glossary", default="phase4/glossary.json",
        help="glossary.json 路径",
    )
    parser.add_argument(
        "--min-confidence",
        choices=["high", "medium", "low"],
        default="medium",
        help="只应用 >= 此置信度的修正（默认 medium）",
    )
    parser.add_argument(
        "--dry-run", action="store_true",
        help="只打印会发生的修改，不实际写入",
    )
    args = parser.parse_args()

    project_root = resolve_project(args.project)
    input_path = project_root / args.input
    output_path = project_root / (args.output or args.input)
    glossary_path = project_root / args.glossary

    if not input_path.exists():
        raise SystemExit(f"输入文件不存在：{input_path}")
    if not glossary_path.exists():
        raise SystemExit(f"glossary 不存在：{glossary_path}")

    glossary = load_glossary(glossary_path)
    corrections = extract_corrections(glossary, args.min_confidence)

    print(f"Project: {project_root.name}")
    print(f"Input:   {input_path.relative_to(project_root)}")
    print(f"Output:  {output_path.relative_to(project_root)}")
    print(f"Glossary: {glossary_path.relative_to(project_root)} ({len(glossary)} terms)")
    print(f"Min confidence: {args.min_confidence}")
    print(f"候选修正: {len(corrections)} 条")
    print()

    if not corrections:
        print("没有需要应用的修正。")
        return 0

    original = input_path.read_text(encoding="utf-8")
    new_text, results = apply_corrections(original, corrections)

    applied = [(c, n) for c, n in results if n > 0]
    skipped = [(c, n) for c, n in results if n == 0]

    print(f"=== 已应用 {len(applied)} 条修正（共替换 {sum(n for _, n in applied)} 处）===")
    for c, n in sorted(applied, key=lambda x: -x[1]):
        marker = "EN" if c.kind == "en" else "ZH"
        print(f"  [{marker}][{c.confidence}] {c.wrong!r} → {c.correct!r}  (×{n})")
        print(f"       原因: {c.reason[:120]}")

    if skipped:
        print(f"\n=== 跳过 {len(skipped)} 条（正文未出现该错误写法）===")
        for c, _ in skipped[:10]:
            marker = "EN" if c.kind == "en" else "ZH"
            print(f"  [{marker}] {c.wrong!r} → {c.correct!r}  (0 hit)")
        if len(skipped) > 10:
            print(f"  …还有 {len(skipped) - 10} 条")

    if args.dry_run:
        print("\n(--dry-run：未写入)")
        return 0

    output_path.write_text(new_text, encoding="utf-8")
    delta = len(new_text) - len(original)
    print(f"\n✓ 已写入 {output_path.relative_to(project_root)}  (字符变化 {delta:+d})")
    return 0


if __name__ == "__main__":
    sys.exit(main())
