"""
RBSP Wildcard and Prefix Query Support.

Provides wildcard pattern matching (``*`` for any characters, ``?`` for a
single character) and optimised prefix matching over term vocabularies.

Wildcard patterns are compiled to ``re.Pattern`` objects for correctness.
Prefix matching uses binary search (``bisect``) over a sorted term list for
O(log n + k) retrieval where *k* is the number of matching terms.
"""

from __future__ import annotations

import bisect
import re
from dataclasses import dataclass
from typing import Iterable, List


@dataclass(frozen=True, slots=True)
class WildcardQuery:
    """A query containing wildcard characters.

    Attributes:
        pattern: Wildcard pattern where ``*`` matches zero or more characters
                 and ``?`` matches exactly one character.
    """

    pattern: str


@dataclass(frozen=True, slots=True)
class PrefixQuery:
    """A query matching all terms that start with a given prefix.

    Attributes:
        prefix: The fixed prefix string to match against.
    """

    prefix: str


def wildcard_to_regex(pattern: str) -> re.Pattern[str]:
    """Convert a wildcard pattern to a compiled regular expression.

    ``*`` is translated to ``.*`` and ``?`` to ``.``.  All other regex
    metacharacters in the pattern are escaped so they are matched literally.

    The resulting regex is anchored (``^...$``) to enforce full-string matching.

    Args:
        pattern: Wildcard pattern string.

    Returns:
        A compiled ``re.Pattern`` that matches exactly the strings described
        by *pattern*.
    """
    parts: List[str] = []
    i = 0
    while i < len(pattern):
        ch = pattern[i]
        if ch == "*":
            parts.append(".*")
        elif ch == "?":
            parts.append(".")
        else:
            parts.append(re.escape(ch))
        i += 1
    return re.compile("^" + "".join(parts) + "$")


class WildcardMatcher:
    """Matches terms against a wildcard pattern.

    Args:
        pattern: Wildcard pattern (``*`` and ``?`` are special).
    """

    def __init__(self, pattern: str) -> None:
        self._pattern = pattern
        self._regex = wildcard_to_regex(pattern)
        # Pre-compute prefix (everything before the first wildcard character)
        self._prefix = _extract_prefix(pattern)

    def matches(self, term: str) -> bool:
        """Return ``True`` if *term* matches the wildcard pattern."""
        return self._regex.match(term) is not None

    def is_prefix_only(self) -> bool:
        """Return ``True`` if the pattern is a simple prefix query (e.g. ``foo*``).

        A pattern is prefix-only when the only wildcard is a single trailing
        ``*`` with no ``?`` characters.
        """
        if "?" in self._pattern:
            return False
        stripped = self._pattern.rstrip("*")
        # After removing trailing stars, there should be no wildcards left,
        # and there must have been at least one trailing star.
        return "*" not in stripped and len(stripped) < len(self._pattern)

    def get_prefix(self) -> str:
        """Return the fixed prefix before the first wildcard character."""
        return self._prefix

    def find_matches(self, terms: Iterable[str]) -> list[str]:
        """Return all terms from *terms* that match the wildcard pattern.

        If the pattern is prefix-only and *terms* is a list, a fast prefix
        scan is used.  Otherwise every term is tested against the compiled
        regex.

        Args:
            terms: Iterable of candidate terms.

        Returns:
            List of matching terms (order mirrors iteration order of *terms*).
        """
        regex = self._regex
        return [t for t in terms if regex.match(t) is not None]


class PrefixMatcher:
    """Matches terms that start with a given prefix.

    Designed for use with a **sorted** term list so that matching can be
    performed via binary search in O(log n + k) time.

    Args:
        prefix: The prefix string to match.
    """

    def __init__(self, prefix: str) -> None:
        self._prefix = prefix

    def matches(self, term: str) -> bool:
        """Return ``True`` if *term* starts with the prefix."""
        return term.startswith(self._prefix)

    def find_matches(self, sorted_terms: list[str]) -> list[str]:
        """Find all terms in a **sorted** list that start with the prefix.

        Uses ``bisect`` for O(log n) lookup of the first matching position,
        then scans forward to collect the *k* matching terms — overall
        O(log n + k).

        Args:
            sorted_terms: A lexicographically sorted list of terms.

        Returns:
            List of matching terms in sorted order.
        """
        prefix = self._prefix
        if not prefix:
            # Empty prefix matches everything
            return list(sorted_terms)

        lo = bisect.bisect_left(sorted_terms, prefix)
        results: List[str] = []
        for i in range(lo, len(sorted_terms)):
            term = sorted_terms[i]
            if term.startswith(prefix):
                results.append(term)
            else:
                break
        return results


# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------

def _extract_prefix(pattern: str) -> str:
    """Return the portion of *pattern* before the first wildcard character."""
    for i, ch in enumerate(pattern):
        if ch in ("*", "?"):
            return pattern[:i]
    return pattern
