"""
RBSP Query Parser.

Parses natural language queries into structured query objects supporting:
    - Keyword search (default)
    - Phrase search: "exact phrase"
    - Boolean operators: AND, OR, NOT
    - Semantic queries: ~term (find semantically similar)
    - Exact queries: =term (exact match only)
    - File type filters: type:py, type:js
    - Path filters: path:src/, path:tests/
    - Scope filters: scope:file, scope:codebase

All parsing is rule-based with no external NLP dependencies.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import List, Optional


class QueryType(Enum):
    """Classification of the overall query intent."""
    KEYWORD = auto()      # Default keyword search
    PHRASE = auto()        # Exact phrase match
    BOOLEAN = auto()       # Boolean expression
    SEMANTIC = auto()      # Semantic similarity search
    EXACT = auto()         # Exact content match


class TokenType(Enum):
    """Types of tokens in a parsed query."""
    TERM = auto()          # Regular search term
    PHRASE = auto()        # Quoted phrase
    AND = auto()           # Boolean AND
    OR = auto()            # Boolean OR
    NOT = auto()           # Boolean NOT / exclusion
    SEMANTIC = auto()      # Semantic prefix ~term
    EXACT = auto()         # Exact prefix =term
    FILTER_TYPE = auto()   # type:ext filter
    FILTER_PATH = auto()   # path:prefix filter
    FILTER_SCOPE = auto()  # scope:name filter


@dataclass(frozen=True, slots=True)
class QueryToken:
    """A single parsed token from a query.

    Attributes:
        token_type: The semantic type of this token.
        value:      The token value (term text, phrase, filter value, etc.).
    """
    token_type: TokenType
    value: str


@dataclass(frozen=True, slots=True)
class ParsedQuery:
    """Structured representation of a parsed query.

    Attributes:
        query_type: The dominant query type.
        tokens:     Ordered list of parsed query tokens.
        terms:      Plain search terms (keywords).
        phrases:    Exact phrases to match.
        excluded:   Terms to exclude from results.
        semantic_terms: Terms for semantic expansion.
        exact_terms:    Terms requiring exact match.
        type_filters:   File extension filters (e.g., ["py", "js"]).
        path_filters:   Path prefix filters (e.g., ["src/", "tests/"]).
        scope:          Search scope (e.g., "file", "codebase", "system").
        raw_query:      The original unparsed query string.
    """
    query_type: QueryType
    tokens: tuple[QueryToken, ...]
    terms: tuple[str, ...]
    phrases: tuple[str, ...]
    excluded: tuple[str, ...]
    semantic_terms: tuple[str, ...]
    exact_terms: tuple[str, ...]
    type_filters: tuple[str, ...]
    path_filters: tuple[str, ...]
    scope: Optional[str]
    raw_query: str


# Regex for tokenizing: matches quoted strings, filter:value, operators, ~term, =term, words
_TOKEN_RE = re.compile(
    r'"([^"]*)"'             # Group 1: quoted phrase
    r"|'([^']*)'"            # Group 2: single-quoted phrase
    r'|\b(AND|OR|NOT)\b'     # Group 3: boolean operator
    r'|(type:\S+)'           # Group 4: type filter
    r'|(path:\S+)'           # Group 5: path filter
    r'|(scope:\S+)'          # Group 6: scope filter
    r'|(~\S+)'               # Group 7: semantic prefix
    r'|(=\S+)'               # Group 8: exact prefix
    r'|(\S+)'                # Group 9: plain term
)


class QueryParser:
    """Rule-based parser for RBSP queries.

    Converts raw query strings into structured ``ParsedQuery`` objects that
    the Signal Encoder and Resonance Matcher can consume.

    The parser recognizes:
        - ``"quoted phrases"`` for exact matching
        - ``AND``, ``OR``, ``NOT`` boolean operators
        - ``~term`` for semantic similarity expansion
        - ``=term`` for exact-only matching
        - ``type:ext`` for file type filtering
        - ``path:prefix`` for path filtering
        - ``scope:name`` for search scope selection

    Usage::

        parser = QueryParser()
        parsed = parser.parse('~algorithm type:py NOT test')
    """

    def parse(self, query: str) -> ParsedQuery:
        """Parse a raw query string into a structured ParsedQuery.

        Args:
            query: The raw query string.

        Returns:
            A ParsedQuery containing all extracted tokens, terms, filters,
            and the inferred query type.
        """
        tokens: List[QueryToken] = []
        terms: List[str] = []
        phrases: List[str] = []
        excluded: List[str] = []
        semantic_terms: List[str] = []
        exact_terms: List[str] = []
        type_filters: List[str] = []
        path_filters: List[str] = []
        scope: Optional[str] = None

        negate_next = False

        for match in _TOKEN_RE.finditer(query):
            dq_phrase = match.group(1)
            sq_phrase = match.group(2)
            boolean_op = match.group(3)
            type_filt = match.group(4)
            path_filt = match.group(5)
            scope_filt = match.group(6)
            semantic = match.group(7)
            exact = match.group(8)
            plain = match.group(9)

            if dq_phrase is not None or sq_phrase is not None:
                phrase_val = dq_phrase if dq_phrase is not None else sq_phrase
                tokens.append(QueryToken(TokenType.PHRASE, phrase_val))
                if negate_next:
                    excluded.append(phrase_val)
                    negate_next = False
                else:
                    phrases.append(phrase_val)

            elif boolean_op is not None:
                if boolean_op == "AND":
                    tokens.append(QueryToken(TokenType.AND, "AND"))
                elif boolean_op == "OR":
                    tokens.append(QueryToken(TokenType.OR, "OR"))
                elif boolean_op == "NOT":
                    tokens.append(QueryToken(TokenType.NOT, "NOT"))
                    negate_next = True

            elif type_filt is not None:
                ext = type_filt[5:]  # strip "type:"
                tokens.append(QueryToken(TokenType.FILTER_TYPE, ext))
                type_filters.append(ext)

            elif path_filt is not None:
                prefix = path_filt[5:]  # strip "path:"
                tokens.append(QueryToken(TokenType.FILTER_PATH, prefix))
                path_filters.append(prefix)

            elif scope_filt is not None:
                scope_val = scope_filt[6:]  # strip "scope:"
                tokens.append(QueryToken(TokenType.FILTER_SCOPE, scope_val))
                scope = scope_val

            elif semantic is not None:
                term = semantic[1:]  # strip "~"
                tokens.append(QueryToken(TokenType.SEMANTIC, term))
                semantic_terms.append(term)

            elif exact is not None:
                term = exact[1:]  # strip "="
                tokens.append(QueryToken(TokenType.EXACT, term))
                exact_terms.append(term)

            elif plain is not None:
                if negate_next:
                    tokens.append(QueryToken(TokenType.TERM, plain))
                    excluded.append(plain.lower())
                    negate_next = False
                else:
                    tokens.append(QueryToken(TokenType.TERM, plain))
                    terms.append(plain.lower())

        query_type = self._infer_type(tokens, terms, phrases, semantic_terms, exact_terms)

        return ParsedQuery(
            query_type=query_type,
            tokens=tuple(tokens),
            terms=tuple(terms),
            phrases=tuple(phrases),
            excluded=tuple(excluded),
            semantic_terms=tuple(semantic_terms),
            exact_terms=tuple(exact_terms),
            type_filters=tuple(type_filters),
            path_filters=tuple(path_filters),
            scope=scope,
            raw_query=query,
        )

    @staticmethod
    def _infer_type(
        tokens: List[QueryToken],
        terms: List[str],
        phrases: List[str],
        semantic_terms: List[str],
        exact_terms: List[str],
    ) -> QueryType:
        """Infer the dominant query type from parsed tokens."""
        has_boolean = any(
            t.token_type in (TokenType.AND, TokenType.OR, TokenType.NOT)
            for t in tokens
        )
        if has_boolean:
            return QueryType.BOOLEAN
        if semantic_terms and not terms and not phrases and not exact_terms:
            return QueryType.SEMANTIC
        if exact_terms and not terms and not phrases and not semantic_terms:
            return QueryType.EXACT
        if phrases and not terms and not semantic_terms and not exact_terms:
            return QueryType.PHRASE
        return QueryType.KEYWORD
