"""Shorthand filter parser for JSONB structured content filtering.

Provides a concise syntax for building structured filters without writing JSON.
Multiple -w options are combined with AND logic.

Syntax Reference:
    field=value         Equal (string)
    field:=value        Equal (JSON: number, bool, null, array, object)
    field!=value        Not equal
    field>value         Greater than
    field>=value        Greater than or equal
    field<value         Less than
    field<=value        Less than or equal
    field~=pattern      Case-insensitive LIKE (ilike)
    field~~pattern      Case-sensitive LIKE
    field?=a,b,c        In list (comma-separated)
    field!?=a,b,c       Not in list
    field?              Key exists
    field!?             Key does not exist
    field@=jsonb        JSONB contains

Nested paths use dot notation:
    data.count>=10
    metadata.user.email~=%@gmail.com%

Examples:
    -w status=complete                    # status equals "complete"
    -w "count:=10"                        # count equals 10 (number)
    -w "active:=true"                     # active equals true (boolean)
    -w "data.score>=85"                   # nested path, gte comparison
    -w "email~=%@example.com%"            # case-insensitive pattern match
    -w "status?=pending,review,approved"  # status in list
    -w "error?"                           # error key exists
    -w "deprecated!?"                     # deprecated key does not exist

Usage with Typer:
    from cli.infrastructure.filter_parser import parse_where_filters

    @app.command()
    def my_command(
        where: Annotated[
            list[str] | None,
            typer.Option("--where", "-w", help="Filter conditions"),
        ] = None,
    ) -> None:
        filter_spec = parse_where_filters(where)
        # filter_spec is ConditionSpec | LogicalSpec | None
"""

import json
import re
from typing import Any, cast

import typer

from alloy_runtime_types.dtos.structured_filter import (
    ConditionOperator,
    ConditionSpec,
    LogicalSpec,
)

# Operator patterns ordered by specificity (longest match first)
# Format: (regex_pattern, literal_str, operator, has_value, value_parser)
# The regex_pattern is for matching, literal_str is the actual operator string
OPERATOR_PATTERNS: list[tuple[str, str, str, bool, str]] = [
    # Multi-char operators first (order matters - longer/more specific first)
    (r"!\?=", "!?=", "nin", True, "list"),  # not in list
    (r"\?=", "?=", "in", True, "list"),  # in list
    (r">=", ">=", "gte", True, "auto"),  # greater than or equal
    (r"<=", "<=", "lte", True, "auto"),  # less than or equal
    (r"~=", "~=", "ilike", True, "string"),  # case-insensitive like
    (r"~~", "~~", "like", True, "string"),  # case-sensitive like
    (r"@=", "@=", "contains", True, "json"),  # jsonb contains
    (r":=", ":=", "eq", True, "json"),  # equal with JSON value
    (r"!=", "!=", "ne", True, "auto"),  # not equal
    (r"!\?", "!?", "not_exists", False, "none"),  # key does not exist
    (r"\?", "?", "exists", False, "none"),  # key exists (must be after ?=)
    # Single-char operators last
    (r">", ">", "gt", True, "auto"),  # greater than
    (r"<", "<", "lt", True, "auto"),  # less than
    (r"=", "=", "eq", True, "auto"),  # equal (default string)
]

# Compile regex pattern for finding operators
# Matches field name, then operator, then optional value
FILTER_REGEX = re.compile(
    r"^([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)"  # field path
    r"(" + "|".join(p[0] for p in OPERATOR_PATTERNS) + r")"  # operator
    r"(.*)$"  # value (may be empty)
)


def _parse_auto_value(value: str) -> Any:
    """Parse a value with automatic type detection.

    Tries JSON parsing first, falls back to string.
    """
    if not value:
        return ""

    # Try to parse as JSON (handles numbers, bools, null)
    try:
        return json.loads(value)
    except json.JSONDecodeError:
        # Return as string
        return value


