"""Pre-Indexer Engine for RBSP.

Walks a directory tree and indexes all files at creation time, ensuring the
"pre-indexed universe" invariant: data is queryable the instant it enters the
system, not after a separate indexing pass.

Integrates with:
- ``SignatureGenerator`` to produce natural signatures from content
- ``SignatureRegistry`` to store signatures
- ``InvertedIndex`` for token-based keyword search
- ``SemanticClusterEngine`` for LSH-based semantic grouping
- ``IncrementalEngine`` for efficient change detection

Thread-safe.  Supports progress reporting.
"""

from __future__ import annotations

import hashlib
import logging
import os
import time
from pathlib import Path
from typing import Callable, Dict, List, Optional

from rbsp.core.encoding import NaturalSignature
from rbsp.core.lsh import ngram_shingles, LSHIndex
from rbsp.core.signature import SignatureGenerator
from rbsp.core.registry import SignatureRegistry
from rbsp.indexer.inverted import InvertedIndex, tokenize_unique
from rbsp.indexer.clusters import SemanticClusterEngine
from rbsp.indexer.incremental import IncrementalEngine

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Signature ID generation
# ---------------------------------------------------------------------------

def _make_sig_id(content: bytes) -> str:
    """Generate a deterministic signature ID from content bytes.

    Uses BLAKE2b for fast, collision-resistant hashing.
    """
    return hashlib.blake2b(content, digest_size=16).hexdigest()


# ---------------------------------------------------------------------------
# LSH bucket extraction
# ---------------------------------------------------------------------------

def _extract_lsh_buckets(
    lsh_index: LSHIndex,
    text: str,
    sig_id: str,
    ngram_size: int = 3,
) -> List[int]:
    """Insert content into the LSH index and return band bucket hashes.

    The LSH index uses banding to assign items to buckets.  We insert the
    item and then extract which buckets it was placed in (one per band).
    """
    shingles = ngram_shingles(text, n=ngram_size)
    if not shingles:
        return []
    sig = lsh_index.insert(sig_id, shingles)
    # Compute band hashes (same logic as LSHIndex.insert)
    buckets: List[int] = []
    for band_idx in range(lsh_index.bands):
        start = band_idx * lsh_index.rows
        band = tuple(sig[start : start + lsh_index.rows])
        buckets.append(hash(band) & 0xFFFFFFFF)  # 32-bit positive bucket ID
    return buckets


# ---------------------------------------------------------------------------
# Progress tracking
# ---------------------------------------------------------------------------

class IndexProgress:
    """Progress snapshot for a pre-indexing operation."""

    __slots__ = (
        "total_files", "processed_files", "indexed_files",
        "skipped_files", "error_files", "start_time", "elapsed",
    )

    def __init__(self) -> None:
        self.total_files: int = 0
        self.processed_files: int = 0
        self.indexed_files: int = 0
        self.skipped_files: int = 0
        self.error_files: int = 0
        self.start_time: float = 0.0
        self.elapsed: float = 0.0

    @property
    def percent(self) -> float:
        if self.total_files == 0:
            return 0.0
        return (self.processed_files / self.total_files) * 100.0

    @property
    def files_per_second(self) -> float:
        if self.elapsed == 0:
            return 0.0
        return self.processed_files / self.elapsed

    def __repr__(self) -> str:
        return (
            f"IndexProgress({self.processed_files}/{self.total_files} "
            f"[{self.percent:.1f}%], "
            f"indexed={self.indexed_files}, skipped={self.skipped_files}, "
            f"errors={self.error_files}, {self.files_per_second:.0f} files/s)"
        )


# Progress callback type
ProgressCallback = Callable[[IndexProgress], None]


# ---------------------------------------------------------------------------
# Pre-Indexer
# ---------------------------------------------------------------------------

