"""Expression serialization and deserialization.

This module provides functionality to serialize expression objects to strings
and deserialize strings back to expression objects using a grammar-based parser.
"""

from __future__ import annotations

import re
from collections.abc import Callable
from typing import Any
from urllib.parse import quote, unquote

import tatsu  # type: ignore[import-untyped]
from tatsu.ast import AST  # type: ignore[import-untyped]

from therismos.expr._expr import (
    AllExpr,
    AnyExpr,
    Eq,
    Expr,
    ExprVisitor,
    F,
    FalseExpr,
    Ge,
    Gt,
    In,
    IsNull,
    Le,
    Lt,
    Ne,
    NotExpr,
    Regex,
    TrueExpr,
)

# TatSu-compatible grammar
_GRAMMAR = r"""
@@grammar::CQL
@@ignorecase::False
@@whitespace :: /[ \t\r\n]+/
@@comments :: /#.*?$/

start
    =
    expr
    $
    ;

# -------------------------
# Precedence: NOT > AND > OR
# -------------------------

expr
    =
    or_expr
    ;

or_expr
    =
    and_expr {',' and_expr}*
    ;

and_expr
    =
    not_expr {';' not_expr}*
    ;

not_expr
    =
      '!' not_expr
    | primary
    ;

primary
    =
      atom
    | '(' expr ')'
    ;

# -------------------------
# Atoms
# -------------------------

atom
    =
      boolean_const
    | null_check
    | regex_expr
    | in_expr
    | comparison
    ;

boolean_const
    =
      'true()'
    | 'false()'
    ;

null_check
    =
    field:field null_op:null_op 'null'
    ;

null_op
    =
      '=='
    | '!='
    ;

comparison
    =
    field:field comp_op:comp_op value:value
    ;

comp_op
    =
      '=='
    | '!='
    | '<='
    | '>='
    | '<'
    | '>'
    ;

in_expr
    =
    field:field '=in=' '(' values:value_list ')'
    ;

value_list
    =
    value {',' value}*
    ;

regex_expr
    =
    field:field '~regex' '(' strings:string_list ')'
    ;

string_list
    =
    string [',' string]
    ;

# -------------------------
# Field with optional type annotation
# -------------------------

field
    =
    ident:ident [type_annot:type_annot]
    ;

type_annot
    =
    '{' type_name:type_name '}'
    ;

type_name
    =
    ident
    ;

# -------------------------
# Values
# -------------------------

value
    =
      string
    | number
    | boolean
    | 'null'
    | ident
    ;

boolean
    =
      'true'
    | 'false'
    ;

# -------------------------
# Lexical rules
# -------------------------

ident
    =
    /[A-Za-z_][A-Za-z0-9_\.]*/
    ;

number
    =
    /-?\d+(?:\.\d+)?/
    ;

# Double-quoted string with JSON-like escapes:
# \" \\ \n \t \r
string
    =
    /"(?:\\.|[^"\\])*"/
    ;
"""

# Compile the grammar once at module load time
_PARSER = tatsu.compile(_GRAMMAR)