def _parse_json_value(value: str, field: str) -> Any:
    """Parse a value as JSON, raising error if invalid."""
    try:
        return json.loads(value)
    except json.JSONDecodeError as e:
        raise typer.BadParameter(
            f"Invalid JSON value for field '{field}': {value}. Error: {e}"
        )


def _parse_list_value(value: str) -> list[str]:
    """Parse a comma-separated list of values."""
    if not value:
        return []
    return [v.strip() for v in value.split(",") if v.strip()]


def parse_single_filter(expr: str) -> ConditionSpec:
    """Parse a single filter expression into a ConditionSpec.

    Args:
        expr: Filter expression (e.g., "status=complete", "count>=10")

    Returns:
        ConditionSpec for the filter

    Raises:
        typer.BadParameter: If expression is invalid
    """
    expr = expr.strip()
    if not expr:
        raise typer.BadParameter("Empty filter expression")

    match = FILTER_REGEX.match(expr)
    if not match:
        raise typer.BadParameter(
            f"Invalid filter syntax: '{expr}'. "
            "Expected format: field<op>value (e.g., status=complete, count>=10)"
        )

    field = match.group(1)
    op_str = match.group(2)
    raw_value = match.group(3)

    # Find the operator details by matching the literal string
    op_name = None
    has_value = True
    value_parser = "auto"

    for _regex_pattern, literal_str, name, needs_value, parser in OPERATOR_PATTERNS:
        if op_str == literal_str:
            op_name = name
            has_value = needs_value
            value_parser = parser
            break

    if op_name is None:
        raise typer.BadParameter(f"Unknown operator in: '{expr}'")

    # Validate value presence
    if has_value and not raw_value:
        raise typer.BadParameter(f"Missing value for operator '{op_str}' in: '{expr}'")

    if not has_value and raw_value:
        raise typer.BadParameter(
            f"Operator '{op_str}' does not take a value, but got: '{raw_value}'"
        )

    # Parse the value based on type
    value: Any = None
    if has_value:
        if value_parser == "json":
            value = _parse_json_value(raw_value, field)
        elif value_parser == "list":
            value = _parse_list_value(raw_value)
        elif value_parser == "string":
            value = raw_value
        else:  # auto
            value = _parse_auto_value(raw_value)

    # Cast op_name to the literal type since we've validated it above
    op_literal = cast(ConditionOperator, op_name)
    return ConditionSpec(field=field, op=op_literal, value=value)


def parse_where_filters(
    filters: list[str] | None,
) -> ConditionSpec | LogicalSpec | None:
    """Parse multiple -w filter expressions into a filter spec.

    Multiple filters are combined with AND logic.

    Args:
        filters: List of filter expressions from repeated -w options

    Returns:
        - None if no filters provided
        - ConditionSpec if single filter
        - LogicalSpec (AND) if multiple filters

    Raises:
        typer.BadParameter: If any expression is invalid

    Examples:
        >>> parse_where_filters(["status=complete"])
        ConditionSpec(field="status", op="eq", value="complete")

        >>> parse_where_filters(["status=complete", "count>=10"])
        LogicalSpec(op="and", conditions=[...])
    """
    if not filters:
        return None

    conditions = [parse_single_filter(f) for f in filters]

    if len(conditions) == 1:
        return conditions[0]

    # Cast to the expected type for LogicalSpec
    return LogicalSpec(op="and", conditions=list(conditions))


def merge_filter_specs(
    where_spec: ConditionSpec | LogicalSpec | None,
    json_spec: ConditionSpec | LogicalSpec | None,
) -> ConditionSpec | LogicalSpec | None:
    """Merge filter specs from -w and --structured-filter options.

    If both are provided, combines them with AND logic.

    Args:
        where_spec: Filter spec from -w options
        json_spec: Filter spec from --structured-filter JSON

    Returns:
        Combined filter spec, or None if both are None
    """
    if where_spec is None:
        return json_spec
    if json_spec is None:
        return where_spec

    # Both exist - combine with AND
    return LogicalSpec(op="and", conditions=[where_spec, json_spec])
