"""
Result hydration for converting SPARQL JSON bindings to Model instances.

This module provides the ResultHydrator class which handles the conversion of
raw SPARQL query results into typed Python model instances with proper
datatype conversion and language tag preservation.
"""

from datetime import date, datetime
from decimal import Decimal
from typing import TYPE_CHECKING, Any, Generic, TypeVar

# Import model types for field type checking and hydration
from sparqlmojo.orm.model import (
    COLLECTION_CONCAT_SUFFIX,
    DEFAULT_COLLECTION_SEPARATOR,
    LANG_VARIABLE_SUFFIX,
    SAMPLE_VARIABLE_SUFFIX,
    is_collection_field,
)

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

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


def split_concat_value(
    concat_value: str, separator: str = DEFAULT_COLLECTION_SEPARATOR
) -> list[str]:
    """
    Split a GROUP_CONCAT result string into a list of values.

    Handles empty strings and empty results gracefully, always returning
    a list (possibly empty).

    Args:
        concat_value: The concatenated string from GROUP_CONCAT
        separator: The separator used in GROUP_CONCAT (default: U+001F × 3)

    Returns:
        List of individual values, or empty list if input is empty

    Example:
        >>> # With default separator (three U+001F characters)
        >>> split_concat_value("a\x1f\x1f\x1fb\x1f\x1f\x1fc")
        ['a', 'b', 'c']
        >>> split_concat_value("")
        []
        >>> # With custom separator
        >>> split_concat_value("a|||b|||c", "|||")
        ['a', 'b', 'c']
    """
    if not concat_value:
        return []
    return [v for v in concat_value.split(separator) if v]