class _SerializerVisitor(ExprVisitor[str]):
    """Visitor that converts expression objects to grammar-compliant strings."""

    def __init__(
        self,
        type_registry: dict[type | Callable[..., Any], str],
        include_all_types: bool,
        url_encode: bool,
    ) -> None:
        """Initialize the serializer visitor.

        :param type_registry: Mapping from Python types to type name strings
        :type type_registry: dict[type | Callable[..., Any], str]
        :param include_all_types: Whether to include type annotations for all fields
        :type include_all_types: bool
        :param url_encode: Whether to URL-encode the output
        :type url_encode: bool
        """
        self.type_registry = type_registry
        self.include_all_types = include_all_types
        self.url_encode = url_encode

    def _escape_string(self, s: str) -> str:
        """Escape a string for grammar output.

        :param s: The string to escape
        :type s: str
        :returns: The escaped string with quotes
        :rtype: str
        """
        # Escape special characters: \ " \n \t \r
        s = s.replace("\\", "\\\\")
        s = s.replace('"', '\\"')
        s = s.replace("\n", "\\n")
        s = s.replace("\t", "\\t")
        s = s.replace("\r", "\\r")
        return f'"{s}"'

    def _serialize_value(self, value: Any) -> str:
        """Serialize a value to grammar format.

        :param value: The value to serialize
        :type value: Any
        :returns: String representation of the value
        :rtype: str
        """
        if value is None:
            return "null"
        elif isinstance(value, bool):
            return "true" if value else "false"
        elif isinstance(value, str):
            return self._escape_string(value)
        elif isinstance(value, (int, float)):
            return str(value)
        else:
            # For other types, convert to string and escape
            return self._escape_string(str(value))

    def _format_field(self, field: Any) -> str:
        """Format a field with optional type annotation.

        :param field: The field object
        :type field: Any
        :returns: Formatted field string
        :rtype: str
        """
        field_name = field.name
        type_annot = ""

        # Add type annotation if requested or if explicitly typed
        if field.type_ is not None and self.include_all_types:
            type_name = self.type_registry.get(field.type_, str(field.type_))
            type_annot = f"{{{type_name}}}"
            # For explicit type casting, we could check if the field was explicitly typed
            # For now, we only include if include_all_types is True

        return f"{field_name}{type_annot}"

    def visit_eq(self, expr: Eq) -> str:
        """Visit an equality expression.

        :param expr: The equality expression
        :type expr: Eq
        :returns: String representation
        :rtype: str
        """
        field = self._format_field(expr.field)
        value = self._serialize_value(expr.casted_value())
        return f"{field}=={value}"

    def visit_ne(self, expr: Ne) -> str:
        """Visit an inequality expression.

        :param expr: The inequality expression
        :type expr: Ne
        :returns: String representation
        :rtype: str
        """
        field = self._format_field(expr.field)
        value = self._serialize_value(expr.casted_value())
        return f"{field}!={value}"

    def visit_lt(self, expr: Lt) -> str:
        """Visit a less-than expression.

        :param expr: The less-than expression
        :type expr: Lt
        :returns: String representation
        :rtype: str
        """
        field = self._format_field(expr.field)
        value = self._serialize_value(expr.casted_value())
        return f"{field}<{value}"

    def visit_le(self, expr: Le) -> str:
        """Visit a less-than-or-equal expression.

        :param expr: The less-than-or-equal expression
        :type expr: Le
        :returns: String representation
        :rtype: str
        """
        field = self._format_field(expr.field)
        value = self._serialize_value(expr.casted_value())
        return f"{field}<={value}"

    def visit_gt(self, expr: Gt) -> str:
        """Visit a greater-than expression.

        :param expr: The greater-than expression
        :type expr: Gt
        :returns: String representation
        :rtype: str
        """
        field = self._format_field(expr.field)
        value = self._serialize_value(expr.casted_value())
        return f"{field}>{value}"

    def visit_ge(self, expr: Ge) -> str:
        """Visit a greater-than-or-equal expression.

        :param expr: The greater-than-or-equal expression
        :type expr: Ge
        :returns: String representation
        :rtype: str
        """
        field = self._format_field(expr.field)
        value = self._serialize_value(expr.casted_value())
        return f"{field}>={value}"

    def visit_in(self, expr: In) -> str:
        """Visit an IN expression.

        :param expr: The IN expression
        :type expr: In
        :returns: String representation
        :rtype: str
        """
        field = self._format_field(expr.field)
        values = ",".join(self._serialize_value(v) for v in expr.casted_values())
        return f"{field}=in=({values})"

    def visit_regex(self, expr: Regex) -> str:
        """Visit a regex expression.

        :param expr: The regex expression
        :type expr: Regex
        :returns: String representation
        :rtype: str
        """
        field = self._format_field(expr.field)
        pattern = self._escape_string(expr.pattern)
        if expr.flags is not None:
            # Convert int flags to string and escape it
            flags = self._escape_string(str(expr.flags))
            return f"{field}~regex({pattern},{flags})"
        else:
            return f"{field}~regex({pattern})"

    def visit_is_null(self, expr: IsNull) -> str:
        """Visit an IS NULL expression.

        :param expr: The IS NULL expression
        :type expr: IsNull
        :returns: String representation
        :rtype: str
        """
        field = self._format_field(expr.field)
        op = "==" if expr.is_null else "!="
        return f"{field}{op}null"

    def visit_all(self, expr: AllExpr) -> str:
        """Visit an AND expression.

        :param expr: The AND expression
        :type expr: AllExpr
        :returns: String representation
        :rtype: str
        """
        if not expr.exprs:
            return "true()"
        parts = [e.accept(self) for e in expr.exprs]
        return "(" + ";".join(parts) + ")"

    def visit_any(self, expr: AnyExpr) -> str:
        """Visit an OR expression.

        :param expr: The OR expression
        :type expr: AnyExpr
        :returns: String representation
        :rtype: str
        """
        if not expr.exprs:
            return "false()"
        parts = [e.accept(self) for e in expr.exprs]
        return "(" + ",".join(parts) + ")"

    def visit_not(self, expr: NotExpr) -> str:
        """Visit a NOT expression.

        :param expr: The NOT expression
        :type expr: NotExpr
        :returns: String representation
        :rtype: str
        """
        inner = expr.expr.accept(self)
        # Add parentheses if the inner expression is compound
        if isinstance(expr.expr, (AllExpr, AnyExpr)):
            return f"!{inner}"
        else:
            return f"!({inner})"

    def visit_true(self, expr: TrueExpr) -> str:
        """Visit a TRUE constant expression.

        :param expr: The TRUE expression
        :type expr: TrueExpr
        :returns: String representation
        :rtype: str
        """
        return "true()"

    def visit_false(self, expr: FalseExpr) -> str:
        """Visit a FALSE constant expression.

        :param expr: The FALSE expression
        :type expr: FalseExpr
        :returns: String representation
        :rtype: str
        """
        return "false()"