class PreIndexer:
    """Pre-indexes a directory tree at creation time.

    Walks all files, generates natural signatures, and stores them in the
    registry, inverted index, and semantic cluster engine.  Uses the
    :class:`IncrementalEngine` to skip files that haven't changed.

    Args:
        registry:        The signature registry to store signatures in.
        inverted_index:  The inverted index for keyword search.
        cluster_engine:  The semantic cluster engine.
        sig_generator:   The signature generator (default: new instance).
        lsh_index:       Shared LSH index for bucket computation (default: new instance).
        state_path:      Path for incremental state persistence.
        ngram_size:      N-gram size for shingling (default 3).

    Usage::

        pre = PreIndexer(
            registry=registry,
            inverted_index=inv_idx,
            cluster_engine=clusters,
        )
        progress = pre.index_directory("/path/to/src", patterns=["*.py"])
    """

    def __init__(
        self,
        registry: SignatureRegistry,
        inverted_index: InvertedIndex,
        cluster_engine: SemanticClusterEngine,
        *,
        sig_generator: Optional[SignatureGenerator] = None,
        lsh_index: Optional[LSHIndex] = None,
        state_path: Optional[str] = None,
        ngram_size: int = 3,
    ) -> None:
        self._registry = registry
        self._inverted = inverted_index
        self._clusters = cluster_engine
        self._generator = sig_generator or SignatureGenerator()
        self._lsh_index = lsh_index or LSHIndex()
        self._ngram_size = ngram_size
        self._incremental = IncrementalEngine(
            state_path=state_path,
            on_index=self._index_file,
            on_remove=self._remove_file,
        )
        self._progress = IndexProgress()

    # ------------------------------------------------------------------
    # Core indexing logic
    # ------------------------------------------------------------------

    def _index_file(self, path: str, content: bytes) -> None:
        """Index a single file: generate signature, add to all stores."""
        sig_id = _make_sig_id(content)

        # Generate natural signature
        sig = self._generator.generate(content)

        # Store in registry
        self._registry.add(sig_id, sig, source_path=path)

        # Add to inverted index (tokenize content)
        try:
            text = content.decode("utf-8", errors="replace")
        except Exception:
            text = ""
        tokens = tokenize_unique(text)
        # Also index the filename
        filename = os.path.basename(path)
        tokens.update(tokenize_unique(filename))
        self._inverted.add_document(sig_id, tokens)

        # Add to semantic clusters via LSH buckets
        buckets = _extract_lsh_buckets(
            self._lsh_index, text, sig_id, self._ngram_size,
        )
        if buckets:
            self._clusters.add(sig_id, buckets)

        logger.debug("Indexed %s -> %s", path, sig_id)

    def _remove_file(self, path: str) -> None:
        """Remove a file's index entries from all stores."""
        sid = self._registry.get_sig_id_by_path(path)
        if sid is None:
            return
        self._registry.remove(sid)
        self._inverted.remove_document(sid)
        self._clusters.remove(sid)
        self._lsh_index.remove(sid)
        logger.debug("Removed index for %s (sig=%s)", path, sid)

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def index_file(self, path: str) -> bool:
        """Index a single file (with incremental change detection).

        Returns:
            True if the file was indexed, False if skipped.
        """
        return self._incremental.process_file(path)

    def index_directory(
        self,
        directory: str,
        *,
        patterns: Optional[List[str]] = None,
        recursive: bool = True,
        progress_callback: Optional[ProgressCallback] = None,
    ) -> IndexProgress:
        """Walk and index an entire directory tree.

        Uses incremental change detection to skip unchanged files.

        Args:
            directory:         Root directory to index.
            patterns:          Glob patterns to filter files.
            recursive:         Walk subdirectories (default True).
            progress_callback: Called periodically with :class:`IndexProgress`.

        Returns:
            Final :class:`IndexProgress` with statistics.
        """
        self._progress = IndexProgress()
        self._progress.start_time = time.time()

        def _on_progress(processed: int, total: int) -> None:
            self._progress.total_files = total
            self._progress.processed_files = processed
            self._progress.elapsed = time.time() - self._progress.start_time
            if progress_callback:
                progress_callback(self._progress)

        stats = self._incremental.scan(
            [directory],
            recursive=recursive,
            patterns=patterns,
            progress_callback=_on_progress,
        )

        self._progress.total_files = stats.total
        self._progress.processed_files = stats.total
        self._progress.indexed_files = stats.indexed
        self._progress.skipped_files = stats.skipped
        self._progress.error_files = stats.errors
        self._progress.elapsed = time.time() - self._progress.start_time

        logger.info("Pre-index complete: %s", self._progress)
        return self._progress

    def index_directories(
        self,
        directories: List[str],
        *,
        patterns: Optional[List[str]] = None,
        recursive: bool = True,
        progress_callback: Optional[ProgressCallback] = None,
    ) -> IndexProgress:
        """Index multiple directories.

        Args:
            directories:       List of root directories.
            patterns:          Glob patterns to filter files.
            recursive:         Walk subdirectories (default True).
            progress_callback: Called periodically with :class:`IndexProgress`.

        Returns:
            Aggregated :class:`IndexProgress`.
        """
        self._progress = IndexProgress()
        self._progress.start_time = time.time()

        stats = self._incremental.scan(
            directories,
            recursive=recursive,
            patterns=patterns,
            progress_callback=lambda p, t: None,
        )

        self._progress.total_files = stats.total
        self._progress.processed_files = stats.total
        self._progress.indexed_files = stats.indexed
        self._progress.skipped_files = stats.skipped
        self._progress.error_files = stats.errors
        self._progress.elapsed = time.time() - self._progress.start_time

        if progress_callback:
            progress_callback(self._progress)

        return self._progress

    @property
    def progress(self) -> IndexProgress:
        """Current or last indexing progress."""
        return self._progress

    def clear(self) -> None:
        """Clear all index data and incremental state."""
        self._registry.clear()
        self._inverted.clear()
        self._clusters.clear()
        self._incremental.clear_state()
