"""规则引擎 — 工具输出 → 认知事实

设计原则：
1. 快速退出 — 不匹配的工具 0ms 返回
2. 只提取确定性信息 — 不做推理
3. 去重 — 同一 assertion 不重复
"""

from __future__ import annotations

import json
import re
from pathlib import Path


def extract(
    tool_name: str,
    tool_input: dict,
    tool_output: str,
    collection: str,
    session_id: str | None,
) -> list[dict]:
    """规则引擎入口：工具输出 → 基础 facts 列表

    仅处理 Write/Edit/MultiEdit/Bash，其他工具立即返回空列表。
    """
    extractors = {
        "Write": _capture_file_edit,
        "Edit": _capture_file_edit,
        "MultiEdit": _capture_file_edit,
        "Bash": _capture_bash,
    }
    extractor = extractors.get(tool_name)
    if not extractor:
        return []

    return extractor(tool_input, tool_output, collection, session_id)


def _capture_file_edit(
    tool_input: dict,
    tool_output: str,
    collection: str,
    session_id: str | None,
) -> list[dict]:
    """文件编辑 → 深度 diff 解析，提取具体变更内容"""
    facts = []
    file_path = tool_input.get("file_path", "")
    if not file_path:
        return facts

    # 深度 diff 解析：从 old_string/new_string 提取具体变更
    # Write 工具用 content 字段（新建/全量写入），Edit 用 old_string/new_string
    old_string = tool_input.get("old_string", "")
    new_string = tool_input.get("new_string", "") or tool_input.get("content", "")
    detail = _extract_diff_detail(old_string, new_string, file_path)

    if detail:
        assertion = f"{file_path}: {detail}"
    else:
        assertion = f"Modified {file_path}"

    facts.append(_make_fact(
        assertion=assertion,
        category="code",
        collection=collection,
        session_id=session_id,
        scope_files=[file_path],
    ))

    # (#264) 符号信息合并到主 fact 的 scope_entities，不再单独生成 fact
    symbols = _extract_symbols(tool_output, file_path)
    if symbols and facts:
        entities = [name for _, name in symbols]
        facts[0]["scope_entities"] = json.dumps(entities)

    return facts


# ── 通用错误检测 ──

_ERROR_PATTERNS = re.compile(
    r'Traceback \(most recent call last\)|'
    r'^Error:|'
    r'^error\[|'
    r'(?:^|\s)(?:File|Module)NotFoundError:|'
    r'(?:^|\s)(?:TypeError|ValueError|KeyError|AttributeError|ImportError'
    r'|RuntimeError|PermissionError|OSError|IOError|ConnectionError'
    r'|TimeoutError|FileExistsError|NotImplementedError):|'
    r'Permission denied|'
    r'command not found|'
    r'No such file or directory|'
    r'FATAL:|'
    r'panic:',
    re.MULTILINE | re.IGNORECASE,
)


def _is_error_output(output: str) -> bool:
    """检查输出是否包含错误信息"""
    if not output or len(output) < 10:
        return False
    return bool(_ERROR_PATTERNS.search(output))


def _extract_error_summary(output: str, max_len: int = 200) -> str:
    """提取第一行有意义的错误信息"""
    # 优先找 Traceback 的最后一行（实际错误）
    tb_match = re.search(
        r'^(\w+(?:Error|Exception|Warning)\b.+)$',
        output,
        re.MULTILINE,
    )
    if tb_match:
        return tb_match.group(1).strip()[:max_len]

    # 找 "Error:" 或 "error:" 行
    err_match = re.search(
        r'^.*(?:Error|error|FATAL|panic)[:\[].*$',
        output,
        re.MULTILINE,
    )
    if err_match:
        return err_match.group(0).strip()[:max_len]

    # 找 "Permission denied" 等
    for pattern in ("Permission denied", "command not found", "No such file"):
        idx = output.find(pattern)
        if idx >= 0:
            # 取包含该模式的整行
            start = output.rfind("\n", 0, idx) + 1
            end = output.find("\n", idx)
            if end == -1:
                end = len(output)
            return output[start:end].strip()[:max_len]

    return ""