class _DeserializerSemantics:
    """TatSu semantics class for converting parsed AST to expression objects."""

    def __init__(
        self,
        type_registry: dict[str, type | Callable[..., Any]],
        field_types: dict[str, type | Callable[..., Any]],
        sanitizer: Callable[[str, Any, type | Callable[..., Any] | None], Any] | None = None,
    ) -> None:
        """Initialize the deserializer semantics.

        :param type_registry: Mapping from type name strings to Python types
        :type type_registry: dict[str, type | Callable[..., Any]]
        :param field_types: Mapping from field names to implicit types
        :type field_types: dict[str, type | Callable[..., Any]]
        :param sanitizer: Optional callback for sanitizing values. Receives field name,
                         value, and expected type. Should return the sanitized value.
        :type sanitizer: Callable[[str, Any, type | Callable[..., Any] | None], Any] | None
        """
        self.type_registry = type_registry
        self.field_types = field_types
        self.sanitizer = sanitizer

    def _parse_field(self, ast: AST) -> Any:
        """Parse a field AST node.

        :param ast: The AST node for a field
        :type ast: AST
        :returns: A Field object
        :rtype: Any
        """
        field_name = ast.ident if hasattr(ast, "ident") else ast["ident"]
        field_type = None

        type_annot = ast.type_annot if hasattr(ast, "type_annot") else ast.get("type_annot")
        if type_annot:
            # Explicit type annotation in the expression string
            type_name = (
                type_annot.type_name
                if hasattr(type_annot, "type_name")
                else type_annot["type_name"]
            )
            field_type = self.type_registry.get(type_name)
        elif field_name in self.field_types:
            # Use implicit type mapping if no explicit annotation
            field_type = self.field_types[field_name]

        return F(field_name, field_type)

    def _parse_value(self, val: Any) -> Any:
        """Parse a value from the AST.

        :param val: The value from AST
        :type val: Any
        :returns: The parsed Python value
        :rtype: Any
        """
        if isinstance(val, str):
            # Check if it's a string literal (starts with quote)
            if val.startswith('"'):
                # Unescape the string
                s = val[1:-1]  # Remove quotes
                s = s.replace("\\n", "\n")
                s = s.replace("\\t", "\t")
                s = s.replace("\\r", "\r")
                s = s.replace('\\"', '"')
                s = s.replace("\\\\", "\\")
                return s
            elif val == "null":
                return None
            elif val == "true":
                return True
            elif val == "false":
                return False
            elif re.match(r"^-?\d+(?:\.\d+)?$", val):
                # It's a number
                if "." in val:
                    return float(val)
                else:
                    return int(val)
            else:
                # It's an identifier (unquoted string)
                return val
        return val

    def boolean_const(self, ast: str) -> TrueExpr | FalseExpr:
        """Parse a boolean constant.

        :param ast: The AST node
        :type ast: str
        :returns: TrueExpr or FalseExpr
        :rtype: TrueExpr | FalseExpr
        """
        if ast == "true()":
            return TrueExpr()
        else:
            return FalseExpr()

    def null_check(self, ast: AST) -> IsNull:
        """Parse a null check expression.

        :param ast: The AST node
        :type ast: AST
        :returns: IsNull expression
        :rtype: IsNull
        """
        field = self._parse_field(ast.field if hasattr(ast, "field") else ast["field"])
        null_op = ast.null_op if hasattr(ast, "null_op") else ast["null_op"]
        is_null = null_op == "=="
        return IsNull(field, is_null)

    def comparison(self, ast: AST) -> Eq | Ne | Lt | Le | Gt | Ge:
        """Parse a comparison expression.

        :param ast: The AST node
        :type ast: AST
        :returns: Comparison expression (Eq, Ne, Lt, Le, Gt, Ge)
        :rtype: Eq | Ne | Lt | Le | Gt | Ge
        """
        field = self._parse_field(ast.field if hasattr(ast, "field") else ast["field"])
        op = ast.comp_op if hasattr(ast, "comp_op") else ast["comp_op"]
        value = self._parse_value(ast.value if hasattr(ast, "value") else ast["value"])

        # Apply sanitizer if provided
        if self.sanitizer is not None:
            value = self.sanitizer(field.name, value, field.type_)

        # Cast value to field's type if type is specified
        if field.type_ is not None:
            value = field.cast(value)

        if op == "==":
            return Eq(field, value)
        elif op == "!=":
            return Ne(field, value)
        elif op == "<":
            return Lt(field, value)
        elif op == "<=":
            return Le(field, value)
        elif op == ">":
            return Gt(field, value)
        elif op == ">=":
            return Ge(field, value)
        else:
            raise ValueError(f"Unknown comparison operator: {op}")

    def in_expr(self, ast: AST) -> In:
        """Parse an IN expression.

        :param ast: The AST node
        :type ast: AST
        :returns: In expression
        :rtype: In
        """
        field = self._parse_field(ast.field if hasattr(ast, "field") else ast["field"])

        # Handle value_list structure: (first, [[sep, second], [sep, third], ...])
        # Use dictionary access to avoid AST method collision
        values_raw = ast.get("values") or ast.get("values_") or ast["values"]

        if isinstance(values_raw, (list, tuple)) and len(values_raw) == 2:
            first_val = values_raw[0]
            rest_vals = values_raw[1]

            values = [self._parse_value(first_val)]
            if rest_vals:
                for sep_val_pair in rest_vals:
                    if isinstance(sep_val_pair, list) and len(sep_val_pair) == 2:
                        values.append(self._parse_value(sep_val_pair[1]))
        else:
            # Fallback for single value or unexpected structure
            values = [self._parse_value(values_raw)]

        # Apply sanitizer if provided
        if self.sanitizer is not None:
            values = [self.sanitizer(field.name, v, field.type_) for v in values]

        # Cast values to field's type if type is specified
        if field.type_ is not None:
            values = [field.cast(v) for v in values]

        return In(field, tuple(values))

    def regex_expr(self, ast: AST) -> Regex:
        """Parse a regex expression.

        :param ast: The AST node
        :type ast: AST
        :returns: Regex expression
        :rtype: Regex
        """
        field = self._parse_field(ast.field if hasattr(ast, "field") else ast["field"])

        # Handle strings structure: either a single string or (string, ',', string)
        # Use dictionary access to avoid AST method collision
        strings_raw = ast.get("strings") or ast["strings"]

        if isinstance(strings_raw, str):
            # Single string (no flags)
            pattern = self._parse_value(strings_raw)
            flags = None
        elif isinstance(strings_raw, (list, tuple)):
            # List/tuple: (pattern, ',', flags) for optional second string
            pattern = self._parse_value(strings_raw[0])
            flags_parsed = None
            if len(strings_raw) >= 3:  # Has second string after comma
                flags_str = self._parse_value(strings_raw[2])
                # Convert string flags to int
                try:
                    flags_parsed = int(flags_str)
                except (ValueError, TypeError):
                    # If it's not a valid int, keep as None
                    flags_parsed = None
            flags = flags_parsed
        else:
            # Unexpected structure
            pattern = self._parse_value(strings_raw)
            flags = None

        return Regex(field, pattern, flags)

    def not_expr(self, ast: Any) -> Expr:
        """Parse a NOT expression.

        :param ast: The AST node
        :type ast: Any
        :returns: NotExpr or the inner expression
        :rtype: Expr
        """
        # Check for NOT structure: tuple/list like ('!', inner_expr) or nested structure
        if isinstance(ast, (tuple, list)):
            # Flatten and check for '!' marker
            flat = self._flatten_primary(ast)
            if isinstance(flat, tuple) and len(flat) == 2 and flat[0] == "!":
                return NotExpr(flat[1])

        # Not a NOT expression, return as-is
        return ast  # type: ignore[no-any-return]

    def _flatten_primary(self, ast: Any) -> Any:
        """Flatten primary expression parentheses: ('(', expr, ')') -> expr."""
        if isinstance(ast, (tuple, list)):
            if len(ast) == 3 and ast[0] == "(" and ast[2] == ")":
                # Parenthesized expression
                return ast[1]
            elif len(ast) == 2 and ast[0] == "!":
                # NOT expression
                return ("!", self._flatten_primary(ast[1]))
        return ast

    def primary(self, ast: Any) -> Expr:
        """Handle primary expressions (atoms or parenthesized expressions).

        :param ast: The AST node
        :type ast: Any
        :returns: The expression
        :rtype: Expr
        """
        # Flatten parentheses and return the inner expression
        return self._flatten_primary(ast)  # type: ignore[no-any-return]

    def and_expr(self, ast: Any) -> Expr:
        """Parse an AND expression.

        :param ast: The AST node
        :type ast: Any
        :returns: AllExpr or single expression
        :rtype: Expr
        """
        # TatSu returns (first, [[sep, second], [sep, third], ...]) for {sep elem}*
        if isinstance(ast, (list, tuple)) and len(ast) == 2:
            first, rest_parts = ast

            if not rest_parts or len(rest_parts) == 0:
                # Only one element
                return first  # type: ignore[no-any-return]
            else:
                # Multiple elements: extract from [[';', expr], [';', expr], ...]
                exprs = [first]
                for sep_expr_pair in rest_parts:
                    if isinstance(sep_expr_pair, list) and len(sep_expr_pair) == 2:
                        exprs.append(sep_expr_pair[1])  # Get the expression, skip the ';'
                return AllExpr(*exprs)

        return ast  # type: ignore[no-any-return]

    def or_expr(self, ast: Any) -> Expr:
        """Parse an OR expression.

        :param ast: The AST node
        :type ast: Any
        :returns: AnyExpr or single expression
        :rtype: Expr
        """
        # TatSu returns (first, [[sep, second], [sep, third], ...]) for {sep elem}*
        if isinstance(ast, (list, tuple)) and len(ast) == 2:
            first, rest_parts = ast

            if not rest_parts or len(rest_parts) == 0:
                # Only one element
                return first  # type: ignore[no-any-return]
            else:
                # Multiple elements: extract from [[',', expr], [',', expr], ...]
                exprs = [first]
                for sep_expr_pair in rest_parts:
                    if isinstance(sep_expr_pair, list) and len(sep_expr_pair) == 2:
                        exprs.append(sep_expr_pair[1])  # Get the expression, skip the ','
                return AnyExpr(*exprs)

        return ast  # type: ignore[no-any-return]


