"""Query values mixin — VALUES clause handling and validation."""

from __future__ import annotations

import types
from typing import TYPE_CHECKING, Annotated, Any, Self, Union, get_args, get_origin

from rdflib import URIRef

from sparqlmojo.exc.exceptions import QueryError
from sparqlmojo.orm.model import IRIField
from sparqlmojo.orm.query._constants import SUBJECT_VARIABLE_NAME
from sparqlmojo.orm.security import (
    DANGEROUS_IRI_KEYWORDS,
    SPARQLInjectionError,
    escape_sparql_iri,
    format_sparql_value_secure,
    validate_no_sparql_keywords,
)

if TYPE_CHECKING:
    from sparqlmojo.engine.session import Session
    from sparqlmojo.orm.filtering import FieldFilter
    from sparqlmojo.orm.model import Model


class QueryValuesMixin:
    """Mixin providing VALUES clause handling for queries."""

    # Type stubs for attributes set in Query.__init__
    if TYPE_CHECKING:
        model: type[Model]
        _session: Session | None
        _values: dict[str, list[Any]] | None

    def values(
        self,
        field_or_bindings: "FieldFilter | dict[str, list[Any]]",
        values_list: list[Any] | None = None,
        *,
        validate_against_model: bool = True,
    ) -> Self:
        """Add VALUES clause to constrain query with explicit value sets.

        Supports two calling conventions:

        ORM-style (recommended):
            query.values(Model.field, [value1, value2])

            The ORM-style API automatically:
            - Maps SubjectField to ?s variable
            - Formats IRIs with angle brackets for SubjectField/IRIField
            - Formats literals with quotes for LiteralField
            - Performs type validation based on field annotations

        Dict-style (for multiple fields):
            query.values({'name': ['Alice', 'Bob'], 'age': [25, 30]})

        For SubjectField, the ORM-style automatically maps to the correct
        SPARQL variable (?s), eliminating the need to use raw variable names.

        Args:
            field_or_bindings: Either a FieldFilter (Model.field) for ORM-style,
                             or a dict mapping field names to value lists.
            values_list: List of values when using ORM-style (required if
                        field_or_bindings is a FieldFilter).
            validate_against_model: If True (default), validates that variable
                                  names match model fields and types are
                                  compatible. Set to False for raw variable
                                  names like 's'.

        Returns:
            Self for method chaining

        Raises:
            QueryError: If validation fails (empty bindings, inconsistent list
                      lengths, invalid field names, SPARQL injection attempts,
                      or type mismatches)

        Examples:
            >>> # ORM-style - type-safe, model-aware
            >>> query = session.query(Label).values(Label.entity_iri, [uri1, uri2])
            >>> # Generates: VALUES (?s) { (<uri1>) (<uri2>) }
            >>>
            >>> # Dict-style - for multiple fields
            >>> query = session.query(Person).values({
            ...     'name': ['Bob', 'Alice'],
            ...     'age': [30, 25]
            ... })
            >>> # Generates: VALUES (?name ?age) { ("Bob" 30) ("Alice" 25) }
        """

        # Convert input to normalized bindings format
        bindings, is_orm_subject_field = self._normalize_values_input(
            field_or_bindings, values_list
        )

        # Validate bindings structure and variable names
        self._validate_values_bindings(bindings)

        # Validate values for security (injection prevention)
        self._validate_values_security(bindings)

        # Validate against model if requested
        # Skip validation for ORM-style SubjectField (already type-safe)
        # and for 's' variable which won't be in model fields
        if validate_against_model and self.model and not is_orm_subject_field:
            fields_to_validate: dict[str, list[Any]] = {
                k: v for k, v in bindings.items() if k != SUBJECT_VARIABLE_NAME
            }
            if fields_to_validate:
                self._validate_values_against_model(fields_to_validate)

        self._values = bindings
        return self

    def _normalize_values_input(
        self,
        field_or_bindings: "FieldFilter | dict[str, list[Any]]",
        values_list: list[Any] | None,
    ) -> tuple[dict[str, list[Any]], bool]:
        """Convert ORM-style or dict-style input to normalized bindings.

        SubjectField names in dict-style input are automatically mapped to
        the 's' variable for consistency with ORM-style API (Issue #129).

        Args:
            field_or_bindings: Either a FieldFilter (Model.field) for ORM-style,
                             or a dict mapping field names to value lists.
            values_list: List of values when using ORM-style.

        Returns:
            Tuple of (bindings dict, is_orm_subject_field flag). The flag is
            True when a SubjectField was referenced by its model field name
            (not when 's' is used directly).

        Raises:
            QueryError: If ORM-style is used without providing values_list.
        """
        from sparqlmojo.orm.filtering import FieldFilter

        is_orm_subject_field: bool = False

        if isinstance(field_or_bindings, FieldFilter):
            if values_list is None:
                raise QueryError(
                    "values() requires a list of values when using ORM-style syntax: "
                    "query.values(Model.field, [value1, value2])"
                )

            field_name: str = field_or_bindings.field_name
            var_name: str = self._get_sparql_variable_for_field(field_name)

            if self._is_orm_subject_field_mapping(field_name, var_name):
                is_orm_subject_field = True

            bindings: dict[str, list[Any]] = {var_name: values_list}
        else:
            # Dict-style input - map SubjectField names to "s" for consistency
            # with ORM-style API (Issue #129)
            bindings = {}
            for var_name, values in field_or_bindings.items():
                mapped_name: str = self._get_sparql_variable_for_field(var_name)
                if self._is_orm_subject_field_mapping(var_name, mapped_name):
                    is_orm_subject_field = True
                # Detect conflicting mappings to the same variable (e.g., both
                # "s" and "entity_iri" when entity_iri is a SubjectField)
                if mapped_name in bindings:
                    raise QueryError(
                        f"Conflicting bindings for variable '{mapped_name}': "
                        f"both '{var_name}' and another key map to the same "
                        f"SPARQL variable. Use only one of them."
                    )
                bindings[mapped_name] = values

        return bindings, is_orm_subject_field

    def _is_orm_subject_field_mapping(
        self, original_name: str, mapped_name: str
    ) -> bool:
        """Check if field is an ORM SubjectField mapped to 's' variable.

        Args:
            original_name: Original field name from user input.
            mapped_name: SPARQL variable name after mapping.

        Returns:
            True if this is a SubjectField mapped from a model field name
            (not when 's' is used directly).
        """

        return (
            mapped_name == SUBJECT_VARIABLE_NAME
            and original_name != SUBJECT_VARIABLE_NAME
        )

    def _get_sparql_variable_for_field(self, field_name: str) -> str:
        """Get SPARQL variable name for a model field.

        SubjectField maps to 's', other fields use their field name.

        Args:
            field_name: The name of the field in the model.

        Returns:
            SPARQL variable name (without ? prefix).
        """

        if self.model:
            rdf_fields: dict[str, Any] = getattr(self.model, "_rdf_fields", {})
            if field_name in rdf_fields:
                field_info = rdf_fields[field_name]
                if getattr(field_info, "is_subject_field", False):
                    return SUBJECT_VARIABLE_NAME
        return field_name

    def _validate_values_bindings(self, bindings: dict[str, list[Any]]) -> None:
        """Validate bindings structure and variable names.

        Args:
            bindings: Dictionary mapping variable names to lists of values.

        Raises:
            QueryError: If bindings are empty, lists have inconsistent lengths,
                      or variable names are invalid.
        """
        if not bindings:
            raise QueryError("VALUES clause requires at least one variable binding")

        # Check no value lists are empty
        for var_name, vals in bindings.items():
            if len(vals) == 0:
                raise QueryError(
                    f"VALUES clause for variable '{var_name}' has an empty "
                    f"value list. Provide at least one value."
                )

        # Check all lists have same length
        list_lengths: list[int] = [len(vals) for vals in bindings.values()]
        if len(set(list_lengths)) != 1:
            raise QueryError(
                f"VALUES clause requires all value lists to have the same length. "
                f"Got lengths: {list_lengths}"
            )

        # Check for empty or whitespace-only string values
        for var_name, vals in bindings.items():
            for i, val in enumerate(vals):
                if isinstance(val, str) and (not val or not val.strip()):
                    raise QueryError(
                        f"VALUES clause for variable '{var_name}' contains an "
                        f"empty or whitespace-only value at index {i}. "
                        f"All values must be non-empty."
                    )

        # Validate each variable name
        for var_name in bindings.keys():
            self._validate_sparql_variable_name(var_name)

    def _validate_sparql_variable_name(self, var_name: str) -> None:
        """Validate a SPARQL variable name.

        Args:
            var_name: The variable name to validate.

        Raises:
            QueryError: If variable name is empty, contains dangerous SPARQL
                      keywords, or is not a valid identifier.
        """
        if not var_name or not var_name.strip():
            raise QueryError("Variable names cannot be empty")

        # Check for dangerous SPARQL keywords using word boundary matching
        # from security.py to avoid false positives
        try:
            validate_no_sparql_keywords(
                var_name, DANGEROUS_IRI_KEYWORDS, "Variable name"
            )
        except SPARQLInjectionError as e:
            raise QueryError(
                f"Invalid variable name: '{var_name}' contains SPARQL keywords"
            ) from e

        # Validate variable name format
        if not var_name.replace(" ", "").isidentifier():
            raise QueryError(
                f"Invalid variable name: '{var_name}'. Must be a valid Python "
                "identifier"
            )

    def _validate_values_security(self, bindings: dict[str, list[Any]]) -> None:
        """Check for SPARQL injection in values.

        Args:
            bindings: Dictionary mapping variable names to lists of values.

        Raises:
            QueryError: If any value contains dangerous SPARQL patterns.
        """
        from sparqlmojo.orm.security import escape_sparql_literal

        for var_name, vals in bindings.items():
            for value in vals:
                if isinstance(value, str):
                    try:
                        escape_sparql_literal(value)
                    except Exception as e:
                        raise QueryError(
                            f"Invalid value for variable '{var_name}': {e}"
                        )

    def _validate_values_against_model(self, bindings: dict[str, list[Any]]) -> None:
        """Validate VALUES bindings against the model's field definitions.

        Args:
            bindings: Dictionary mapping variable names to lists of values.

        Raises:
            QueryError: If variable names don't match model fields or
                      value types are incompatible with field types.
        """
        if not self.model:
            return

        # Get model field information
        model_fields = getattr(self.model, "_rdf_fields", {})
        model_annotations = getattr(self.model, "__annotations__", {})

        # Check that all variable names correspond to model fields
        for var_name in bindings.keys():
            if var_name not in model_fields:
                available_fields = list(model_fields.keys())
                raise QueryError(
                    f"Variable '{var_name}' does not correspond to a model field. "
                    f"Available fields: {available_fields}"
                )

            # Get the expected type for this field
            if var_name in model_annotations:
                expected_type = model_annotations[var_name]

                # Handle Annotated types by extracting the base type
                if get_origin(expected_type) is Annotated:
                    # Get the first argument (the actual type)
                    type_args = get_args(expected_type)
                    if type_args:
                        expected_type = type_args[0]

                # Validate that all values match the expected type
                for value in bindings[var_name]:
                    self._validate_value_type(value, expected_type, var_name)

    def _validate_value_type(
        self, value: Any, expected_type: type, field_name: str
    ) -> None:
        """Validate that a value matches the expected type.

        Args:
            value: The value to validate
            expected_type: The expected type
            field_name: Name of the field for error messages

        Raises:
            QueryError: If the value type is incompatible with the expected type
        """
        # Check for both Union (typing.Union) and UnionType (X | Y syntax)
        origin = get_origin(expected_type)
        if origin is Union or isinstance(expected_type, types.UnionType):
            self._validate_union_type(value, expected_type, field_name)
        else:
            self._validate_simple_type(value, expected_type, field_name)

    def _validate_union_type(
        self, value: Any, union_type: type, field_name: str
    ) -> None:
        """Validate value against Union type (including Optional).

        Args:
            value: The value to validate
            union_type: Union type annotation
            field_name: Name of the field for error messages

        Raises:
            QueryError: If value doesn't match any type in the union
        """
        union_types = get_args(union_type)

        # Handle None values
        if value is None:
            if type(None) not in union_types:
                raise QueryError(
                    f"None not allowed for field '{field_name}'. "
                    f"Expected type {union_type}"
                )
            return

        # Check non-None types
        non_none_types = [t for t in union_types if t is not type(None)]
        for expected in non_none_types:
            if self._value_matches_type(value, expected):
                return

        raise QueryError(
            f"Value '{value}' for field '{field_name}' does not match "
            f"expected type {union_type}"
        )

    def _validate_simple_type(
        self, value: Any, expected_type: type, field_name: str
    ) -> None:
        """Validate value against simple (non-Union) type.

        Args:
            value: The value to validate
            expected_type: The expected type
            field_name: Name of the field for error messages

        Raises:
            QueryError: If value doesn't match the expected type
        """
        if not self._value_matches_type(value, expected_type):
            raise QueryError(
                f"Value '{value}' for field '{field_name}' does not match "
                f"expected type {expected_type}"
            )

    def _value_matches_type(self, value: Any, expected_type: type) -> bool:
        """Check if a value matches the expected type."""
        # Handle None values
        if value is None:
            return True  # None is generally acceptable for optional fields

        # Handle basic types
        if expected_type in (str, int, float, bool):
            return isinstance(value, expected_type)

        # Handle more complex cases could be added here
        # For now, be permissive for other types
        return True

    def _format_values_clause_value(self, var_name: str, value: Any) -> str:
        """
        Format a single value for use in VALUES clause.

        Detects whether value should be formatted as IRI or literal based on:
        1. Field type lookup in model (IRIField and its subclasses)
        2. LiteralField with explicit datatype
        3. rdflib.URIRef instance detection
        4. Variable name heuristics (e.g., 's' for subject)

        Args:
            var_name: The SPARQL variable name (without ?)
            value: The value to format

        Returns:
            Formatted SPARQL value string with proper escaping
        """
        from sparqlmojo.orm.model import LiteralField

        # Handle None as UNDEF in SPARQL
        if value is None:
            return "UNDEF"

        # Detect rdflib.URIRef instances
        if isinstance(value, URIRef):
            # URIRef instances are IRIs
            escaped_iri: str = escape_sparql_iri(str(value))
            return f"<{escaped_iri}>"

        # Check if variable corresponds to an IRI field in the model
        # This covers IRIField, ObjectPropertyField, and SubjectField
        if self.model and hasattr(self.model, "_rdf_fields"):
            rdf_fields: dict[str, Any] = self.model._rdf_fields

            if var_name in rdf_fields:
                field_info = rdf_fields[var_name]

                # Check if this is an IRI-type field
                if isinstance(field_info, IRIField):
                    # Use the field's polymorphic format_value method
                    return field_info.format_value(value)

                # Check if this is a LiteralField with explicit datatype
                if (
                    isinstance(field_info, LiteralField)
                    and field_info.datatype is not None
                ):
                    return field_info.format_value(value)

        # Heuristic: subject variable typically represents the subject (IRI)
        if (
            var_name == SUBJECT_VARIABLE_NAME
            and isinstance(value, str)
            and value.strip()
        ):
            escaped_subject_iri: str = escape_sparql_iri(value)
            return f"<{escaped_subject_iri}>"

        # Default: format as literal using secure formatting
        return format_sparql_value_secure(value)
