"""Hybrid search combining vector similarity with full-text matching.

Uses Reciprocal Rank Fusion (RRF) to merge results from ChromaDB HNSW
(semantic) with SQLite FTS5 (keyword) for better recall and precision.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any


@dataclass(slots=True)
class ScoredResult:
    """A unified search result with combined score."""
    id: str
    source: str  # "vector", "fts", or "hybrid"
    score: float
    vector_rank: int | None = None
    fts_rank: int | None = None
    data: dict[str, Any] = field(default_factory=dict)


def reciprocal_rank_fusion(
    *result_lists: list[dict[str, Any]],
    id_key: str = "id",
    k: int = 60,
    top_n: int = 20,
) -> list[ScoredResult]:
    """Merge multiple ranked result lists using Reciprocal Rank Fusion.

    RRF score = sum over lists of 1 / (k + rank_i)

    Higher *k* reduces the impact of high-ranking items. Default k=60 is
    standard in literature.

    Args:
        result_lists: Ranked result lists (best first). Each item must have *id_key*.
        id_key: Key to use for deduplication.
        k: RRF constant.
        top_n: Maximum results to return.

    Returns:
        Merged and re-ranked list of ScoredResult.
    """
    scores: dict[str, float] = {}
    data_map: dict[str, dict[str, Any]] = {}
    ranks: dict[str, dict[int, int]] = {}  # id -> {list_idx: rank}

    for list_idx, results in enumerate(result_lists):
        for rank, item in enumerate(results):
            item_id = str(item.get(id_key, rank))
            rrf_score = 1.0 / (k + rank + 1)
            scores[item_id] = scores.get(item_id, 0.0) + rrf_score
            if item_id not in data_map:
                data_map[item_id] = item
            if item_id not in ranks:
                ranks[item_id] = {}
            ranks[item_id][list_idx] = rank

    # Sort by combined score
    sorted_ids = sorted(scores.keys(), key=lambda x: scores[x], reverse=True)

    results = []
    for item_id in sorted_ids[:top_n]:
        item_ranks = ranks.get(item_id, {})
        results.append(ScoredResult(
            id=item_id,
            source="hybrid" if len(item_ranks) > 1 else ("vector" if 0 in item_ranks else "fts"),
            score=scores[item_id],
            vector_rank=item_ranks.get(0),
            fts_rank=item_ranks.get(1),
            data=data_map.get(item_id, {}),
        ))
    return results


def version_weighted_score(
    relevance_score: float,
    is_latest: bool,
    version: int = 1,
    max_version: int = 1,
) -> float:
    """Apply version-based weighting to a relevance score.

    Latest version gets full score; older versions decay.
    score = relevance * version_weight
    """
    if is_latest:
        return relevance_score * 1.0
    if max_version <= 1:
        return relevance_score * 0.7
    # Decay: latest = 1.0, one behind = 0.7, two behind = 0.5, etc.
    age = max_version - version
    weight = max(0.3, 1.0 - (age * 0.15))
    return relevance_score * weight


def scope_aware_code_rank(
    results: list[dict[str, Any]],
    query: str,
) -> list[dict[str, Any]]:
    """Re-rank code search results with scope-aware scoring.

    Rank: exact match > qualified name > substring.
    Boost: public > private.
    """
    q_lower = query.lower()

    def score_key(r: dict[str, Any]) -> tuple[int, int, str]:
        name = (r.get("name") or "").lower()
        qname = (r.get("qualified_name") or "").lower()
        visibility = (r.get("visibility") or "public").lower()

        # Match quality: 0 = exact, 1 = qualified exact, 2 = substring
        if name == q_lower or qname == q_lower:
            match_rank = 0
        elif q_lower in qname:
            match_rank = 1
        else:
            match_rank = 2

        # Visibility: 0 = public/exported, 1 = protected, 2 = private
        vis_rank = {"public": 0, "exported": 0, "protected": 1, "private": 2}.get(visibility, 1)

        return (match_rank, vis_rank, name)

    return sorted(results, key=score_key)
