"""Query clauses mixin — SELECT/WHERE/FROM clause builders and field patterns."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from sparqlmojo.orm.model import (
    COLLECTION_CONCAT_SUFFIX,
    DEFAULT_COLLECTION_SEPARATOR,
    LANG_VARIABLE_SUFFIX,
    SAMPLE_VARIABLE_SUFFIX,
    IRIField,
    PropertyPath,
    _escape_sparql_string,
    is_collection_field,
)
from sparqlmojo.orm.query._constants import SUBJECT_VARIABLE_NAME
from sparqlmojo.orm.security import escape_sparql_iri, escape_sparql_literal

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


class QueryClausesMixin:
    """Mixin providing SPARQL clause generation for queries."""

    # Type stubs for attributes set in Query.__init__
    if TYPE_CHECKING:
        model: type[Model]
        _session: Session | None
        _conditions: list[FilterLike]
        _values: dict[str, list[Any]] | None
        _from_graph: str | None
        _from_named_graphs: list[str]
        _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
        _distinct: bool

        def _format_values_clause_value(self, var_name: str, value: Any) -> str: ...

    def _add_subject_field_bind(
        self, where_lines: list[str], s_var: str = "?s"
    ) -> None:
        """Add BIND clause for SubjectField if present."""
        if self.model._subject_field_name:
            where_lines.append(f"BIND({s_var} AS ?{self.model._subject_field_name}) .")

    def _get_regular_fields(self) -> dict[str, Any]:
        """Get all RDF fields excluding SubjectField."""
        return {
            name: field
            for name, field in self.model._rdf_fields.items()
            if not field.is_subject()
        }

    def _has_collection_fields(self) -> bool:
        """Check if the model has any collection fields."""
        return any(
            is_collection_field(field) for field in self.model._rdf_fields.values()
        )

    def _get_collection_fields(self) -> dict[str, Any]:
        """Get all collection fields (LiteralList, LangStringList, IRIList)."""
        return {
            name: field
            for name, field in self.model._rdf_fields.items()
            if is_collection_field(field)
        }

    def _get_non_collection_regular_fields(self) -> dict[str, Any]:
        """Get regular fields that are not collection fields or SubjectField."""
        return {
            name: field
            for name, field in self.model._rdf_fields.items()
            if (not is_collection_field(field) and not field.is_subject())
        }

    def _get_flattenable_collection_fields(self) -> dict[str, Any]:
        """Get collection fields that can be flattened (no limit set)."""
        return {
            name: field
            for name, field in self._get_collection_fields().items()
            if getattr(field, "limit", None) is None
        }

    def _get_subquery_collection_fields(self) -> dict[str, Any]:
        """Get collection fields that must remain as subqueries (have limit)."""
        return {
            name: field
            for name, field in self._get_collection_fields().items()
            if getattr(field, "limit", None) is not None
        }

    def _compile_collection_subquery_pattern(
        self,
        field_name: str,
        field_info: Any,  # CollectionFieldMixin subclass with predicate attribute
        s_var: str,
        subject_values_clause: str = "",
        graph_uris: list[str] | None = None,
    ) -> str:
        """
        Generate OPTIONAL subquery pattern for a collection field in WHERE clause.

        Uses OPTIONAL with a subquery that does GROUP BY internally. This avoids
        cartesian product explosion because each subquery returns at most one row
        per subject (already aggregated).

        When subject_values_clause is provided, it is inserted at the start of
        the subquery WHERE block so the endpoint can constrain the scan to only
        the specified subjects. Without this, large endpoints scan the entire
        dataset inside the subquery before joining.

        When graph_uris is provided, the inner triple pattern is wrapped in
        GRAPH clauses so the subquery is scoped to the same named graphs as
        the outer query. SPARQL 1.1 subqueries do not inherit FROM clauses
        from the outer query (SubSelect has no DatasetClause), so FROM in a
        subquery is not valid SPARQL 1.1. GRAPH patterns are the correct way
        to restrict a subquery to a specific named graph.

        Standard SPARQL 1.1 pattern with graph scoping:
            OPTIONAL {
                SELECT ?s (GROUP_CONCAT(?val; separator="...") AS ?field_concat)
                WHERE { VALUES (?s) { ... } GRAPH <graph> { ?s <predicate> ?val } }
                GROUP BY ?s
            }

        Args:
            field_name: Name of the collection field
            field_info: RDF field metadata (must be a collection field type)
            s_var: Subject variable (e.g., "?s")
            subject_values_clause: Optional VALUES clause to push into the
                subquery WHERE block for constraint pushdown
            graph_uris: Optional list of named graph URIs to scope the subquery
                triple pattern via GRAPH clauses. Multiple URIs are UNIONed.

        Returns:
            OPTIONAL subquery pattern string for WHERE clause
        """
        predicate_expr: str = self._resolve_predicate_expr(field_name, field_info)

        sep: str = getattr(field_info, "separator", DEFAULT_COLLECTION_SEPARATOR)
        escaped_sep: str = _escape_sparql_string(sep)
        inner_var: str = f"{field_name}_inner"
        concat_var: str = f"{field_name}{COLLECTION_CONCAT_SUFFIX}"
        limit: int | None = getattr(field_info, "limit", None)

        # Build the subquery SELECT clause using polymorphic method
        # Each collection field type knows how to generate its own GROUP_CONCAT
        group_concat_expr: str = field_info.get_group_concat_expr(
            inner_var, concat_var, escaped_sep
        )
        select_clause: str = f"{s_var} {group_concat_expr}"

        # Build the triple pattern (with optional VALUES pushdown)
        triple_pattern: str = ""
        if subject_values_clause:
            triple_pattern += f"{subject_values_clause} "
        triple_pattern += f"{s_var} {predicate_expr} ?{inner_var}"

        # Scope the triple pattern to named graphs using GRAPH clauses.
        # FROM in a SPARQL 1.1 subquery is not valid (SubSelect has no
        # DatasetClause), so we wrap the triple pattern in GRAPH instead.
        # Multiple graph URIs are UNIONed so values from each are aggregated.
        inner_where: str = self._wrap_triple_in_graph(triple_pattern, graph_uris)

        # Wrap with nested SELECT if limit is specified
        # This ensures LIMIT is applied before GROUP_CONCAT aggregation
        if limit is not None:
            inner_where = (
                f"{{ SELECT {s_var} ?{inner_var} "
                f"WHERE {{ {inner_where} }} "
                f"LIMIT {limit} }}"
            )

        return (
            f"OPTIONAL {{ SELECT {select_clause} "
            f"WHERE {{ {inner_where} }} "
            f"GROUP BY {s_var} }}"
        )

    def _resolve_predicate_expr(self, field_name: str, field_info: Any) -> str:
        """Resolve the SPARQL predicate expression for a field.

        Handles query-level property paths, field-level property paths,
        IRI expansion, and operator detection.

        Args:
            field_name: Name of the field
            field_info: RDF field metadata

        Returns:
            SPARQL predicate expression string
        """
        if field_name in self._property_paths:
            return self._property_paths[field_name].to_sparql()
        if field_info.is_property_path():
            return str(field_info.predicate)
        predicate: str = field_info.predicate
        if self._session:
            predicate = str(self._session.expand_iri(predicate))
        if PropertyPath.has_operators(predicate):
            return predicate
        return f"<{escape_sparql_iri(predicate)}>"

    def _wrap_triple_in_graph(
        self, triple_pattern: str, graph_uris: list[str] | None
    ) -> str:
        """Wrap a triple pattern in GRAPH clause(s) if graph URIs are present.

        Multiple graph URIs are UNIONed so values from each are aggregated.

        Args:
            triple_pattern: The triple pattern string to wrap
            graph_uris: Optional list of named graph URIs

        Returns:
            Triple pattern optionally wrapped in GRAPH clause(s)
        """
        if not graph_uris:
            return triple_pattern
        if len(graph_uris) == 1:
            return f"GRAPH <{escape_sparql_iri(graph_uris[0])}> {{ {triple_pattern} }}"
        return " UNION ".join(
            f"{{ GRAPH <{escape_sparql_iri(uri)}> {{ {triple_pattern} }} }}"
            for uri in graph_uris
        )

    def _get_graph_uris(self) -> list[str]:
        """Collect named graph URIs for GRAPH clauses in subqueries.

        Includes from_graph (also declared as FROM NAMED in the outer query)
        followed by any explicit from_named URIs.

        Returns:
            List of graph URI strings (may be empty)
        """
        graph_uris: list[str] = []
        if self._from_graph:
            graph_uris.append(self._from_graph)
        graph_uris.extend(self._from_named_graphs)
        return graph_uris

    def _extract_subject_conditions(self) -> tuple[list[str], list["FilterLike"]]:
        """
        Extract subject equality conditions for VALUES clause generation.

        When filter_by() is used on a SubjectField, it creates
        Condition("s", "=", value). These conditions should be converted to
        VALUES clauses instead of FILTER clauses for efficient query execution
        on large endpoints.

        Only equality conditions (op == "=") are extracted for VALUES clause
        generation. Non-equality operators (>, <, >=, <=, !=) must remain as
        FILTER clauses because:
        1. VALUES clauses only support exact value matching, not comparisons
        2. Non-equality filters require the SPARQL engine to evaluate each
           candidate against the condition, which inherently requires scanning

        LogicalExpression objects (AND/OR combinations) are also kept as FILTER
        because they may contain complex nested conditions that cannot be
        expressed in a simple VALUES clause.

        Returns:
            Tuple of (subject_values, remaining_conditions) where:
            - subject_values: List of IRI strings from subject equality conditions
            - remaining_conditions: List of conditions that should still become FILTER
        """
        from sparqlmojo.orm.compiler import Condition

        subject_values: list[str] = []
        remaining_conditions: list["FilterLike"] = []

        for cond in self._conditions:
            # Check if this is a Condition on the subject variable with equality
            if (
                isinstance(cond, Condition)
                and cond.field_name == SUBJECT_VARIABLE_NAME
                and cond.op == "="
            ):
                subject_values.append(str(cond.value))
            else:
                remaining_conditions.append(cond)

        return subject_values, remaining_conditions

    def _get_pattern_object(self, field_name: str, iri_binding: str | None) -> str:
        """Get the object part of a triple pattern (IRI or variable).

        This helper reduces duplication in pattern building by centralizing
        the logic for generating the object clause of a triple pattern.

        Args:
            field_name: Name of the field (used for variable binding)
            iri_binding: IRI value for direct binding, or None for variable binding

        Returns:
            Either a wrapped IRI (<http://...>) or a SPARQL variable (?field_name)
        """
        if iri_binding:
            return f"<{escape_sparql_iri(iri_binding)}>"
        return f"?{field_name}"

    def _add_field_patterns(self, where_lines: list[str], s_var: str = "?s") -> None:
        """Add patterns for all regular (non-subject) fields.

        Generates required (non-OPTIONAL) patterns when:
        - Field has VALUES binding (constrains the subject variable)
        - Field has required=True
        - Field has IRI filter_by binding (direct triple pattern binding)

        For IRIField with filter_by binding, generates direct pattern:
            ?s <predicate> <IRI> .
        instead of:
            OPTIONAL { ?s <predicate> ?field . }

        For collection field queries, collection fields are handled by
        OPTIONAL subquery patterns (added separately) rather than simple
        OPTIONAL patterns here.
        """
        # Determine which fields to add patterns for
        # Skip collection fields if query has collection fields (handled by subqueries)
        if self._has_collection_fields():
            fields_to_add: dict[str, Any] = self._get_non_collection_regular_fields()
        else:
            fields_to_add = self._get_regular_fields()

        for name, field in fields_to_add.items():
            self._add_single_field_pattern(where_lines, name, field, s_var)

    def _add_single_field_pattern(
        self,
        where_lines: list[str],
        name: str,
        field: Any,
        s_var: str,
    ) -> None:
        """Build and append the triple pattern for a single field.

        Determines whether the pattern should be required or OPTIONAL,
        resolves the predicate expression and object, and appends the
        appropriate pattern (with optional BIND for IRI defaults).
        """
        iri_binding: str | None = self._filter_by_iri_bindings.get(name)

        field_default: Any = getattr(field, "default", None)
        is_iri_field_with_default: bool = (
            isinstance(field, IRIField) and field_default is not None
        )

        # A field is required when it has VALUES binding, required=True,
        # IRI filter_by binding, or is an IRIField with a default value
        use_required_pattern: bool = (
            (self._values is not None and name in self._values)
            or getattr(field, "required", False)
            or iri_binding is not None
            or is_iri_field_with_default
        )

        predicate_expr: str = self._resolve_predicate_expr(name, field)

        # Resolve the object part: IRI binding, default IRI, or variable
        if iri_binding is not None:
            pattern_object: str = self._get_pattern_object(name, iri_binding)
        elif is_iri_field_with_default:
            default_iri: str = field_default
            if self._session:
                default_iri = self._session.expand_iri(default_iri)
            pattern_object = f"<{escape_sparql_iri(default_iri)}>"
        else:
            pattern_object = self._get_pattern_object(name, None)

        pattern: str = f"{s_var} {predicate_expr} {pattern_object} ."

        if use_required_pattern:
            where_lines.append(pattern)
            if is_iri_field_with_default and not iri_binding:
                where_lines.append(f"BIND({pattern_object} AS ?{name}) .")
        else:
            where_lines.append(f"OPTIONAL {{ {pattern} }}")

    def _add_iri_filter_by_binds(self, where_lines: list[str]) -> None:
        """Add BIND statements for IRI fields used in filter_by().

        When filter_by() binds an IRI value directly in the triple pattern
        (e.g., ?s <predicate> <IRI> .), we need BIND statements to make the
        variable available in SELECT clause.

        This generates patterns like:
            BIND(<http://example.org/work> AS ?work_iri) .

        The BIND ensures the field variable is available for hydration while
        the direct triple pattern ensures correct filtering semantics.
        """
        for field_name, iri_value in self._filter_by_iri_bindings.items():
            iri_wrapped: str = f"<{escape_sparql_iri(iri_value)}>"
            where_lines.append(f"BIND({iri_wrapped} AS ?{field_name}) .")

    def _add_collection_subquery_patterns(
        self,
        where_lines: list[str],
        s_var: str = "?s",
        subject_values_clause: str = "",
    ) -> None:
        """Add OPTIONAL subquery patterns for collection fields.

        Each collection field gets its own OPTIONAL subquery with internal
        GROUP BY. This avoids cartesian product because each subquery returns
        at most one row per subject (already aggregated).

        When subject_values_clause is provided, it is pushed into each
        subquery's WHERE block so the endpoint can constrain the scan.
        """
        if not self._has_collection_fields():
            return

        collection_fields: dict[str, Any] = self._get_collection_fields()
        subquery_graph_uris: list[str] = self._get_graph_uris()
        for name, field_info in collection_fields.items():
            pattern: str = self._compile_collection_subquery_pattern(
                name,
                field_info,
                s_var,
                subject_values_clause,
                subquery_graph_uris or None,
            )
            where_lines.append(pattern)

    def _add_flattened_collection_patterns(
        self,
        where_lines: list[str],
        s_var: str = "?s",
        subject_values_clause: str = "",
    ) -> None:
        """Add OPTIONAL patterns for flattened collection aggregation.

        Flattenable collection fields (no ``limit``) get simple OPTIONAL
        triple patterns. Fields with ``limit`` fall back to the standard
        OPTIONAL subquery approach.

        Args:
            where_lines: List to append WHERE clause lines to
            s_var: Subject variable (e.g., "?s")
            subject_values_clause: Optional VALUES clause for subquery fields
        """
        if not self._has_collection_fields():
            return

        graph_uris: list[str] = self._get_graph_uris()

        # Flattenable fields: simple OPTIONAL triple patterns
        for name, field_info in self._get_flattenable_collection_fields().items():
            inner_var: str = f"{name}_inner"
            predicate_expr: str = self._resolve_predicate_expr(name, field_info)
            triple_pattern: str = f"{s_var} {predicate_expr} ?{inner_var}"
            triple_pattern = self._wrap_triple_in_graph(
                triple_pattern, graph_uris or None
            )
            where_lines.append(f"OPTIONAL {{ {triple_pattern} }}")

        # Subquery fields (with limit): use existing subquery pattern
        subquery_fields: dict[str, Any] = self._get_subquery_collection_fields()
        for name, field_info in subquery_fields.items():
            pattern: str = self._compile_collection_subquery_pattern(
                name,
                field_info,
                s_var,
                subject_values_clause,
                graph_uris or None,
            )
            where_lines.append(pattern)

    def _build_prefix_declarations(self) -> str:
        """
        Build PREFIX declarations from session's prefix registry.

        Returns:
            PREFIX declarations as a string (with trailing newline if non-empty)
        """
        prefix_declarations: str = ""
        if self._session:
            prefix_decls: str = self._session.get_prefix_declarations()
            if prefix_decls:
                prefix_declarations = prefix_decls + "\n"
        return prefix_declarations

    def _build_from_clauses(self) -> str:
        """
        Build FROM and FROM NAMED clauses for the query.

        For from_graph URIs, both FROM <g> (sets the default graph for outer
        WHERE patterns) and FROM NAMED <g> (makes the graph addressable via
        GRAPH <g> in collection subqueries) are emitted. This dual declaration
        is required because SPARQL 1.1 subqueries (SubSelect) have no
        DatasetClause, so subquery scoping must use GRAPH patterns instead of
        FROM, and GRAPH patterns can only address FROM NAMED graphs.

        Returns:
            FROM clauses as a string (with trailing newline if non-empty)
        """
        from_clauses: list[str] = []
        if self._from_graph:
            escaped: str = escape_sparql_iri(self._from_graph)
            from_clauses.append(f"FROM <{escaped}>")
            from_clauses.append(f"FROM NAMED <{escaped}>")
        for named_graph in self._from_named_graphs:
            from_clauses.append(f"FROM NAMED <{escape_sparql_iri(named_graph)}>")

        return "\n".join(from_clauses) + "\n" if from_clauses else ""

    @staticmethod
    def _deduplicate_preserving_order(values: list[str]) -> list[str]:
        """Deduplicate a list while preserving order (first occurrence wins).

        Args:
            values: List of strings to deduplicate.

        Returns:
            Deduplicated list maintaining original order.
        """
        seen: set[str] = set()
        deduplicated: list[str] = []
        for val in values:
            if val not in seen:
                seen.add(val)
                deduplicated.append(val)
        return deduplicated

    def _build_values_clause(
        self, subject_filter_values: list[str] | None = None
    ) -> str:
        """
        Build VALUES clause if values are specified.

        Args:
            subject_filter_values: Optional list of IRI values from subject equality
                conditions (from filter_by on SubjectField). These are merged with
                any existing VALUES on ?s.

        Returns:
            VALUES clause as a string, or empty string if no values
        """

        # Merge subject filter values with existing _values if both exist
        effective_values: dict[str, list[Any]] = {}
        if self._values:
            effective_values = dict(self._values)

        # Handle subject filter values from filter_by() on SubjectField
        if subject_filter_values:
            if SUBJECT_VARIABLE_NAME in effective_values:
                # Merge: combine existing ?s values with filter_by values
                existing_s_values: list[Any] = effective_values[SUBJECT_VARIABLE_NAME]
                merged_s_values: list[str] = (
                    list(existing_s_values) + subject_filter_values
                )
                effective_values[SUBJECT_VARIABLE_NAME] = (
                    self._deduplicate_preserving_order(merged_s_values)
                )
            else:
                # Create new ?s binding from filter_by values
                effective_values[SUBJECT_VARIABLE_NAME] = (
                    self._deduplicate_preserving_order(subject_filter_values)
                )

        if not effective_values:
            return ""

        # Generate variable list
        vars_list: list[str] = [f"?{var}" for var in effective_values.keys()]
        vars_clause: str = " ".join(vars_list)

        # Generate value rows
        rows: list[str] = []
        num_rows: int = len(next(iter(effective_values.values())))
        for i in range(num_rows):
            row_values: list[str] = []
            for var in effective_values.keys():
                value: Any = effective_values[var][i]
                # Format value based on field type (IRI vs literal)
                formatted_value: str = self._format_values_clause_value(var, value)
                row_values.append(formatted_value)
            rows.append(f"({' '.join(row_values)})")

        return f"VALUES ({vars_clause}) {{ {' '.join(rows)} }}"

    def _build_subject_values_clause(
        self, subject_filter_values: list[str] | None = None
    ) -> str:
        """
        Build a VALUES clause containing only ?s bindings for pushing into
        collection subqueries.

        Sources subject IRIs from two places:
        - self._values['s'] (from .values() calls)
        - subject_filter_values (from .filter_by() on SubjectField)

        Deduplicates while preserving order. Returns empty string if no
        subject bindings exist (unconstrained queries are unchanged).

        Args:
            subject_filter_values: Optional list of IRI values from subject
                equality conditions (from filter_by on SubjectField).

        Returns:
            VALUES clause string like ``VALUES (?s) { (<iri1>) (<iri2>) }``
            or empty string if no subject bindings.
        """

        # Collect all subject IRI values
        all_subject_iris: list[str] = []

        # From .values() on ?s
        if self._values and SUBJECT_VARIABLE_NAME in self._values:
            all_subject_iris.extend(self._values[SUBJECT_VARIABLE_NAME])

        # From filter_by() on SubjectField
        if subject_filter_values:
            all_subject_iris.extend(subject_filter_values)

        if not all_subject_iris:
            return ""

        deduplicated: list[str] = self._deduplicate_preserving_order(all_subject_iris)

        # Format as VALUES clause with only ?s
        rows: list[str] = [f"(<{escape_sparql_iri(iri)}>)" for iri in deduplicated]
        return f"VALUES (?s) {{ {' '.join(rows)} }}"

    def _build_where_patterns(self, s_var: str = "?s") -> list[str]:
        """
        Build WHERE clause patterns including VALUES, type, BIND, fields, and filters.

        Subject equality conditions (from filter_by on SubjectField) are converted to
        VALUES clauses for efficient query execution, rather than FILTER clauses which
        cause full table scans on large endpoints.

        Args:
            s_var: Subject variable to use (default: "?s")

        Returns:
            List of WHERE clause pattern strings
        """
        where_lines: list[str] = []

        # Extract subject equality conditions for VALUES clause generation
        # These should NOT become FILTER clauses (causes timeouts on large endpoints)
        subject_filter_values: list[str]
        remaining_conditions: list["FilterLike"]
        subject_filter_values, remaining_conditions = self._extract_subject_conditions()

        # VALUES clause first - provides initial bindings before they're referenced
        # This must come before BIND and field patterns that reference these variables
        # Include subject filter values from filter_by() on SubjectField
        values_clause: str = self._build_values_clause(subject_filter_values or None)
        if values_clause:
            where_lines.append(values_clause)

        # Add BIND clause for SubjectField (if present)
        self._add_subject_field_bind(where_lines, s_var)

        # Add OPTIONAL patterns for non-collection fields
        self._add_field_patterns(where_lines, s_var)

        # Add BIND statements for IRI filter_by fields
        # These make the field variable available in SELECT after direct binding
        self._add_iri_filter_by_binds(where_lines)

        # Compute subject VALUES clause for pushing into collection subqueries.
        # This constrains each subquery to only the specified subjects, preventing
        # full dataset scans on large endpoints (Issue #139).
        subject_values_clause: str = self._build_subject_values_clause(
            subject_filter_values or None
        )

        # Add collection field patterns (subquery or flattened)
        if self._flatten_aggregation and self._has_collection_fields():
            self._add_flattened_collection_patterns(
                where_lines, s_var, subject_values_clause
            )
        else:
            self._add_collection_subquery_patterns(
                where_lines, s_var, subject_values_clause
            )

        # Language filters for LangString fields
        for field_name, lang_codes in self._lang_filters.items():
            if len(lang_codes) == 1:
                # Single language: FILTER(LANG(?field) = "en")
                safe_lang: str = escape_sparql_literal(lang_codes[0])
                where_lines.append(f'FILTER(LANG(?{field_name}) = "{safe_lang}") .')
            else:
                # Multiple languages: FILTER(LANG(?field) IN ("en", "fr"))
                quoted_langs: str = ", ".join(
                    f'"{escape_sparql_literal(lang)}"' for lang in lang_codes
                )
                where_lines.append(f"FILTER(LANG(?{field_name}) IN ({quoted_langs})) .")

        # Filter conditions (excluding subject equality conditions already in VALUES)
        # All condition types (FilterExpression, LogicalExpression,
        # CollectionContainsExpression, Condition) implement to_sparql(model_class)
        for cond in remaining_conditions:
            where_lines.append(cond.to_sparql(self.model))

        return where_lines

    def _get_lang_var_expression(self, field_name: str) -> str | None:
        """
        Generate LANG() extraction expression for a field if it supports language tags.

        Uses the polymorphic get_lang_select_expression() method on the field
        to determine if and how to generate the LANG() expression.

        Args:
            field_name: Name of the field to check

        Returns:
            LANG() expression string like "(LANG(?name) AS ?name_lang)" if field
            supports language tags (e.g., LangString), None otherwise
        """
        field_info = self.model._rdf_fields.get(field_name)
        if field_info is None:
            return None
        # Use polymorphic method - LangString returns the expression, others return None
        return field_info.get_lang_select_expression(field_name)

    def _build_select_clause_aggregate(self, distinct_modifier: str) -> str:
        """
        Build SELECT clause for aggregate queries.

        Args:
            distinct_modifier: "DISTINCT " or "" depending on query settings

        Returns:
            Complete SELECT clause string for aggregate query
        """
        agg_selects: list[str] = []

        for alias, (func, field_name) in self._aggregates.items():
            if field_name == "*":
                # Special case for COUNT(*)
                agg_selects.append(f"({func}(*) as ?{alias})")
            else:
                # Regular field aggregate
                agg_selects.append(f"({func}(?{field_name}) as ?{alias})")

        # Add GROUP BY fields to SELECT if present
        for group_field in self._group_by_fields:
            agg_selects.append(f"?{group_field}")

        return f"SELECT {distinct_modifier}" + " ".join(agg_selects)

    def _build_select_clause_collection(
        self, s_var: str, distinct_modifier: str
    ) -> str:
        """
        Build SELECT clause for collection field queries.

        Collection fields use OPTIONAL subqueries with internal GROUP BY.
        The SELECT references the concatenated result variables from subqueries.

        Args:
            s_var: Subject variable (e.g., "?s")
            distinct_modifier: "DISTINCT " or "" depending on query settings

        Returns:
            Complete SELECT clause string for collection query
        """
        select_vars: list[str] = [s_var]

        # Add SubjectField if present
        if self.model._subject_field_name:
            select_vars.append(f"?{self.model._subject_field_name}")

        # Non-collection fields: use direct variables with LANG() for LangString
        for name, field_info in self._get_non_collection_regular_fields().items():
            select_vars.append(f"?{name}")
            lang_expr: str | None = self._get_lang_var_expression(name)
            if lang_expr:
                select_vars.append(lang_expr)

        # Collection fields: reference the result variables from subqueries
        # For LangStringList, value and language are combined in single variable
        collection_fields: dict[str, Any] = self._get_collection_fields()
        for name in collection_fields:
            concat_var: str = f"{name}{COLLECTION_CONCAT_SUFFIX}"
            select_vars.append(f"?{concat_var}")

        return f"SELECT {distinct_modifier}" + " ".join(select_vars)

    def _build_select_clause_collection_flattened(
        self, s_var: str, distinct_modifier: str
    ) -> str:
        """
        Build SELECT clause for flattened collection aggregation queries.

        Uses a single outer GROUP BY instead of per-field subqueries.
        Non-collection fields are wrapped in SAMPLE() since they are not
        part of the GROUP BY. Collection fields without ``limit`` get inline
        GROUP_CONCAT(DISTINCT ...) expressions. Collection fields with
        ``limit`` reference their subquery result variables.

        Args:
            s_var: Subject variable (e.g., "?s")
            distinct_modifier: "DISTINCT " or "" depending on query settings

        Returns:
            Complete SELECT clause string for flattened collection query
        """
        select_vars: list[str] = [s_var]

        # SubjectField: wrap in SAMPLE() since it's not in GROUP BY
        if self.model._subject_field_name:
            subj_name: str = self.model._subject_field_name
            select_vars.append(
                f"(SAMPLE(?{subj_name}) AS ?{subj_name}{SAMPLE_VARIABLE_SUFFIX})"
            )

        # Non-collection fields: wrap in SAMPLE() due to outer GROUP BY
        for name, field_info in self._get_non_collection_regular_fields().items():
            select_vars.append(f"(SAMPLE(?{name}) AS ?{name}{SAMPLE_VARIABLE_SUFFIX})")
            lang_expr: str | None = self._get_lang_var_expression(name)
            if lang_expr:
                # The hydrator looks for {binding_var}{LANG_VARIABLE_SUFFIX}
                # where binding_var is name_sample, so emit name_sample_lang
                sampled_lang_var: str = (
                    f"{name}{SAMPLE_VARIABLE_SUFFIX}{LANG_VARIABLE_SUFFIX}"
                )
                select_vars.append(f"(SAMPLE(LANG(?{name})) AS ?{sampled_lang_var})")

        # Flattenable collection fields: inline GROUP_CONCAT(DISTINCT ...)
        for name, field_info in self._get_flattenable_collection_fields().items():
            sep: str = getattr(field_info, "separator", DEFAULT_COLLECTION_SEPARATOR)
            escaped_sep: str = _escape_sparql_string(sep)
            inner_var: str = f"{name}_inner"
            concat_var: str = f"{name}{COLLECTION_CONCAT_SUFFIX}"
            group_concat_expr: str = field_info.get_group_concat_expr(
                inner_var, concat_var, escaped_sep, distinct=True
            )
            select_vars.append(group_concat_expr)

        # Subquery collection fields (with limit): reference subquery results
        for name in self._get_subquery_collection_fields():
            concat_var = f"{name}{COLLECTION_CONCAT_SUFFIX}"
            select_vars.append(f"?{concat_var}")

        return f"SELECT {distinct_modifier}" + " ".join(select_vars)

    def _build_select_clause_regular(self, s_var: str, distinct_modifier: str) -> str:
        """
        Build SELECT clause for regular (non-aggregate, non-collection) queries.

        Args:
            s_var: Subject variable (e.g., "?s")
            distinct_modifier: "DISTINCT " or "" depending on query settings

        Returns:
            Complete SELECT clause string for regular query
        """
        select_vars: list[str] = [s_var]

        for name, field_info in self.model._rdf_fields.items():
            select_vars.append(f"?{name}")
            lang_expr: str | None = self._get_lang_var_expression(name)
            if lang_expr:
                select_vars.append(lang_expr)

        return f"SELECT {distinct_modifier}" + " ".join(select_vars)

    def _assemble_query_string(self, select_clause: str, where: str) -> str:
        """Assemble the base SPARQL query string (without modifiers).

        Builds prefix declarations, FROM clauses, and the WHERE block.
        LIMIT, OFFSET, GROUP BY, and ORDER BY are added by the caller.

        Args:
            select_clause: The SELECT ... clause string
            where: The joined WHERE body string

        Returns:
            Base SPARQL query string ending with the closing brace
        """
        prefix_declarations: str = self._build_prefix_declarations()
        from_clause: str = self._build_from_clauses()
        return (
            f"{prefix_declarations}{select_clause}\n"
            f"{from_clause}WHERE {{\n  {where}\n}}\n"
        )
