"""
RBSP Result Ranker.

Ranks query results by resonance strength - a weighted combination of:
    - Exact match score:  direct hash equality (content, semantic, structural)
    - Semantic score:     estimated Jaccard similarity from MinHash/LSH
    - Recency score:      inverse age penalty for fresher content

Default resonance formula:
    resonance = 0.6 * exact_match + 0.3 * semantic_score + 0.1 * recency_score

Weights are fully configurable.  Supports top-K retrieval with score
thresholds and optional boosting functions.
"""

from __future__ import annotations

import heapq
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, Hashable, List, Optional

from rbsp.query.resonance import ResonanceHit


@dataclass(frozen=True, slots=True)
class RankedResult:
    """A fully-ranked search result.

    Attributes:
        rank:             1-based position in the result list.
        item_id:          Unique identifier of the matched item.
        final_score:      The computed resonance strength in [0.0, 1.0].
        exact_score:      Exact match component.
        semantic_score:   Semantic similarity component.
        recency_score:    Recency freshness component.
        boost:            Any applied multiplicative boost.
        metadata:         Optional metadata from the registry.
    """
    rank: int
    item_id: Hashable
    final_score: float
    exact_score: float = 0.0
    semantic_score: float = 0.0
    recency_score: float = 0.0
    boost: float = 1.0
    metadata: Optional[Dict[str, Any]] = None


@dataclass
class RankingWeights:
    """Configurable weights for the resonance ranking formula.

    Attributes:
        exact:    Weight for exact hash match (default 0.6).
        semantic: Weight for semantic similarity (default 0.3).
        recency:  Weight for content freshness (default 0.1).
    """
    exact: float = 0.6
    semantic: float = 0.3
    recency: float = 0.1

    def __post_init__(self) -> None:
        total = self.exact + self.semantic + self.recency
        if total <= 0:
            raise ValueError("Weights must sum to a positive value")

    @property
    def total(self) -> float:
        return self.exact + self.semantic + self.recency


class ResultRanker:
    """Ranks resonance matches into a final ordered result list.

    The ranker applies the resonance strength formula to each match,
    optionally applies per-item boosts, and returns a top-K result list
    filtered by a score threshold.

    Args:
        weights:        Ranking weight configuration.
        recency_halflife: Time in seconds after which recency score drops to
                          0.5 (default 86400 = 24 hours).
        score_threshold:  Minimum final_score to include (default 0.0).
        boost_fn:         Optional function (item_id, metadata) -> float boost
                          multiplier.  Applied after scoring.
    """

    def __init__(
        self,
        weights: Optional[RankingWeights] = None,
        recency_halflife: float = 86400.0,
        score_threshold: float = 0.0,
        boost_fn: Optional[Callable[[Hashable, Optional[Dict[str, Any]]], float]] = None,
    ) -> None:
        self.weights = weights or RankingWeights()
        self.recency_halflife = recency_halflife
        self.score_threshold = score_threshold
        self.boost_fn = boost_fn

    def rank(
        self,
        hits: List[ResonanceHit],
        *,
        top_k: int = 10,
        now: Optional[float] = None,
    ) -> List[RankedResult]:
        """Rank resonance hits by resonance strength.

        Args:
            hits:  List of ResonanceHit from the ResonanceMatcher.
            top_k: Maximum results to return.
            now:   Current time override (default: time.time()).

        Returns:
            Ordered list of RankedResult, best match first.
        """
        current_time = now if now is not None else time.time()
        w = self.weights
        w_total = w.total

        scored: List[tuple[float, int, ResonanceHit, float, float]] = []

        for idx, hit in enumerate(hits):
            # Compute recency score via exponential decay
            recency = self._recency_score(hit.signature.timestamp, current_time)

            # Weighted resonance formula
            raw_score = (
                w.exact * hit.exact_score
                + w.semantic * hit.semantic_score
                + w.recency * recency
            ) / w_total

            # Apply boost if configured
            boost = 1.0
            if self.boost_fn is not None:
                boost = self.boost_fn(hit.item_id, hit.metadata)
            final_score = min(raw_score * boost, 1.0)

            if final_score >= self.score_threshold:
                # Use idx for stable sort on equal scores
                scored.append((final_score, idx, hit, recency, boost))

        # Top-K by descending score, stable on insertion order
        scored = heapq.nsmallest(top_k, scored, key=lambda x: (-x[0], x[1]))

        results: List[RankedResult] = []
        for rank_pos, (final_score, _idx, hit, recency, boost) in enumerate(scored, start=1):
            results.append(RankedResult(
                rank=rank_pos,
                item_id=hit.item_id,
                final_score=final_score,
                exact_score=hit.exact_score,
                semantic_score=hit.semantic_score,
                recency_score=recency,
                boost=boost,
                metadata=hit.metadata,
            ))

        return results

    def _recency_score(self, timestamp: float, now: float) -> float:
        """Compute recency score via exponential decay.

        Returns a value in [0.0, 1.0] where 1.0 is brand new and the
        score halves every ``recency_halflife`` seconds.
        """
        age = max(now - timestamp, 0.0)
        if self.recency_halflife <= 0:
            return 1.0 if age == 0 else 0.0
        # Exponential decay: score = 2^(-age / halflife)
        return 2.0 ** (-age / self.recency_halflife)

    def rerank_with_filters(
        self,
        results: List[RankedResult],
        *,
        type_filters: Optional[tuple[str, ...]] = None,
        path_filters: Optional[tuple[str, ...]] = None,
        excluded_terms: Optional[tuple[str, ...]] = None,
    ) -> List[RankedResult]:
        """Apply post-ranking filters and re-number ranks.

        Args:
            results:        The ranked result list.
            type_filters:   Keep only items with matching file extensions.
            path_filters:   Keep only items whose paths start with a filter prefix.
            excluded_terms: Remove items whose metadata 'content' contains excluded terms.

        Returns:
            Filtered and re-ranked list of RankedResult.
        """
        filtered: List[RankedResult] = []
        for result in results:
            meta = result.metadata or {}

            # Type filter
            if type_filters:
                item_type = meta.get("type", "")
                if item_type not in type_filters:
                    continue

            # Path filter
            if path_filters:
                item_path = meta.get("path", "")
                if not any(item_path.startswith(pf) for pf in path_filters):
                    continue

            # Exclusion filter
            if excluded_terms:
                content = meta.get("content", "").lower()
                if any(term in content for term in excluded_terms):
                    continue

            filtered.append(result)

        # Re-number ranks
        renumbered: List[RankedResult] = []
        for new_rank, result in enumerate(filtered, start=1):
            renumbered.append(RankedResult(
                rank=new_rank,
                item_id=result.item_id,
                final_score=result.final_score,
                exact_score=result.exact_score,
                semantic_score=result.semantic_score,
                recency_score=result.recency_score,
                boost=result.boost,
                metadata=result.metadata,
            ))

        return renumbered
