"""
RBSP Signal Encoder.

Converts query text into an RBSP Signal object - a deterministic,
hash-based representation of the query suitable for resonance matching.

A Signal is the query-side counterpart of a NaturalSignature.  It captures
the same three dimensions (content, semantic, structural) so that matching
can be performed via direct hash comparison rather than scanning.
"""

from __future__ import annotations

import time
from dataclasses import dataclass, field
from typing import List, Optional, Sequence, Set

from rbsp.core.encoding import NaturalSignature, compute_size_class
from rbsp.core.lsh import MinHasher, ngram_shingles, word_shingles
from rbsp.core.signature import rbsp_hash, rbsp_hash_text, structural_hash


_MASK64 = 0xFFFFFFFFFFFFFFFF
_MUL1 = 0x9E3779B97F4A7C15
_MUL2 = 0xBF58476D1CE4E5B9
_MUL3 = 0x94D049BB133111EB


def _rotl64(x: int, r: int) -> int:
    """64-bit left rotation."""
    return ((x << r) | (x >> (64 - r))) & _MASK64


def _fold_minhash(minhash: Sequence[int]) -> int:
    """Fold a MinHash signature vector into a single 64-bit integer."""
    h: int = 0
    for v in minhash:
        h ^= v
        h = _rotl64(h, 5)
        h = (h * _MUL1) & _MASK64
    h ^= h >> 30
    h = (h * _MUL2) & _MASK64
    h ^= h >> 27
    h = (h * _MUL3) & _MASK64
    h ^= h >> 31
    return h


@dataclass(frozen=True, slots=True)
class Signal:
    """Immutable representation of an RBSP query signal.

    A signal is a deterministic transformation of query text into a set of
    hashes and fingerprints that enable O(1) resonance matching against
    indexed NaturalSignatures.

    Attributes:
        content_hash:         64-bit RBSP hash of the normalized query text.
        semantic_fingerprint: 64-bit folded MinHash of the query's n-gram shingles.
        structural_pattern:   64-bit structural hash of the query text.
        query_tokens:         Lowercased tokens extracted from the query.
        minhash_signature:    Full MinHash vector for fine-grained similarity.
        shingles:             N-gram shingle set for LSH bucket lookup.
        timestamp:            Signal creation time (POSIX float).
        raw_query:            Original query text before normalization.
    """

    content_hash: int
    semantic_fingerprint: int
    structural_pattern: int
    query_tokens: tuple[str, ...]
    minhash_signature: tuple[int, ...]
    shingles: frozenset[int]
    timestamp: float
    raw_query: str

    def matches_content(self, signature: NaturalSignature) -> bool:
        """Check if the signal has an exact content hash match."""
        return self.content_hash == signature.content_hash

    def matches_semantic(self, signature: NaturalSignature) -> bool:
        """Check if the signal has an exact semantic hash match."""
        return (
            self.semantic_fingerprint != 0
            and signature.semantic_hash != 0
            and self.semantic_fingerprint == signature.semantic_hash
        )

    def matches_structural(self, signature: NaturalSignature) -> bool:
        """Check if the signal has an exact structural hash match."""
        return (
            self.structural_pattern != 0
            and signature.structural_hash != 0
            and self.structural_pattern == signature.structural_hash
        )


class SignalEncoder:
    """Encodes query text into RBSP Signal objects.

    The encoder applies the same hashing pipeline used for content indexing
    (RBSP hash, MinHash / LSH, structural hash) to query text, producing a
    Signal that can be matched against indexed NaturalSignatures in O(1).

    Args:
        ngram_size: Character n-gram size for shingling (default 3).
        num_hashes: MinHash permutation count (default 128).
        word_ngram_size: Word-level shingle size (default 2).
    """

    def __init__(
        self,
        ngram_size: int = 3,
        num_hashes: int = 128,
        word_ngram_size: int = 2,
    ) -> None:
        self.ngram_size = ngram_size
        self.word_ngram_size = word_ngram_size
        self._hasher = MinHasher(num_hashes=num_hashes)

    def encode(self, query: str) -> Signal:
        """Encode a query string into an RBSP Signal.

        The encoding is fully deterministic: the same query text always
        produces the same Signal (modulo the timestamp field).

        Args:
            query: The raw query string.

        Returns:
            A fully-populated Signal object.
        """
        normalized = query.strip()
        raw_bytes = normalized.encode("utf-8")

        # Content hash - exact identity of the query
        content_hash = rbsp_hash(raw_bytes)

        # Tokenize
        tokens = tuple(normalized.lower().split())

        # Shingles for semantic matching
        char_shingles = ngram_shingles(normalized, n=self.ngram_size)
        w_shingles = word_shingles(normalized, n=self.word_ngram_size)
        all_shingles = char_shingles | w_shingles

        # MinHash signature and folded fingerprint
        mh_sig: tuple[int, ...] = ()
        semantic_fp: int = 0
        if all_shingles:
            mh_list = self._hasher.signature(all_shingles)
            mh_sig = tuple(mh_list)
            semantic_fp = _fold_minhash(mh_list)

        # Structural pattern
        struct_hash = structural_hash(normalized)

        return Signal(
            content_hash=content_hash,
            semantic_fingerprint=semantic_fp,
            structural_pattern=struct_hash,
            query_tokens=tokens,
            minhash_signature=mh_sig,
            shingles=frozenset(all_shingles),
            timestamp=time.time(),
            raw_query=query,
        )

    def encode_tokens(self, tokens: List[str]) -> Signal:
        """Encode a pre-tokenized query into a Signal.

        Args:
            tokens: Pre-split query tokens.

        Returns:
            A Signal generated from the joined token string.
        """
        return self.encode(" ".join(tokens))
