"""认知记忆搜索 — BM25 + Vec 混合搜索 facts

复用 corpus 搜索的 RRF 融合思路，但专门针对 facts 表。
"""

from __future__ import annotations

import json
import logging
from dataclasses import dataclass, field

from ..embed import Embedder
from .db_cog import CogDatabase
from .rules import looks_project_specific

logger = logging.getLogger(__name__)


@dataclass
class RecallResult:
    """认知搜索结果"""
    facts: list[dict] = field(default_factory=list)
    total: int = 0
    mode: str = "bm25_only"
    query_ms: float = 0.0


async def recall(
    query: str,
    db_cog: CogDatabase,
    embedder: Embedder | None = None,
    collection: str | None = None,
    top_k: int = 5,
    min_confidence: float = 0.3,
    include_universal: bool = True,
) -> RecallResult:
    """搜索认知记忆 — BM25 + Vec 混合

    Args:
        query: 搜索文本
        db_cog: 认知数据库
        embedder: embedding 客户端（可选，用于向量搜索）
        collection: 限定集合
        top_k: 返回数量
        min_confidence: 最低置信度
        include_universal: 是否同时搜索跨项目通用知识（默认 True）

    Returns:
        RecallResult with facts, total, mode
    """
    import time
    t0 = time.perf_counter()

    # collection=None 时搜索所有集合（A1 策略：认知经验不限 collection）

    # BM25 搜索
    bm25_results = db_cog.fts_search_facts(
        query, collection, limit=top_k * 2,
        include_universal=include_universal,
    )

    # 向量搜索（如果 embedder 可用）
    vec_results = []
    mode = "bm25_only"
    if embedder and embedder.available:
        try:
            query_vec = await embedder.embed_one(query)
            if query_vec:
                vec_results = db_cog.vec_search_facts(
                    query_vec, collection, limit=top_k * 2,
                    include_universal=include_universal,
                )
                mode = "hybrid"
        except Exception as e:
            logger.debug("认知向量搜索失败: %s", e)

    # RRF 融合
    if vec_results and bm25_results:
        merged = _rrf_merge(bm25_results, vec_results, k=60)
    elif bm25_results:
        merged = bm25_results
    elif vec_results:
        merged = vec_results
        mode = "vec_only"
    else:
        return RecallResult(query_ms=(time.perf_counter() - t0) * 1000)

    # 非融合路径: 附加 rank-based _rrf_score（浅拷贝，不污染上游）
    # NOTE: 单路分数上限 1/(k+1) ≈ 0.0164，融合路径上限 2/(k+1) ≈ 0.0328，
    # MaxScore 归一化会抹平差异，但如果未来需要跨路径比较请注意这个 trade-off。
    if merged and "_rrf_score" not in merged[0]:
        merged = [
            {**f, "_rrf_score": 1.0 / (60 + rank + 1)}
            for rank, f in enumerate(merged)
        ]

    # 过滤 + 截断
    filtered = [
        f for f in merged
        if f.get("confidence", 0) >= min_confidence
        and f.get("status") == "active"
    ][:top_k]

    # MaxScore 归一化（与 corpus search.py:328 一致）
    # 原地修改安全：merged 已在上游浅拷贝（_rrf_merge 或非融合路径列表推导）
    if filtered:
        max_rrf = filtered[0].get("_rrf_score", 0)
        if max_rrf > 0:
            for f in filtered:
                f["_rrf_score"] = round(f.get("_rrf_score", 0) / max_rrf, 4)

    # 批量 touch（单次事务）+ 逐个 promote 检查（多数内存 early-return）
    fact_ids = [f["id"] for f in filtered if f.get("id")]
    if fact_ids:
        try:
            db_cog.batch_touch_facts(fact_ids)
        except Exception as e:
            logger.debug("batch touch 失败: %s", e)
    for f in filtered:
        try:
            _check_promote_on_access(db_cog, f)
        except Exception as e:
            logger.debug("promote %d 失败: %s", f.get("id"), e)

    elapsed = (time.perf_counter() - t0) * 1000
    return RecallResult(
        facts=filtered,
        total=len(filtered),
        mode=mode,
        query_ms=round(elapsed, 1),
    )


def _rrf_merge(
    bm25: list[dict],
    vec: list[dict],
    k: int = 60,
) -> list[dict]:
    """Reciprocal Rank Fusion 合并两路结果

    返回的每个 fact dict 浅拷贝后附加 ``_rrf_score`` 字段（归一化前的
    原始 RRF 分数），供上层 MaxScore 归一化使用。
    """
    scores: dict[int, float] = {}
    facts_map: dict[int, dict] = {}

    for rank, f in enumerate(bm25):
        fid = f["id"]
        scores[fid] = scores.get(fid, 0) + 1.0 / (k + rank + 1)
        facts_map.setdefault(fid, f)  # 保留先到版本，避免后路覆盖

    for rank, f in enumerate(vec):
        fid = f["id"]
        scores[fid] = scores.get(fid, 0) + 1.0 / (k + rank + 1)
        facts_map.setdefault(fid, f)

    sorted_ids = sorted(scores.keys(), key=lambda x: scores[x], reverse=True)
    # 浅拷贝 + 附加 _rrf_score，不污染上游 dict
    return [{**facts_map[fid], "_rrf_score": scores[fid]} for fid in sorted_ids]


def _check_promote_on_access(db_cog: CogDatabase, fact: dict) -> None:
    """增量 promote 检查 — fact 被搜索命中时调用

    在 touch_fact 递增 access_count 后，检查该 fact 是否满足
    提升为 universal 的条件。与 periodic 批量扫描互补。

    条件: scope='project' + status='active' + confidence ≥ 0.85
          + access_count ≥ 3 + 非项目特定 + scope_files 为空

    策略: 先做所有廉价的内存检查，全部通过后才查 DB 获取实际
    access_count，避免每次都多一次 SELECT。
    """
    # — 廉价检查（内存，无 DB 访问）—
    if fact.get("scope") != "project" or fact.get("status") != "active":
        return
    if (fact.get("confidence") or 0) < 0.85:
        return
    if fact.get("category") not in ("decision", "root_cause", "tradeoff", "architecture"):
        return

    # 检查 scope_files 是否为空
    try:
        scope_files = json.loads(fact.get("scope_files") or "[]")
        if scope_files:
            return
    except (json.JSONDecodeError, TypeError):
        pass

    # 检查是否项目特定
    assertion = fact.get("assertion", "")
    if looks_project_specific(assertion):
        return

    # — 所有廉价检查通过，查 DB 获取实际 access_count —
    fact_id = fact.get("id")
    if not fact_id:
        return
    current = db_cog.get_fact(fact_id)
    if not current or (current.get("access_count") or 0) < 3:
        return

    if db_cog.promote_to_universal(fact_id):
        logger.debug("promote on access: [%d] %s", fact_id, assertion[:60])
    else:
        logger.warning("promote 失败: [%d] %s", fact_id, assertion[:60])