def _capture_bash(
    tool_input: dict,
    tool_output: str,
    collection: str,
    session_id: str | None,
) -> list[dict]:
    """Bash 命令 → 提取包管理、测试结果、迁移、git 操作、错误信息"""
    command = tool_input.get("command", "")
    if not command:
        return []

    facts = []

    # 包管理器操作
    pkg_patterns = [
        (r'pip install\s+(.+?)(?:\s*[;&|]|$)', "Installed Python package: {}"),
        (r'npm install\s+(.+?)(?:\s*[;&|]|$)', "Installed npm package: {}"),
        (r'cargo add\s+(.+?)(?:\s*[;&|]|$)', "Added Rust crate: {}"),
    ]
    for pattern, template in pkg_patterns:
        m = re.search(pattern, command)
        if m:
            pkg_name = m.group(1).strip().split()[0]
            facts.append(_make_fact(
                assertion=template.format(pkg_name),
                category="config",
                collection=collection,
                session_id=session_id,
            ))

    # 测试执行结果
    test_patterns = [
        r'pytest\b', r'jest\b', r'cargo test\b',
        r'go test\b', r'npm test\b', r'python -m pytest\b',
        r'python3 -m pytest\b',
    ]
    if any(re.search(p, command) for p in test_patterns):
        passed = bool(re.search(r'passed|PASSED|ok\b', tool_output))
        # "0 failed" 不算失败
        fail_match = re.search(r'(\d+)\s+(?:failed|FAILED)', tool_output)
        failed = bool(fail_match and int(fail_match.group(1)) > 0) or \
                 bool(re.search(r'\bERROR\b|error:', tool_output))
        if passed or failed:
            status = "passed" if passed and not failed else "failed"
            short_cmd = command.split("|")[0].strip()[:80]
            facts.append(_make_fact(
                assertion=f"Tests {status}: {short_cmd}",
                category="code",
                collection=collection,
                session_id=session_id,
                confidence=1.0,
            ))

    # 数据库迁移
    if re.search(r'migrate|migration|alembic|diesel', command, re.I):
        facts.append(_make_fact(
            assertion=f"Database migration executed: {command[:80]}",
            category="architecture",
            collection=collection,
            session_id=session_id,
            confidence=0.9,
        ))

    # Git commit — 增强：conventional commit 语义解析
    git_match = re.search(r'git commit\s+.*-m\s+["\'](.+?)["\']', command)
    if git_match:
        msg = git_match.group(1)[:100]
        commit_fact = _parse_conventional_commit(msg, collection, session_id)
        if commit_fact:
            facts.append(commit_fact)
        else:
            facts.append(_make_fact(
                assertion=f"Committed: {msg}",
                category="code",
                collection=collection,
                session_id=session_id,
                confidence=1.0,
            ))

    # Git revert 命令
    if re.search(r'git revert\b', command):
        revert_desc = command.strip()[:100]
        facts.append(_make_fact(
            assertion=f"Reverted: {revert_desc}",
            category="decision",
            collection=collection,
            session_id=session_id,
            confidence=1.0,
        ))

    # 通用错误检测（在所有专项规则之后，避免重复）
    if tool_output and _is_error_output(tool_output):
        # 只在没有专项规则已捕获 "failed" 时才触发
        already_captured_failure = any(
            "failed" in f.get("assertion", "").lower()
            or "error" in f.get("assertion", "").lower()
            for f in facts
        )
        if not already_captured_failure:
            error_summary = _extract_error_summary(tool_output)
            if error_summary:
                cmd = command.split("|")[0].strip()[:80]
                facts.append(_make_fact(
                    assertion=f"Command error: `{cmd}` — {error_summary}",
                    category="root_cause",
                    collection=collection,
                    session_id=session_id,
                    confidence=0.85,
                ))

    return facts


# ── Conventional Commit 解析 ──

# 决策型 commit 前缀：这些暗含了 "why"
_DECISION_PREFIXES = {"fix", "refactor", "perf", "revert"}


def _parse_conventional_commit(
    message: str, collection: str, session_id: str | None,
) -> dict | None:
    """解析 conventional commit message，决策型 commit 标记为 category='decision'

    格式: type[(scope)][!]: description
    只有 fix/refactor/perf/revert 被标记为 decision（它们暗含了决策理由）。
    feat/docs/style/test/chore 仍是普通 code 类。
    """
    m = re.match(
        r'^(fix|feat|refactor|perf|revert|docs|style|test|chore)'
        r'(?:\(([^)]+)\))?!?\s*:\s*(.+)',
        message, re.IGNORECASE,
    )
    if not m:
        return None

    commit_type = m.group(1).lower()
    scope = m.group(2) or ""
    description = m.group(3).strip()

    is_decision = commit_type in _DECISION_PREFIXES
    category = "decision" if is_decision else "code"

    context = {"commit_type": commit_type}
    if scope:
        context["scope"] = scope

    return _make_fact(
        assertion=f"Committed: {message[:100]}",
        category=category,
        collection=collection,
        session_id=session_id,
        confidence=1.0,
        context_json=json.dumps(context),
    )


