"""Query execution mixin — query execution and result processing methods."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Iterator

from sparqlmojo.exc.exceptions import ConfigurationError, QueryError

if TYPE_CHECKING:
    from sparqlmojo.engine.session import Session
    from sparqlmojo.orm.hydrator import ResultHydrator
    from sparqlmojo.orm.model import Model


class QueryExecutionMixin:
    """Mixin providing query execution methods."""

    # Type stubs for attributes set in Query.__init__
    if TYPE_CHECKING:
        model: type[Model]
        _session: Session | None
        _limit: int | None
        _aggregates: dict[str, tuple[str, str]]
        _group_by_fields: list[str]
        _distinct: bool
        _hydrator: ResultHydrator[Any]

        def compile(self) -> str: ...
        def _build_prefix_declarations(self) -> str: ...
        def _build_from_clauses(self) -> str: ...
        def _build_where_patterns(self, s_var: str = "?s") -> list[str]: ...

    def _require_session(self) -> Session:
        """Validate that a session is bound and return it.

        Returns:
            The bound Session instance

        Raises:
            ConfigurationError: If query not bound to session
        """
        if not self._session:
            raise ConfigurationError("Query not bound to session")
        return self._session

    def _compile_with_limit(self, n: int) -> str:
        """Compile the query with a temporary limit override.

        Args:
            n: Temporary limit to apply during compilation

        Returns:
            Compiled SPARQL query string
        """
        original_limit: int | None = self._limit
        self._limit = n
        try:
            sparql: str = self.compile()
        finally:
            self._limit = original_limit
        return sparql

    def _first_aggregate_or_none(
        self, results: dict[str, Any]
    ) -> dict[str, Any] | None:
        """Process aggregate results and return the first one, or None."""
        aggregate_results: list[dict[str, Any]] = self._process_aggregate_results(
            results
        )
        return aggregate_results[0] if aggregate_results else None

    def _process_aggregate_results(
        self, results: dict[str, Any]
    ) -> list[dict[str, Any]]:
        """
        Process aggregate query results into a list of dictionaries.

        Args:
            results: SPARQL JSON results from query execution

        Returns:
            List of dictionaries with aggregate results
        """
        aggregate_results: list[dict[str, Any]] = []

        for binding in results["results"]["bindings"]:
            result_dict: dict[str, Any] = {}

            # Process aggregate values
            for alias in self._aggregates.keys():
                if alias in binding:
                    value_data: dict[str, Any] = binding[alias]
                    if value_data["type"] == "literal":
                        # Convert datatype if present
                        if "datatype" in value_data:
                            result_dict[alias] = self._hydrator._convert_datatype(
                                value_data["value"], value_data["datatype"]
                            )
                        else:
                            result_dict[alias] = value_data["value"]

            # Process GROUP BY fields if present
            for group_field in self._group_by_fields:
                if group_field in binding:
                    group_value_data: dict[str, Any] = binding[group_field]
                    if group_value_data["type"] == "uri":
                        result_dict[group_field] = group_value_data["value"]
                    elif group_value_data["type"] == "literal":
                        if "datatype" in group_value_data:
                            result_dict[group_field] = self._hydrator._convert_datatype(
                                group_value_data["value"],
                                group_value_data["datatype"],
                            )
                        else:
                            result_dict[group_field] = group_value_data["value"]
                else:
                    # GROUP BY field is unbound (no data) - include as None
                    result_dict[group_field] = None

            aggregate_results.append(result_dict)

        return aggregate_results

    def all(self) -> list[Any]:
        """
        Execute query and return all matching instances.

        Returns:
            List of model instances (may be empty)

        Raises:
            ConfigurationError: If query not bound to session
        """
        session: Session = self._require_session()

        sparql: str = self.compile()
        results: dict[str, Any] | None = session.execute_sparql(sparql, construct=False)

        if not results or "results" not in results:
            return []

        if self._aggregates:
            return self._process_aggregate_results(results)

        instances: list[Any] = []
        for binding in results["results"]["bindings"]:
            instance = self._hydrator.hydrate_instance(binding)
            instances.append(instance)

        return instances

    def first(self) -> Any:
        """
        Execute query and return first result or None.

        Returns:
            First model instance or None if no results

        Raises:
            ConfigurationError: If query not bound to session
        """
        session: Session = self._require_session()

        sparql: str = self._compile_with_limit(1)

        # Execute and get first binding
        results: dict[str, Any] | None = session.execute_sparql(sparql, construct=False)

        if not results or "results" not in results:
            return None

        bindings: list[dict[str, Any]] = results["results"]["bindings"]
        if not bindings:
            return None

        if self._aggregates:
            return self._first_aggregate_or_none(results)

        return self._hydrator.hydrate_instance(bindings[0])

    def one(self) -> Any:
        """
        Execute query and return exactly one result.

        Returns:
            Single model instance

        Raises:
            ConfigurationError: If query not bound to session
            QueryError: If zero or multiple results found
        """
        session: Session = self._require_session()

        sparql: str = self._compile_with_limit(2)

        results: dict[str, Any] | None = session.execute_sparql(sparql, construct=False)

        if not results or "results" not in results:
            raise QueryError(f"No instances of {self.model.__name__} found")

        bindings: list[dict[str, Any]] = results["results"]["bindings"]

        if len(bindings) == 0:
            raise QueryError(f"No instances of {self.model.__name__} found")

        if len(bindings) > 1:
            raise QueryError(
                f"Multiple instances found (at least {len(bindings)}), "
                f"expected exactly one"
            )

        if self._aggregates:
            return self._first_aggregate_or_none(results)

        return self._hydrator.hydrate_instance(bindings[0])

    def one_or_none(self) -> Any:
        """
        Execute query and return one result or None.

        Returns:
            Single model instance or None

        Raises:
            ConfigurationError: If query not bound to session
            QueryError: If multiple results found
        """
        session: Session = self._require_session()

        sparql: str = self._compile_with_limit(2)

        results: dict[str, Any] | None = session.execute_sparql(sparql, construct=False)

        if not results or "results" not in results:
            return None

        bindings: list[dict[str, Any]] = results["results"]["bindings"]

        if len(bindings) == 0:
            return None

        if len(bindings) > 1:
            raise QueryError(
                f"Multiple instances found (at least {len(bindings)}), "
                f"expected at most one"
            )

        if self._aggregates:
            return self._first_aggregate_or_none(results)

        return self._hydrator.hydrate_instance(bindings[0])

    def __iter__(self) -> Iterator[Any]:
        """
        Allow iteration over query results with lazy evaluation.

        This method provides true lazy iteration by executing the query once
        and yielding results one at a time, rather than loading all results
        into memory at once.

        Usage: for person in session.query(Person):

        Returns:
            Iterator that yields model instances one at a time

        Raises:
            ConfigurationError: If query not bound to session
        """
        session: Session = self._require_session()

        # Compile the query for execution
        sparql: str = self.compile()

        # Execute the query
        results: dict[str, Any] | None = session.execute_sparql(sparql, construct=False)

        if not results or "results" not in results:
            return iter([])  # Empty iterator

        if self._aggregates:
            return iter(self._process_aggregate_results(results))

        # Create a generator that yields instances one by one
        def result_generator() -> Iterator[Any]:
            for binding in results["results"]["bindings"]:
                instance = self._hydrator.hydrate_instance(binding)
                yield instance

        return result_generator()

    def count(self) -> int:
        """
        Return count of matching entities using SPARQL COUNT aggregate.

        This method is more efficient than len(query.all()) as it uses
        a COUNT(*) aggregate query that doesn't need to retrieve all
        the actual data.

        Returns:
            Count of matching entities

        Raises:
            ConfigurationError: If query not bound to session
        """
        session: Session = self._require_session()

        # Build a COUNT query instead of fetching all data
        s_var: str = self.model.subject_var()
        distinct_modifier: str = "DISTINCT " if self._distinct else ""

        prefix_declarations: str = self._build_prefix_declarations()
        from_clause: str = self._build_from_clauses()
        where_lines: list[str] = self._build_where_patterns(s_var)
        where: str = "\n  ".join(where_lines)

        # Build the COUNT query
        count_query: str = (
            f"{prefix_declarations}SELECT (COUNT({distinct_modifier}*) as ?count)\n"
            f"{from_clause}WHERE {{\n  {where}\n}}"
        )

        # Execute the COUNT query
        count_results: dict[str, Any] | None = session.execute_sparql(
            count_query, construct=False
        )

        if not count_results or "results" not in count_results:
            return 0

        # Extract the count value
        bindings: list[dict[str, Any]] = count_results["results"]["bindings"]
        if not bindings:
            return 0

        count_binding: dict[str, Any] = bindings[0]
        if "count" in count_binding:
            count_data: dict[str, Any] = count_binding["count"]
            if count_data["type"] == "literal":
                return int(count_data["value"])

        return 0
