"""
Field-level filtering implementation for sparqlmojo ORM.

This module provides comparison operators and filtering expressions that
allow intuitive field-level filtering syntax like:
    session.query(Label).filter(Label.entity_iri == 'Q682')
    session.query(Person).filter(Person.age > 18)
    session.query(Product).filter(Product.name.contains("book"))
"""

from enum import Enum, StrEnum
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union

from sparqlmojo.exc import UnknownFieldError

from .model import IRIField, LiteralField
from .security import escape_sparql_literal, validate_sparql_operator

if TYPE_CHECKING:
    from .model import Model, RDFFieldInfo

# Type variable for model classes - bound to Model for type safety
T = TypeVar("T", bound="Model")


class FilterLike(Protocol):
    """Protocol for objects that can be converted to SPARQL FILTER clauses."""

    def to_sparql(self, model_class: type[T] | None = None) -> str:
        """Convert to SPARQL FILTER clause string."""
        ...


class FilterOperator(Enum):
    """Enumeration of supported filter operators."""

    EQ = "="
    NE = "!="
    LT = "<"
    GT = ">"
    LE = "<="
    GE = ">="
    CONTAINS = "CONTAINS"
    STARTSWITH = "STRSTARTS"
    ENDSWITH = "STRENDS"
    IN = "IN"
    NOT_IN = "NOT IN"


class LogicalOperator(Enum):
    """Enumeration of supported logical operators."""

    AND = "&&"
    OR = "||"
    NOT = "NOT"  # Use "NOT" not "!" to match to_sparql() check


class StringFunctionWrapper(StrEnum):
    """SPARQL string transformation functions.

    Uses StrEnum so members can be used directly in string formatting
    without .value (e.g., f"{STR}(?var)" produces "STR(?var)").
    """

    STR = "STR"  # Convert IRI to string
    LCASE = "LCASE"  # Convert to lowercase
    UCASE = "UCASE"  # Convert to uppercase


class FilterExpression:
    """
    Base class for filter expressions.

    Filter expressions represent conditions that can be evaluated
    in SPARQL FILTER clauses.
    """

    def __init__(
        self,
        field_name: str,
        operator: FilterOperator,
        value: Any,
        string_wrappers: list["StringFunctionWrapper"] | None = None,
    ):
        """
        Initialize a filter expression.

        Args:
            field_name: Name of the field to filter on
            operator: Filter operator to use
            value: Value to compare against
            string_wrappers: Optional list of string functions to wrap the variable
        """
        self.field_name = field_name
        self.operator = operator
        self.value = value
        self.string_wrappers: list[StringFunctionWrapper] = string_wrappers or []

    def __repr__(self) -> str:
        """Return string representation for debugging."""
        return (
            f"FilterExpression(field={self.field_name!r}, "
            f"operator={self.operator!r}, value={self.value!r})"
        )

    def _get_field_info(self, model_class: type[T] | None) -> "RDFFieldInfo | None":
        """Get field info from model class if available.

        Args:
            model_class: Optional model class to look up field info from

        Returns:
            RDFFieldInfo if field exists in model, None otherwise
        """
        if model_class is None:
            return None

        rdf_fields: dict[str, RDFFieldInfo] = getattr(model_class, "_rdf_fields", {})
        return rdf_fields.get(self.field_name)

    def _format_value(self, value: Any, field_info: "RDFFieldInfo | None") -> str:
        """Format a single value for SPARQL based on field type.

        Uses polymorphic field_info.format_value() for IRI fields,
        ensuring consistent formatting across the codebase.

        Args:
            value: Value to format
            field_info: Optional field info for type-aware formatting

        Returns:
            Formatted SPARQL value string
        """

        # Use polymorphic formatting for IRI fields
        if field_info is not None and isinstance(field_info, IRIField):
            return field_info.format_value(value)

        # Use polymorphic formatting for typed LiteralFields
        if (
            field_info is not None
            and isinstance(field_info, LiteralField)
            and field_info.datatype is not None
        ):
            return field_info.format_value(value)

        # Format based on Python type for other fields
        if isinstance(value, str):
            escaped: str = escape_sparql_literal(value)
            return f'"{escaped}"'
        elif isinstance(value, (int, float, bool)):
            return str(value)
        else:
            return str(value)

    def _format_list_value(
        self, values: list[Any], field_info: "RDFFieldInfo | None"
    ) -> str:
        """Format a list of values for IN/NOT IN operators.

        Args:
            values: List of values to format
            field_info: Optional field info for type-aware formatting

        Returns:
            Formatted SPARQL value list string like "(val1, val2, val3)"
        """
        items: list[str] = [self._format_value(item, field_info) for item in values]
        return f"({', '.join(items)})"

    def _build_wrapped_variable(self, var: str) -> str:
        """Apply string function wrappers to a variable (inner to outer).

        Args:
            var: Variable name (e.g., "?field_name")

        Returns:
            Wrapped variable string (e.g., "LCASE(STR(?field_name))")
        """
        result: str = var
        for wrapper in self.string_wrappers:
            result = f"{wrapper}({result})"
        return result

    def to_sparql(self, model_class: type[T] | None = None) -> str:
        """
        Convert the filter expression to SPARQL FILTER syntax.

        Args:
            model_class: Optional model class for IRI field detection

        Returns:
            SPARQL FILTER clause string
        """
        var = f"?{self.field_name}"

        # Validate operator for security
        operator_str = validate_sparql_operator(self.operator.value)

        # Get field info for type-aware formatting
        # When string wrappers are present (STR), we're converting to string,
        # so skip field-specific formatting and use string literal formatting
        if self.string_wrappers:
            field_info: RDFFieldInfo | None = None
        else:
            field_info = self._get_field_info(model_class)

        # Format value based on field type
        if isinstance(self.value, list):
            if self.operator in (FilterOperator.IN, FilterOperator.NOT_IN):
                value_str: str = self._format_list_value(self.value, field_info)
            else:
                raise ValueError(
                    f"List values only supported with IN/NOT IN operators, "
                    f"got {self.operator}"
                )
        else:
            value_str = self._format_value(self.value, field_info)

        # Special handling for string functions
        if self.operator in (
            FilterOperator.CONTAINS,
            FilterOperator.STARTSWITH,
            FilterOperator.ENDSWITH,
        ):
            wrapped_var: str = self._build_wrapped_variable(f"?{self.field_name}")
            return f"FILTER({self.operator.value}({wrapped_var}, {value_str}))"
        elif self.operator in (FilterOperator.IN, FilterOperator.NOT_IN):
            return f"FILTER(?{self.field_name} {self.operator.value} {value_str})"
        else:
            # For numeric comparisons, cast the variable to ensure proper type handling
            # This handles cases where numeric values are stored as strings in RDF
            if isinstance(self.value, (int, float)) and self.operator in (
                FilterOperator.LT,
                FilterOperator.GT,
                FilterOperator.LE,
                FilterOperator.GE,
                FilterOperator.EQ,
                FilterOperator.NE,
            ):
                # Determine appropriate datatype for casting
                if isinstance(self.value, float):
                    cast_function = "xsd:decimal"
                else:
                    cast_function = "xsd:integer"

                return f"FILTER({cast_function}({var}) {operator_str} {value_str})"
            else:
                return f"FILTER({var} {operator_str} {value_str})"