def _extract_symbols(diff_text: str, file_path: str) -> list[tuple[str, str]]:
    """从工具输出中用正则提取被修改的函数/类名"""
    ext = Path(file_path).suffix
    patterns: dict[str, list[tuple[str, str]]] = {
        ".py": [
            (r'(?:async\s+)?def\s+(\w+)', "function"),
            (r'class\s+(\w+)', "class"),
        ],
        ".js": [
            (r'(?:export\s+)?(?:async\s+)?function\s+(\w+)', "function"),
            (r'class\s+(\w+)', "class"),
            (r'(?:const|let|var)\s+(\w+)\s*=', "variable"),
        ],
        ".ts": [
            (r'(?:export\s+)?(?:async\s+)?function\s+(\w+)', "function"),
            (r'class\s+(\w+)', "class"),
            (r'interface\s+(\w+)', "interface"),
            (r'type\s+(\w+)', "type"),
        ],
        ".tsx": [
            (r'(?:export\s+)?(?:async\s+)?function\s+(\w+)', "function"),
            (r'class\s+(\w+)', "class"),
        ],
        ".rs": [
            (r'(?:pub\s+)?fn\s+(\w+)', "function"),
            (r'(?:pub\s+)?struct\s+(\w+)', "struct"),
            (r'(?:pub\s+)?enum\s+(\w+)', "enum"),
            (r'impl\s+(\w+)', "impl"),
        ],
        ".go": [
            (r'func\s+(?:\([^)]+\)\s+)?(\w+)', "function"),
            (r'type\s+(\w+)\s+struct', "struct"),
        ],
    }

    lang_patterns = patterns.get(ext)
    if not lang_patterns:
        return []

    skip_names = {"self", "cls", "main", "test", "__init__", "_"}
    symbols: set[tuple[str, str]] = set()
    for pattern, symbol_type in lang_patterns:
        for m in re.finditer(pattern, diff_text):
            name = m.group(1)
            if name not in skip_names:
                symbols.add((symbol_type, name))

    return list(symbols)


def extract_fix_then_pass(
    session_events: list[dict],
    collection: str,
    session_id: str | None,
) -> list[dict]:
    """从 session 事件序列中检测 fix-then-pass 模式

    序列：测试失败 → 修改文件 → 测试通过 → 产出 root_cause fact。
    session_events 由 HookHandler 维护，每个元素含 type/detail 键。
    """
    if len(session_events) < 3:
        return []

    facts = []
    for i in range(2, len(session_events)):
        ev = session_events[i]
        if ev.get("type") != "test_passed":
            continue

        # 往回找：最近的文件修改和测试失败
        modified_files: list[str] = []
        saw_failure = False

        for j in range(i - 1, max(i - 6, -1), -1):
            prev = session_events[j]
            if prev.get("type") == "file_modified":
                modified_files.append(prev.get("detail", ""))
            elif prev.get("type") == "test_failed":
                saw_failure = True
                break

        if saw_failure and modified_files:
            files_str = ", ".join(modified_files[:3])
            facts.append(_make_fact(
                assertion=f"Fixed test failure by modifying {files_str}",
                category="root_cause",
                collection=collection,
                session_id=session_id,
                confidence=0.8,
                scope_files=modified_files[:3],
            ))

    return facts


# ── Task 提取 ──

_TASK_PATTERNS = [
    re.compile(r'(?:fix|处理|修复)\s*(?:#|issue\s*)?(\d+)', re.IGNORECASE),
    re.compile(r'(?:implement|实现|做)\s+(.+?)(?:\s*[。.!?]|\s{2,}|$)', re.IGNORECASE),
    re.compile(r'(?:add|添加|新增)\s+(.+?)(?:\s*[。.!?]|\s{2,}|$)', re.IGNORECASE),
    re.compile(r'#(\d+)\b'),  # 简写 #80
]


def extract_task_from_prompt(prompt: str) -> str:
    """从用户 prompt 中提取任务关键词

    匹配到 → 返回任务描述字符串。
    不匹配 → 返回空字符串（保持上一个 task，不清空）。
    """
    if not prompt:
        return ""
    # 取 prompt 前 200 字符（任务通常在开头）
    text = prompt[:200]
    for pattern in _TASK_PATTERNS:
        m = pattern.search(text)
        if m:
            task = m.group(0).strip()
            return task[:100]  # 限制长度
    return ""


