# -*- coding: utf-8 -*-
"""
术语提取 + 累积词典更新 + 本期热词表生成
=========================================
三层架构:
  A) 规则层(必跑) — 正则抓型号/番号
  B) AI 层(可选) — DeepSeek 通读 A 稿补抓专名并归类
  C) 合并 + 词典累积 + 热词表
"""

import json
import os
import re
import sys
from pathlib import Path
from typing import Dict, List, Set, Tuple, Optional

# ====================================================================
# 统一 LLM 客户端(DeepSeek,OpenAI 兼容协议)
# ====================================================================
from .llm import chat as llm_chat, LLMConfigError, LLM_MODEL


# ====================================================================
# 常量
# ====================================================================

PACKAGE_DIR = Path(__file__).resolve().parent.parent  # doco/src/doco -> doco/src
PROJECT_DIR = PACKAGE_DIR.parent  # doco/
DICT_PATH = PROJECT_DIR / "data" / "term_dict.json"
SEED_PATH = PROJECT_DIR / "data" / "seed_hotwords.txt"

MAX_HOTWORDS = 200
MIN_TERM_LEN = 2
MAX_TERM_LEN = 16

VALID_DOMAINS = {"weapon", "model", "unit_designation", "person", "org", "tech_concept"}


# ====================================================================
# A) 规则层 — 正则
# ====================================================================

RE_MODEL_NUMBER = re.compile(
    r"(?:^|[^\w\u4e00-\u9fff\u3000])"
    r"("
      r"(?:歼|轰|运|直|武直|飞豹|霹雳|红旗|红箭|鹰击|长剑|东风|巨浪|"
      r"卫士|神鹰|暗剑|攻击|无侦|彩虹|翼龙|云影|"
      r"箭|见证者|流星|铁穹|大卫|泥石|爱国者|萨德|标准|飞毛腿|卡桑|霍拉姆沙赫尔|塔米尔|投石索)"
      r"[\u2014\u2013\u002d\u2212\uff0d\-\u00ad\s]*"
      r"(?:\d{1,4}[A-Za-z]?|[\u2160-\u216b]+|[A-Z]+|[IV]+)?"
    r"|"
      r"[A-Z]{1,6}(?:/[A-Z]{1,3})?(?:-\d{1,4}[A-Za-z]*)+(?:/[A-Z]{1,3})?"
    r"|"
      r"\d{2,4}[A-Z]+"
    r")"
    r"(?:$|[^\w\u4e00-\u9fff])"
)

PURE_CN_MODEL_TERMS = [
    "萨德", "铁穹", "塔米尔", "泥石", "卡桑", "飞毛腿", "箭式", "投石索",
    "爱国者", "标准", "霍拉姆沙赫尔",
]

RE_CN_WEAPON = re.compile(
    r"(?:反导|防空|弹道|巡航|高超音速|超高声速)(?:导弹|武器)?"
    r"|(?:中段反导|末端拦截|低空拦截|超低空突防)"
    r"|(?:[A-Z]+\s*)?(?:(?:远程|中程|近程|短程|末端)(?:防空|反导|拦截)(?:系统|导弹)?)"
    r"|(?:相控阵预警雷达|天基预警卫星|陆基远程雷达|红外传感器|低空搜索雷达)"
    r"|(?:动能撞击|动能战斗部|动能拦截弹)"
    r"|(?:无人机蜂群|自杀式无人机|察打一体无人机|攻击无人机)"
    r"|(?:二冲程发动机|惯性制导|GPS模块|诱饵弹|干扰弹|子弹头)"
    r"|(?:电磁干扰|强电磁干扰|复合诱饵欺骗)"
    r"|(?:集成化指挥控制中心|一体化指挥系统)"
    r"|(?:杀伤链|杀伤网|OODA循环|联合火力)"
)

RE_COUNTRY = re.compile(
    r"(?:伊朗|以色列|美国|俄罗斯|中国|朝鲜|韩国|日本|伊拉克|叙利亚|乌克兰|"
    r"黎巴嫩|巴勒斯坦|沙特|阿联酋|卡塔尔|约旦|土耳其|印度|巴基斯坦)"
)

RE_ORG = re.compile(
    r"(?:伊斯兰革命卫队|国防部|五角大楼|北约|欧盟|联合国|"
    r"国会|海军|空军|陆军|火箭军|战略支援部队|"
    r"拉斐尔公司|洛克希德马丁|波音|雷神|诺斯罗普格鲁曼|"
    r"哈马斯|真主党|胡塞武装|塔利班)"
)

