"""Query builder mixin — chainable API methods for constructing queries."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Self, Union

from sparqlmojo.exc.exceptions import QueryError
from sparqlmojo.orm.model import PropertyPath

if TYPE_CHECKING:
    from sparqlmojo.engine.session import Session
    from sparqlmojo.orm.compiler import Condition
    from sparqlmojo.orm.filtering import FilterExpression, FilterLike, LogicalExpression
    from sparqlmojo.orm.hydrator import ResultHydrator
    from sparqlmojo.orm.model import Model


class QueryBuilderMixin:
    """Mixin providing chainable query-building methods."""

    # Type stubs for attributes set in Query.__init__
    if TYPE_CHECKING:
        model: type[Model]
        _session: Session | None
        _conditions: list[FilterLike]
        _limit: int | None
        _offset: int | None
        _from_graph: str | None
        _from_named_graphs: list[str]
        _values: dict[str, list[Any]] | None
        _order_by_field: str | None
        _order_descending: bool
        _distinct: bool
        _property_paths: dict[str, PropertyPath]
        _aggregates: dict[str, tuple[str, str]]
        _group_by_fields: list[str]
        _lang_filters: dict[str, list[str]]
        _filter_by_iri_bindings: dict[str, str]
        _flatten_aggregation: bool
        _hydrator: ResultHydrator[Any]

    def _validate_field_exists(self, field: str) -> None:
        """Validate that a field exists in the model.

        Args:
            field: Field name to validate

        Raises:
            QueryError: If field doesn't exist in model
        """
        if field not in self.model._rdf_fields:
            available_fields: list[str] = list(self.model._rdf_fields.keys())
            raise QueryError(
                f"Field '{field}' not found in model. "
                f"Available fields: {available_fields}"
            )

    def filter(
        self, *conditions: Union[Condition, "FilterExpression", "LogicalExpression"]
    ) -> Self:
        """
        Add filter conditions to the query.

        Note: Using Union instead of | operator because this involves forward references
        which are not supported by the | operator in Python's type system.

        Note: Unlike filter_by(), this method always generates FILTER clauses,
        even for IRIField. Use filter_by() for IRI fields when you want direct
        triple pattern binding (which provides better query semantics and
        ensures non-existent IRIs return no results).

        Args:
            *conditions: One or more filter conditions (Condition,
                FilterExpression, or LogicalExpression)

        Returns:
            Self for method chaining

        Example:
            >>> # Old style with Condition
            >>> query.filter(Condition("name", "=", "Alice"))
            >>>
            >>> # New style with field-level filtering
            >>> query.filter(Person.name == "Alice")
            >>> query.filter(Person.age > 18, Person.name.contains("Smith"))
        """
        # Import here to avoid circular imports

        for condition in conditions:
            self._conditions.append(condition)
        return self

    def _handle_filter_by_field(
        self, field_name: str, value: Any, field_info: Any
    ) -> None:
        """Handle filter_by() for a single field based on its type.

        This method implements a field-type-based dispatch pattern:
        - SubjectField: Uses VALUES clause via Condition (handled by
          _extract_subject_conditions)
        - IRIField (including ObjectPropertyField): Uses direct triple pattern
          binding for correct query semantics
        - LiteralField: Uses FILTER clause for equality comparison

        Args:
            field_name: Name of the field being filtered
            value: The value to filter by
            field_info: RDF field metadata from model._rdf_fields
        """
        # Import IRIField here to avoid circular imports at module level
        from sparqlmojo.orm.compiler import Condition
        from sparqlmojo.orm.model import IRIField

        if field_info.is_subject():
            # SubjectField uses "s" as the variable name (subject variable)
            # and uses VALUES clause (handled by _extract_subject_conditions)
            condition: Condition = Condition("s", "=", value)
            self._conditions.append(condition)
        elif isinstance(field_info, IRIField):
            # IRIField (and subclasses like ObjectPropertyField):
            # Store binding for direct triple pattern generation
            # This avoids OPTIONAL + FILTER which causes incorrect results
            self._filter_by_iri_bindings[field_name] = str(value)
        else:
            # Literal fields use FILTER as before
            condition = Condition(field_name, "=", value)
            self._conditions.append(condition)

    def filter_by(self, **kwargs: Any) -> Self:
        """
        SQLAlchemy-style filtering using keyword arguments.

        For IRIField and its subclasses (ObjectPropertyField), this generates
        direct triple pattern bindings instead of OPTIONAL + FILTER patterns:
            ?s <predicate> <IRI> .
            BIND(<IRI> AS ?field) .

        This ensures correct query semantics where non-existent IRIs return
        no results, rather than incorrectly matching unrelated subjects.

        Note: Unlike filter(), which always generates FILTER clauses, filter_by()
        uses field-type-aware dispatching:
        - SubjectField: VALUES clause for efficient subject binding
        - IRIField/ObjectPropertyField: Direct triple pattern binding
        - LiteralField: FILTER clause for equality comparison

        Args:
            **kwargs: Field name to value mappings for equality filters

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).filter_by(name="Alice", age=30)
            >>> # Filter by subject IRI using SubjectField
            >>> session.query(Label).filter_by(entity_iri="http://example.org/entity")
            >>> # Filter by IRI field (generates direct triple pattern)
            >>> session.query(Translation).filter_by(work_iri="http://example.org/work")
        """
        for field_name, value in kwargs.items():
            self._validate_field_exists(field_name)
            field_info = self.model._rdf_fields[field_name]
            self._handle_filter_by_field(field_name, value, field_info)
        return self

    def filter_lang(self, field_name: str, langs: str | list[str]) -> Self:
        """
        Filter a LangString field by language tag(s).

        Args:
            field_name: Name of the LangString field to filter
            langs: Single language code or list of language codes (e.g., 'en',
                   ['en', 'fr'])

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field does not exist or is not a LangString field

        Example:
            >>> # Filter to only English labels
            >>> session.query(Label).filter_lang("text", "en")
            >>>
            >>> # Filter to English or French labels
            >>> session.query(Label).filter_lang("text", ["en", "fr"])
        """
        self._validate_field_exists(field_name)

        # Validate field supports language filtering
        field_info = self.model._rdf_fields[field_name]
        if not field_info.supports_language_filtering():
            raise QueryError(
                f"Field '{field_name}' does not support language filtering. "
                f"filter_lang() can only be used with LangString fields."
            )

        # Normalize langs to a list
        lang_list: list[str] = [langs] if isinstance(langs, str) else list(langs)

        # Store the language filter
        self._lang_filters[field_name] = lang_list

        return self

    def order_by(self, field: str, descending: bool = False) -> Self:
        """
        Add ORDER BY clause to query.

        Args:
            field: Field name to order by
            descending: If True, order descending (default: False)

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).order_by("age", descending=True)
        """
        self._validate_field_exists(field)
        self._order_by_field = field
        self._order_descending = descending
        return self

    def distinct(self) -> Self:
        """
        Add DISTINCT modifier to query.

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).distinct()
        """
        self._distinct = True
        return self

    def subquery_aggregation(self) -> Self:
        """
        Use per-field subquery aggregation for collection fields.

        By default, queries with collection fields use flattened aggregation
        (single GROUP BY with GROUP_CONCAT(DISTINCT ...)). This method opts
        out, generating separate OPTIONAL subqueries with internal GROUP BY
        for each collection field instead.

        This may be needed when filter_lang() targets a collection field,
        since the flattened strategy renames variables in a way that is
        incompatible with FILTER(LANG(...)).

        Returns:
            Self for method chaining

        Example:
            >>> session.query(EntityModel).subquery_aggregation().all()
        """
        self._flatten_aggregation = False
        return self

    def count_(self, field: str = "*", alias: str = "count") -> Self:
        """
        Add COUNT aggregate to query.

        Args:
            field: Field name to count, or "*" for all results
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).count_().first()
            >>> session.query(Person).count_('age', 'age_count').first()
        """
        if field != "*":
            self._validate_field_exists(field)
        self._aggregates[alias] = ("COUNT", field)
        return self

    def _add_aggregate(self, func: str, field: str, alias: str) -> Self:
        """Add an aggregate function to the query.

        Args:
            func: SPARQL aggregate function name (SUM, AVG, MIN, MAX)
            field: Field name to aggregate
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining
        """
        self._validate_field_exists(field)
        self._aggregates[alias] = (func, field)
        return self

    def sum(self, field: str, alias: str = "sum") -> Self:
        """
        Add SUM aggregate to query.

        Args:
            field: Field name to sum
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).sum('age').first()
        """
        return self._add_aggregate("SUM", field, alias)

    def avg(self, field: str, alias: str = "avg") -> Self:
        """
        Add AVG aggregate to query.

        Args:
            field: Field name to average
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).avg('age').first()
        """
        return self._add_aggregate("AVG", field, alias)

    def min(self, field: str, alias: str = "min") -> Self:
        """
        Add MIN aggregate to query.

        Args:
            field: Field name to find minimum
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).min('age').first()
        """
        return self._add_aggregate("MIN", field, alias)

    def max(self, field: str, alias: str = "max") -> Self:
        """
        Add MAX aggregate to query.

        Args:
            field: Field name to find maximum
            alias: Alias for the aggregate result variable

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).max('age').first()
        """
        return self._add_aggregate("MAX", field, alias)

    def group_by(self, *fields: str) -> Self:
        """
        Add GROUP BY clause to query.

        Args:
            *fields: Field names to group by

        Returns:
            Self for method chaining

        Example:
            >>> session.query(Person).group_by('age').count_().all()
        """
        for field in fields:
            self._validate_field_exists(field)
        self._group_by_fields.extend(fields)
        return self

    def limit(self, n: int) -> Self:
        self._limit = n
        return self

    def offset(self, n: int) -> Self:
        self._offset = n
        return self

    def from_graph(self, graph_uri: str) -> Self:
        """Specify the default graph for this query.

        Args:
            graph_uri: URI of the default graph to query

        Returns:
            Self for method chaining
        """
        self._from_graph = graph_uri
        return self

    def from_named(self, graph_uri: str) -> Self:
        """Add a named graph to query from.

        Args:
            graph_uri: URI of the named graph to query

        Returns:
            Self for method chaining
        """
        if graph_uri not in self._from_named_graphs:
            self._from_named_graphs.append(graph_uri)
        return self

    def clear_graphs(self) -> Self:
        """Clear all graph specifications for this query.

        Returns:
            Self for method chaining
        """
        self._from_graph = None
        self._from_named_graphs = []
        return self

    def path(self, field: str, path: PropertyPath) -> Self:
        """
        Use property path for field instead of simple predicate.

        Property paths allow traversing multiple relationship levels and using
        path operators like zero-or-more (*), one-or-more (+), zero-or-one (?),
        alternative paths (|), and inverse paths (^).

        Args:
            field: Field name to apply property path to
            path: PropertyPath object defining the path

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model

        Examples:
            >>> # Find all transitive knows relationships
            >>> session.query(Person).path(
            ...     'knows',
            ...     PropertyPath('schema:knows+')
            ... ).all()

            >>> # Find managers (boss or boss's boss, etc.)
            >>> session.query(Person).path(
            ...     'manager',
            ...     PropertyPath('org:reportsTo*')
            ... ).all()

            >>> # Find parents or guardians
            >>> session.query(Person).path(
            ...     'parent',
            ...     PropertyPath('schema:parent|schema:guardian')
            ... ).all()

            >>> # Find children (inverse path)
            >>> session.query(Person).path(
            ...     'child',
            ...     PropertyPath('^schema:child')
            ... ).all()
        """
        self._validate_field_exists(field)

        # Store property path mapping
        self._property_paths[field] = path
        return self

    def _apply_path_operator(self, field: str, path_template: str) -> Self:
        """Apply a property path operator to a field using a template.

        Args:
            field: Field name to apply operator to
            path_template: Template string with {} for the formatted predicate

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model
        """
        self._validate_field_exists(field)
        field_info = self.model._rdf_fields[field]
        formatted_pred: str = PropertyPath.format_predicate(field_info.predicate)
        path: PropertyPath = PropertyPath(path_template.format(formatted_pred))
        self._property_paths[field] = path
        return self

    def transitive(self, field: str) -> Self:
        """
        Apply transitive (one-or-more) property path operator to a field.

        This is a convenience method equivalent to:
            .path(field, PropertyPath(predicate+))
        where predicate is inferred from the field's definition.

        Args:
            field: Field name to apply transitive operator to

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model

        Examples:
            >>> # Find all transitive "knows" relationships
            >>> session.query(Person).transitive('knows').all()

            >>> # Find people Alice knows (directly or indirectly)
            >>> session.query(Person).transitive('knows').filter_by(name='Alice')
        """
        return self._apply_path_operator(field, "{}+")

    def zero_or_more(self, field: str) -> Self:
        """
        Apply zero-or-more property path operator to a field.

        This is a convenience method equivalent to:
            .path(field, PropertyPath(predicate*))
        where predicate is inferred from the field's definition.

        Args:
            field: Field name to apply zero-or-more operator to

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model

        Examples:
            >>> # Find all managers (direct and indirect)
            >>> session.query(Person).zero_or_more('manager').all()

            >>> # Find management chain for a person
            >>> query = session.query(Person).zero_or_more('manager')
            >>> query = query.filter_by(name='Alice')
        """
        return self._apply_path_operator(field, "{}*")

    def zero_or_one(self, field: str) -> Self:
        """
        Apply zero-or-one property path operator to a field.

        This is a convenience method equivalent to:
            .path(field, PropertyPath(predicate?))
        where predicate is inferred from the field's definition.

        Args:
            field: Field name to apply zero-or-one operator to

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model

        Examples:
            >>> # Find people who may or may not have a parent
            >>> session.query(Person).zero_or_one('parent').all()
        """
        return self._apply_path_operator(field, "{}?")

    def alternative(self, field: str, *alternative_fields: str) -> Self:
        """
        Apply alternative paths operator to multiple fields.

        This is a convenience method that creates a property path using the | operator
        for the predicates of the specified fields.

        Args:
            field: Primary field name
            *alternative_fields: Additional field names for alternatives

        Returns:
            Self for method chaining

        Raises:
            QueryError: If any field doesn't exist in model

        Examples:
            >>> # Find people who have either a parent or guardian
            >>> session.query(Person).alternative('parent', 'guardian').all()

            >>> # Find contact via email or phone
            >>> session.query(Person).alternative('email', 'phone', 'fax').all()
        """
        # Validate all fields exist
        all_fields: list[str] = [field] + list(alternative_fields)
        for f in all_fields:
            self._validate_field_exists(f)

        # Collect predicates and format for property path
        predicates: list[str] = []
        for f in all_fields:
            field_info = self.model._rdf_fields[f]
            predicates.append(PropertyPath.format_predicate(field_info.predicate))

        # Create property path with | operator
        path_str: str = "|".join(predicates)
        path: PropertyPath = PropertyPath(path_str)
        self._property_paths[field] = path
        return self

    def inverse(self, field: str) -> Self:
        """
        Apply inverse property path operator to a field.

        This is a convenience method equivalent to:
            .path(field, PropertyPath(^predicate))
        where predicate is inferred from the field's definition.

        Args:
            field: Field name to apply inverse operator to

        Returns:
            Self for method chaining

        Raises:
            QueryError: If field doesn't exist in model

        Examples:
            >>> # Find children (inverse of parent relationship)
            >>> session.query(Person).inverse('parent').all()

            >>> # Find subordinates (inverse of manager relationship)
            >>> session.query(Person).inverse('manager').all()
        """
        return self._apply_path_operator(field, "^{}")
