"""Query Understanding Layer — 查询预处理模块

输入原始查询 → 输出 EnrichedQuery（短语、同义词、路由、权重）
纯函数设计，不依赖 db.py / graph.py / embed.py（符号表通过参数注入）

调用链: 用户查询 → query.py → unified_search.py → search.py
"""

from __future__ import annotations

import re
import time
import unicodedata
from dataclasses import dataclass, field

from .db import tokenize_for_fts5

# ── CJK 检测 ──

_CJK_RE = re.compile(r"[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]")

# ── 关系查询模式 ──

_RELATION_PATTERNS: list[tuple[re.Pattern, str]] = [
    (re.compile(r"谁调用了|who calls|callers? of|调用方", re.IGNORECASE), "callers"),
    (re.compile(r"依赖|depends on|imports?|引用了", re.IGNORECASE), "dependencies"),
    (re.compile(r"影响|affects|used by|被.*调用|引用", re.IGNORECASE), "references"),
]

# ── CamelCase / snake_case 检测 ──

_CAMEL_CASE_RE = re.compile(r"\b[A-Za-z][a-z]+(?:[A-Z][a-z]+)+\b")
_CAMEL_SPLIT_RE = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
_SNAKE_CASE_RE = re.compile(r"[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)+")
_HYPHEN_CASE_RE = re.compile(r"[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)+")

# ── 连续大写词短语（如 "Event Buffer"）──

_CONSECUTIVE_UPPER_RE = re.compile(r"\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b")

# ── 内置同义词词典（base → expansions，expansion 不递归展开）──

_SYNONYMS: dict[str, list[str]] = {
    "审查": ["review"],
    "修复": ["fix", "bugfix"],
    "重构": ["refactor"],
    "优化": ["optimize"],
    "配置": ["config", "configuration"],
    "索引": ["index"],
    "搜索": ["search"],
    "缓存": ["cache"],
    "测试": ["test"],
    "部署": ["deploy"],
    "删除": ["delete", "remove"],
    "添加": ["add"],
    "创建": ["create"],
    "更新": ["update"],
    "迁移": ["migrate", "migration"],
}

# ── 长查询关键签名提取 ──

_ERROR_FILE_RE = re.compile(r"(?:at\s+|in\s+)?(\w+\.(?:py|js|ts|rs|go|java))[:\s]")
_ERROR_LINE_RE = re.compile(r":(\d+)")
_ERROR_TYPE_RE = re.compile(r"\b([A-Z]\w*(?:Error|Exception|Warning|Fault))\b")


# ── 数据结构 ──


@dataclass
class QueryTrace:
    """调试追踪信息 — debug 模式开启时填充"""

    rules_fired: list[str] = field(default_factory=list)
    phrase_source: dict[str, str] = field(default_factory=dict)
    step_timing: dict[str, float] = field(default_factory=dict)


@dataclass
class EnrichedQuery:
    """预处理后的查询对象 — 所有字段为基本类型，支持 asdict() 序列化"""

    original: str
    normalized: str
    phrases: list[str] = field(default_factory=list)
    synonyms: dict[str, list[str]] = field(default_factory=dict)
    token_query: str = ""
    phrase_query: str | None = None
    proximity_query: str | None = None  # FTS5 NEAR() 邻近度查询（#190 P2-4）

    # 意图分解
    entities: list[str] | None = None
    actions: list[str] | None = None

    # 路由决策
    route: str = "text_search"  # text_search | symbol_lookup | graph_query
    symbol_hits: list[str] | None = None

    # 动态权重
    branch_weights: dict[str, float] = field(default_factory=dict)

    # 查询特征标记
    compressed: bool = False
    short_query: bool = False

    # 调试追踪
    trace: QueryTrace | None = None


# ── 主入口 ──


