"""
RBSP Natural Signature Generator.

Produces deterministic, content-derived natural signatures that combine:
    - A fast custom content hash (RBSP hash)
    - An LSH-based semantic fingerprint via n-gram shingling + MinHash
    - A structural hash capturing code/document shape
    - Logarithmic size class and timestamp metadata

All hashing is done through the novel RBSP hash function, a non-cryptographic
hash designed for speed and good distribution on textual content.
"""

from __future__ import annotations

import hashlib
import math
import re
import time
from typing import Optional, Sequence

from rbsp.core.encoding import (
    FLAG_HAS_SEMANTIC,
    FLAG_HAS_STRUCTURAL,
    NaturalSignature,
    compute_size_class,
)
from rbsp.core.lsh import MinHasher, ngram_shingles


# ---------------------------------------------------------------------------
# RBSP Hash — a fast, non-cryptographic content hash
# ---------------------------------------------------------------------------
#
# Design goals:
#   1. Fast on short-to-medium text (sub-microsecond for < 4 KiB).
#   2. Good avalanche: flipping one input bit flips ~50 % of output bits.
#   3. Few collisions on natural-language / source-code corpora.
#
# The algorithm mixes three independent accumulators (h1, h2, h3) that each
# see every byte but with different multipliers and rotations.  After the
# byte loop the three states are folded together with a finaliser based on
# SplitMix64's bit-mixing constants.  This avoids relying on SHA/MD5 while
# still being a custom design.

_RBSP_SEED = 0xA5B9C3D7E1F20463
_MUL1 = 0x9E3779B97F4A7C15  # golden-ratio-derived
_MUL2 = 0xBF58476D1CE4E5B9  # SplitMix64 constant
_MUL3 = 0x94D049BB133111EB  # SplitMix64 constant
_MASK64 = 0xFFFFFFFFFFFFFFFF


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


def rbsp_hash(data: bytes, seed: int = _RBSP_SEED) -> int:
    """Compute a 64-bit RBSP content hash over *data*.

    Uses hashlib.blake2b (C implementation) for ~50-100x speedup
    over the legacy byte-by-byte Python loop.
    """
    seed_bytes = (seed & _MASK64).to_bytes(8, "little")
    h = hashlib.blake2b(data, digest_size=8, key=seed_bytes)
    return int.from_bytes(h.digest(), "little")


def _rbsp_hash_legacy(data: bytes, seed: int = _RBSP_SEED) -> int:
    """Legacy RBSP hash - kept for reference."""
    h1 = seed
    h2 = seed ^ _MUL1
    h3 = seed ^ _MUL2

    for i, b in enumerate(data):
        h1 ^= b
        h1 = (h1 * _MUL1) & _MASK64
        h1 = ((h1 << 31) | (h1 >> 33)) & _MASK64

        h2 ^= b
        h2 = (h2 * _MUL2) & _MASK64
        h2 = ((h2 << 27) | (h2 >> 37)) & _MASK64

        h3 ^= b
        h3 = (h3 * _MUL3) & _MASK64
        h3 = ((h3 << 33) | (h3 >> 31)) & _MASK64

        # Cross-feed every 8 bytes for extra mixing
        if (i & 7) == 7:
            h1 = (h1 + h2) & _MASK64
            h2 = (h2 ^ h3) & _MASK64
            h3 = (h3 + h1) & _MASK64

    # Finaliser — fold accumulators together
    h = (h1 ^ h2 ^ h3) & _MASK64
    h ^= h >> 30
    h = (h * _MUL2) & _MASK64
    h ^= h >> 27
    h = (h * _MUL3) & _MASK64
    h ^= h >> 31
    return h


def rbsp_hash_text(text: str, seed: int = _RBSP_SEED) -> int:
    """Convenience wrapper: hash a unicode string as UTF-8."""
    return rbsp_hash(text.encode("utf-8"), seed)


# ---------------------------------------------------------------------------
# Structural hash helpers
# ---------------------------------------------------------------------------

# Regex patterns capturing code-like structural tokens
_STRUCTURAL_RE = re.compile(
    r"[{}\[\]();]"           # brackets, parens, semicolons
    r"|(?:^[ \t]+)"          # leading whitespace (indentation)
    r"|(?:\b(?:def|class|function|if|else|for|while|return|import|from|try|except|with)\b)",
    re.MULTILINE,
)


