"""
RBSP Multi-Scope Resolver.

Provides a unified interface for searching across different scopes
(file, directory, codebase, system).  Aggregates results from multiple
index stores, performs cross-scope deduplication, and merges rankings.

Scopes:
    - FILE:      Single file resolution
    - DIRECTORY: All files in a directory tree
    - CODEBASE:  Entire project/repository
    - SYSTEM:    Cross-project / system-wide search
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Any, Dict, Hashable, List, Optional, Set, Tuple

from rbsp.core.lsh import LSHIndex
from rbsp.query.parser import ParsedQuery, QueryParser
from rbsp.query.ranker import RankedResult, RankingWeights, ResultRanker
from rbsp.query.resonance import ResonanceHit, ResonanceMatcher, SignatureRegistry
from rbsp.query.semantic import SemanticResolver
from rbsp.query.signal import Signal, SignalEncoder


class Scope(Enum):
    """Search scope levels."""
    FILE = auto()
    DIRECTORY = auto()
    CODEBASE = auto()
    SYSTEM = auto()


@dataclass
class ScopeStore:
    """A named index store representing a single scope.

    Attributes:
        name:     Human-readable scope name (e.g., "my-project").
        scope:    The scope level this store covers.
        registry: The SignatureRegistry for this scope.
        lsh_index: Optional LSHIndex for semantic resolution in this scope.
        metadata:  Additional metadata about this scope.
    """
    name: str
    scope: Scope
    registry: SignatureRegistry
    lsh_index: Optional[LSHIndex] = None
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class ResolvedResult:
    """A fully-resolved search result with scope information.

    Attributes:
        item_id:     Unique identifier of the matched item.
        scope_name:  Name of the scope store the result came from.
        scope:       Scope level.
        final_score: Resonance-ranked score in [0.0, 1.0].
        exact_score: Exact match component.
        semantic_score: Semantic similarity component.
        recency_score:  Recency freshness component.
        metadata:    Combined metadata from registry and scope.
    """
    item_id: Hashable
    scope_name: str
    scope: Scope
    final_score: float
    exact_score: float = 0.0
    semantic_score: float = 0.0
    recency_score: float = 0.0
    metadata: Optional[Dict[str, Any]] = None


class MultiScopeResolver:
    """Unified search interface across multiple scopes.

    Manages a collection of ScopeStores and resolves queries across all
    of them (or a filtered subset), deduplicating and merging results.

    Args:
        encoder:   SignalEncoder for converting queries to signals.
        parser:    QueryParser for parsing raw query strings.
        weights:   Ranking weights for result scoring.
    """

    def __init__(
        self,
        encoder: Optional[SignalEncoder] = None,
        parser: Optional[QueryParser] = None,
        weights: Optional[RankingWeights] = None,
    ) -> None:
        self._encoder = encoder or SignalEncoder()
        self._parser = parser or QueryParser()
        self._ranker = ResultRanker(weights=weights)
        self._stores: Dict[str, ScopeStore] = {}

    def add_store(self, store: ScopeStore) -> None:
        """Register a scope store for resolution.

        Args:
            store: The ScopeStore to add.

        Raises:
            ValueError: If a store with the same name already exists.
        """
        if store.name in self._stores:
            raise ValueError(f"Store '{store.name}' already registered")
        self._stores[store.name] = store

    def remove_store(self, name: str) -> bool:
        """Remove a scope store.

        Returns:
            True if the store was found and removed.
        """
        return self._stores.pop(name, None) is not None

    def get_store(self, name: str) -> Optional[ScopeStore]:
        """Retrieve a registered scope store by name."""
        return self._stores.get(name)

    @property
    def store_names(self) -> List[str]:
        """List all registered store names."""
        return list(self._stores.keys())

    def resolve(
        self,
        query: str,
        *,
        scopes: Optional[List[Scope]] = None,
        store_names: Optional[List[str]] = None,
        top_k: int = 20,
        min_score: float = 0.0,
        deduplicate: bool = True,
    ) -> List[ResolvedResult]:
        """Resolve a query across multiple scopes.

        Parses the query, encodes it into a signal, matches against all
        applicable scope stores, and returns a deduplicated, merged result
        list ranked by resonance strength.

        Args:
            query:       Raw query string.
            scopes:      Limit search to these scope levels (None = all).
            store_names: Limit search to these named stores (None = all).
            top_k:       Maximum total results.
            min_score:   Minimum score threshold.
            deduplicate: Remove duplicate items across scopes (default True).

        Returns:
            List of ResolvedResult sorted by descending final_score.
        """
        parsed = self._parser.parse(query)
        signal = self._encoder.encode(parsed.raw_query)

        # Determine which scope from parsed query or parameters
        effective_scopes = scopes
        if parsed.scope is not None and effective_scopes is None:
            scope_map = {
                "file": Scope.FILE,
                "directory": Scope.DIRECTORY,
                "dir": Scope.DIRECTORY,
                "codebase": Scope.CODEBASE,
                "project": Scope.CODEBASE,
                "system": Scope.SYSTEM,
            }
            mapped = scope_map.get(parsed.scope.lower())
            if mapped is not None:
                effective_scopes = [mapped]

        # Select stores
        target_stores = self._select_stores(effective_scopes, store_names)

        # Resolve across all target stores
        all_results: List[ResolvedResult] = []
        for store in target_stores:
            store_results = self._resolve_in_store(
                signal, parsed, store, top_k=top_k, min_score=min_score
            )
            all_results.extend(store_results)

        # Deduplicate: keep highest-scoring entry per item_id
        if deduplicate:
            all_results = self._deduplicate(all_results)

        # Final sort and truncation
        all_results.sort(key=lambda r: r.final_score, reverse=True)

        # Apply post-ranking filters from parsed query
        if parsed.type_filters or parsed.path_filters or parsed.excluded:
            all_results = self._apply_filters(all_results, parsed)

        return all_results[:top_k]

    def resolve_signal(
        self,
        signal: Signal,
        *,
        scopes: Optional[List[Scope]] = None,
        top_k: int = 20,
        min_score: float = 0.0,
    ) -> List[ResolvedResult]:
        """Resolve a pre-encoded Signal across scopes.

        Args:
            signal:  Pre-built Signal object.
            scopes:  Limit to these scope levels.
            top_k:   Maximum results.
            min_score: Minimum score threshold.

        Returns:
            List of ResolvedResult sorted by descending final_score.
        """
        target_stores = self._select_stores(scopes, None)

        all_results: List[ResolvedResult] = []
        for store in target_stores:
            matcher = ResonanceMatcher(
                registry=store.registry,
                lsh_index=store.lsh_index,
            )
            hits = matcher.match(signal, top_k=top_k, min_score=min_score)
            ranked = self._ranker.rank(hits, top_k=top_k)
            for rr in ranked:
                all_results.append(ResolvedResult(
                    item_id=rr.item_id,
                    scope_name=store.name,
                    scope=store.scope,
                    final_score=rr.final_score,
                    exact_score=rr.exact_score,
                    semantic_score=rr.semantic_score,
                    recency_score=rr.recency_score,
                    metadata=rr.metadata,
                ))

        all_results = self._deduplicate(all_results)
        all_results.sort(key=lambda r: r.final_score, reverse=True)
        return all_results[:top_k]

    def _select_stores(
        self,
        scopes: Optional[List[Scope]],
        names: Optional[List[str]],
    ) -> List[ScopeStore]:
        """Select stores matching the given scope and name filters."""
        stores = list(self._stores.values())

        if names is not None:
            name_set = set(names)
            stores = [s for s in stores if s.name in name_set]

        if scopes is not None:
            scope_set = set(scopes)
            stores = [s for s in stores if s.scope in scope_set]

        return stores

    def _resolve_in_store(
        self,
        signal: Signal,
        parsed: ParsedQuery,
        store: ScopeStore,
        *,
        top_k: int,
        min_score: float,
    ) -> List[ResolvedResult]:
        """Run resonance matching within a single store."""
        matcher = ResonanceMatcher(
            registry=store.registry,
            lsh_index=store.lsh_index,
        )

        hits = matcher.match(signal, top_k=top_k, min_score=min_score)

        # Semantic expansion if the query has semantic terms
        if parsed.semantic_terms and store.lsh_index is not None:
            sem_resolver = SemanticResolver(store.lsh_index)
            sem_results = sem_resolver.resolve(signal, top_k=top_k)
            # Merge semantic results as low-exact, high-semantic hits
            for sr in sem_results:
                entry = store.registry.get_item(sr.item_id)
                if entry is not None:
                    sig, meta = entry
                    # Only add if not already in hits
                    existing_ids = {h.item_id for h in hits}
                    if sr.item_id not in existing_ids:
                        hits.append(ResonanceHit(
                            item_id=sr.item_id,
                            signature=sig,
                            resonance_score=sr.similarity * 0.3,
                            exact_score=0.0,
                            semantic_score=sr.similarity,
                            structural_score=0.0,
                            metadata=meta,
                        ))

        ranked = self._ranker.rank(hits, top_k=top_k)

        results: List[ResolvedResult] = []
        for rr in ranked:
            results.append(ResolvedResult(
                item_id=rr.item_id,
                scope_name=store.name,
                scope=store.scope,
                final_score=rr.final_score,
                exact_score=rr.exact_score,
                semantic_score=rr.semantic_score,
                recency_score=rr.recency_score,
                metadata=rr.metadata,
            ))

        return results

    @staticmethod
    def _deduplicate(results: List[ResolvedResult]) -> List[ResolvedResult]:
        """Remove duplicate items, keeping the highest-scoring entry."""
        seen: Dict[Hashable, ResolvedResult] = {}
        for result in results:
            existing = seen.get(result.item_id)
            if existing is None or result.final_score > existing.final_score:
                seen[result.item_id] = result
        return list(seen.values())

    @staticmethod
    def _apply_filters(
        results: List[ResolvedResult],
        parsed: ParsedQuery,
    ) -> List[ResolvedResult]:
        """Apply query-derived filters to results."""
        filtered: List[ResolvedResult] = []
        for result in results:
            meta = result.metadata or {}

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

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

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

            filtered.append(result)

        return filtered
