"""L0 Diff 信号解析 + Importance 评分 (#145 Phase B)

纯函数模块，零外部依赖。4 个消费者共用：
1. hooks.py observe() — importance 门控
2. db_cog.py summarize_raw_event() — token bag 生成
3. observer/__init__.py build_tool_prompt() — LLM 结构化上下文
4. session_review — 事件索引摘要

评分表来源：6 轮讨论定稿 + EM-LLM surprise-based 原则。
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field

# ── 预编译正则 ──────────────────────────────────────────────
_RE_DEF = re.compile(
    r'^\s*(?:export\s+)?(?:async\s+)?(?:pub\s+)?'
    r'(?:def|class|fn|struct|enum|impl|function|type)\s+(\w+)',
    re.MULTILINE,
)
_RE_IMPORT = re.compile(r'^\s*(?:from\s+(\S+)\s+import|import\s+(\S+))', re.MULTILINE)
_RE_ASSIGN = re.compile(r'^([A-Z_][A-Z0-9_]*)\s*=', re.MULTILINE)

# Bash 命令分类正则
_RE_BASH_TEST = re.compile(r'\b(pytest|cargo\s+test|npm\s+test|go\s+test|jest|vitest)\b')
_RE_BASH_INSTALL = re.compile(r'\b(pip|pipx|npm|yarn|pnpm|cargo|brew)\s+install\b')
_RE_BASH_GIT = re.compile(r'\bgit\s+(commit|push|pull|merge|rebase|checkout|branch)\b')
_RE_BASH_EXPLORE = re.compile(r'^\s*(ls|cat|head|tail|wc|file|which|echo|pwd)\b')

# ── Importance 阈值 ─────────────────────────────────────────
IMPORTANCE_THRESHOLD = 0.5  # 低于此值不触发 LLM observe


@dataclass
class DiffSignals:
    """Edit/Write 工具的 diff 信号（纯数据，无副作用）"""

    added_functions: list[str] = field(default_factory=list)
    removed_functions: list[str] = field(default_factory=list)
    added_imports: list[str] = field(default_factory=list)
    removed_imports: list[str] = field(default_factory=list)
    added_constants: list[str] = field(default_factory=list)
    changed_identifiers: list[str] = field(default_factory=list)
    is_new_file: bool = False
    file_path: str = ""

    def has_structural_change(self) -> bool:
        """是否有结构性变更（函数/类/import 增删）"""
        return bool(
            self.added_functions or self.removed_functions
            or self.added_imports or self.removed_imports
        )

    def to_dict(self) -> dict:
        """序列化为 JSON-safe dict（用于 signals_json 存储）"""
        d: dict = {}
        if self.added_functions:
            d["added_fn"] = self.added_functions
        if self.removed_functions:
            d["removed_fn"] = self.removed_functions
        if self.added_imports:
            d["added_import"] = self.added_imports
        if self.removed_imports:
            d["removed_import"] = self.removed_imports
        if self.added_constants:
            d["added_const"] = self.added_constants
        if self.changed_identifiers:
            d["changed_ids"] = self.changed_identifiers
        if self.is_new_file:
            d["new_file"] = True
        if self.file_path:
            d["file"] = self.file_path
        return d


def parse_diff_signals(tool_name: str, tool_input: dict) -> DiffSignals:
    """纯函数：从工具输入提取 diff 信号

    支持 Edit/MultiEdit/Write。其他工具返回空 DiffSignals。
    """
    signals = DiffSignals()

    file_path = tool_input.get("file_path", "")
    if file_path:
        signals.file_path = file_path

    if tool_name in ("Edit", "MultiEdit"):
        old = tool_input.get("old_string", "")
        new = tool_input.get("new_string", "")
        if old and new:
            _extract_diff(signals, old, new)
        elif new and not old:
            # MultiEdit 有时只有 new_string
            signals.is_new_file = False
            _extract_new_content(signals, new)
    elif tool_name == "Write":
        content = tool_input.get("content", "")
        if content:
            signals.is_new_file = True
            _extract_new_content(signals, content)

    return signals


def _extract_diff(signals: DiffSignals, old: str, new: str) -> None:
    """从 old/new 提取变更信号"""
    old_defs = set(_RE_DEF.findall(old))
    new_defs = set(_RE_DEF.findall(new))
    signals.added_functions = sorted(new_defs - old_defs)
    signals.removed_functions = sorted(old_defs - new_defs)

    old_imports = {m.group(1) or m.group(2) for m in _RE_IMPORT.finditer(old)}
    new_imports = {m.group(1) or m.group(2) for m in _RE_IMPORT.finditer(new)}
    signals.added_imports = sorted(new_imports - old_imports)
    signals.removed_imports = sorted(old_imports - new_imports)

    old_consts = set(_RE_ASSIGN.findall(old))
    new_consts = set(_RE_ASSIGN.findall(new))
    signals.added_constants = sorted(new_consts - old_consts)

    # changed_identifiers: 所有变更标识符的并集
    all_changed: set[str] = set()
    all_changed.update(signals.added_functions)
    all_changed.update(signals.removed_functions)
    all_changed.update(signals.added_imports)
    all_changed.update(signals.added_constants)
    signals.changed_identifiers = sorted(all_changed)


def _extract_new_content(signals: DiffSignals, content: str) -> None:
    """从新文件内容提取信号"""
    defs = set(_RE_DEF.findall(content[:4000]))
    signals.added_functions = sorted(defs)

    imports = {m.group(1) or m.group(2) for m in _RE_IMPORT.finditer(content[:2000])}
    signals.added_imports = sorted(imports)

    consts = set(_RE_ASSIGN.findall(content[:2000]))
    signals.added_constants = sorted(consts)

    signals.changed_identifiers = sorted(
        set(signals.added_functions) | set(signals.added_imports) | set(signals.added_constants)
    )


# ── Importance 评分 ─────────────────────────────────────────


def score_importance(
    tool_name: str,
    tool_input: dict,
    tool_output: str,
    signals: DiffSignals | None = None,
    *,
    same_file_streak: int = 0,
    prev_was_error: bool = False,
) -> float:
    """L0 纯规则评估事件 importance（0.0 ~ 1.0）

    评分表（6 轮讨论定稿）:
    - error/traceback/fail in output: 0.9
    - test result: 0.8
    - new file creation: 0.7
    - function/class added: 0.6
    - config/dependency change: 0.6
    - edit existing code: 0.4
    - bash install: 0.3
    - bash git: 0.2
    - bash explore: 0.1
    - read/grep/glob: 0.05

    修正因子:
    - 连续同文件编辑: score *= 0.7^(streak-1)
    - 前一事件是错误: score = max(score, 0.7)
    """
    output_head = tool_output[:500].lower()

    # ── 基础分（工具类型 + 输出特征）──
    if tool_name in ("Read", "Grep", "Glob"):
        score = 0.05
    elif tool_name == "Bash":
        score = _score_bash(tool_input, output_head)
    elif tool_name == "Write":
        score = 0.7  # 新文件创建
    elif tool_name in ("Edit", "MultiEdit"):
        score = _score_edit(signals, output_head)
    else:
        score = 0.1  # 未知工具

    # ── 输出特征覆盖（跨工具，error 优先级 > test）──
    # 注意: error 先于 test 检查 — 含 traceback 的测试失败走 error 分支(0.9)
    if _has_error(output_head):
        score = max(score, 0.9)
    elif _has_test_result(output_head):
        score = max(score, 0.8)

    # ── 修正因子 ──
    if same_file_streak > 1:
        score *= 0.7 ** (same_file_streak - 1)

    if prev_was_error:
        score = max(score, 0.7)

    return round(min(1.0, max(0.0, score)), 3)


def _score_bash(tool_input: dict, output_head: str) -> float:
    """Bash 命令细分评分"""
    cmd = tool_input.get("command", "")
    if _RE_BASH_TEST.search(cmd):
        return 0.8
    if _RE_BASH_INSTALL.search(cmd):
        return 0.3
    if _RE_BASH_GIT.search(cmd):
        return 0.2
    if _RE_BASH_EXPLORE.search(cmd):
        return 0.1
    # 默认 Bash: 可能是构建/运行/自定义命令
    return 0.4


def _score_edit(signals: DiffSignals | None, output_head: str) -> float:
    """Edit 评分：有结构性变更更高"""
    if signals and signals.has_structural_change():
        return 0.6
    return 0.4


def _has_error(output_head: str) -> bool:
    """检测输出中是否有错误信号"""
    return any(kw in output_head for kw in (
        "traceback", "exception", "error:", "error ",
        "failed:", "fatal", "panic",
    ))


def _has_test_result(output_head: str) -> bool:
    """检测输出中是否有测试结果"""
    return any(kw in output_head for kw in (
        "passed", "pytest", "test result", "tests run",
    ))