class FieldFilter:
    """
    Field filter proxy that enables intuitive filtering syntax.

    This class provides comparison operators that return FilterExpression
    objects when used with field references.

    Usage:
        # Direct usage
        field_filter = FieldFilter("name")
        expr = field_filter == "Alice"

        # Or via model class (recommended)
        expr = Person.name == "Alice"
    """

    def __init__(self, field_name: str):
        """
        Initialize a field filter for a specific field.

        Args:
            field_name: Name of the field to filter on
        """
        self.field_name = field_name

    def __repr__(self) -> str:
        """Return string representation for debugging."""
        return f"FieldFilter(field_name={self.field_name!r})"

    @classmethod
    def for_model(cls, model_class: type[T], field_name: str) -> "FieldFilter":
        """
        Create a FieldFilter for a specific field on a model class.

        Args:
            model_class: The model class
            field_name: Name of the field to filter on

        Returns:
            FieldFilter object for the specified field

        Raises:
            UnknownFieldError: If the field doesn't exist in the model
        """

        # Check if field exists in model
        rdf_fields = getattr(model_class, "_rdf_fields", {})
        if field_name not in rdf_fields:
            raise UnknownFieldError(model_class, field_name)

        return cls(field_name)

    def __eq__(self, other: Any) -> FilterExpression:  # type: ignore[override]
        """Equality operator (==)."""
        return FilterExpression(self.field_name, FilterOperator.EQ, other)

    def __ne__(self, other: Any) -> FilterExpression:  # type: ignore[override]
        """Inequality operator (!=)."""
        return FilterExpression(self.field_name, FilterOperator.NE, other)

    def __lt__(self, other: Any) -> FilterExpression:
        """Less than operator (<)."""
        return FilterExpression(self.field_name, FilterOperator.LT, other)

    def __le__(self, other: Any) -> FilterExpression:
        """Less than or equal operator (<=)."""
        return FilterExpression(self.field_name, FilterOperator.LE, other)

    def __gt__(self, other: Any) -> FilterExpression:
        """Greater than operator (>)."""
        return FilterExpression(self.field_name, FilterOperator.GT, other)

    def __ge__(self, other: Any) -> FilterExpression:
        """Greater than or equal operator (>=)."""
        return FilterExpression(self.field_name, FilterOperator.GE, other)

    def contains(self, substring: str) -> FilterExpression:
        """String contains operation."""
        return FilterExpression(self.field_name, FilterOperator.CONTAINS, substring)

    def startswith(self, prefix: str) -> FilterExpression:
        """String starts with operation."""
        return FilterExpression(self.field_name, FilterOperator.STARTSWITH, prefix)

    def endswith(self, suffix: str) -> FilterExpression:
        """String ends with operation."""
        return FilterExpression(self.field_name, FilterOperator.ENDSWITH, suffix)

    def in_(self, values: list[Any]) -> FilterExpression:
        """IN operator for checking membership in a list."""
        return FilterExpression(self.field_name, FilterOperator.IN, values)

    def not_in(self, values: list[Any]) -> FilterExpression:
        """NOT IN operator for checking non-membership in a list."""
        return FilterExpression(self.field_name, FilterOperator.NOT_IN, values)

    def str(self) -> "StringExpression":
        """Convert field to string for string operations on IRI fields."""
        return StringExpression(self, wrappers=[StringFunctionWrapper.STR])


