"""Configurable text analysis pipeline (Lucene-style Analyzer).

An Analyzer is a chain of: Tokenizer -> TokenFilter -> TokenFilter -> ...
Each component is pluggable, allowing custom text processing pipelines.
"""

from __future__ import annotations

import re
import unicodedata
from dataclasses import dataclass
from typing import List

from rbsp.core.stemmer import stem


# ---------------------------------------------------------------------------
# Token
# ---------------------------------------------------------------------------

@dataclass
class Token:
    """A single token produced by the analysis pipeline."""

    text: str
    start: int
    end: int
    position: int


# ---------------------------------------------------------------------------
# Tokenizers
# ---------------------------------------------------------------------------

class Tokenizer:
    """Base class for tokenizers."""

    def tokenize(self, text: str) -> List[Token]:
        raise NotImplementedError


class WhitespaceTokenizer(Tokenizer):
    """Split on whitespace, preserving character offsets."""

    def tokenize(self, text: str) -> List[Token]:
        tokens: List[Token] = []
        pos = 0
        for m in re.finditer(r"\S+", text):
            tokens.append(Token(
                text=m.group(),
                start=m.start(),
                end=m.end(),
                position=pos,
            ))
            pos += 1
        return tokens


# Pre-compiled pattern for StandardTokenizer
_CAMEL_BOUNDARY = re.compile(
    r"(?<=[a-z])(?=[A-Z])"   # lowerUPPER boundary
    r"|(?<=[A-Z])(?=[A-Z][a-z])"  # ABCDef -> ABC + Def
)
_NON_ALNUM = re.compile(r"[^a-zA-Z0-9]+")


class StandardTokenizer(Tokenizer):
    """Split on non-alphanumeric characters and camelCase boundaries.

    ``parseJSON`` -> [``parse``, ``JSON``]
    ``foo_bar-baz`` -> [``foo``, ``bar``, ``baz``]
    """

    def tokenize(self, text: str) -> List[Token]:
        tokens: List[Token] = []
        pos = 0

        # First split on non-alphanumeric
        for m in re.finditer(r"[a-zA-Z0-9]+", text):
            chunk = m.group()
            chunk_start = m.start()

            # Sub-split on camelCase boundaries
            parts = _CAMEL_BOUNDARY.split(chunk)
            offset = 0
            for part in parts:
                if not part:
                    continue
                start = chunk_start + offset
                end = start + len(part)
                tokens.append(Token(
                    text=part,
                    start=start,
                    end=end,
                    position=pos,
                ))
                pos += 1
                offset += len(part)

        return tokens


# ---------------------------------------------------------------------------
# Token Filters
# ---------------------------------------------------------------------------

class TokenFilter:
    """Base class for token filters."""

    def filter(self, tokens: List[Token]) -> List[Token]:
        raise NotImplementedError


class LowercaseFilter(TokenFilter):
    """Lowercase all token text."""

    def filter(self, tokens: List[Token]) -> List[Token]:
        for t in tokens:
            t.text = t.text.lower()
        return tokens


# Default English stop words (common across Lucene / NLTK)
_STOP_WORDS = frozenset({
    "a", "an", "and", "are", "as", "at", "be", "but", "by", "for",
    "if", "in", "into", "is", "it", "no", "not", "of", "on", "or",
    "such", "that", "the", "their", "then", "there", "these", "they",
    "this", "to", "was", "will", "with",
})


class StopWordFilter(TokenFilter):
    """Remove common English stop words."""

    def __init__(self, stop_words: frozenset[str] | None = None) -> None:
        self._stop_words = stop_words if stop_words is not None else _STOP_WORDS

    def filter(self, tokens: List[Token]) -> List[Token]:
        return [t for t in tokens if t.text not in self._stop_words]


class StemFilter(TokenFilter):
    """Apply Porter stemming via ``rbsp.core.stemmer.stem``."""

    def filter(self, tokens: List[Token]) -> List[Token]:
        for t in tokens:
            t.text = stem(t.text)
        return tokens


class LengthFilter(TokenFilter):
    """Remove tokens shorter than *min_length*."""

    def __init__(self, min_length: int = 2) -> None:
        self._min_length = min_length

    def filter(self, tokens: List[Token]) -> List[Token]:
        return [t for t in tokens if len(t.text) >= self._min_length]


