"""
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 .security import escape_sparql_literal, validate_sparql_operator
from .values import Literal

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 is_graph_pattern_expression(self) -> bool:
        """
        Check if this expression generates graph patterns (vs FILTER clauses).

        Returns:
            False - FilterExpression generates FILTER clauses, not graph patterns
        """
        return False

    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 FILTER expressions.

        Uses polymorphic dispatch:
        1. If value is None → return unquoted "None"
        2. If value has to_sparql_filter() → value formats itself
        3. If field_info provided → field wraps and formats the value
        4. Otherwise → wrap in Literal and format for filter

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

        Returns:
            Formatted SPARQL value string
        """
        # None is formatted as unquoted None
        if value is None:
            return "None"

        # Value types know how to format themselves for filters
        if hasattr(value, "to_sparql_filter"):
            result: str = value.to_sparql_filter()
            return result

        # Field wraps and formats plain values
        if field_info is not None:
            return field_info.format_value_for_filter(value)

        # Default: wrap in Literal (numbers stay unquoted, strings quoted)
        return Literal(value).to_sparql_filter()

    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 (substring match)."""
        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 CollectionFieldFilter(FieldFilter):
    """
    Field filter for collection fields (LiteralList, IRIList, etc.).

    This subclass overrides contains() to check for collection membership
    rather than substring matching. This follows SQLAlchemy conventions where
    contains() behaves appropriately based on field type.

    Usage:
        # For collection fields, contains() checks membership
        query.filter(Book.genres.contains("Science Fiction"))

        # Generates triple pattern: ?s <predicate> "Science Fiction" .
        # Rather than: FILTER(CONTAINS(?genres, "Science Fiction"))
    """

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

    def contains(self, value: Any) -> "CollectionContainsExpression":  # type: ignore[override]
        """
        Check if the collection contains a specific value.

        For collection fields, this generates SPARQL that matches the direct
        triple pattern rather than using FILTER(CONTAINS()), which is for
        substring matching on single values.

        Args:
            value: The value to check for in the collection

        Returns:
            CollectionContainsExpression that generates appropriate SPARQL

        Example:
            >>> # Find books where genres contains "Science Fiction"
            >>> query.filter(Book.genres.contains("Science Fiction"))
        """
        return CollectionContainsExpression(self.field_name, value)


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 is_graph_pattern_expression(self) -> bool:
        """
        Check if this expression generates graph patterns (vs FILTER clauses).

        Returns:
            True if any child expression is a graph pattern expression
        """
        for expr in self.expressions:
            if (
                hasattr(expr, "is_graph_pattern_expression")
                and expr.is_graph_pattern_expression()
            ):
                return True
        return False

    def _all_graph_patterns(self) -> bool:
        """Check if ALL child expressions are graph pattern expressions."""
        if not self.expressions:
            return False
        return all(
            hasattr(expr, "is_graph_pattern_expression")
            and expr.is_graph_pattern_expression()
            for expr in self.expressions
        )

    def _has_mixed_expression_types(self) -> bool:
        """
        Check if this logical expression has mixed expression types.

        Returns:
            True if some children are graph patterns and some are filter expressions
        """
        if not self.expressions:
            return False

        has_graph_pattern: bool = False
        has_filter_expr: bool = False

        for expr in self.expressions:
            if (
                hasattr(expr, "is_graph_pattern_expression")
                and expr.is_graph_pattern_expression()
            ):
                has_graph_pattern = True
            else:
                has_filter_expr = True

        return has_graph_pattern and has_filter_expr

    def _to_sparql_filter_expressions(self, model_class: type[T] | None) -> str:
        """Generate SPARQL for pure FILTER expressions (original behavior)."""
        if self.operator == LogicalOperator.NOT:
            if len(self.expressions) != 1:
                raise ValueError("NOT operator requires exactly one expression")
            child_expr: str = self.expressions[0].to_sparql(model_class)
            # Remove "FILTER(" and ")" from child expression
            if child_expr.startswith("FILTER(") and child_expr.endswith(")"):
                inner_expr: str = child_expr[7:-1]
                return f"FILTER(!({inner_expr}))"
            else:
                return f"FILTER(!({child_expr}))"
        else:
            # AND or OR
            child_exprs: list[str] = []
            for expr in self.expressions:
                expr_str: 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: str = f" {self.operator.value} ".join(child_exprs)
            return f"FILTER({combined})"

    def _to_sparql_graph_patterns(self, model_class: type[T] | None) -> str:
        """Generate SPARQL for graph pattern expressions.

        Uses UNION for OR, combined patterns for AND, etc.
        """
        if self.operator == LogicalOperator.OR:
            # Generate UNION blocks for OR
            pattern_blocks: list[str] = []
            for expr in self.expressions:
                pattern: str = expr.to_sparql(model_class)
                # Indent all lines of the pattern for UNION blocks
                indented_pattern: str = self._indent_pattern(pattern, "    ")
                # Wrap each pattern in braces for UNION
                pattern_blocks.append(f"  {{\n{indented_pattern}\n  }}")

            return "\n  UNION\n".join(pattern_blocks)

        elif self.operator == LogicalOperator.AND:
            # Generate combined patterns for AND
            patterns: list[str] = []
            for expr in self.expressions:
                pattern = expr.to_sparql(model_class)
                # Indent all lines of the pattern consistently
                indented_pattern = self._indent_pattern(pattern, "  ")
                patterns.append(indented_pattern)

            return "\n".join(patterns)

        elif self.operator == LogicalOperator.NOT:
            # Generate FILTER NOT EXISTS for NOT
            if len(self.expressions) != 1:
                raise ValueError("NOT operator requires exactly one expression")

            pattern = self.expressions[0].to_sparql(model_class)
            # Indent all lines of the pattern for FILTER NOT EXISTS block
            indented_pattern = self._indent_pattern(pattern, "    ")
            return f"  FILTER NOT EXISTS {{\n{indented_pattern}\n  }}"

        return ""

    def _indent_pattern(self, pattern: str, indent: str) -> str:
        """Indent all lines of a multi-line pattern consistently.

        Args:
            pattern: The pattern string (may contain newlines)
            indent: The indentation to apply to each line

        Returns:
            Pattern with all lines indented
        """
        lines: list[str] = pattern.split("\n")
        indented_lines: list[str] = [f"{indent}{line}" for line in lines]
        return "\n".join(indented_lines)

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

        Detects if expressions are graph patterns and generates appropriate SPARQL:
        - Graph patterns with OR: UNION blocks
        - Graph patterns with AND: Combined patterns
        - Graph patterns with NOT: FILTER NOT EXISTS
        - Filter expressions: FILTER clauses (original behavior)

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

        Returns:
            SPARQL logical expression string

        Raises:
            ValueError: If mixing graph pattern expressions with filter expressions
        """
        # Validate expression types - cannot mix graph patterns with filter
        # expressions
        if self._has_mixed_expression_types():
            raise ValueError(
                f"Cannot mix graph pattern expressions "
                f"(like collection.contains()) "
                f"with filter expressions (like field == value) "
                f"in the same {self.operator.name} operation. "
                f"Graph patterns require UNION/triple patterns "
                f"while filter expressions use FILTER clauses. "
                f"Consider splitting into separate queries "
                f"or using only one expression type."
            )

        # Check if we have graph pattern expressions
        all_patterns: bool = self._all_graph_patterns()

        # All graph patterns: generate UNION/blocks
        if all_patterns:
            return self._to_sparql_graph_patterns(model_class)

        # All filters: original behavior
        return self._to_sparql_filter_expressions(model_class)


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 CollectionContainsExpression:
    """
    Filter expression for checking if a collection field contains a specific value.

    This class generates SPARQL that matches the direct triple pattern for
    collection fields, rather than using FILTER(CONTAINS()) which is for
    substring matching on single values.

    For collection fields (LiteralList, IRIList, etc.), this generates:
        ?subject <predicate> <formatted_value> .
        BIND(<formatted_value> AS ?field_name) .

    This ensures correct query semantics where the collection actually contains
    the specified value.
    """

    def __init__(self, field_name: str, value: Any):
        """
        Initialize a collection contains expression.

        Args:
            field_name: Name of the collection field to filter on
            value: The value to check for in the collection
        """
        self.field_name = field_name
        self.value = value

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

    def is_graph_pattern_expression(self) -> bool:
        """
        Check if this expression generates graph patterns (vs FILTER clauses).

        Returns:
            True - CollectionContainsExpression generates graph patterns
        """
        return True

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

        Args:
            model_class: Optional model class for field type detection

        Returns:
            SPARQL string with triple pattern and BIND for the collection field

        Raises:
            ValueError: If the field is not a collection field
        """
        # Import here to avoid circular imports
        from .model import (
            PropertyPath,
            is_collection_field,
        )

        if model_class is None:
            raise ValueError(
                f"CollectionContainsExpression requires model_class to determine "
                f"field type for field '{self.field_name}'"
            )

        # Get field info to determine type and formatting
        rdf_fields: dict[str, Any] = getattr(model_class, "_rdf_fields", {})
        if self.field_name not in rdf_fields:
            raise ValueError(
                f"Field '{self.field_name}' not found in model '{model_class.__name__}'"
            )

        field_info = rdf_fields[self.field_name]

        # Verify this is actually a collection field
        if not is_collection_field(field_info):
            raise ValueError(
                f"Field '{self.field_name}' is not a collection field. "
                f"contains() can only be used with LiteralList, IRIList, "
                f"LangStringList, or TypedLiteralList fields."
            )

        # Format the value using polymorphic format_value
        # All field types (IRIField, LiteralField, LangString, etc.) implement
        # format_value(), so we delegate directly to the field
        formatted_value: str = self._format_value(self.value, field_info)

        # Generate SPARQL: triple pattern only
        # Note: We don't include BIND here because:
        # 1. The collection field value is already populated by the
        #    OPTIONAL GROUP_CONCAT subquery
        # 2. Multiple contains() operations with AND would create
        #    conflicting BIND statements for the same variable
        #    (e.g., BIND("SciFi" AS ?genres) and
        #    BIND("Fantasy" AS ?genres))
        # We need to get the predicate from the field info
        predicate = field_info.predicate

        # Format the predicate consistently with the rest of the codebase
        if field_info.is_property_path():
            property_path = field_info.get_property_path()
            if property_path:
                predicate = property_path.to_sparql()
        else:
            # Use PropertyPath.format_predicate for consistent IRI escaping
            predicate = PropertyPath.format_predicate(predicate)

        return f"?s {predicate} {formatted_value} ."

    def _format_value(self, value: Any, field_info: Any) -> str:
        """
        Format a value for SPARQL using the field's format_value method.

        All collection field types (LiteralList, IRIList, LangStringList,
        TypedLiteralList) implement format_value(), so this delegates
        directly to the field for proper type-aware formatting.

        Args:
            value: The value to format
            field_info: The field information object

        Returns:
            Formatted SPARQL value string
        """
        # Delegate to the field's format_value method
        if hasattr(field_info, "format_value"):
            return str(field_info.format_value(value))

        # Last resort: format as string literal

        escaped: str = escape_sparql_literal(str(value))
        return f'"{escaped}"'


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),
        )