RE_PERSON = re.compile(r"(?:主持人|专家)\s*\d+")

_re_json_object = re.compile(r"\{(?:[^{}]|\{[^{}]*\})*\}")


def parse_json_robust(raw: str):
    """
    容错 JSON 解析:
    1) 先尝试标准 json.loads
    2) 失败则用正则提取单个 {term, domain} 对象,逐个解析后拼成 list
    返回 list 或 None(彻底无法解析)
    """
    raw = raw.strip()
    if not raw:
        return None

    # 1) 标准解析
    try:
        return json.loads(raw)
    except json.JSONDecodeError:
        pass

    # 2) 正则提取所有 {...} 对象
    matches = _re_json_object.findall(raw)
    if not matches:
        return None

    items = []
    for m in matches:
        try:
            obj = json.loads(m)
            if isinstance(obj, dict):
                items.append(obj)
        except json.JSONDecodeError:
            continue

    return items if items else None


def extract_rules(text: str) -> List[Dict]:
    """规则层提取。返回: [{term, domain, source}, ...]"""
    candidates: List[Dict] = []

    for term in PURE_CN_MODEL_TERMS:
        if term in text:
            candidates.append({"term": term, "domain": "model", "source": "rule_model_cn"})

    for m in RE_MODEL_NUMBER.finditer(text):
        term = m.group(1).strip()
        if not term or re.match(r"^\d{1,2}$", term):
            continue
        candidates.append({"term": term, "domain": "model", "source": "rule_model"})

    for m in RE_CN_WEAPON.finditer(text):
        term = m.group(0).strip()
        if not term:
            continue
        candidates.append({"term": term, "domain": "weapon", "source": "rule_cn_weapon"})

    for m in RE_COUNTRY.finditer(text):
        candidates.append({"term": m.group(0).strip(), "domain": "org", "source": "rule_country"})

    for m in RE_ORG.finditer(text):
        candidates.append({"term": m.group(0).strip(), "domain": "org", "source": "rule_org"})

    for m in RE_PERSON.finditer(text):
        term = m.group(0).strip().replace("\u00a0", " ").replace("\xa0", " ")
        candidates.append({"term": term, "domain": "person", "source": "rule_person"})

    return candidates


# ====================================================================
# B) AI 层(DeepSeek)
# ====================================================================

LLM_TERM_PROMPT = """你是一名军事科技编辑,请从以下节目文稿中提取专有名词,用于构建术语词典。

任务要求:
1. 提取所有武器/装备名称、型号/番号、单位/部队番号、人物(主持人/专家)、组织机构、技术概念
2. 每条标注 domain:
   - weapon: 武器/装备名称(如"爱国者-3防空反导系统")
   - model: 具体型号/番号(如"F/A-18E/F"、"歼-10"、"SM-3"、"萨德")
   - unit_designation: 单位/部队番号
   - person: 人物(主持人、专家、历史人物)
   - org: 组织机构(国家、军队、公司、政治组织)
   - tech_concept: 技术概念(如"动能撞击"、"饱和攻击")
3. 只返回 JSON 数组,不要任何解释、不要 markdown 代码块包裹、不要多余文字

文稿内容:
---
{text}
---

只返回 JSON:"""


def call_llm(text: str) -> List[Dict]:
    """调 DeepSeek API(通过统一 LLM 客户端)提取术语"""
    model = LLM_MODEL
    print(f"[C1] 调用模型: {model}", file=sys.stderr)
    try:
        raw = llm_chat(
            messages=[{"role": "user", "content": LLM_TERM_PROMPT.format(text=text[:32000])}],
            model=model,
            temperature=0.0,
            max_tokens=4000,
            thinking=False,
        )
    except LLMConfigError as e:
        print(f"[C1 WARNING] LLM 配置异常: {e},回退为仅规则层", file=sys.stderr)
        return []
    except Exception as e:
        # llm.py 已打印原始错误响应体,这里不再吞错
        print(f"[C1 WARNING] DeepSeek API 调用失败,回退为仅规则层", file=sys.stderr)
        return []

    raw = raw.strip()
    if not raw:
        print("[C1 WARNING] DeepSeek 返回空内容,回退为仅规则层", file=sys.stderr)
        return []

    # 去 markdown 包裹
    if raw.startswith("```"):
        lines = raw.splitlines()
        if lines and lines[0].startswith("```"):
            lines = lines[1:]
        if lines and lines[-1].strip() == "```":
            lines = lines[:-1]
        raw = "\n".join(lines)

    # 解析 JSON(容错:截断/格式断裂时用正则提取单个对象)
    items = parse_json_robust(raw)
    if items is None:
        print(f"[C1 WARNING] DeepSeek 返回 JSON 不可解析, 前200字符={raw[:200]}", file=sys.stderr)
        return []
    if not isinstance(items, list):
        print(f"[C1 WARNING] DeepSeek 返回非 JSON 数组(类型={type(items).__name__}),回退为仅规则层", file=sys.stderr)
        return []

    result = []
    for item in items:
        if not isinstance(item, dict):
            continue
        # DeepSeek 有时用 "name" 有时用 "term",两者均兼容
        term = (item.get("term") or item.get("name") or "").strip()
        domain = (item.get("domain") or "").strip().lower()
        if not term or domain not in VALID_DOMAINS:
            continue
        result.append({"term": term, "domain": domain, "source": "ai_deepseek"})

    return result


