"""Reciprocal Rank Fusion (RRF) for combining ranked result lists.

RRF merges several independently-ranked lists (e.g. dense vector results and
sparse BM25 results) without needing comparable score scales: each list
contributes ``weight / (k + rank)`` to a document's fused score. It is robust and
parameter-light, which is why it is the default hybrid fusion here.
"""

from __future__ import annotations

from collections.abc import Sequence

from aop.data.models import SearchHit


def reciprocal_rank_fusion(
    ranked_lists: Sequence[Sequence[SearchHit]],
    *,
    k: int = 60,
    weights: Sequence[float] | None = None,
) -> list[SearchHit]:
    """Fuse ranked lists of hits into one ranking via weighted RRF.

    Args:
        ranked_lists: Lists of hits, each already ordered best-first.
        k: RRF damping constant (larger = flatter rank influence).
        weights: Optional per-list weights (defaults to all 1.0).

    Returns:
        A single list of hits ordered by fused score (descending). Each returned
        hit carries its fused score; the textual content is taken from the first
        list in which the chunk appears.
    """
    if weights is None:
        weights = [1.0] * len(ranked_lists)

    fused_score: dict[str, float] = {}
    representative: dict[str, SearchHit] = {}
    for weight, hits in zip(weights, ranked_lists, strict=True):
        for rank, hit in enumerate(hits):
            fused_score[hit.chunk_id] = fused_score.get(hit.chunk_id, 0.0) + weight / (k + rank + 1)
            representative.setdefault(hit.chunk_id, hit)

    ordered = sorted(fused_score.items(), key=lambda kv: kv[1], reverse=True)
    out: list[SearchHit] = []
    for chunk_id, score in ordered:
        hit = representative[chunk_id]
        out.append(
            SearchHit(
                chunk_id=hit.chunk_id,
                document_id=hit.document_id,
                score=score,
                text=hit.text,
                metadata=hit.metadata,
            )
        )
    return out
