"""
Simple compiler that turns a Query object into SPARQL. Supports basic filters,
limit, and projecting declared fields.
"""

from dataclasses import dataclass
from typing import (
    TYPE_CHECKING,
    Any,
    Generic,
    TypeVar,
)

from .hydrator import ResultHydrator
from .query import (
    QueryBuilderMixin,
    QueryClausesMixin,
    QueryExecutionMixin,
    QueryPolymorphicMixin,
    QueryValuesMixin,
)
from .security import (
    escape_sparql_iri,
    format_sparql_value_secure,
    validate_sparql_operator,
)

# Forward references for type hints to avoid circular imports
if TYPE_CHECKING:
    from sparqlmojo.engine.session import Session

    from .filtering import FilterLike
    from .model import Model, PropertyPath

# TypeVar for generic Query class
T = TypeVar("T", bound="Model")

# Re-export for backward compatibility with code that imports from compiler
from .query._constants import SUBJECT_VARIABLE_NAME  # noqa: E402


@dataclass(frozen=True)
class Condition:
    field_name: str
    op: str
    value: Any

    def _format_filter_value(
        self, field_name: str, value: Any, model_class: "type[T] | None" = None
    ) -> str:
        """Format a value for use in SPARQL FILTER clause.

        Args:
            field_name: Name of the field being filtered
            value: Value to format
            model_class: Optional model class for IRI field detection

        Returns:
            Formatted SPARQL value string
        """
        from .model import IRIField, LiteralField

        # Subject variable filters on IRIs, not literals
        # Format as IRI with angle brackets: <http://example.org/resource>
        if field_name == SUBJECT_VARIABLE_NAME:
            escaped_iri: str = escape_sparql_iri(str(value))
            return f"<{escaped_iri}>"

        # Check if field is an IRI-type field in the model
        # This covers IRIField, ObjectPropertyField, and SubjectField
        if model_class is not None:
            rdf_fields: dict[str, Any] = getattr(model_class, "_rdf_fields", {})
            if field_name in rdf_fields:
                field_info = rdf_fields[field_name]
                if isinstance(field_info, IRIField):
                    return field_info.format_value(value)
                if (
                    isinstance(field_info, LiteralField)
                    and field_info.datatype is not None
                ):
                    return field_info.format_value(value)

        # Values with to_sparql_filter() format themselves (e.g. LangLiteral).
        # TODO: duplicated in filtering.FieldFilter._format_value(); extract a shared
        # helper if a third call-site appears.
        if hasattr(value, "to_sparql_filter"):
            return str(value.to_sparql_filter())

        # Regular fields use secure value formatting for literals
        return format_sparql_value_secure(value)

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

        Args:
            model_class: Optional model class for IRI field detection

        Security:
        - Validates operators against a whitelist of safe operators
        - Uses secure value formatting that escapes string literals
        - Detects and rejects SPARQL keywords in user-provided values
        - Provides protection against SPARQL injection attacks
        """
        var: str = f"?{self.field_name}"

        # Validate operator for security
        safe_op: str = validate_sparql_operator(self.op)

        # Format value based on field type (pass model_class for IRI detection)
        val: str = self._format_filter_value(self.field_name, self.value, model_class)

        return f"FILTER({var} {safe_op} {val}) ."


class Query(
    QueryExecutionMixin,
    QueryPolymorphicMixin,
    QueryClausesMixin,
    QueryValuesMixin,
    QueryBuilderMixin,
    Generic[T],
):
    def __init__(
        self,
        model_cls: "type[T]",
        session: "Session | None" = None,
    ) -> None:
        """Initialize a query for model_cls.

        Args:
            model_cls: Model class to query
            session: Optional session for execution
        """
        self.model: type[T] = model_cls
        self._session: "Session | None" = session
        self._conditions: list["FilterLike"] = []
        self._limit: int | None = None
        self._offset: int | None = None
        self._from_graph: str | None = None
        self._from_named_graphs: list[str] = []
        self._values: dict[str, list[Any]] | None = None
        self._order_by_field: str | None = None
        self._order_descending: bool = False
        self._distinct: bool = False
        self._property_paths: dict[str, "PropertyPath"] = {}
        self._aggregates: dict[str, tuple[str, str]] = {}  # {alias: (func, field)}
        self._group_by_fields: list[str] = []  # Fields to group by
        self._lang_filters: dict[str, list[str]] = {}  # {field_name: [lang_codes]}
        # Track IRI field bindings from filter_by() for direct triple pattern binding
        # Instead of OPTIONAL + FILTER, these generate: ?s <predicate> <IRI> .
        self._filter_by_iri_bindings: dict[str, str] = {}  # {field_name: iri_value}
        self._flatten_aggregation: bool = True
        # ResultHydrator for converting SPARQL JSON to model instances
        self._hydrator: ResultHydrator[T] = ResultHydrator(model_cls, session)

    def compile(self) -> str:
        # Polymorphic query path: auto-detect registered subclasses
        subclass_models: list[type] = self._get_polymorphic_subclass_models()
        if subclass_models:
            return self._compile_polymorphic(subclass_models)
        return self._compile_standard()

    def _compile_polymorphic(self, subclass_models: list[type]) -> str:
        """Compile a polymorphic query for the given subclass models."""
        s_var: str = self.model.subject_var()
        distinct_modifier: str = "DISTINCT " if self._distinct else ""

        all_fields: dict[str, Any] = self._collect_polymorphic_fields(subclass_models)
        select_clause: str = self._build_select_clause_polymorphic(
            s_var, distinct_modifier, all_fields
        )
        where_lines: list[str] = self._build_where_patterns_polymorphic(
            s_var, subclass_models, all_fields
        )
        where: str = "\n  ".join(where_lines)
        q: str = self._assemble_query_string(select_clause, where)
        if self._limit is not None:
            q += f"LIMIT {self._limit}\n"
        if self._offset is not None:
            q += f"OFFSET {self._offset}\n"
        return q

    def _compile_standard(self) -> str:
        """Compile a standard (non-polymorphic) query."""
        s_var: str = self.model.subject_var()
        distinct_modifier: str = "DISTINCT " if self._distinct else ""

        # Auto-fallback: filter_lang() on collection fields is incompatible
        # with flattened aggregation (variable renaming breaks LANG filter).
        # Fall back to subquery mode automatically.
        if self._flatten_aggregation and self._lang_filters:
            collection_lang_fields: list[str] = [
                name
                for name in self._lang_filters
                if name in self._get_collection_fields()
            ]
            if collection_lang_fields:
                self._flatten_aggregation = False

        # Auto-fallback: if all collection fields have limits, none can be
        # flattened — fall back to subquery mode to avoid invalid GROUP BY.
        if (
            self._flatten_aggregation
            and self._has_collection_fields()
            and not self._get_flattenable_collection_fields()
        ):
            self._flatten_aggregation = False

        # Build SELECT clause using appropriate helper based on query type
        select_clause: str
        if self._aggregates:
            select_clause = self._build_select_clause_aggregate(distinct_modifier)
        elif self._flatten_aggregation and self._has_collection_fields():
            select_clause = self._build_select_clause_collection_flattened(
                s_var, distinct_modifier
            )
        elif self._has_collection_fields():
            select_clause = self._build_select_clause_collection(
                s_var, distinct_modifier
            )
        else:
            select_clause = self._build_select_clause_regular(s_var, distinct_modifier)

        # Build query components using helper methods
        where_lines: list[str] = self._build_where_patterns(s_var)
        where: str = "\n  ".join(where_lines)
        q: str = self._assemble_query_string(select_clause, where)

        # Add GROUP BY clause
        if self._flatten_aggregation and self._has_collection_fields():
            q += f"GROUP BY {s_var}\n"
        elif self._group_by_fields:
            group_by_fields_str: str = " ".join(
                [f"?{field}" for field in self._group_by_fields]
            )
            q += f"GROUP BY {group_by_fields_str}\n"

        # Add ORDER BY clause if specified
        if self._order_by_field:
            order_direction: str = "DESC" if self._order_descending else "ASC"
            q += f"ORDER BY {order_direction}(?{self._order_by_field})\n"
        if self._limit is not None:
            q += f"LIMIT {self._limit}\n"
        if self._offset is not None:
            q += f"OFFSET {self._offset}\n"

        return q


class SPARQLCompiler:
    @staticmethod
    def compile_query(query: Query[Any]) -> str:
        return query.compile()