# ====================================================================
# C) 合并 + 词典累积 + 热词表
# ====================================================================


def load_dict() -> Dict:
    DICT_PATH.parent.mkdir(parents=True, exist_ok=True)
    if DICT_PATH.exists():
        with open(DICT_PATH, "r", encoding="utf-8") as f:
            data = json.load(f)
        if isinstance(data, dict):
            return data
    return {}


def save_dict(data: Dict) -> None:
    DICT_PATH.parent.mkdir(parents=True, exist_ok=True)
    with open(DICT_PATH, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def merge_candidates(rule_candidates: List[Dict], ai_candidates: List[Dict]) -> List[Dict]:
    merged: Dict[str, Dict] = {}
    for c in rule_candidates:
        term = c["term"]
        if term not in merged:
            merged[term] = {"term": term, "domain": c["domain"], "source": c["source"]}
    for c in ai_candidates:
        term = c["term"]
        if term in merged:
            merged[term]["domain"] = c["domain"]
            merged[term]["source"] += "+" + c["source"]
        else:
            merged[term] = {"term": term, "domain": c["domain"], "source": c["source"]}
    return list(merged.values())


def update_dict(dict_data: Dict, merged_terms: List[Dict], episode_id: str) -> Tuple[int, Dict]:
    new_count = 0
    for entry in merged_terms:
        term = entry["term"]
        if term in dict_data:
            existing = dict_data[term]
            if episode_id not in existing.get("episodes", []):
                existing["episodes"].append(episode_id)
            existing["domain"] = entry["domain"]
        else:
            dict_data[term] = {
                "term": term,
                "domain": entry["domain"],
                "episodes": [episode_id],
                "frequency": 1,
            }
            new_count += 1
        dict_data[term]["frequency"] = len(set(dict_data[term].get("episodes", [])))
    return new_count, dict_data


HOTWORD_BLOCKLIST = {
    "主持人", "主持人1", "主持人2", "主持人3", "主持人4", "主持人5",
    "主持人 4", "主持人 5",
    "专家1", "专家2", "专家3", "专家4", "专家5", "专家6", "专家7", "专家8",
    # 清洗1:讯飞本就认得的常见词(只从热词表剔,词典保留)
    "以色列", "伊朗", "伊拉克", "美国", "韩国", "黎巴嫩",
    "国会", "国防部",
    "标准", "防空", "反导",
}

# 清洗2:复合变体后缀白名单
_SUFFIX_WHITELIST = {
    "系统", "导弹", "武器", "拦截弹", "反导系统", "防御系统",
    "防空反导系统", "近程防空系统", "火箭弹", "弹道导弹",
    "自杀式无人机", "无人机", "军事行动", "行动", "模块", "卫星",
}

# 清洗2:限定前缀白名单
_PREFIX_WHITELIST = {"以色列", "伊朗", "黎巴嫩", "民用"}


def _clean_compound_variants(terms: List[str]) -> List[str]:
    """
    清洗2:复合变体收敛。
    A 是 B 的子串时:
    - 若 B = A + 通用后缀 → 丢 B 留 A
    - 若 B = 限定前缀 + A → 丢 B 留 A
    - 否则(A与B差异是型号数字/字母) → 两个都留
    """
    drop: Set[str] = set()
    sorted_terms = sorted(terms, key=len)  # 短的在前
    n = len(sorted_terms)
    for i in range(n):
        a = sorted_terms[i]
        if a in drop:
            continue
        for j in range(i + 1, n):
            b = sorted_terms[j]
            if b in drop:
                continue
            if a not in b:
                continue
            # a 是 b 的子串

            # 规则1: b = a + 白名单后缀
            suffix = b[len(a):]  # b 去掉 a(前缀)后的剩余部分
            if suffix in _SUFFIX_WHITELIST:
                drop.add(b)
                continue

            # 规则2: b = 白名单前缀 + a
            prefix = b[:len(b) - len(a)]  # b 前面的引导部分
            if prefix in _PREFIX_WHITELIST and b[len(prefix):] == a:
                drop.add(b)
                continue

            # 否则两个都留(如 爱国者 vs 爱国者-3)
    return [t for t in terms if t not in drop]


def _merge_near_duplicates(terms: List[str]) -> List[str]:
    """
    清洗3:近重复合并。
    如"真实承诺-4军事行动"和"真实承诺4行动"→合并留一条(短的)。
    策略:
      a) 去标点/空格后完全相同的 → 留最短原文
      b) 去标点后 A 是 B 的子串,且差异部分仅在白名单后缀上 → 丢 B 留 A
    """
    # a) 精确规范匹配
    norm_map: Dict[str, List[str]] = {}  # normalized -> [original...]
    _strip = re.compile(r"[\s\-\u2014\u2013\u002d\u2212\uff0d]")
    for t in terms:
        norm = _strip.sub("", t)
        norm_map.setdefault(norm, []).append(t)

    keep: Set[str] = set()
    drop: Set[str] = set()
    for norm, originals in norm_map.items():
        if len(originals) == 1:
            keep.add(originals[0])
        else:
            shortest = min(originals, key=len)
            keep.add(shortest)

    # b) 模糊子串: 去标点后 A 是 B 的子串,B 比 A 多出的仅是白名单后缀 → 丢 B
    kept_list = [t for t in terms if t in keep]
    for a in sorted(kept_list, key=len):
        if a in drop:
            continue
        a_norm = _strip.sub("", a)
        for b in kept_list:
            if b in drop or b == a:
                continue
            b_norm = _strip.sub("", b)
            if a_norm in b_norm:
                tail = b_norm[len(a_norm):]
                if tail in _SUFFIX_WHITELIST:
                    drop.add(b)

    # c) 核心词匹配: 两者都去掉尾部白名单后缀后,剩余部分相同 → 丢长的
    kept_list2 = [t for t in terms if t in keep and t not in drop]
    for a in sorted(kept_list2, key=len):
        if a in drop:
            continue
        a_norm = _strip.sub("", a)
        a_core = a_norm
        for sfx in sorted(_SUFFIX_WHITELIST, key=len, reverse=True):
            if a_core.endswith(sfx):
                a_core = a_core[:-len(sfx)]
                break
        for b in kept_list2:
            if b in drop or b == a:
                continue
            b_norm = _strip.sub("", b)
            b_core = b_norm
            for sfx in sorted(_SUFFIX_WHITELIST, key=len, reverse=True):
                if b_core.endswith(sfx):
                    b_core = b_core[:-len(sfx)]
                    break
            if a_core == b_core and len(a) < len(b):
                # 都指向同一核心词,留短的
                drop.add(b)

    keep.difference_update(drop)
    return list(keep)


def build_hotwords(dict_data: Dict, episode_id: str) -> Tuple[str, int, int]:
    """
    生成本期热词表(| 分隔,≤200 条)。
    返回 (hotwords_str, 清洗前条数, 清洗后条数)
    """
    episode_terms: List[str] = []
    for term, entry in dict_data.items():
        if episode_id not in entry.get("episodes", []):
            continue
        if len(term) < MIN_TERM_LEN or len(term) > MAX_TERM_LEN:
            continue
        if term in HOTWORD_BLOCKLIST:
            continue
        episode_terms.append(term)

    # 按词频降序 + 同频按字典序
    episode_terms.sort(key=lambda t: (-dict_data[t].get("frequency", 0), t))
    before = len(episode_terms)

    # 清洗2: 复合变体收敛
    episode_terms = _clean_compound_variants(episode_terms)

    # 清洗3: 近重复合并
    episode_terms = _merge_near_duplicates(episode_terms)

    # 清洗后再次排序(可能丢了一些高频词,用剩下列表的词典频率重排)
    episode_terms.sort(key=lambda t: (-dict_data[t].get("frequency", 0), t))
    after = len(episode_terms)

    selected = episode_terms[:MAX_HOTWORDS]
    return "|".join(selected), before, after


def write_hotwords(hotwords: str, episode_id: str) -> Path:
    program_dir = PROJECT_DIR / "programs" / episode_id
    program_dir.mkdir(parents=True, exist_ok=True)
    out = program_dir / "本期热词表.txt"
    out.write_text(hotwords, encoding="utf-8")
    return out


def write_audit_trail(episode_id, rule_candidates, ai_raw, merged) -> Path:
    program_dir = PROJECT_DIR / "programs" / episode_id
    program_dir.mkdir(parents=True, exist_ok=True)
    out = program_dir / "c1_term_candidates.json"
    audit = {
        "episode_id": episode_id,
        "rule_candidates": rule_candidates,
        "ai_raw_response": ai_raw,
        "merged_candidates": merged,
        "stats": {
            "rule_count": len(rule_candidates),
            "ai_count": len(ai_raw),
            "merged_count": len(merged),
        },
    }
    out.write_text(json.dumps(audit, ensure_ascii=False, indent=2), encoding="utf-8")
    return out


# ====================================================================
# 主流程
# ====================================================================


def run_terms(episode_id: str, a_script_path: Path, no_ai: bool = False) -> Dict:
    """C1 术语提取 + 累积词典 + 本期热词表主流程"""
    if hasattr(sys.stdout, "reconfigure"):
        try:
            sys.stdout.reconfigure(encoding="utf-8")
        except Exception:
            pass

    a_script_path = Path(a_script_path)
    if not a_script_path.exists():
        raise FileNotFoundError(f"A 稿文件不存在: {a_script_path}")

    # 支持 .docx 和 .txt
    if a_script_path.suffix.lower() == ".docx":
        from docx import Document
        doc = Document(str(a_script_path))
        parts = []
        for p in doc.paragraphs:
            if p.text.strip():
                parts.append(p.text.strip())
        # 表格内容也提取
        for table in doc.tables:
            for row in table.rows:
                for cell in row.cells:
                    if cell.text.strip():
                        parts.append(cell.text.strip())
        text = "\n".join(parts)
    else:
        text = a_script_path.read_text(encoding="utf-8")

    # 日志打印实际文件名(含扩展名) + 字符数
    print(f"[C1] 实际读取文件: {a_script_path.name}")
    print(f"[C1] 字符数: {len(text)}")

    print("[C1] 规则层提取...")
    rule_candidates = extract_rules(text)
    seen = set()
    rule_dedup = []
    for c in rule_candidates:
        if c["term"] not in seen:
            seen.add(c["term"])
            rule_dedup.append(c)
    print(f"[C1] 规则候选: {len(rule_dedup)} 条")

    ai_candidates: List[Dict] = []
    if no_ai:
        print("[C1] --no-ai,跳过 AI 层")
    else:
        print("[C1] AI 层提取(DeepSeek)...")
        ai_candidates = call_llm(text)
        seen = set()
        ai_dedup = []
        for c in ai_candidates:
            if c["term"] not in seen:
                seen.add(c["term"])
                ai_dedup.append(c)
        ai_candidates = ai_dedup
        print(f"[C1] AI 候选: {len(ai_candidates)} 条")

    merged = merge_candidates(rule_dedup, ai_candidates)
    print(f"[C1] 合并后: {len(merged)} 条")

    print("[C1] 更新累积词典...")
    dict_data = load_dict()
    new_count, dict_data = update_dict(dict_data, merged, episode_id)
    save_dict(dict_data)
    print(f"[C1] 词典入库: 新增 {new_count} 条, 词典共 {len(dict_data)} 条")

    print("[C1] 生成本期热词表...")
    hotwords_str, hw_before, hw_after = build_hotwords(dict_data, episode_id)
    hw_path = write_hotwords(hotwords_str, episode_id)
    hw_count = len(hotwords_str.split("|")) if hotwords_str else 0
    print(f"[C1] 热词清洗: {hw_before} → {hw_after} 条(清洗后 {hw_count} 条)")
    print(f"[C1] 本期热词表: {hw_count} 条 → {hw_path}")

    audit_path = write_audit_trail(episode_id, rule_dedup, ai_candidates, merged)
    print(f"[C1] 留痕: {audit_path}")

    return {
        "rule_count": len(rule_dedup),
        "ai_count": len(ai_candidates),
        "merged_count": len(merged),
        "dict_new_entries": new_count,
        "dict_total": len(dict_data),
        "hotword_count": hw_count,
        "hotwords_path": str(hw_path),
        "audit_path": str(audit_path),
        "dict_path": str(DICT_PATH),
    }