def _structural_tokens(text: str) -> list[str]:
    """Extract structural tokens from text/code for hashing."""
    return _STRUCTURAL_RE.findall(text)


def structural_hash(text: str) -> int:
    """Produce a 64-bit hash capturing the structural shape of *text*.

    For source code this captures indentation patterns, bracket structure,
    and keyword distribution.  For prose it degrades gracefully to
    punctuation patterns.
    """
    tokens = _structural_tokens(text)
    if not tokens:
        return 0
    combined = "|".join(tokens)
    return rbsp_hash(combined.encode("utf-8"), seed=0xDEADBEEF12345678)


# ---------------------------------------------------------------------------
# Natural Signature Generator
# ---------------------------------------------------------------------------

class SignatureGenerator:
    """Generates RBSP natural signatures from content.

    A natural signature deterministically fingerprints content using three
    orthogonal hash dimensions (content, semantic, structural) packed into
    a compact 64-byte binary format.

    Args:
        ngram_size:      Shingle size for semantic hashing (default 3).
        num_hashes:      Number of MinHash permutations (default 128).
        enable_semantic: Compute LSH semantic hash (default True).
        enable_structural: Compute structural hash (default True).
    """

    def __init__(
        self,
        ngram_size: int = 3,
        num_hashes: int = 128,
        enable_semantic: bool = True,
        enable_structural: bool = True,
    ) -> None:
        self.ngram_size = ngram_size
        self.enable_semantic = enable_semantic
        self.enable_structural = enable_structural
        self._hasher = MinHasher(num_hashes=num_hashes)

    def generate(
        self,
        content: str | bytes,
        *,
        timestamp: Optional[float] = None,
    ) -> NaturalSignature:
        """Generate a natural signature for *content*.

        Args:
            content:   The content to fingerprint (str or bytes).
            timestamp: Override the creation timestamp (default ``time.time()``).

        Returns:
            A fully-populated ``NaturalSignature``.
        """
        if isinstance(content, str):
            raw = content.encode("utf-8")
            text = content
        else:
            raw = content
            text = content.decode("utf-8", errors="replace")

        ts = timestamp if timestamp is not None else time.time()
        flags = 0

        # 1. Content hash
        c_hash = rbsp_hash(raw)

        # 2. Semantic hash (LSH via n-gram shingling + MinHash)
        s_hash = 0
        if self.enable_semantic and len(text) >= self.ngram_size:
            shingles = ngram_shingles(text, n=self.ngram_size)
            if shingles:
                mh = self._hasher.signature(shingles)
                # Fold the MinHash signature into a single 64-bit fingerprint
                s_hash = self._fold_minhash(mh)
                flags |= FLAG_HAS_SEMANTIC

        # 3. Structural hash
        st_hash = 0
        if self.enable_structural:
            st_hash = structural_hash(text)
            if st_hash:
                flags |= FLAG_HAS_STRUCTURAL

        # 4. Size class
        sc = compute_size_class(len(raw))

        return NaturalSignature(
            content_hash=c_hash,
            semantic_hash=s_hash,
            structural_hash=st_hash,
            timestamp=ts,
            size_class=sc,
            flags=flags,
        )

    def generate_bytes(
        self,
        content: str | bytes,
        *,
        timestamp: Optional[float] = None,
    ) -> bytes:
        """Shorthand: generate a signature and return its wire-format bytes."""
        return self.generate(content, timestamp=timestamp).to_bytes()

    @staticmethod
    def _fold_minhash(minhash: Sequence[int]) -> int:
        """Fold a MinHash signature vector into a single 64-bit integer.

        XOR-folds all values, then runs the RBSP finaliser to disperse bits.
        """
        h: int = 0
        for v in minhash:
            h ^= v
            h = _rotl64(h, 5)
            h = (h * _MUL1) & _MASK64
        # Final mix
        h ^= h >> 30
        h = (h * _MUL2) & _MASK64
        h ^= h >> 27
        h = (h * _MUL3) & _MASK64
        h ^= h >> 31
        return h
