"""Core expression module for therismos query modeling library.

This module provides the Abstract Syntax Tree (AST) implementation for modeling
generic queries and filters in a backend-agnostic way.
"""

from __future__ import annotations

import re
from abc import ABC, abstractmethod
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic, Protocol, TypeVar

T = TypeVar("T", covariant=True)


class ExprVisitor(Protocol, Generic[T]):
    """Visitor interface for traversing expression trees.

    Implementations should provide visit methods for each expression type.
    The generic type parameter T specifies the return type of visit methods.
    """

    def visit_eq(self, expr: Eq) -> T: ...
    def visit_ne(self, expr: Ne) -> T: ...
    def visit_lt(self, expr: Lt) -> T: ...
    def visit_le(self, expr: Le) -> T: ...
    def visit_gt(self, expr: Gt) -> T: ...
    def visit_ge(self, expr: Ge) -> T: ...
    def visit_regex(self, expr: Regex) -> T: ...
    def visit_in(self, expr: In) -> T: ...
    def visit_is_null(self, expr: IsNull) -> T: ...
    def visit_true(self, expr: TrueExpr) -> T: ...
    def visit_false(self, expr: FalseExpr) -> T: ...
    def visit_all(self, expr: AllExpr) -> T: ...
    def visit_any(self, expr: AnyExpr) -> T: ...
    def visit_not(self, expr: NotExpr) -> T: ...