# ── 深度 Diff 解析 ──

# 噪音过滤集 — 被 _extract_defs / _diff_assignments / _diff_method_calls / _diff_func_calls 共用
_DIFF_SKIP_NAMES = frozenset({
    "self", "cls", "None", "True", "False", "return", "pass",
    "if", "else", "elif", "for", "while", "try", "except",
    "import", "from", "as", "in", "not", "and", "or", "is",
})

# 函数调用专用排除集 — _DIFF_SKIP_NAMES + 定义关键字 + 内置函数
_FUNC_CALL_SKIP = _DIFF_SKIP_NAMES | frozenset({
    "def", "class", "fn", "func", "function", "struct", "enum", "impl",
    "print", "len", "str", "int", "float", "bool", "list", "dict", "set",
    "tuple", "type", "range", "super", "isinstance", "hasattr", "getattr",
})

# (#264) 方法调用排除集 — 常见无信息量的内置方法
_METHOD_CALL_SKIP = _DIFF_SKIP_NAMES | frozenset({
    "get", "set", "append", "extend", "insert", "remove", "pop",
    "strip", "split", "join", "replace", "format", "encode", "decode",
    "keys", "values", "items", "update", "copy", "clear",
    "add", "discard", "lower", "upper", "startswith", "endswith",
    "read", "write", "close", "flush", "seek",
    "debug", "info", "warning", "error", "exception",
})


def _extract_diff_detail(
    old_string: str,
    new_string: str,
    file_path: str,
) -> str:
    """从 Edit 的 old_string/new_string 提取具体变更描述

    纯 L0 正则，不依赖任何模型。返回空字符串表示无法提取。
    """
    if not old_string and not new_string:
        return ""

    # Write 工具：只有 new_string（新建文件 / 全量写入）
    if not old_string and new_string:
        return _summarize_new_content(new_string, file_path)

    parts: list[str] = []

    # 1. 新增的 import
    added_imports = _diff_imports(old_string, new_string)
    if added_imports:
        parts.append(f"import {', '.join(added_imports)}")

    # 2. 新增/删除的函数/类定义
    old_defs = _extract_defs(old_string)
    new_defs = _extract_defs(new_string)
    added_defs = new_defs - old_defs
    removed_defs = old_defs - new_defs
    if added_defs:
        parts.append(f"添加 {', '.join(sorted(added_defs))}")
    if removed_defs:
        parts.append(f"删除 {', '.join(sorted(removed_defs))}")

    # 3. 新增的变量/常量赋值
    added_vars = _diff_assignments(old_string, new_string)
    if added_vars:
        parts.append(f"新增 {', '.join(sorted(added_vars))}")

    # 4. 新增的函数/方法调用（排除已检测到的定义名）
    added_calls = _diff_method_calls(old_string, new_string)
    added_funcs = _diff_func_calls(old_string, new_string) - added_defs - removed_defs
    call_parts = []
    if added_calls:
        call_parts.extend(f".{c}()" for c in sorted(added_calls))
    if added_funcs:
        call_parts.extend(f"{f}()" for f in sorted(added_funcs))
    if call_parts:
        parts.append(f"调用 {', '.join(call_parts)}")

    # 5. 赋值值变化（同名变量，值不同）
    value_changes = _diff_values(old_string, new_string)
    if value_changes:
        parts.extend(value_changes)

    # 6. 新增的字典/列表条目
    new_entries = _diff_entries(old_string, new_string)
    if new_entries:
        parts.append(f"新增条目 {', '.join(new_entries)}")

    if parts:
        return "; ".join(parts)

    # 7. 兜底：如果以上都没匹配，看 diff 大小
    old_lines = old_string.strip().splitlines()
    new_lines = new_string.strip().splitlines()
    added = len(new_lines) - len(old_lines)
    # (#264) 小变更（<5 行）不生成行数描述 — 信息量太低
    if added >= 5:
        return f"增加 {added} 行"
    elif added <= -5:
        return f"减少 {-added} 行"
    return ""


def _summarize_new_content(content: str, file_path: str) -> str:
    """Write 工具：总结新建文件的内容"""
    defs = _extract_defs(content)
    if defs:
        return f"新建，包含 {', '.join(sorted(defs))}"
    lines = len(content.strip().splitlines())
    return f"新建 ({lines} 行)"


