"""
DSL Tokenizer and Parser for EMU trigger expressions.

This module provides lexical analysis of trigger DSL expressions,
breaking them into tokens for validation.

Trigger DSL Grammar (simplified):
    expression  := term ((AND | OR) term)*
    term        := NOT? factor
    factor      := accessor comparison_op value
                 | accessor IN duration
                 | EXISTS accessor
                 | COUNT accessor (WHERE condition)? IN duration comparison_op number
                 | '(' expression ')'
    accessor    := (state | tag | event) '.' identifier ('.' identifier)*
    value       := string | number | boolean | list
    duration    := string (ISO 8601 format)
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from enum import Enum, auto
from typing import Iterator


class TokenType(Enum):
    """Token types for the DSL lexer."""

    # Literals
    STRING = auto()
    NUMBER = auto()
    BOOLEAN = auto()

    # Identifiers and accessors
    IDENTIFIER = auto()
    STATE = auto()
    TAG = auto()
    EVENT = auto()
    DOT = auto()

    # Operators
    AND = auto()
    OR = auto()
    NOT = auto()
    IN = auto()
    EXISTS = auto()
    COUNT = auto()
    WHERE = auto()

    # Comparison operators
    EQ = auto()       # ==
    NEQ = auto()      # !=
    LT = auto()       # <
    GT = auto()       # >
    LTE = auto()      # <=
    GTE = auto()      # >=

    # Delimiters
    LPAREN = auto()
    RPAREN = auto()
    LBRACKET = auto()
    RBRACKET = auto()
    COMMA = auto()

    # Special
    EOF = auto()
    WHITESPACE = auto()


@dataclass
class Token:
    """A token from the DSL lexer."""

    type: TokenType
    value: str
    line: int
    column: int

    def __repr__(self) -> str:
        return f"Token({self.type.name}, {self.value!r}, line={self.line}, col={self.column})"


# Token patterns (order matters - more specific patterns first)
TOKEN_PATTERNS: list[tuple[str, TokenType | None]] = [
    # Whitespace (skip)
    (r'\s+', None),

    # Keywords (case-sensitive, uppercase)
    (r'\bAND\b', TokenType.AND),
    (r'\bOR\b', TokenType.OR),
    (r'\bNOT\b', TokenType.NOT),
    (r'\bIN\b', TokenType.IN),
    (r'\bEXISTS\b', TokenType.EXISTS),
    (r'\bCOUNT\b', TokenType.COUNT),
    (r'\bWHERE\b', TokenType.WHERE),

    # Accessor prefixes (lowercase)
    (r'\bstate\b', TokenType.STATE),
    (r'\btag\b', TokenType.TAG),
    (r'\bevent\b', TokenType.EVENT),

    # Boolean literals
    (r'\b(true|false|True|False)\b', TokenType.BOOLEAN),

    # Comparison operators (longer first)
    (r'==', TokenType.EQ),
    (r'!=', TokenType.NEQ),
    (r'<=', TokenType.LTE),
    (r'>=', TokenType.GTE),
    (r'<', TokenType.LT),
    (r'>', TokenType.GT),

    # String literals (single or double quoted)
    (r'"(?:[^"\\]|\\.)*"', TokenType.STRING),
    (r"'(?:[^'\\]|\\.)*'", TokenType.STRING),

    # Numbers (integer or float)
    (r'-?\d+\.?\d*', TokenType.NUMBER),

    # Delimiters
    (r'\(', TokenType.LPAREN),
    (r'\)', TokenType.RPAREN),
    (r'\[', TokenType.LBRACKET),
    (r'\]', TokenType.RBRACKET),
    (r',', TokenType.COMMA),
    (r'\.', TokenType.DOT),

    # Identifiers (must be last)
    (r'[a-z_][a-z0-9_]*', TokenType.IDENTIFIER),
]

# Compile patterns
COMPILED_PATTERNS = [(re.compile(pattern), token_type) for pattern, token_type in TOKEN_PATTERNS]


class LexerError(Exception):
    """Error during lexical analysis."""

    def __init__(self, message: str, line: int, column: int):
        self.line = line
        self.column = column
        super().__init__(f"Line {line}, column {column}: {message}")


def tokenize(source: str) -> Iterator[Token]:
    """
    Tokenize a DSL expression into tokens.

    Args:
        source: The DSL expression string

    Yields:
        Token objects

    Raises:
        LexerError: If an unrecognized character is encountered
    """
    line = 1
    column = 1
    pos = 0

    while pos < len(source):
        match = None

        for pattern, token_type in COMPILED_PATTERNS:
            match = pattern.match(source, pos)
            if match:
                value = match.group(0)

                # Only emit non-whitespace tokens
                if token_type is not None:
                    yield Token(token_type, value, line, column)

                # Update position tracking
                for char in value:
                    if char == '\n':
                        line += 1
                        column = 1
                    else:
                        column += 1

                pos = match.end()
                break

        if not match:
            raise LexerError(f"Unexpected character: {source[pos]!r}", line, column)

    yield Token(TokenType.EOF, "", line, column)


def validate_accessor_format(tokens: list[Token]) -> list[str]:
    """
    Validate accessor format (state.key.path, tag.kind, event.topic).

    Returns list of error messages.
    """
    errors: list[str] = []
    i = 0

    while i < len(tokens):
        token = tokens[i]

        if token.type in (TokenType.STATE, TokenType.TAG, TokenType.EVENT):
            accessor_type = token.value
            parts: list[str] = []
            start_col = token.column

            # Expect DOT after accessor type
            i += 1
            while i < len(tokens) and tokens[i].type == TokenType.DOT:
                i += 1
                if i < len(tokens) and tokens[i].type == TokenType.IDENTIFIER:
                    parts.append(tokens[i].value)
                    i += 1
                else:
                    errors.append(f"Expected identifier after '.' at column {tokens[i-1].column}")
                    break

            # Validate part count based on accessor type
            if accessor_type == "state":
                # State keys: 2-7 parts (e.g., user.tier or user.account.settings.theme)
                if len(parts) < 1:
                    errors.append(f"State accessor requires at least 1 key part at column {start_col}")
                elif len(parts) > 6:
                    errors.append(f"State accessor key has too many parts (max 6) at column {start_col}")

            elif accessor_type == "tag":
                # Tag: exactly 1 part (the kind)
                if len(parts) != 1:
                    errors.append(f"Tag accessor requires exactly 1 kind at column {start_col}")

            elif accessor_type == "event":
                # Event topics: 3-4 parts (S.V.O or P.S.V.O)
                if len(parts) < 3:
                    errors.append(f"Event topic requires at least 3 parts (subject.verb.object) at column {start_col}")
                elif len(parts) > 4:
                    errors.append(f"Event topic has too many parts (max 4) at column {start_col}")

            continue

        i += 1

    return errors


def validate_duration_format(value: str) -> bool:
    """
    Validate ISO 8601 duration format.

    Examples: PT1H, P7D, PT30M, P1DT12H
    """
    # Remove quotes if present
    if value.startswith(("'", '"')) and value.endswith(("'", '"')):
        value = value[1:-1]

    # ISO 8601 duration pattern
    pattern = r'^P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?$'
    return bool(re.match(pattern, value)) and value != "P" and value != "PT"


def validate_string_literal(token: Token) -> list[str]:
    """Validate string literal format."""
    errors: list[str] = []
    value = token.value

    # Check for balanced quotes
    if value.startswith('"') and not value.endswith('"'):
        errors.append(f"Unterminated string at column {token.column}")
    elif value.startswith("'") and not value.endswith("'"):
        errors.append(f"Unterminated string at column {token.column}")

    return errors