def preprocess_query(
    query: str,
    known_symbols: set[str] | None = None,
    debug: bool = False,
    proximity_max_distance: int = 0,
) -> EnrichedQuery:
    """查询预处理主函数

    处理流程（固定顺序）：
    1. normalize        — NFKC + 空白合并
    2. detect_phrases   — 连续大写 + CamelCase + 下划线/连字符 + 子串吞并
    3. symbol_route     — 查 known_symbols，命中 → route="symbol_lookup"
    4. graph_route      — 正则检测关系查询 → route="graph_query"
    5. long_query       — >80字符 → 提取关键签名
    6. short_query      — <3字符(EN) / <2字符(CJK) → 标记
    7. decompose_intent — 分离 entities(高权重) / actions(低权重)
    8. expand_synonyms  — 内置词典
    9. build_fts5       — 构造 token_query + phrase_query（复用 tokenize_for_fts5）
    10. branch_weights  — 有短语→三分支，无短语→二分支

    Args:
        query: 原始查询文本
        known_symbols: 已索引的符号名集合（函数名/类名），由调用方注入
        debug: 是否填充 trace 信息
    """
    trace = QueryTrace() if debug else None

    # Step 1: 归一化
    t0 = time.monotonic()
    normalized = _normalize(query)
    if trace:
        trace.step_timing["normalize"] = _elapsed_ms(t0)

    enriched = EnrichedQuery(original=query, normalized=normalized, trace=trace)

    # 空查询直接返回
    if not normalized:
        enriched.branch_weights = {"token": 0.5, "vector": 0.5}
        return enriched

    # Step 2: 短语检测
    t0 = time.monotonic()
    phrases, phrase_sources = _detect_phrases(normalized)
    enriched.phrases = phrases
    if trace:
        trace.step_timing["phrase_detect"] = _elapsed_ms(t0)
        trace.phrase_source = phrase_sources

    # Step 3: 关系查询路由（优先于符号路由 — "谁调用了X" 是关系查询不是符号查找）
    t0 = time.monotonic()
    relation = _check_graph_route(normalized)
    if relation:
        enriched.route = "graph_query"
        if trace:
            trace.rules_fired.append(f"graph_query:{relation}")
    if trace:
        trace.step_timing["graph_check"] = _elapsed_ms(t0)

    # Step 4: 符号路由（仅在 graph_query 未命中时）
    if enriched.route != "graph_query" and known_symbols:
        t0 = time.monotonic()
        hits = _check_symbol_route(normalized, known_symbols)
        if hits:
            enriched.route = "symbol_lookup"
            enriched.symbol_hits = hits
            if trace:
                trace.rules_fired.append(f"symbol_lookup:{','.join(hits[:3])}")
        if trace:
            trace.step_timing["symbol_check"] = _elapsed_ms(t0)

    # Step 5: 长查询压缩
    if len(normalized) > 80:
        t0 = time.monotonic()
        compressed = _compress_long_query(normalized)
        if compressed != normalized:
            enriched.compressed = True
            normalized = compressed
            enriched.normalized = compressed
            if trace:
                trace.rules_fired.append("long_query_compress")
                trace.step_timing["long_compress"] = _elapsed_ms(t0)

    # Step 6: 极短查询标记
    stripped = normalized.strip()
    if _CJK_RE.search(stripped):
        # CJK: 去掉空格后 <2 个字符
        cjk_chars = _CJK_RE.findall(stripped)
        if len(cjk_chars) < 2 and len(stripped) < 4:
            enriched.short_query = True
    elif len(stripped) < 3:
        enriched.short_query = True
    if enriched.short_query and trace:
        trace.rules_fired.append("short_query")

    # Step 7: 意图分解（实体 vs 动作）
    t0 = time.monotonic()
    entities, actions = _decompose_intent(normalized, enriched.phrases)
    enriched.entities = entities if entities else None
    enriched.actions = actions if actions else None
    if trace:
        trace.step_timing["decompose"] = _elapsed_ms(t0)

    # Step 8: 同义词扩展
    t0 = time.monotonic()
    enriched.synonyms = _expand_synonyms(normalized)
    if trace:
        for term in enriched.synonyms:
            trace.rules_fired.append(f"synonym_expand:{term}")
        trace.step_timing["synonyms"] = _elapsed_ms(t0)

    # Step 9: 构造 FTS5 查询
    t0 = time.monotonic()
    enriched.token_query = _build_token_query(normalized, enriched.synonyms)
    enriched.phrase_query = _build_phrase_query(enriched.phrases)
    if proximity_max_distance > 0:
        enriched.proximity_query = _build_proximity_query(
            enriched.phrases, proximity_max_distance
        )
    if trace:
        trace.step_timing["build_fts5"] = _elapsed_ms(t0)

    # Step 10: 动态权重
    enriched.branch_weights = _compute_branch_weights(enriched)

    return enriched


# ── 内部实现函数 ──


def _elapsed_ms(t0: float) -> float:
    return round((time.monotonic() - t0) * 1000, 2)


def _normalize(query: str) -> str:
    """归一化：strip + NFKC（全角→半角）+ 多余空白合并"""
    q = query.strip()
    q = unicodedata.normalize("NFKC", q)
    q = re.sub(r"\s+", " ", q)
    return q