class Expr(ABC):
    """Abstract base class for all expressions.

    All expression nodes are immutable and implement the visitor pattern
    via the accept method.
    """

    @abstractmethod
    def accept(self, visitor: ExprVisitor[T]) -> T:
        """Accept a visitor and dispatch to the appropriate visit method.

        :param visitor: The visitor to accept.
        :type visitor: ExprVisitor[T]
        :returns: The result of the visitor's visit method.
        :rtype: T
        """
        ...

    @abstractmethod
    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate this expression against the provided data.

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: The boolean result of evaluating the expression.
        :rtype: bool
        :raises KeyError: If a required field is not present in data.
        :raises TypeError: If a field value cannot be cast to the expected type.
        :raises ValueError: If a field value cannot be cast to the expected type.
        """
        ...

    def __and__(self, other: Expr) -> AllExpr:
        """Combine this expression with another using AND logic."""
        return AllExpr(self, other)

    def __or__(self, other: Expr) -> AnyExpr:
        """Combine this expression with another using OR logic."""
        return AnyExpr(self, other)

    def __invert__(self) -> NotExpr:
        """Negate this expression using NOT logic."""
        return NotExpr(self)


@dataclass(frozen=True)
class Field:
    """Represents a typed field in a query.

    Fields can optionally declare an expected type for automatic casting
    at the conversion level.

    :ivar name: The logical field name.
    :vartype name: str
    :ivar type_: Optional Python type or cast function for value conversion.
    :vartype type_: type[Any] | Callable[[Any], Any] | None
    """

    name: str
    type_: type[Any] | Callable[[Any], Any] | None = None

    def __str__(self) -> str:
        """Return a string representation of the field."""
        return self.name

    def cast(self, value: Any) -> Any:
        """Cast a value to this field's type.

        :param value: The value to cast.
        :type value: Any
        :returns: The casted value.
        :rtype: Any
        :raises TypeError: If the cast fails.
        :raises ValueError: If the cast fails.
        """
        if self.type_ is None:
            return value

        # If the type is a class (not a callable), check if value is already an instance
        if isinstance(self.type_, type) and isinstance(value, self.type_):
            return value

        try:
            return self.type_(value)
        except (TypeError, ValueError) as e:
            raise type(e)(f"Failed to cast {value!r} to {self.type_}: {e}") from e

    def __eq__(self, other: Any) -> Eq:  # type: ignore[override]
        """Create an equality expression."""
        return Eq(self, other)

    def __ne__(self, other: Any) -> Ne:  # type: ignore[override]
        """Create an inequality expression."""
        return Ne(self, other)

    def __lt__(self, other: Any) -> Lt:
        """Create a less-than expression."""
        return Lt(self, other)

    def __le__(self, other: Any) -> Le:
        """Create a less-than-or-equal expression."""
        return Le(self, other)

    def __gt__(self, other: Any) -> Gt:
        """Create a greater-than expression."""
        return Gt(self, other)

    def __ge__(self, other: Any) -> Ge:
        """Create a greater-than-or-equal expression."""
        return Ge(self, other)

    def matches(self, pattern: str, flags: int | None = None) -> Regex:
        """Create a regex matching expression.

        :param pattern: The regular expression pattern.
        :type pattern: str
        :param flags: Optional regex flags (e.g., re.IGNORECASE).
        :type flags: int | None
        :returns: A Regex expression.
        :rtype: Regex
        """
        return Regex(self, pattern, flags)

    def is_in(self, *values: Any) -> In:
        """Create a membership expression with individual values.

        :param values: The values to check membership against.
        :type values: Any
        :returns: An In expression.
        :rtype: In
        """
        return In(self, values)

    def is_one_of(self, iterable: tuple[Any, ...] | list[Any]) -> In:
        """Create a membership expression from an iterable.

        :param iterable: An iterable of values to check membership against.
        :type iterable: tuple[Any, ...] | list[Any]
        :returns: An In expression.
        :rtype: In
        """
        return In(self, tuple(iterable))

    def is_null(self) -> IsNull:
        """Create a null-checking expression.

        :returns: An IsNull expression with is_null=True.
        :rtype: IsNull
        """
        return IsNull(self, is_null=True)

    def is_not_null(self) -> IsNull:
        """Create a not-null-checking expression.

        :returns: An IsNull expression with is_null=False.
        :rtype: IsNull
        """
        return IsNull(self, is_null=False)


def F(name: str, type_: type[Any] | Callable[[Any], Any] | None = None) -> Field:  # noqa: N802
    """Helper function for creating fields.

    :param name: The field name.
    :type name: str
    :param type_: Optional type or cast function.
    :type type_: type[Any] | Callable[[Any], Any] | None
    :returns: A Field instance.
    :rtype: Field
    """
    return Field(name, type_)


# ============================================================================
# Atomic Expressions
# ============================================================================


@dataclass(frozen=True)
class Eq(Expr):
    """Equality comparison expression."""

    field: Field
    value: Any

    def __str__(self) -> str:
        """Return a string representation of the equality expression."""
        return f"{self.field} == {self.value!r}"

    def casted_value(self) -> Any:
        """Get the value casted to the field's type."""
        return self.field.cast(self.value)

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_eq(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate equality comparison.

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if field value equals the expected value.
        :rtype: bool
        """
        field_value = self.field.cast(data[self.field.name])
        expected_value = self.casted_value()
        return bool(field_value == expected_value)


@dataclass(frozen=True)
class Ne(Expr):
    """Inequality comparison expression."""

    field: Field
    value: Any

    def __str__(self) -> str:
        """Return a string representation of the inequality expression."""
        return f"{self.field} != {self.value!r}"

    def casted_value(self) -> Any:
        """Get the value casted to the field's type."""
        return self.field.cast(self.value)

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_ne(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate inequality comparison.

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if field value does not equal the expected value.
        :rtype: bool
        """
        field_value = self.field.cast(data[self.field.name])
        expected_value = self.casted_value()
        return bool(field_value != expected_value)


@dataclass(frozen=True)
class Lt(Expr):
    """Less-than comparison expression."""

    field: Field
    value: Any

    def __str__(self) -> str:
        """Return a string representation of the less-than expression."""
        return f"{self.field} < {self.value!r}"

    def casted_value(self) -> Any:
        """Get the value casted to the field's type."""
        return self.field.cast(self.value)

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_lt(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate less-than comparison.

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if field value is less than the expected value.
        :rtype: bool
        """
        field_value = self.field.cast(data[self.field.name])
        expected_value = self.casted_value()
        return bool(field_value < expected_value)


@dataclass(frozen=True)
class Le(Expr):
    """Less-than-or-equal comparison expression."""

    field: Field
    value: Any

    def __str__(self) -> str:
        """Return a string representation of the less-than-or-equal expression."""
        return f"{self.field} <= {self.value!r}"

    def casted_value(self) -> Any:
        """Get the value casted to the field's type."""
        return self.field.cast(self.value)

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_le(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate less-than-or-equal comparison.

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if field value is less than or equal to the expected value.
        :rtype: bool
        """
        field_value = self.field.cast(data[self.field.name])
        expected_value = self.casted_value()
        return bool(field_value <= expected_value)


@dataclass(frozen=True)
class Gt(Expr):
    """Greater-than comparison expression."""

    field: Field
    value: Any

    def __str__(self) -> str:
        """Return a string representation of the greater-than expression."""
        return f"{self.field} > {self.value!r}"

    def casted_value(self) -> Any:
        """Get the value casted to the field's type."""
        return self.field.cast(self.value)

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_gt(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate greater-than comparison.

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if field value is greater than the expected value.
        :rtype: bool
        """
        field_value = self.field.cast(data[self.field.name])
        expected_value = self.casted_value()
        return bool(field_value > expected_value)


@dataclass(frozen=True)
class Ge(Expr):
    """Greater-than-or-equal comparison expression."""

    field: Field
    value: Any

    def __str__(self) -> str:
        """Return a string representation of the greater-than-or-equal expression."""
        return f"{self.field} >= {self.value!r}"

    def casted_value(self) -> Any:
        """Get the value casted to the field's type."""
        return self.field.cast(self.value)

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_ge(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate greater-than-or-equal comparison.

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if field value is greater than or equal to the expected value.
        :rtype: bool
        """
        field_value = self.field.cast(data[self.field.name])
        expected_value = self.casted_value()
        return bool(field_value >= expected_value)


@dataclass(frozen=True)
class Regex(Expr):
    """Regular expression matching expression."""

    field: Field
    pattern: str
    flags: int | None = None

    def __str__(self) -> str:
        """Return a string representation of the regex expression."""
        if self.flags is not None:
            return f"{self.field} MATCHES {self.pattern!r} (flags={self.flags})"
        return f"{self.field} MATCHES {self.pattern!r}"

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_regex(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate regex pattern matching.

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if field value matches the regex pattern.
        :rtype: bool
        """
        field_value = str(data[self.field.name])
        flags = self.flags if self.flags is not None else 0
        return re.search(self.pattern, field_value, flags) is not None


@dataclass(frozen=True)
class In(Expr):
    """Membership (IN) expression."""

    field: Field
    values: tuple[Any, ...]

    def __str__(self) -> str:
        """Return a string representation of the IN expression."""
        values_str = ", ".join(repr(v) for v in self.values)
        return f"{self.field} IN ({values_str})"

    def casted_values(self) -> tuple[Any, ...]:
        """Get the values casted to the field's type."""
        return tuple(self.field.cast(v) for v in self.values)

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_in(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate membership check.

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if field value is in the list of allowed values.
        :rtype: bool
        """
        field_value = self.field.cast(data[self.field.name])
        casted_values = self.casted_values()
        return field_value in casted_values


@dataclass(frozen=True)
class IsNull(Expr):
    """Null-checking expression."""

    field: Field
    is_null: bool = True

    def __str__(self) -> str:
        """Return a string representation of the null-checking expression."""
        if self.is_null:
            return f"{self.field} IS NULL"
        return f"{self.field} IS NOT NULL"

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_is_null(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate null check.

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if the null condition matches expectation.
        :rtype: bool
        """
        field_value = data[self.field.name]
        is_none = field_value is None
        return is_none if self.is_null else not is_none


@dataclass(frozen=True)
class TrueExpr(Expr):
    """Logical TRUE constant expression."""

    def __str__(self) -> str:
        """Return a string representation of the TRUE expression."""
        return "TRUE"

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_true(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate TRUE constant.

        :param data: Dictionary mapping field names to their values (unused).
        :type data: dict[str, Any]
        :returns: Always True.
        :rtype: bool
        """
        return True


@dataclass(frozen=True)
class FalseExpr(Expr):
    """Logical FALSE constant expression."""

    def __str__(self) -> str:
        """Return a string representation of the FALSE expression."""
        return "FALSE"

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_false(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate FALSE constant.

        :param data: Dictionary mapping field names to their values (unused).
        :type data: dict[str, Any]
        :returns: Always False.
        :rtype: bool
        """
        return False


# Singleton instances for TRUE and FALSE
TRUE = TrueExpr()
FALSE = FalseExpr()


# ============================================================================
# Compound Expressions
# ============================================================================


@dataclass(frozen=True)
class AllExpr(Expr):
    """AND expression that automatically flattens nested AllExpr nodes.

    AllExpr([a, AllExpr([b, c]), d]) normalizes to AllExpr([a, b, c, d])
    """

    exprs: tuple[Expr, ...]

    def __init__(self, *exprs: Expr) -> None:
        """Initialize an AllExpr with automatic flattening.

        :param exprs: The expressions to combine with AND logic.
        :type exprs: Expr
        """
        # Flatten nested AllExpr nodes
        flattened: list[Expr] = []
        for expr in exprs:
            if isinstance(expr, AllExpr):
                flattened.extend(expr.exprs)
            else:
                flattened.append(expr)

        # Use object.__setattr__ since the class is frozen
        object.__setattr__(self, "exprs", tuple(flattened))

    def __str__(self) -> str:
        """Return a string representation of the AND expression."""
        if not self.exprs:
            return "AND()"
        if len(self.exprs) == 1:
            return f"AND({self.exprs[0]})"
        parts = " AND ".join(str(expr) for expr in self.exprs)
        return f"({parts})"

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_all(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate AND expression (all subexpressions must be True).

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if all subexpressions evaluate to True.
        :rtype: bool
        """
        return all(expr.eval(data) for expr in self.exprs)


@dataclass(frozen=True)
class AnyExpr(Expr):
    """OR expression that automatically flattens nested AnyExpr nodes.

    AnyExpr([a, AnyExpr([b, c]), d]) normalizes to AnyExpr([a, b, c, d])
    """

    exprs: tuple[Expr, ...]

    def __init__(self, *exprs: Expr) -> None:
        """Initialize an AnyExpr with automatic flattening.

        :param exprs: The expressions to combine with OR logic.
        :type exprs: Expr
        """
        # Flatten nested AnyExpr nodes
        flattened: list[Expr] = []
        for expr in exprs:
            if isinstance(expr, AnyExpr):
                flattened.extend(expr.exprs)
            else:
                flattened.append(expr)

        # Use object.__setattr__ since the class is frozen
        object.__setattr__(self, "exprs", tuple(flattened))

    def __str__(self) -> str:
        """Return a string representation of the OR expression."""
        if not self.exprs:
            return "OR()"
        if len(self.exprs) == 1:
            return f"OR({self.exprs[0]})"
        parts = " OR ".join(str(expr) for expr in self.exprs)
        return f"({parts})"

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_any(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate OR expression (at least one subexpression must be True).

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if at least one subexpression evaluates to True.
        :rtype: bool
        """
        return any(expr.eval(data) for expr in self.exprs)


@dataclass(frozen=True)
class NotExpr(Expr):
    """NOT expression."""

    expr: Expr

    def __str__(self) -> str:
        """Return a string representation of the NOT expression."""
        return f"NOT ({self.expr})"

    def accept(self, visitor: ExprVisitor[T]) -> T:
        return visitor.visit_not(self)

    def eval(self, data: dict[str, Any]) -> bool:
        """Evaluate NOT expression (negation of subexpression).

        :param data: Dictionary mapping field names to their values.
        :type data: dict[str, Any]
        :returns: True if the subexpression evaluates to False.
        :rtype: bool
        """
        return not self.expr.eval(data)


__all__ = [
    "Expr",
    "Field",
    "F",
    "ExprVisitor",
    # Atomic expressions
    "Eq",
    "Ne",
    "Lt",
    "Le",
    "Gt",
    "Ge",
    "Regex",
    "In",
    "IsNull",
    "TrueExpr",
    "FalseExpr",
    "TRUE",
    "FALSE",
    # Compound expressions
    "AllExpr",
    "AnyExpr",
    "NotExpr",
]