class ResultHydrator(Generic[T]):
    """
    Responsible for converting SPARQL JSON bindings to Model instances.

    Separates result hydration logic from query compilation, following the
    Single Responsibility Principle.
    """

    def __init__(self, model_cls: "type[T]", session: "Session | None" = None) -> None:
        """
        Initialize the result hydrator.

        Args:
            model_cls: The model class to hydrate into
            session: Optional session for identity map access
        """
        self.model: type[T] = model_cls
        self._session: "Session | None" = session

    def _extract_language_tag(
        self,
        binding: dict[str, Any],
        field_name: str,
        value_data: dict[str, Any],
        binding_var: str | None = None,
    ) -> str | None:
        """
        Extract language tag from SPARQL binding.

        Checks two sources for language information:
        1. xml:lang attribute directly in value_data (from language-tagged literals)
        2. {field_name}_lang binding from LANG() function in SELECT clause

        Args:
            binding: Full SPARQL result binding row
            field_name: Name of the field being processed
            value_data: The value data dict for this field
            binding_var: Actual variable name in binding (may have _sample suffix)

        Returns:
            Language tag string if found, None otherwise
        """
        # Check for xml:lang attribute (language-tagged literals)
        if "xml:lang" in value_data:
            return value_data.get("xml:lang")

        # Check for LANG() function result in separate binding
        # Use binding_var if provided (for sampled variables in collection queries)
        base_var: str = binding_var if binding_var else field_name
        lang_var_name: str = f"{base_var}{LANG_VARIABLE_SUFFIX}"
        if lang_var_name in binding:
            lang_binding: dict[str, Any] = binding[lang_var_name]
            if lang_binding.get("type") == "literal":
                lang_value: str = lang_binding.get("value", "")
                if lang_value:
                    return lang_value

        return None

    def _process_literal_value(
        self,
        field_name: str,
        field_info: Any,
        raw_value: str,
        value_data: dict[str, Any],
        binding: dict[str, Any],
        binding_var: str | None = None,
    ) -> Any:
        """
        Process a literal value based on field type.

        Handles datatype conversion for typed literals and language tag
        preservation for LangString fields using polymorphic hydrate_value().

        Args:
            field_name: Name of the field being processed
            field_info: RDF field metadata (RDFFieldInfo subclass)
            raw_value: Raw string value from SPARQL result
            value_data: Full value data dict with type/datatype/xml:lang
            binding: Full SPARQL result binding row
            binding_var: Actual variable name in binding (may have _sample suffix)

        Returns:
            Processed value (converted type, LangLiteral, or raw string)
        """
        # Check for XSD datatype - requires type conversion
        if "datatype" in value_data:
            datatype_iri: str = value_data["datatype"]
            return self._convert_datatype(raw_value, datatype_iri)

        # Extract language tag for potential use in hydration
        lang_tag: str | None = self._extract_language_tag(
            binding, field_name, value_data, binding_var=binding_var
        )

        # Use polymorphic hydrate_value() - handles LangString returning LangLiteral
        # and plain literals returning string
        return field_info.hydrate_value(raw_value, language=lang_tag)

    def _process_collection_field(
        self, field_name: str, field_info: Any, binding: dict[str, Any]
    ) -> list[Any]:
        """
        Process a collection field from GROUP_CONCAT result.

        Collection fields use GROUP_CONCAT to aggregate multiple values into a
        single string. This method splits that string back into a list and
        handles type-specific processing (e.g., LangStringList returns
        LangLiteral objects with preserved language tags).

        Args:
            field_name: Name of the collection field
            field_info: RDF field metadata (must be a collection field)
            binding: Full SPARQL result binding row

        Returns:
            List of values (strings, LangLiterals, etc.), or empty list if no data
        """
        # Look for the GROUP_CONCAT result variable
        concat_var: str = f"{field_name}{COLLECTION_CONCAT_SUFFIX}"
        if concat_var not in binding:
            return []

        # Extract the concatenated value
        concat_value: str = binding[concat_var].get("value", "")
        separator: str = getattr(field_info, "separator", DEFAULT_COLLECTION_SEPARATOR)
        values: list[str] = split_concat_value(concat_value, separator)

        if not values:
            return []

        # Use polymorphic method - each field type knows how to hydrate its values
        result: list[Any] = field_info.hydrate_values(values, self._convert_datatype)
        return result

    def hydrate_instance(self, binding: dict[str, Any]) -> T:
        """
        Convert SPARQL JSON binding to Model instance.

        Creates a fresh instance for each binding. Does NOT use identity map,
        as query results may contain multiple rows with the same IRI but
        different field values (e.g., multiple labels for an entity).

        Args:
            binding: Single row from SPARQL JSON results['bindings']

        Returns:
            Hydrated model instance with all fields populated
        """
        # Extract subject IRI (variable is "s" based on Query.compile())
        iri: str | None = None
        if "s" in binding:
            s_data: dict[str, Any] = binding["s"]
            if s_data["type"] == "uri":
                iri = s_data["value"]

        # Build kwargs for Model constructor
        field_values: dict[str, Any] = {}

        # Populate fields from bindings
        for field_name, field_info in self.model._rdf_fields.items():
            # SubjectField gets its value from the subject IRI, not from a binding
            if field_info.is_subject():
                if iri:
                    field_values[field_name] = iri
                continue

            # Collection field handling - process GROUP_CONCAT results into lists
            if is_collection_field(field_info):
                field_values[field_name] = self._process_collection_field(
                    field_name, field_info, binding
                )
                continue

            # Check for field in binding (may have _sample suffix in collection queries)
            binding_var: str = field_name
            if field_name not in binding:
                # Try the sampled variable name (used in collection queries)
                sampled_name: str = f"{field_name}{SAMPLE_VARIABLE_SUFFIX}"
                if sampled_name in binding:
                    binding_var = sampled_name

            if binding_var in binding:
                value_data: dict[str, Any] = binding[binding_var]

                if value_data["type"] == "uri":
                    # ObjectPropertyField or IRIField
                    field_values[field_name] = value_data["value"]

                elif value_data["type"] == "literal":
                    # Process literal value (handles datatype and language)
                    raw_value: str = value_data["value"]
                    field_values[field_name] = self._process_literal_value(
                        field_name,
                        field_info,
                        raw_value,
                        value_data,
                        binding,
                        binding_var=binding_var,
                    )

        # Create instance using Pydantic validation
        instance: T = self.model(**field_values)

        # Mark as clean since loaded from database
        instance.mark_clean()

        return instance

    def _convert_datatype(self, value: str, datatype_iri: str) -> Any:
        """
        Convert XSD datatype string to Python type.

        Args:
            value: String value from SPARQL result
            datatype_iri: XSD datatype IRI

        Returns:
            Converted Python value
        """
        # Map common XSD types to converter functions
        xsd: str = "http://www.w3.org/2001/XMLSchema#"

        converters: dict[str, Any] = {
            f"{xsd}integer": int,
            f"{xsd}decimal": Decimal,
            f"{xsd}float": float,
            f"{xsd}double": float,
            f"{xsd}boolean": lambda v: v.lower() in ("true", "1"),
            f"{xsd}dateTime": lambda v: datetime.fromisoformat(
                v.replace("Z", "+00:00")
            ),
            f"{xsd}date": date.fromisoformat,
        }

        # Get converter or default to identity function (return as string)
        converter = converters.get(datatype_iri, lambda v: v)
        return converter(value)