def _detect_phrases(text: str) -> tuple[list[str], dict[str, str]]:
    """检测短语，返回 (去重后的短语列表, {短语: 来源})

    检测器：
    1. 连续大写开头词: "Event Buffer", "Search Response"
    2. CamelCase: "EventBuffer" → "event buffer"
    3. snake_case: "safe_db_write" → "safe db write"
    4. hyphen-case: "cross-encoder" → "cross encoder"

    后处理：去重 + 子串吞并
    """
    phrases: dict[str, str] = {}  # phrase → source

    # 1. 连续大写开头词
    for m in _CONSECUTIVE_UPPER_RE.finditer(text):
        phrase = m.group(1)
        phrases[phrase] = "consecutive_uppercase"

    # 2. CamelCase
    for m in _CAMEL_CASE_RE.finditer(text):
        word = m.group(0)
        parts = _CAMEL_SPLIT_RE.split(word)
        if len(parts) >= 2:
            phrase = " ".join(p.lower() for p in parts)
            phrases[phrase] = "camelcase_split"

    # 3. snake_case
    for m in _SNAKE_CASE_RE.finditer(text):
        word = m.group(0)
        phrase = word.replace("_", " ")
        phrases[phrase] = "snake_case_split"

    # 4. hyphen-case
    for m in _HYPHEN_CASE_RE.finditer(text):
        word = m.group(0)
        phrase = word.replace("-", " ")
        phrases[phrase] = "hyphen_case_split"

    # 子串吞并：移除被更长短语包含的子短语
    result = _subsume_phrases(list(phrases.keys()))
    sources = {p: phrases[p] for p in result}
    return result, sources


def _subsume_phrases(phrases: list[str]) -> list[str]:
    """子串吞并：如果短语 A 包含短语 B，移除 B"""
    if len(phrases) <= 1:
        return phrases

    # 按长度降序排列
    sorted_phrases = sorted(phrases, key=len, reverse=True)
    result: list[str] = []

    for phrase in sorted_phrases:
        lower = phrase.lower()
        # 检查是否被已选短语包含
        subsumed = False
        for existing in result:
            if lower in existing.lower() and lower != existing.lower():
                subsumed = True
                break
        if not subsumed:
            result.append(phrase)

    return result


def _check_symbol_route(
    query: str, known_symbols: set[str]
) -> list[str] | None:
    """检查查询是否命中已知符号名

    匹配策略：
    1. 精确匹配（去掉关系查询前缀后）
    2. 查询中包含的符号名
    """
    # 去掉关系查询前缀
    cleaned = re.sub(
        r"^(谁调用了|who calls|callers? of|依赖|depends on|影响|affects|used by)\s*",
        "",
        query,
        flags=re.IGNORECASE,
    ).strip()

    hits: list[str] = []

    # 精确匹配
    if cleaned in known_symbols:
        hits.append(cleaned)
        return hits

    # 查询中包含已知符号（遍历 tokens 查 set，O(n) 而非 O(symbols)）
    tokens = set(re.split(r"[\s,;]+", cleaned))
    for tok in tokens:
        if len(tok) >= 3 and tok in known_symbols:
            hits.append(tok)

    return hits if hits else None


def _check_graph_route(query: str) -> str | None:
    """检测关系查询意图，返回关系类型或 None"""
    for pattern, relation_type in _RELATION_PATTERNS:
        if pattern.search(query):
            return relation_type
    return None


def _compress_long_query(query: str) -> str:
    """长查询（>80字符）压缩 — 提取关键签名

    场景：用户粘贴错误信息作为查询
    提取：文件名 + 错误类型 + 行号
    """
    parts: list[str] = []

    # 提取文件名
    for m in _ERROR_FILE_RE.finditer(query):
        parts.append(m.group(1))

    # 提取错误类型
    for m in _ERROR_TYPE_RE.finditer(query):
        parts.append(m.group(1))

    if parts:
        return " ".join(dict.fromkeys(parts))  # 去重保序
    return query


def _decompose_intent(
    query: str, phrases: list[str]
) -> tuple[list[str], list[str]]:
    """分离实体（高权重）和动作（低权重）

    - 英文标识符/短语 → 实体
    - 中文动词 → 动作
    """
    entities: list[str] = []
    actions: list[str] = []

    # 短语本身是实体
    entities.extend(phrases)

    # 中文动作词
    action_re = re.compile(
        r"(审查|修复|重构|优化|添加|删除|更新|创建|部署|搜索|测试|迁移|修改|实现|设计)"
    )

    remaining = query
    # 移除已识别的短语
    for phrase in phrases:
        remaining = remaining.replace(phrase, " ")

    for m in action_re.finditer(remaining):
        actions.append(m.group(1))

    # 剩余的英文标识符 → 实体
    ident_re = re.compile(r"\b[a-zA-Z_][a-zA-Z0-9_]*\b")
    for m in ident_re.finditer(remaining):
        word = m.group(0)
        if len(word) >= 2 and word.lower() not in {"or", "and", "not", "the", "of", "in", "to", "is", "at", "on"}:
            if word not in entities:
                entities.append(word)

    return entities, actions


