"""
Boolean expression parser for stop conditions.

Supports AND, OR operators with parentheses for grouping.
Evaluates against row field values to determine if enrichment is complete.

Examples:
    "email AND domain"
    "email OR phone"
    "(email AND domain) OR (linkedin_url AND phone)"
"""

from dataclasses import dataclass
from typing import Callable, Dict, List, Optional, Union, TYPE_CHECKING

if TYPE_CHECKING:
    from .models import RowState


class StopConditionError(Exception):
    """Error parsing or evaluating stop condition."""

    pass


# Token types
TOKEN_AND = "AND"
TOKEN_OR = "OR"
TOKEN_LPAREN = "("
TOKEN_RPAREN = ")"
TOKEN_FIELD = "FIELD"
TOKEN_EOF = "EOF"


@dataclass
class Token:
    """Lexer token."""

    type: str
    value: str
    position: int


class Lexer:
    """Tokenize stop condition expression."""

    def __init__(self, expression: str):
        self.expression = expression
        self.pos = 0
        self.length = len(expression)

    def next_token(self) -> Token:
        """Get next token from expression."""
        self._skip_whitespace()

        if self.pos >= self.length:
            return Token(TOKEN_EOF, "", self.pos)

        char = self.expression[self.pos]

        # Parentheses
        if char == "(":
            self.pos += 1
            return Token(TOKEN_LPAREN, "(", self.pos - 1)
        if char == ")":
            self.pos += 1
            return Token(TOKEN_RPAREN, ")", self.pos - 1)

        # Keywords (AND, OR) or field names
        if char.isalpha() or char == "_":
            return self._read_word()

        raise StopConditionError(f"Unexpected character '{char}' at position {self.pos}")

    def _skip_whitespace(self) -> None:
        """Skip whitespace characters."""
        while self.pos < self.length and self.expression[self.pos].isspace():
            self.pos += 1

    def _read_word(self) -> Token:
        """Read a word (keyword or field name)."""
        start = self.pos
        while self.pos < self.length and (
            self.expression[self.pos].isalnum() or self.expression[self.pos] == "_"
        ):
            self.pos += 1

        word = self.expression[start : self.pos]
        upper = word.upper()

        if upper == "AND":
            return Token(TOKEN_AND, "AND", start)
        elif upper == "OR":
            return Token(TOKEN_OR, "OR", start)
        else:
            return Token(TOKEN_FIELD, word, start)

    def tokenize(self) -> List[Token]:
        """Tokenize entire expression."""
        tokens = []
        while True:
            token = self.next_token()
            tokens.append(token)
            if token.type == TOKEN_EOF:
                break
        return tokens


# AST Node types
@dataclass
class FieldNode:
    """Leaf node representing a field name."""

    field: str


@dataclass
class BinaryOpNode:
    """Binary operation node (AND/OR)."""

    op: str  # "AND" or "OR"
    left: Union["FieldNode", "BinaryOpNode"]
    right: Union["FieldNode", "BinaryOpNode"]


ASTNode = Union[FieldNode, BinaryOpNode]


class Parser:
    """Parse tokenized stop condition into AST.

    Grammar:
        expr     -> or_expr
        or_expr  -> and_expr (OR and_expr)*
        and_expr -> primary (AND primary)*
        primary  -> FIELD | LPAREN expr RPAREN
    """

    def __init__(self, tokens: List[Token]):
        self.tokens = tokens
        self.pos = 0

    def parse(self) -> ASTNode:
        """Parse expression into AST."""
        if not self.tokens or self.tokens[0].type == TOKEN_EOF:
            raise StopConditionError("Empty stop condition expression")

        result = self._parse_or_expr()

        if self._current().type != TOKEN_EOF:
            raise StopConditionError(
                f"Unexpected token '{self._current().value}' at position {self._current().position}"
            )

        return result

    def _current(self) -> Token:
        """Get current token."""
        if self.pos < len(self.tokens):
            return self.tokens[self.pos]
        return Token(TOKEN_EOF, "", -1)

    def _advance(self) -> Token:
        """Advance and return current token."""
        token = self._current()
        self.pos += 1
        return token

    def _parse_or_expr(self) -> ASTNode:
        """Parse OR expression (lowest precedence)."""
        left = self._parse_and_expr()

        while self._current().type == TOKEN_OR:
            self._advance()  # consume OR
            right = self._parse_and_expr()
            left = BinaryOpNode(op="OR", left=left, right=right)

        return left

    def _parse_and_expr(self) -> ASTNode:
        """Parse AND expression (higher precedence than OR)."""
        left = self._parse_primary()

        while self._current().type == TOKEN_AND:
            self._advance()  # consume AND
            right = self._parse_primary()
            left = BinaryOpNode(op="AND", left=left, right=right)

        return left

    def _parse_primary(self) -> ASTNode:
        """Parse primary expression (field or parenthesized expr)."""
        token = self._current()

        if token.type == TOKEN_FIELD:
            self._advance()
            return FieldNode(field=token.value)

        if token.type == TOKEN_LPAREN:
            self._advance()  # consume (
            expr = self._parse_or_expr()
            if self._current().type != TOKEN_RPAREN:
                raise StopConditionError(
                    f"Expected ')' at position {self._current().position}, got '{self._current().value}'"
                )
            self._advance()  # consume )
            return expr

        raise StopConditionError(
            f"Expected field name or '(' at position {token.position}, got '{token.value}'"
        )


