"""
RBSP Resonance Matcher.

The core matching engine that finds indexed NaturalSignatures resonating
with a query Signal.  Matching is performed via direct hash resolution
(O(1)) rather than linear scanning.

Matching tiers:
    1. Exact match:   direct content_hash lookup in the registry.
    2. Partial match:  semantic_fingerprint lookup in LSH buckets, plus
                       optional bloom filter pre-check.
    3. Structural match: structural_pattern hash lookup.

Each match produces a resonance score that combines exact, semantic, and
structural similarity into a single ranking metric.
"""

from __future__ import annotations

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

from rbsp.core.encoding import NaturalSignature
from rbsp.core.lsh import LSHIndex, MinHasher
from rbsp.query.signal import Signal


@dataclass(frozen=True, slots=True)
class ResonanceHit:
    """A single matching result from resonance matching.

    Attributes:
        item_id:          Unique identifier of the matched item.
        signature:        The NaturalSignature that resonated.
        resonance_score:  Combined resonance strength in [0.0, 1.0].
        exact_score:      Contribution from exact hash matching.
        semantic_score:   Contribution from semantic similarity.
        structural_score: Contribution from structural similarity.
        metadata:         Optional metadata attached to the item.
    """
    item_id: Hashable
    signature: NaturalSignature
    resonance_score: float
    exact_score: float = 0.0
    semantic_score: float = 0.0
    structural_score: float = 0.0
    metadata: Optional[Dict[str, Any]] = None


class BloomFilter:
    """A simple counting bloom filter for fast negative lookups.

    Uses multiple hash functions derived from the RBSP hash to test
    set membership in O(1) with a configurable false-positive rate.

    Args:
        size:       Number of bits in the filter (default 2^16).
        num_hashes: Number of hash functions (default 7).
    """

    def __init__(self, size: int = 65536, num_hashes: int = 7) -> None:
        self._size = size
        self._num_hashes = num_hashes
        self._bits = bytearray(size)

    def _hash_positions(self, key: int) -> List[int]:
        """Compute filter positions for a given key using double hashing."""
        h1 = key & 0xFFFFFFFF
        h2 = (key >> 32) & 0xFFFFFFFF
        if h2 == 0:
            h2 = 0x9E3779B9
        positions = []
        for i in range(self._num_hashes):
            pos = (h1 + i * h2) % self._size
            positions.append(pos)
        return positions

    def add(self, key: int) -> None:
        """Add a key to the bloom filter."""
        for pos in self._hash_positions(key):
            self._bits[pos] = min(self._bits[pos] + 1, 255)

    def remove(self, key: int) -> None:
        """Remove a key from the counting bloom filter."""
        for pos in self._hash_positions(key):
            if self._bits[pos] > 0:
                self._bits[pos] -= 1

    def might_contain(self, key: int) -> bool:
        """Test if a key might be present (no false negatives)."""
        return all(self._bits[pos] > 0 for pos in self._hash_positions(key))