def _expand_synonyms(query: str) -> dict[str, list[str]]:
    """同义词扩展（只用内置词典，不递归展开）"""
    result: dict[str, list[str]] = {}

    for term, expansions in _SYNONYMS.items():
        if term in query:
            result[term] = expansions

    return result


def _build_token_query(query: str, synonyms: dict[str, list[str]]) -> str:
    """构造 FTS5 token 查询（含同义词扩展）

    使用 tokenize_for_fts5 保证与索引时分词一致。
    """
    tokenized = tokenize_for_fts5(query)

    # 转义 FTS5 特殊字符
    cleaned = re.sub(r'[(){}*"^~:\-]', " ", tokenized)
    cleaned = re.sub(r"\b(AND|OR|NOT|NEAR)\b", " ", cleaned, flags=re.IGNORECASE)
    tokens = [t for t in cleaned.split() if t.strip()]

    if not tokens:
        return '""'

    # 基础 token 查询
    parts = [f'"{t}"' for t in tokens]

    # 添加同义词 token
    for _term, expansions in synonyms.items():
        for exp in expansions:
            exp_tokenized = tokenize_for_fts5(exp)
            for t in exp_tokenized.split():
                t = t.strip()
                if t and f'"{t}"' not in parts:
                    parts.append(f'"{t}"')

    return " OR ".join(parts)


def _build_phrase_query(phrases: list[str]) -> str | None:
    """构造 FTS5 短语查询

    每个短语用 FTS5 双引号语法包裹（要求 token 相邻匹配）。
    CJK 短语需要过 jieba 分词后再构造。
    多个短语用 OR 连接。
    """
    if not phrases:
        return None

    fts5_phrases: list[str] = []

    for phrase in phrases:
        # 过 tokenize_for_fts5 保证分词一致
        tokenized = tokenize_for_fts5(phrase)
        tokens = tokenized.split()
        if len(tokens) >= 2:
            # FTS5 短语查询: "token1 token2"
            fts5_phrases.append('"' + " ".join(tokens) + '"')
        elif len(tokens) == 1:
            # 单 token 短语没意义，跳过
            continue

    if not fts5_phrases:
        return None

    return " OR ".join(fts5_phrases)


def _build_proximity_query(phrases: list[str], max_distance: int = 5) -> str | None:
    """构造 FTS5 NEAR() 邻近度查询（#190 P2-4）

    与 phrase_query 的区别：phrase 要求 token 严格相邻，
    proximity 允许 token 间最多有 max_distance 个间隔词。
    """
    if not phrases:
        return None

    near_clauses: list[str] = []

    for phrase in phrases:
        tokenized = tokenize_for_fts5(phrase)
        tokens = tokenized.split()
        if len(tokens) >= 2:
            # FTS5 NEAR 语法: NEAR("t1" "t2", distance)
            quoted = " ".join(f'"{t}"' for t in tokens)
            near_clauses.append(f"NEAR({quoted}, {max_distance})")

    if not near_clauses:
        return None

    return " OR ".join(near_clauses)


def _compute_branch_weights(enriched: EnrichedQuery) -> dict[str, float]:
    """根据查询特征计算动态 RRF 权重

    proximity 分支仅在 proximity_query 存在时启用，
    从 phrase 和 vector 各分走一部分权重。
    """
    has_phrases = bool(enriched.phrase_query)
    has_proximity = bool(enriched.proximity_query)
    is_cjk = bool(_CJK_RE.search(enriched.normalized))
    is_short = enriched.short_query

    if has_phrases:
        # 三/四分支：token + phrase + [proximity] + vector
        if is_cjk:
            weights = {"token": 0.15, "phrase": 0.35, "vector": 0.50}
        else:
            weights = {"token": 0.25, "phrase": 0.40, "vector": 0.35}
        if has_proximity:
            # proximity 从 phrase 和 vector 各分 0.05
            weights["phrase"] -= 0.05
            weights["vector"] -= 0.05
            weights["proximity"] = 0.10
    elif is_short:
        # 极短查询：语义信号更有用
        weights = {"token": 0.3, "vector": 0.7}
    elif is_cjk:
        # CJK 无短语：保持现有比例
        weights = {"token": 0.2, "vector": 0.8}
    else:
        # 默认双分支
        weights = {"token": 0.5, "vector": 0.5}

    return weights