# Mapping of common Unicode characters to ASCII equivalents
_ASCII_MAP = str.maketrans({
    "\u00e0": "a", "\u00e1": "a", "\u00e2": "a", "\u00e3": "a", "\u00e4": "a", "\u00e5": "a",
    "\u00e8": "e", "\u00e9": "e", "\u00ea": "e", "\u00eb": "e",
    "\u00ec": "i", "\u00ed": "i", "\u00ee": "i", "\u00ef": "i",
    "\u00f2": "o", "\u00f3": "o", "\u00f4": "o", "\u00f5": "o", "\u00f6": "o",
    "\u00f9": "u", "\u00fa": "u", "\u00fb": "u", "\u00fc": "u",
    "\u00f1": "n", "\u00e7": "c", "\u00df": "ss",
    "\u00c0": "A", "\u00c1": "A", "\u00c2": "A", "\u00c3": "A", "\u00c4": "A", "\u00c5": "A",
    "\u00c8": "E", "\u00c9": "E", "\u00ca": "E", "\u00cb": "E",
    "\u00cc": "I", "\u00cd": "I", "\u00ce": "I", "\u00cf": "I",
    "\u00d2": "O", "\u00d3": "O", "\u00d4": "O", "\u00d5": "O", "\u00d6": "O",
    "\u00d9": "U", "\u00da": "U", "\u00db": "U", "\u00dc": "U",
    "\u00d1": "N", "\u00c7": "C",
})


class ASCIIFoldingFilter(TokenFilter):
    """Normalize Unicode characters to their closest ASCII equivalents.

    Uses ``unicodedata.normalize`` (NFD decomposition) with a fallback
    translation table for characters that don't decompose cleanly.
    """

    def filter(self, tokens: List[Token]) -> List[Token]:
        for t in tokens:
            # NFD decomposition then strip combining marks
            nfd = unicodedata.normalize("NFD", t.text)
            stripped = "".join(
                ch for ch in nfd if unicodedata.category(ch) != "Mn"
            )
            # Fallback for chars that don't decompose (e.g. \u00df -> ss)
            t.text = stripped.translate(_ASCII_MAP)
        return tokens


# ---------------------------------------------------------------------------
# Analyzer
# ---------------------------------------------------------------------------

class Analyzer:
    """A configurable text analysis pipeline.

    Chains a :class:`Tokenizer` with zero or more :class:`TokenFilter` steps.
    """

    def __init__(self, tokenizer: Tokenizer, filters: List[TokenFilter]) -> None:
        self._tokenizer = tokenizer
        self._filters = filters

    def analyze(self, text: str) -> List[Token]:
        """Run the full analysis pipeline, returning :class:`Token` objects."""
        tokens = self._tokenizer.tokenize(text)
        for f in self._filters:
            tokens = f.filter(tokens)
        return tokens

    def analyze_to_strings(self, text: str) -> List[str]:
        """Convenience method: run the pipeline and return just the text values."""
        return [t.text for t in self.analyze(text)]


# ---------------------------------------------------------------------------
# Factory functions for common analyzer configurations
# ---------------------------------------------------------------------------

def StandardAnalyzer() -> Analyzer:
    """StandardTokenizer + Lowercase + StopWord + Stem + Length(min=2)."""
    return Analyzer(
        tokenizer=StandardTokenizer(),
        filters=[
            LowercaseFilter(),
            StopWordFilter(),
            StemFilter(),
            LengthFilter(min_length=2),
        ],
    )


def SimpleAnalyzer() -> Analyzer:
    """WhitespaceTokenizer + Lowercase."""
    return Analyzer(
        tokenizer=WhitespaceTokenizer(),
        filters=[
            LowercaseFilter(),
        ],
    )


def CodeAnalyzer() -> Analyzer:
    """StandardTokenizer + Lowercase + Length(min=2). No stemming for code."""
    return Analyzer(
        tokenizer=StandardTokenizer(),
        filters=[
            LowercaseFilter(),
            LengthFilter(min_length=2),
        ],
    )
