"""共享规则函数 — realtime + maintenance 共用

纯函数，无状态，无外部依赖（仅标准库）。
两个模块各自 import 此文件，互不影响。

规则语义：基于文本模式 + scope 重叠，不依赖 LLM（L0 纯规则）。

调用方：
- realtime.py: on_fact_saved() / periodic_l0()
- maintenance.py: _infer_edges_l0() / _step_promote_universal()
- search_cog.py: on_fact_accessed()

修改规则时需同步验证 test_realtime + test_maintenance + test_cross_project 测试。
"""

from __future__ import annotations

import json
import re


# 通用标准库/知名库前缀 — 三段点分但不算项目特定
_COMMON_LIBS = frozenset({
    "logging", "requests", "asyncio", "pathlib", "typing", "collections",
    "urllib", "http", "json", "os", "sys", "re", "io", "abc",
    "concurrent", "importlib", "unittest", "dataclasses", "functools",
    "itertools", "contextlib", "sqlite3", "subprocess", "threading",
    "multiprocessing", "email", "html", "xml",
    # 常见第三方库
    "flask", "fastapi", "django", "sqlalchemy", "pydantic", "celery",
    "numpy", "pandas", "torch", "pytest", "click", "aiohttp", "httpx",
})


def looks_project_specific(assertion: str) -> bool:
    """L0 规则：检查 assertion 是否包含项目特定路径（不应提升为 universal）

    匹配模式:
    - 路径前缀: src/xxx, lib/xxx, app/xxx, tests/xxx, ./xxx, utils/xxx
    - 裸文件名: xxx.py, xxx.rs, xxx.js 等（无路径前缀但有代码文件扩展名）
    - 模块引用: package.module.class (三段以上点分，排除通用标准库/知名库)

    Returns:
        True if project-specific, False if potentially universal
    """
    # 含路径前缀的文件引用
    if re.search(r'(?:src/|lib/|app/|tests/|utils/|\./)[\w/]+\.\w+', assertion):
        return True
    # 裸文件名（如 db_cog.py, main.rs）— 无路径前缀但有代码扩展名
    if re.search(r'\b[\w-]+\.(?:py|rs|js|ts|go|java|rb|cpp|c|h)\b', assertion):
        return True
    # 含模块引用（三段点分），但排除通用标准库/知名库
    m = re.search(r'(\w+)\.\w+\.\w+', assertion)
    if m:
        root_pkg = m.group(1).lower()
        if root_pkg not in _COMMON_LIBS:
            return True
    return False


def scope_overlap(fact_a: dict, fact_b: dict) -> bool:
    """检查两个 fact 的 scope_files 是否有交集"""
    try:
        files_a = json.loads(fact_a.get("scope_files") or "[]")
        files_b = json.loads(fact_b.get("scope_files") or "[]")
    except (json.JSONDecodeError, TypeError):
        return False
    if not files_a or not files_b:
        return False
    return bool(set(files_a) & set(files_b))


def match_edge_rule(
    prev: dict, curr: dict,
) -> tuple[str, float] | None:
    """检查两个相邻 fact 是否匹配因果规则

    规则:
    1. root_cause → test pass → ("causes", 0.8)
    2. file modified → committed (scope 重叠) → ("leads_to", 0.7)
    3. install/config → test pass → ("supports", 0.6)

    Returns:
        (relation, confidence) 或 None
    """
    prev_text = (prev.get("assertion") or "").lower()
    curr_text = (curr.get("assertion") or "").lower()

    # 规则 1: root_cause → test pass
    if prev.get("category") == "root_cause" and "test" in curr_text and "pass" in curr_text:
        return ("causes", 0.8)

    # 规则 2: file modified → committed（需 scope 重叠）
    if ("modified" in prev_text or "edited" in prev_text) and "committed" in curr_text:
        if scope_overlap(prev, curr):
            return ("leads_to", 0.7)

    # 规则 3: install/config → test pass
    if any(kw in prev_text for kw in ("install", "pip", "配置", "config")) and \
       "pass" in curr_text:
        return ("supports", 0.6)

    return None


def match_triple_rule(
    prev2: dict, prev: dict, curr: dict,
) -> list[tuple[int, int, str, float]] | None:
    """检查三元组 test_failed → modified → test_passed 模式

    Returns:
        [(from_id, to_id, relation, confidence), ...] 或 None
    """
    prev2_text = (prev2.get("assertion") or "").lower()
    prev_text = (prev.get("assertion") or "").lower()
    curr_text = (curr.get("assertion") or "").lower()

    prev2_id = prev2.get("id")
    prev_id = prev.get("id")
    curr_id = curr.get("id")

    if not all([prev2_id, prev_id, curr_id]):
        return None

    if "pass" in curr_text and "test" in curr_text and \
       "fail" in prev2_text and \
       ("modified" in prev_text or "edited" in prev_text):
        if scope_overlap(prev, curr) or scope_overlap(prev2, prev):
            return [
                (prev2_id, prev_id, "causes", 0.7),
                (prev_id, curr_id, "causes", 0.7),
            ]

    return None


# ── Facts→LLM 清洗（所有拼接点共用）────────────────────────

# 模板回声短语 — LLM 可能把模板指令误读为数据
TEMPLATE_ECHO_PHRASES = frozenset({
    "prefer x over y",
    "preferences expressed",
    "hard rules stated",
    "why x was chosen",
    "chose/decided/instead of",
    "must/never/always",
})

_MULTI_SPACE_RE = re.compile(r"\s+")


def sanitize_fact_for_prompt(assertion: str) -> str | None:
    """清洗 fact 文本用于 LLM prompt。返回 None 表示应跳过。

    防止自投毒反馈循环：facts 喂回 LLM → 生成垃圾 → 写入 DB → 再喂回。
    所有 facts→LLM 拼接点必须调用此函数。
    """
    if not assertion:
        return None
    # 1. 截断（限制链式增长）
    assertion = assertion[:300]
    # 2. 嵌套标签检测（链式投毒特征）
    if assertion.count("[preference]") >= 2 or assertion.count("[constraint]") >= 2:
        return None
    # 3. 模板回声过滤
    lower = assertion.lower()
    if any(phrase in lower for phrase in TEMPLATE_ECHO_PHRASES):
        return None
    # 4. 格式噪音清理
    assertion = _MULTI_SPACE_RE.sub(" ", assertion).strip()
    return assertion if len(assertion) >= 10 else None
