"""Text tokenization for Itasca documentation search system.

This module provides tokenization functionality optimized for technical
documentation, handling special cases like hyphenated terms, numbers,
and technical abbreviations.
"""

import re

from itasca_mcp.knowledge.search.preprocessing.stopwords import is_stopword


class TextTokenizer:
    """Tokenizer for technical documentation text.

    Features:
    - Preserves technical terms and numbers
    - Handles hyphenated terms (e.g., "ball-ball" → ["ball", "ball"])
    - Handles underscored terms (e.g., "vel_x" → ["vel", "x"])
    - Smart dot splitting for API paths vs. decimals
    - Removes stopwords while preserving technical vocabulary
    - Lowercases for consistent matching

    Design:
    - Regex-based for performance
    - No stemming (preserves exact technical terms)
    - No lemmatization (not needed for partial matching)
    - CamelCase splitting for better term matching
    - Splits on hyphens, underscores, dots, and CamelCase boundaries

    Usage:
        >>> tokenizer = TextTokenizer()
        >>> tokenizer.tokenize("Create ball with radius 1.5")
        ['create', 'ball', 'radius', '1.5']

        >>> tokenizer.tokenize("Ball.vel velocity")
        ['ball', 'vel', 'velocity']

        >>> tokenizer.tokenize("itasca.ball.Ball.vel")
        ['itasca', 'ball', 'ball', 'vel']

        >>> tokenizer.tokenize("ball-ball contact model")
        ['ball', 'ball', 'contact', 'model']

        >>> tokenizer.tokenize("velocity vel_x component")
        ['velocity', 'vel', 'x', 'component']

        >>> tokenizer.tokenize("BallBallContact")
        ['ball', 'ball', 'contact']

        >>> tokenizer.tokenize("IterMechanical system")
        ['iter', 'mechanical', 'system']
    """

    # Regex pattern for word extraction
    # Matches: alphanumeric + hyphens + underscores + dots (for numbers)
    WORD_PATTERN = re.compile(r"\b[\w.-]+\b")

    # Pattern for numeric values (decimals, scientific notation)
    # Examples: 1.5, 0.001, .5, 1e-5, 2.5e10, -1.5e+20
    NUMERIC_PATTERN = re.compile(
        r"^"
        r"[+-]?"  # Optional sign
        r"(?:"
        r"\d+\.?\d*"  # Integer or decimal (1, 1., 1.5)
        r"|"
        r"\.\d+"  # Decimal starting with dot (.5)
        r")"
        r"(?:[eE][+-]?\d+)?"  # Optional scientific notation
        r"$"
    )

    # Minimum word length (avoid single-char noise)
    MIN_WORD_LENGTH = 2

    # Technical single-character tokens that should be preserved
    # These are common in scientific/engineering contexts
    TECHNICAL_SINGLE_CHARS = {"x", "y", "z", "r", "n", "t"}  # Coordinates, radius, normal, tangent

    def __init__(self, remove_stopwords: bool = True, min_length: int = 2):
        """Initialize tokenizer.

        Args:
            remove_stopwords: Whether to filter out stopwords
            min_length: Minimum word length to keep (default: 2)
        """
        self.remove_stopwords = remove_stopwords
        self.min_length = min_length

    def tokenize(self, text: str) -> list[str]:
        """Tokenize text into words.

        Args:
            text: Input text to tokenize

        Returns:
            List of tokens (lowercase, filtered)

        Example:
            >>> tokenizer = TextTokenizer()
            >>> tokenizer.tokenize("Distribute balls with overlaps")
            ['distribute', 'balls', 'overlaps']

            >>> tokenizer.tokenize("2D simulation with PFC")
            ['2d', 'simulation', 'pfc']

            >>> tokenizer.tokenize("ball-ball contact")
            ['ball', 'ball', 'contact']
        """
        if not text:
            return []

        # 1. Extract words BEFORE lowercasing (to preserve CamelCase info)
        original_words = self.WORD_PATTERN.findall(text)

        # 2. Create lowercase version for matching
        text_lower = text.lower()
        lowercase_words = self.WORD_PATTERN.findall(text_lower)

        # 3. Process each word (preserve original for CamelCase detection)
        tokens = []
        for original_word, word in zip(original_words, lowercase_words):
            # Handle dotted terms (smart split: API paths vs. decimals)
            # Context: API paths like "itasca.ball.Ball.vel" vs. decimals like "1.5"
            if "." in word:
                # Check if it's a numeric value (decimal or scientific notation)
                if self.NUMERIC_PATTERN.match(word):
                    # Numeric value - keep intact
                    if self._is_valid_token(word, from_technical_context=False):
                        tokens.append(word)
                else:
                    # API path - split on dots
                    # "itasca.ball.Ball.vel" → ["itasca", "ball", "ball", "vel"]
                    # Split both original and lowercase to preserve CamelCase info
                    original_parts = original_word.split(".")
                    lowercase_parts = word.split(".")
                    for original_part, part in zip(original_parts, lowercase_parts):
                        # Process each path component (may contain underscores)
                        if "_" in part:
                            # Handle underscores within path components
                            # "force_global" → ["force", "global"]
                            subparts = part.split("_")
                            for subpart in subparts:
                                if self._is_valid_token(subpart, from_technical_context=True):
                                    tokens.append(subpart)
                        elif "-" in part:
                            # Handle hyphens within path components
                            # "ball-ball" → ["ball", "ball"]
                            subparts = part.split("-")
                            for subpart in subparts:
                                if self._is_valid_token(subpart, from_technical_context=True):
                                    tokens.append(subpart)
                        else:
                            # Simple path component - check for CamelCase
                            # Example: "Ball" in "itasca.ball.Ball.vel"
                            camel_parts = self._split_camel_case(original_part)
                            for camel_part in camel_parts:
                                if self._is_valid_token(camel_part, from_technical_context=True):
                                    tokens.append(camel_part)
            # Handle hyphenated terms (split and keep parts)
            # Context: technical compound terms like "ball-ball"
            elif "-" in word:
                parts = word.split("-")
                for part in parts:
                    # In hyphenated context, preserve technical single chars
                    if self._is_valid_token(part, from_technical_context=True):
                        tokens.append(part)
            # Handle underscored terms (e.g., "vel_x", "set_pos", "end_1")
            # Split to improve matching: "vel_x" → ["vel", "x"]
            # Context: API naming conventions with direction/index suffixes
            elif "_" in word:
                parts = word.split("_")
                for part in parts:
                    # In underscore context, preserve technical single chars
                    if self._is_valid_token(part, from_technical_context=True):
                        tokens.append(part)
            else:
                # Check if original word is CamelCase and split it
                # Example: "BallBallContact" → ["ballballcontact", "ball", "ball", "contact"]
                # Use original_word to preserve CamelCase info
                camel_parts = self._split_camel_case(original_word)

                # Emit full compound word for exact name matching
                if len(camel_parts) > 1:
                    full = original_word.lower()
                    if self._is_valid_token(full, from_technical_context=False):
                        tokens.append(full)

                for part in camel_parts:
                    if self._is_valid_token(part, from_technical_context=False):
                        tokens.append(part)
                        # Add stemmed form
                        stem = self._stem(part)
                        if stem and stem != part and self._is_valid_token(stem, from_technical_context=False):
                            tokens.append(stem)

        return tokens

    def tokenize_to_set(self, text: str) -> set[str]:
        """Tokenize text into a set of unique words.

        Useful for matching operations where duplicates don't matter.

        Args:
            text: Input text to tokenize

        Returns:
            Set of unique tokens

        Example:
            >>> tokenizer = TextTokenizer()
            >>> tokenizer.tokenize_to_set("ball ball contact")
            {'ball', 'contact'}
        """
        return set(self.tokenize(text))

    # Suffixes to strip for stemming
    _SUFFIX_RULES = [
        "ment",
        "ness",
        "able",
        "ible",
        "ious",
        "ous",
        "ive",
        "ful",
        "less",
        "ing",
        "ion",
        "ity",
        "ial",
        "al",
        "ic",
        "ly",
        "er",
        "ed",
        "es",
    ]

    def _split_camel_case(self, word: str) -> list[str]:
        """Split CamelCase word into components, keeping uppercase runs together.

        Args:
            word: Word to split (e.g., "BallBallContact", "IterMechanical")

        Returns:
            List of lowercase components

        Example:
            >>> self._split_camel_case("BallBallContact")
            ['ball', 'ball', 'contact']
            >>> self._split_camel_case("IterMechanical")
            ['iter', 'mechanical']
            >>> self._split_camel_case("simple")
            ['simple']  # Not CamelCase, return as-is
        """
        # Insert space before uppercase-lowercase transitions and between lower-upper
        spaced = re.sub(r"([A-Z]+)(?=[A-Z][a-z])", r"\1 ", word)
        spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", spaced)

        # Split on spaces and lowercase
        parts = spaced.split()

        # Only return split if there are multiple parts (actual CamelCase)
        if len(parts) > 1:
            return [p.lower() for p in parts]
        else:
            return [word.lower()]

    def _stem(self, word: str) -> str | None:
        """Simple suffix-stripping stemmer for search matching."""
        for suffix in self._SUFFIX_RULES:
            if word.endswith(suffix) and len(word) - len(suffix) >= 3:
                return word[: -len(suffix)]
        return None

    def _is_valid_token(self, word: str, from_technical_context: bool = False) -> bool:
        """Check if a word is a valid token.

        Args:
            word: Word to check
            from_technical_context: True if word comes from underscore/hyphen splitting
                                   (e.g., "x" from "vel_x", "1" from "end_1")

        Returns:
            True if word should be kept, False otherwise

        Filtering rules:
        1. Length >= min_length (except numbers and technical single chars)
        2. Not a stopword (unless it's a technical term)
        3. Contains at least one alphanumeric character

        Special handling:
        - Technical single characters (x, y, z, r, n, t) ALWAYS preserved
        - Single-digit numbers always preserved
        - Rationale: In scientific/engineering docs, x/y/z are semantically meaningful
        """
        # Allow single-digit numbers (e.g., "1" from "end_1")
        if word.isdigit():
            return True

        # ALWAYS preserve technical single characters (regardless of context)
        # This is critical for keywords like "force global y", "contact force x"
        # where x/y/z are semantically important but not in underscore/hyphen context
        if len(word) == 1 and word.lower() in self.TECHNICAL_SINGLE_CHARS:
            return True

        # Must have minimum length for other tokens
        if len(word) < self.min_length:
            return False

        # Must contain at least one alphanumeric character
        if not any(c.isalnum() for c in word):
            return False

        # Check stopwords (if enabled)
        if self.remove_stopwords and is_stopword(word):
            return False

        return True

    @staticmethod
    def normalize_query(query: str) -> str:
        """Normalize query text for consistent processing.

        Args:
            query: User query string

        Returns:
            Normalized query (lowercase, whitespace normalized)

        Example:
            >>> TextTokenizer.normalize_query("  Create   Ball  ")
            'create ball'
        """
        # Remove extra whitespace and lowercase
        query = " ".join(query.split())
        return query.lower()