class LogicalExpression:
    """
    Base class for logical expressions (AND, OR, NOT).
    """

    def __init__(
        self,
        operator: LogicalOperator,
        *expressions: Union[FilterExpression, "LogicalExpression"],
    ):
        """
        Initialize a logical expression.

        Args:
            operator: Logical operator (AND, OR, NOT)
            *expressions: Child expressions to combine
        """
        self.operator = operator
        self.expressions = expressions

    def __repr__(self) -> str:
        """Return string representation for debugging."""
        return (
            f"LogicalExpression(operator={self.operator!r}, "
            f"expressions={self.expressions!r})"
        )

    def to_sparql(self, model_class: type[T] | None = None) -> str:
        """
        Convert the logical expression to SPARQL syntax.

        Args:
            model_class: Optional model class (passed to child expressions)

        Returns:
            SPARQL logical expression string
        """
        if self.operator == LogicalOperator.NOT:
            if len(self.expressions) != 1:
                raise ValueError("NOT operator requires exactly one expression")
            child_expr = self.expressions[0].to_sparql(model_class)
            # Remove "FILTER(" and ")" from child expression
            if child_expr.startswith("FILTER(") and child_expr.endswith(")"):
                inner_expr = child_expr[7:-1]
                return f"FILTER(!({inner_expr}))"
            else:
                return f"FILTER(!({child_expr}))"
        else:
            # AND or OR
            child_exprs = []
            for expr in self.expressions:
                expr_str = expr.to_sparql(model_class)
                # Remove "FILTER(" and ")" from each expression
                if expr_str.startswith("FILTER(") and expr_str.endswith(")"):
                    inner_expr = expr_str[7:-1]
                    child_exprs.append(inner_expr)
                else:
                    child_exprs.append(expr_str)

            combined = f" {self.operator.value} ".join(child_exprs)
            return f"FILTER({combined})"


def and_(*expressions: Union[FilterExpression, LogicalExpression]) -> LogicalExpression:
    """Combine expressions with AND operator."""
    return LogicalExpression(LogicalOperator.AND, *expressions)


def or_(*expressions: Union[FilterExpression, LogicalExpression]) -> LogicalExpression:
    """Combine expressions with OR operator."""
    return LogicalExpression(LogicalOperator.OR, *expressions)


def not_(expression: Union[FilterExpression, LogicalExpression]) -> LogicalExpression:
    """Negate an expression with NOT operator."""
    return LogicalExpression(LogicalOperator.NOT, expression)


class StringExpression:
    """
    Intermediate expression for applying SPARQL string functions to fields.

    Enables chainable string transformations:
        Model.iri_field.str().lower().contains("pdf")

    Generates:
        FILTER(CONTAINS(LCASE(STR(?iri_field)), "pdf"))
    """

    def __init__(
        self,
        field_filter: FieldFilter,
        wrappers: list[StringFunctionWrapper] | None = None,
    ):
        """
        Initialize a string expression.

        Args:
            field_filter: The field filter to wrap
            wrappers: List of string function wrappers already applied
        """
        self.field_filter = field_filter
        self.wrappers: list[StringFunctionWrapper] = wrappers or []

    @property
    def field_name(self) -> str:
        """Get the field name from the underlying field filter."""
        return self.field_filter.field_name

    def lower(self) -> "StringExpression":
        """Apply LCASE() transformation."""
        return StringExpression(
            self.field_filter, self.wrappers + [StringFunctionWrapper.LCASE]
        )

    def upper(self) -> "StringExpression":
        """Apply UCASE() transformation."""
        return StringExpression(
            self.field_filter, self.wrappers + [StringFunctionWrapper.UCASE]
        )

    def contains(self, substring: str) -> FilterExpression:
        """String contains operation with applied transformations."""
        return FilterExpression(
            self.field_name,
            FilterOperator.CONTAINS,
            substring,
            string_wrappers=list(self.wrappers),
        )

    def startswith(self, prefix: str) -> FilterExpression:
        """String starts with operation with applied transformations."""
        return FilterExpression(
            self.field_name,
            FilterOperator.STARTSWITH,
            prefix,
            string_wrappers=list(self.wrappers),
        )

    def endswith(self, suffix: str) -> FilterExpression:
        """String ends with operation with applied transformations."""
        return FilterExpression(
            self.field_name,
            FilterOperator.ENDSWITH,
            suffix,
            string_wrappers=list(self.wrappers),
        )