class SignatureRegistry:
    """Registry of indexed NaturalSignatures for O(1) lookup.

    Maintains multiple hash-based indexes for fast resolution:
        - content_hash -> (item_id, signature, metadata)
        - semantic_hash -> set of item_ids
        - structural_hash -> set of item_ids
        - bloom filter for quick negative checks

    Args:
        bloom_size: Bloom filter size in bits (default 2^16).
    """

    def __init__(self, bloom_size: int = 65536) -> None:
        # Primary index: content_hash -> list of (item_id, sig, metadata)
        self._by_content: Dict[int, List[Tuple[Hashable, NaturalSignature, Dict[str, Any]]]] = {}
        # Secondary: semantic_hash -> set of item_ids
        self._by_semantic: Dict[int, Set[Hashable]] = {}
        # Tertiary: structural_hash -> set of item_ids
        self._by_structural: Dict[int, Set[Hashable]] = {}
        # item_id -> (signature, metadata) for full retrieval
        self._items: Dict[Hashable, Tuple[NaturalSignature, Dict[str, Any]]] = {}
        # Bloom filter for fast negative check on content hashes
        self._bloom = BloomFilter(size=bloom_size)

    def register(
        self,
        item_id: Hashable,
        signature: NaturalSignature,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> None:
        """Register an item's signature for lookup.

        Args:
            item_id:   Unique identifier for the item.
            signature: The item's NaturalSignature.
            metadata:  Optional metadata dict to attach.
        """
        meta = metadata or {}
        self._items[item_id] = (signature, meta)

        # Content hash index
        ch = signature.content_hash
        if ch not in self._by_content:
            self._by_content[ch] = []
        self._by_content[ch].append((item_id, signature, meta))
        self._bloom.add(ch)

        # Semantic hash index
        if signature.semantic_hash:
            if signature.semantic_hash not in self._by_semantic:
                self._by_semantic[signature.semantic_hash] = set()
            self._by_semantic[signature.semantic_hash].add(item_id)

        # Structural hash index
        if signature.structural_hash:
            if signature.structural_hash not in self._by_structural:
                self._by_structural[signature.structural_hash] = set()
            self._by_structural[signature.structural_hash].add(item_id)

    def unregister(self, item_id: Hashable) -> bool:
        """Remove an item from the registry.

        Returns:
            True if the item was found and removed, False otherwise.
        """
        entry = self._items.pop(item_id, None)
        if entry is None:
            return False

        sig, _meta = entry

        # Remove from content index
        ch = sig.content_hash
        if ch in self._by_content:
            self._by_content[ch] = [
                e for e in self._by_content[ch] if e[0] != item_id
            ]
            if not self._by_content[ch]:
                del self._by_content[ch]
                self._bloom.remove(ch)

        # Remove from semantic index
        if sig.semantic_hash and sig.semantic_hash in self._by_semantic:
            self._by_semantic[sig.semantic_hash].discard(item_id)
            if not self._by_semantic[sig.semantic_hash]:
                del self._by_semantic[sig.semantic_hash]

        # Remove from structural index
        if sig.structural_hash and sig.structural_hash in self._by_structural:
            self._by_structural[sig.structural_hash].discard(item_id)
            if not self._by_structural[sig.structural_hash]:
                del self._by_structural[sig.structural_hash]

        return True

    def lookup_content(self, content_hash: int) -> List[Tuple[Hashable, NaturalSignature, Dict[str, Any]]]:
        """O(1) lookup by content hash."""
        if not self._bloom.might_contain(content_hash):
            return []
        return self._by_content.get(content_hash, [])

    def lookup_semantic(self, semantic_hash: int) -> Set[Hashable]:
        """O(1) lookup by semantic hash."""
        return self._by_semantic.get(semantic_hash, set())

    def lookup_structural(self, structural_hash: int) -> Set[Hashable]:
        """O(1) lookup by structural hash."""
        return self._by_structural.get(structural_hash, set())

    def get_item(self, item_id: Hashable) -> Optional[Tuple[NaturalSignature, Dict[str, Any]]]:
        """Retrieve an item's signature and metadata by ID."""
        return self._items.get(item_id)

    def __len__(self) -> int:
        return len(self._items)

    def __contains__(self, item_id: Hashable) -> bool:
        return item_id in self._items


class ResonanceMatcher:
    """Core resonance matching engine.

    Given a query Signal and a SignatureRegistry, finds matching signatures
    through a multi-tier resolution strategy:

        1. **Exact match**: O(1) content_hash lookup.
        2. **Semantic match**: O(1) semantic_hash lookup + LSH bucket expansion.
        3. **Structural match**: O(1) structural_hash lookup.

    Results are scored by weighted resonance strength combining all three
    match dimensions.

    Args:
        registry:         The SignatureRegistry to search against.
        lsh_index:        Optional LSHIndex for semantic similarity expansion.
        exact_weight:     Weight for exact hash match score (default 0.6).
        semantic_weight:  Weight for semantic similarity score (default 0.3).
        structural_weight: Weight for structural match score (default 0.1).
    """

    def __init__(
        self,
        registry: SignatureRegistry,
        lsh_index: Optional[LSHIndex] = None,
        exact_weight: float = 0.6,
        semantic_weight: float = 0.3,
        structural_weight: float = 0.1,
    ) -> None:
        self.registry = registry
        self.lsh_index = lsh_index
        self.exact_weight = exact_weight
        self.semantic_weight = semantic_weight
        self.structural_weight = structural_weight
        self._hasher = MinHasher(num_hashes=128)

    def match(
        self,
        signal: Signal,
        *,
        top_k: int = 20,
        min_score: float = 0.0,
    ) -> List[ResonanceHit]:
        """Find all signatures resonating with the given signal.

        Args:
            signal:    The query Signal to match against.
            top_k:     Maximum number of results to return.
            min_score: Minimum resonance score threshold.

        Returns:
            List of ResonanceHit objects sorted by descending resonance_score.
        """
        # Accumulator: item_id -> (exact, semantic, structural) scores
        scores: Dict[Hashable, List[float]] = {}

        # Tier 1: Exact content hash match
        self._match_exact(signal, scores)

        # Tier 2: Semantic match via hash + LSH
        self._match_semantic(signal, scores)

        # Tier 3: Structural match
        self._match_structural(signal, scores)

        # Build ranked results
        results: List[ResonanceHit] = []
        for item_id, (exact, semantic, structural) in scores.items():
            resonance = (
                self.exact_weight * exact
                + self.semantic_weight * semantic
                + self.structural_weight * structural
            )
            if resonance < min_score:
                continue

            entry = self.registry.get_item(item_id)
            if entry is None:
                continue
            sig, meta = entry

            results.append(ResonanceHit(
                item_id=item_id,
                signature=sig,
                resonance_score=resonance,
                exact_score=exact,
                semantic_score=semantic,
                structural_score=structural,
                metadata=meta,
            ))

        return heapq.nlargest(top_k, results, key=lambda h: h.resonance_score)

    def _match_exact(self, signal: Signal, scores: Dict[Hashable, List[float]]) -> None:
        """Tier 1: direct content hash lookup (O(1))."""
        hits = self.registry.lookup_content(signal.content_hash)
        for item_id, _sig, _meta in hits:
            if item_id not in scores:
                scores[item_id] = [0.0, 0.0, 0.0]
            scores[item_id][0] = 1.0  # Perfect exact match

    def _match_semantic(self, signal: Signal, scores: Dict[Hashable, List[float]]) -> None:
        """Tier 2: semantic hash lookup + LSH bucket expansion."""
        # Direct semantic hash match
        if signal.semantic_fingerprint:
            exact_sem_ids = self.registry.lookup_semantic(signal.semantic_fingerprint)
            for item_id in exact_sem_ids:
                if item_id not in scores:
                    scores[item_id] = [0.0, 0.0, 0.0]
                scores[item_id][1] = max(scores[item_id][1], 1.0)

        # LSH expansion for approximate semantic matches
        if self.lsh_index is not None and signal.shingles:
            lsh_hits = self.lsh_index.query(
                signal.shingles,
                top_k=50,
                min_similarity=0.1,
            )
            for item_id, similarity in lsh_hits:
                if item_id not in scores:
                    scores[item_id] = [0.0, 0.0, 0.0]
                scores[item_id][1] = max(scores[item_id][1], similarity)

    def _match_structural(self, signal: Signal, scores: Dict[Hashable, List[float]]) -> None:
        """Tier 3: structural hash lookup (O(1))."""
        if signal.structural_pattern:
            struct_ids = self.registry.lookup_structural(signal.structural_pattern)
            for item_id in struct_ids:
                if item_id not in scores:
                    scores[item_id] = [0.0, 0.0, 0.0]
                scores[item_id][2] = 1.0