def _diff_imports(old: str, new: str) -> list[str]:
    """提取新增的 import 模块名"""
    pat = re.compile(r'^\s*(?:from\s+(\S+)\s+import|import\s+(\S+))', re.MULTILINE)
    old_imports = {(m.group(1) or m.group(2)) for m in pat.finditer(old)}
    new_imports = {(m.group(1) or m.group(2)) for m in pat.finditer(new)}
    return sorted(new_imports - old_imports)


def _extract_defs(text: str) -> set[str]:
    """提取函数/类定义名（Python/Rust/JS/TS/Go）"""
    pat = re.compile(
        r'^\s*(?:export\s+)?(?:async\s+)?'
        r'(?:pub\s+)?'
        r'(?:def|class|fn|struct|enum|impl|function|type)\s+(\w+)',
        re.MULTILINE,
    )
    return {m.group(1) for m in pat.finditer(text)} - _DIFF_SKIP_NAMES


def _diff_assignments(old: str, new: str) -> set[str]:
    """提取新增的顶层变量/常量赋值"""
    pat = re.compile(r'^([A-Z_][A-Z_0-9]*)\s*=', re.MULTILINE)
    old_vars = {m.group(1) for m in pat.finditer(old)}
    new_vars = {m.group(1) for m in pat.finditer(new)}
    return (new_vars - old_vars) - _DIFF_SKIP_NAMES


def _diff_method_calls(old: str, new: str) -> set[str]:
    """提取新增的方法调用（.method() 模式）"""
    pat = re.compile(r'\.(\w+)\(')
    old_calls = {m.group(1) for m in pat.finditer(old)}
    new_calls = {m.group(1) for m in pat.finditer(new)}
    return (new_calls - old_calls) - _METHOD_CALL_SKIP


def _diff_func_calls(old: str, new: str) -> set[str]:
    """提取新增的独立函数调用（func() 模式，排除方法调用和定义）"""
    pat = re.compile(r'(?<![.\w])(\w+)\(')
    old_calls = {m.group(1) for m in pat.finditer(old)}
    new_calls = {m.group(1) for m in pat.finditer(new)}
    return (new_calls - old_calls) - _FUNC_CALL_SKIP


def _diff_values(old: str, new: str) -> list[str]:
    """提取赋值语句的值变化：同名变量，值不同 → name: old → new"""
    pat = re.compile(r'^(\w[\w.]*)\s*=\s*(.+)$', re.MULTILINE)
    old_map: dict[str, str] = {}
    for m in pat.finditer(old):
        old_map[m.group(1)] = m.group(2).strip()
    changes = []
    for m in pat.finditer(new):
        name, new_val = m.group(1), m.group(2).strip()
        old_val = old_map.get(name)
        if old_val is not None and old_val != new_val:
            changes.append(f"{name}: {old_val[:60]} → {new_val[:60]}")
    return changes


def _diff_entries(old: str, new: str) -> list[str]:
    """提取新增的字典条目（key: value, 模式）

    只匹配字典风格的条目：数字键、字符串键、或标识符键 + 逗号结尾。
    排除 def/except/for/class 等 Python 语法行。
    """
    # 匹配: 数字/字符串/标识符 作为 key，后跟冒号和值，行尾有逗号
    pat = re.compile(
        r'^\s+(\d+|"[^"]*"|\'[^\']*\'|\w+)\s*:\s*(.+),\s*$',
        re.MULTILINE,
    )
    old_keys = {m.group(1).strip() for m in pat.finditer(old)}
    entries = []
    for m in pat.finditer(new):
        key = m.group(1).strip()
        val = m.group(2).strip().rstrip(',')
        if key not in old_keys:
            entry = f"{key}: {val}"
            if len(entry) <= 60:
                entries.append(entry)
    return entries[:5]


def _make_fact(
    assertion: str,
    category: str,
    collection: str,
    session_id: str | None,
    confidence: float = 1.0,
    scope_files: list[str] | None = None,
    scope_entities: list[str] | None = None,
    context_json: str = "{}",
) -> dict:
    """构造标准 fact 字典"""
    return {
        "assertion": assertion,
        "category": category,
        "collection": collection,
        "session_id": session_id,
        "scope_files": json.dumps(scope_files or []),
        "scope_modules": "[]",
        "scope_entities": json.dumps(scope_entities or []),
        "confidence": confidence,
        "source": "hook",
        "status": "active",
        "context_json": context_json,
    }