class Serializer:
    """Serializer for converting expressions to/from string representation."""

    _DEFAULT_TYPE_REGISTRY: dict[type | Callable[..., Any], str] = {
        int: "int",
        float: "float",
        str: "str",
        bool: "bool",
    }

    def __init__(
        self,
        type_registry: dict[type | Callable[..., Any], str] | None = None,
        field_types: dict[str, type | Callable[..., Any]] | None = None,
        include_all_types: bool = False,
        url_encode: bool = False,
        sanitizer: Callable[[str, Any, type | Callable[..., Any] | None], Any] | None = None,
    ) -> None:
        """Initialize the serializer.

        :param type_registry: Custom type registry for type name mapping.
                             If None, uses default registry.
        :type type_registry: dict[type | Callable[..., Any], str] | None
        :param field_types: Implicit type mapping for field names. Maps field names
                           to their expected types, so `field_name==value` is treated
                           as `field_name{type}==value` automatically.
        :type field_types: dict[str, type | Callable[..., Any]] | None
        :param include_all_types: Whether to include type annotations for all fields
                                 when serializing. If False, only explicit type casts
                                 are included.
        :type include_all_types: bool
        :param url_encode: Whether to URL-encode the serialized output and
                          URL-decode input during deserialization. Useful when
                          expressions are used in URL query strings.
        :type url_encode: bool
        :param sanitizer: Optional callback for sanitizing values during deserialization.
                         Receives field name, value, and expected type. Should return
                         the sanitized value.
        :type sanitizer: Callable[[str, Any, type | Callable[..., Any] | None], Any] | None
        """
        if type_registry is None:
            self.type_registry = self._DEFAULT_TYPE_REGISTRY.copy()
        else:
            # Merge with defaults
            self.type_registry = {**self._DEFAULT_TYPE_REGISTRY, **type_registry}

        self.field_types: dict[str, type | Callable[..., Any]] = field_types or {}
        self.include_all_types = include_all_types
        self.url_encode = url_encode
        self.sanitizer = sanitizer

        # Create reverse mapping for deserialization
        self._reverse_type_registry: dict[str, type | Callable[..., Any]] = {
            v: k for k, v in self.type_registry.items()
        }

    def register_type(self, type_obj: type | Callable[..., Any], type_name: str) -> None:
        """Register a custom type for serialization/deserialization.

        :param type_obj: The Python type or callable
        :type type_obj: type | Callable[..., Any]
        :param type_name: The string name to use in serialized form
        :type type_name: str
        """
        self.type_registry[type_obj] = type_name
        self._reverse_type_registry[type_name] = type_obj

    def register_field_type(self, field_name: str, field_type: type | Callable[..., Any]) -> None:
        """Register an implicit type for a field name.

        When deserializing, fields with this name will automatically use the
        specified type even without an explicit type annotation.

        :param field_name: The field name
        :type field_name: str
        :param field_type: The Python type or callable to use for this field
        :type field_type: type | Callable[..., Any]
        """
        self.field_types[field_name] = field_type

    def serialize(self, expr: Expr) -> str:
        """Serialize an expression to a string.

        :param expr: The expression to serialize
        :type expr: Expr
        :returns: String representation of the expression
        :rtype: str
        """
        visitor = _SerializerVisitor(self.type_registry, self.include_all_types, self.url_encode)
        result = expr.accept(visitor)
        if self.url_encode:
            result = quote(result, safe="")
        return result

    def deserialize(self, s: str) -> Expr:
        """Deserialize a string to an expression.

        :param s: The string to deserialize
        :type s: str
        :returns: The parsed expression object
        :rtype: Expr
        :raises: tatsu.exceptions.FailedParse if the string is not valid
        """
        if self.url_encode:
            s = unquote(s)
        semantics = _DeserializerSemantics(
            self._reverse_type_registry, self.field_types, self.sanitizer
        )
        result = _PARSER.parse(s, semantics=semantics)
        return result  # type: ignore[no-any-return]


__all__ = ["Serializer"]