class StopCondition:
    """Compiled stop condition that can evaluate against row data.

    A field is considered "filled" if its value is a non-empty, non-whitespace string.
    """

    def __init__(self, expression: str):
        """Parse and compile stop condition expression.

        Args:
            expression: Boolean expression like "(email AND domain) OR phone"
        """
        self.expression = expression
        self._ast = self._compile(expression)
        self._fields = self._extract_fields(self._ast)

    @staticmethod
    def _compile(expression: str) -> ASTNode:
        """Compile expression into AST."""
        lexer = Lexer(expression)
        tokens = lexer.tokenize()
        parser = Parser(tokens)
        return parser.parse()

    @staticmethod
    def _extract_fields(node: ASTNode) -> List[str]:
        """Extract all field names from AST."""
        if isinstance(node, FieldNode):
            return [node.field]
        elif isinstance(node, BinaryOpNode):
            left_fields = StopCondition._extract_fields(node.left)
            right_fields = StopCondition._extract_fields(node.right)
            return left_fields + right_fields
        return []

    @property
    def required_fields(self) -> List[str]:
        """Return list of all fields referenced in the expression."""
        return list(set(self._fields))

    def evaluate(self, get_value: Callable[[str], Optional[str]]) -> bool:
        """Evaluate stop condition against field values.

        Args:
            get_value: Function that returns value for a field name, or None if not filled

        Returns:
            True if stop condition is satisfied
        """
        return self._evaluate_node(self._ast, get_value)

    def evaluate_row(self, row: Dict[str, str]) -> bool:
        """Evaluate stop condition against a row dictionary.

        Args:
            row: Dictionary mapping field names to values

        Returns:
            True if stop condition is satisfied
        """

        def get_value(field: str) -> Optional[str]:
            val = row.get(field)
            if val is None:
                return None
            if isinstance(val, str) and not val.strip():
                return None
            return val

        return self.evaluate(get_value)

    def evaluate_row_state(self, row_state: "RowState") -> bool:
        """Evaluate stop condition against a RowState object.

        Args:
            row_state: RowState with field_requests and original_row

        Returns:
            True if stop condition is satisfied
        """

        def get_value(field: str) -> Optional[str]:
            # Check field_requests first
            if field in row_state.field_requests:
                req = row_state.field_requests[field]
                if req.is_filled():
                    return req.value

            # Check original row
            val = row_state.original_row.get(field)
            if val and isinstance(val, str) and val.strip():
                return val

            return None

        return self.evaluate(get_value)

    def _evaluate_node(
        self, node: ASTNode, get_value: Callable[[str], Optional[str]]
    ) -> bool:
        """Recursively evaluate AST node."""
        if isinstance(node, FieldNode):
            value = get_value(node.field)
            return value is not None

        elif isinstance(node, BinaryOpNode):
            left_result = self._evaluate_node(node.left, get_value)

            if node.op == "AND":
                # Short-circuit: if left is False, result is False
                if not left_result:
                    return False
                return self._evaluate_node(node.right, get_value)

            elif node.op == "OR":
                # Short-circuit: if left is True, result is True
                if left_result:
                    return True
                return self._evaluate_node(node.right, get_value)

        return False

    def __repr__(self) -> str:
        return f"StopCondition({self.expression!r})"


# Convenience function
def parse_stop_condition(expression: str) -> StopCondition:
    """Parse a stop condition expression.

    Args:
        expression: Boolean expression like "(email AND domain) OR phone"

    Returns:
        Compiled StopCondition

    Raises:
        StopConditionError: If expression is invalid
    """
    return StopCondition(expression)
