"""
Collection field types that aggregate multiple RDF values into lists.

This module provides collection field types that use SPARQL GROUP_CONCAT
to aggregate multi-valued properties into Python lists:
- CollectionFieldMixin: Marker mixin for collection fields
- LiteralList: Aggregates literal values
- LangStringList: Aggregates language-tagged literals
- IRIList: Aggregates IRI references
- TypedLiteralList: Aggregates typed literals with datatype preservation
"""

from typing import TYPE_CHECKING, Any, Callable, cast, override

from ..values import LangLiteral, TypedLiteral
from .constants import (
    DEFAULT_COLLECTION_SEPARATOR,
    VALUE_METADATA_SEPARATOR,
    _escape_sparql_string,
)
from .field_base import RDFFieldInfo
from .iri_fields import IRIField
from .literal_fields import LangString, LiteralField
from .property_path import PropertyPath

if TYPE_CHECKING:
    from rdflib import Graph, URIRef


class CollectionFieldMixin:
    """
    Mixin marking fields that aggregate multiple RDF values into Python lists.

    Collection fields use SPARQL's GROUP_CONCAT function to concatenate all
    values of a multi-valued property into a single string, which is then
    split back into a list during hydration.

    This mixin provides the `is_collection` marker and configurable `separator`
    for GROUP_CONCAT. Subclasses inherit from both this mixin and a concrete
    field type (LiteralField, LangString, or IRIField).

    Attributes:
        is_collection: Always True, marks this as a collection field
        separator: String separator for GROUP_CONCAT (default: triple unit separator)
        limit: Maximum number of values to aggregate (None = unlimited)

    Note:
        This class is not meant to be instantiated directly. Use the concrete
        collection field classes: :class:`LiteralList`, :class:`LangStringList`,
        or :class:`IRIList`.

    Example:
        >>> class EntityLabels(Model):
        ...     entity_iri: Annotated[str, SubjectField()]
        ...     labels: Annotated[list[LangLiteral], LangStringList("rdfs:label")]
        >>>
        >>> result = session.query(EntityLabels).filter_by(entity_iri="wd:Q682").one()
        >>> len(result.labels)  # All labels aggregated into a single list
        200
    """

    is_collection: bool = True
    separator: str = DEFAULT_COLLECTION_SEPARATOR
    limit: int | None = None

    def __init__(
        self,
        predicate: "str | PropertyPath",
        separator: str = DEFAULT_COLLECTION_SEPARATOR,
        limit: int | None = None,
        **kwargs: Any,
    ) -> None:
        """
        Initialize a collection field.

        Args:
            predicate: The RDF predicate IRI or PropertyPath for this field
            separator: String used for GROUP_CONCAT (default: triple unit separator)
            limit: Maximum number of values to aggregate (default: None = unlimited).
                Use this to prevent memory issues when querying properties with
                millions of values. Only the first N matching values will be aggregated.
            **kwargs: Additional arguments passed to parent field class

        Raises:
            TypeError: If limit is not an integer
            ValueError: If limit is not a positive integer
        """
        if limit is not None:
            if not isinstance(limit, int):
                raise TypeError(f"limit must be an integer, got {type(limit).__name__}")
            if limit <= 0:
                raise ValueError(f"limit must be a positive integer, got {limit}")

        self.separator = separator
        self.limit = limit
        super().__init__(predicate, **kwargs)  # type: ignore[call-arg]

    def _copy_kwargs(self) -> dict[str, Any]:
        """Return separator and limit for copy(), merged with parent kwargs."""
        return {
            **super()._copy_kwargs(),  # type: ignore[misc]
            "separator": self.separator,
            "limit": self.limit,
        }

    def get_group_concat_expr(
        self,
        inner_var: str,
        concat_var: str,
        escaped_sep: str,
        distinct: bool = False,
    ) -> str:
        """
        Generate the GROUP_CONCAT expression for this collection field.

        Subclasses override this to customize how values are aggregated.
        The base implementation simply concatenates raw values.

        Args:
            inner_var: Variable name for inner values (e.g., "labels_inner")
            concat_var: Variable name for concatenated result (e.g., "labels_concat")
            escaped_sep: SPARQL-escaped separator string
            distinct: If True, add DISTINCT to deduplicate values before
                concatenation (required for flattened aggregation queries)

        Returns:
            GROUP_CONCAT expression string (without leading SELECT keyword)
        """
        distinct_kw: str = "DISTINCT " if distinct else ""
        return (
            f'(GROUP_CONCAT({distinct_kw}?{inner_var}; separator="{escaped_sep}") '
            f"AS ?{concat_var})"
        )

    def _parse_metadata_pair(self, combined: str) -> tuple[str, str | None] | None:
        """
        Split a value+metadata string on VALUE_METADATA_SEPARATOR.

        Returns (value, metadata_or_None) on success, or None if the value
        part is empty — which indicates an unbound variable in GROUP_CONCAT.
        """
        if VALUE_METADATA_SEPARATOR in combined:
            value: str
            metadata: str
            value, metadata = combined.split(VALUE_METADATA_SEPARATOR, 1)
            if not value:
                return None
            return value, metadata if metadata else None
        return combined, None

    def _hydrate_metadata_values(
        self,
        values: list[str],
        convert_datatype: "Callable[[str, str], Any] | None" = None,
    ) -> list[Any]:
        """
        Parse metadata-separated values and build typed objects via _make_item.

        Shared loop for LangStringList and TypedLiteralList. Splits each value
        on VALUE_METADATA_SEPARATOR, skips empty (unbound) entries, optionally
        converts via convert_datatype, and delegates object construction to
        _make_item.
        """
        result: list[Any] = []
        for combined in values:
            pair: tuple[str, str | None] | None = self._parse_metadata_pair(combined)
            if pair is None:
                continue
            value: str | Any = pair[0]
            metadata: str | None = pair[1]
            if convert_datatype and metadata:
                value = convert_datatype(value, metadata)
            item: Any = self._make_item(value, metadata)
            result.append(item)
        return result

    def _make_item(
        self,
        value: str,
        metadata: str | None,
    ) -> Any:
        """
        Construct a typed object from a parsed (value, metadata) pair.

        Subclasses override this to create LangLiteral, TypedLiteral, etc.
        """
        return value

    def hydrate_values(
        self,
        values: list[str],
        convert_datatype: "Callable[[str, str], Any] | None" = None,
    ) -> list[Any]:
        """
        Convert raw string values from GROUP_CONCAT into typed Python objects.

        Subclasses override this to customize how values are parsed and converted.
        The base implementation returns values as-is (list of strings).

        Args:
            values: List of string values from split GROUP_CONCAT result
            convert_datatype: Optional callback to convert typed literals.
                Signature: (raw_value: str, datatype_iri: str) -> Any

        Returns:
            List of typed Python objects (strings, LangLiteral, TypedLiteral, etc.)
        """
        return values

    def collect_from_graph(
        self,
        graph: "Graph",
        subject: "URIRef",
        predicate: "URIRef",
        convert_fn: Callable[[Any, "RDFFieldInfo"], Any],
    ) -> list[Any]:
        """
        Collect ALL matching values from an RDF graph into a list.

        This overrides the base RDFFieldInfo implementation which returns only
        the first matching value. Collection fields need all values aggregated.

        Args:
            graph: The rdflib Graph to query
            subject: The subject URIRef to match
            predicate: The predicate URIRef for this field
            convert_fn: Function to convert RDF values to Python types.
                       Signature: (rdf_value, field_info) -> Any

        Returns:
            List of all converted field values, or empty list if no matches.
        """
        values: list[Any] = []
        for _, _, obj in graph.triples((subject, predicate, None)):
            # Cast self to RDFFieldInfo - CollectionFieldMixin is always used with
            # multiple inheritance (e.g., IRIList(CollectionFieldMixin, IRIField))
            values.append(convert_fn(obj, cast("RDFFieldInfo", self)))
        return values if values else []

    def generate_triples(self, subject: str, predicate: str, value: Any) -> list[str]:
        """
        Generate SPARQL triple strings for each item in the collection.

        Collection fields contain lists, so this generates one triple per item.

        Args:
            subject: The formatted subject (e.g., "<http://example.org/person>")
            predicate: The formatted predicate (e.g., "<http://schema.org/knows>")
            value: List of Python values to format

        Returns:
            List of complete triple strings, one per list item.

        Example:
            >>> field = IRIList("http://schema.org/knows")
            >>> field.generate_triples("<s>", "<p>", ["http://ex/a", "http://ex/b"])
            ['<s> <p> <http://ex/a> .', '<s> <p> <http://ex/b> .']
        """
        if not isinstance(value, list):
            # Fallback for single value - wrap in list
            value = [value]

        triples: list[str] = []
        for item in value:
            # Use parent class format_value for each individual item
            formatted: str = super().format_value(item)  # type: ignore[misc]
            triples.append(f"{subject} {predicate} {formatted} .")
        return triples

    def generate_triple_tuples(
        self, subject: str, predicate: str, value: Any
    ) -> list[tuple[str, str, str]]:
        """
        Generate (subject, predicate, object) tuples for each item in the collection.

        Collection fields contain lists, so this generates one tuple per item.

        Args:
            subject: The formatted subject (e.g., "<http://example.org/person>")
            predicate: The formatted predicate (e.g., "<http://schema.org/knows>")
            value: List of Python values to format

        Returns:
            List of (subject, predicate, object) tuples, one per list item.

        Example:
            >>> field = IRIList("http://schema.org/knows")
            >>> field.generate_triple_tuples("<s>", "<p>", ["http://ex/a", "http://ex/b"])
            [('<s>', '<p>', '<http://ex/a>'), ('<s>', '<p>', '<http://ex/b>')]
        """
        if not isinstance(value, list):
            # Fallback for single value - wrap in list
            value = [value]

        tuples: list[tuple[str, str, str]] = []
        for item in value:
            # Use parent class format_value for each individual item
            formatted: str = super().format_value(item)  # type: ignore[misc]
            tuples.append((subject, predicate, formatted))
        return tuples


class LiteralList(CollectionFieldMixin, LiteralField):
    """
    Collection field for aggregating multiple literal values into a list.

    This field uses GROUP_CONCAT in SPARQL to aggregate all values of a
    multi-valued literal property, returning them as a Python list of strings.

    Usage:
        class Document(Model):
            iri: Annotated[str, SubjectField()]
            rdf_type: Annotated[str, IRIField(RDF_TYPE, default="http://example.org/Document")]
            keywords: Annotated[list[str], LiteralList("http://purl.org/dc/terms/subject")]

        # Returns a single Document with all keywords in a list
        doc = session.query(Document).filter_by(iri="http://example.org/doc1").one()
        print(doc.keywords)  # ["python", "sparql", "rdf"]

        # With limit to prevent memory issues on large collections
        class LargeDocument(Model):
            iri: Annotated[str, SubjectField()]
            tags: Annotated[list[str], LiteralList("ex:tag", limit=1000)]

    Args:
        predicate: The RDF predicate IRI for the literal values
        separator: Custom separator for GROUP_CONCAT (default: triple unit separator)
        limit: Maximum number of values to aggregate (default: None = unlimited).
            Use this to prevent memory issues when querying properties with millions
            of values. Only the first N matching values will be aggregated.
        **kwargs: Additional arguments passed to LiteralField

    Note:
        Empty collections are returned as empty lists [], not None.

    See Also:
        TypedLiteralList: Use instead when you need to preserve XSD datatype
            information (e.g., distinguishing integers from decimals). TypedLiteralList
            returns list[TypedLiteral] with both the converted Python value and
            the datatype IRI. LiteralList is simpler when you only need string values.
    """

    pass


def _lang_group_concat_expr(
    inner_var: str,
    concat_var: str,
    escaped_sep: str,
    distinct: bool = False,
) -> str:
    """
    Generate a GROUP_CONCAT expression that combines value and language tag.

    Format: "text1<RS>lang1<SEP>text2<RS>lang2" where RS=record separator.
    Used by both LangStringList and MultiLangString to produce lang-aware
    GROUP_CONCAT SPARQL without duplicating logic.

    Args:
        inner_var: Variable name for inner values (e.g., "labels_inner")
        concat_var: Variable name for concatenated result (e.g., "labels_concat")
        escaped_sep: SPARQL-escaped separator string
        distinct: If True, add DISTINCT to deduplicate values

    Returns:
        GROUP_CONCAT expression string (without leading SELECT keyword)
    """
    escaped_lang_sep: str = _escape_sparql_string(VALUE_METADATA_SEPARATOR)
    distinct_kw: str = "DISTINCT " if distinct else ""
    return (
        f"(GROUP_CONCAT({distinct_kw}"
        f'CONCAT(STR(?{inner_var}), "{escaped_lang_sep}", '
        f'COALESCE(LANG(?{inner_var}), "")); '
        f'separator="{escaped_sep}") AS ?{concat_var})'
    )


class LangStringList(CollectionFieldMixin, LangString):
    """
    Collection field for aggregating multiple language-tagged literals into a list.

    This field uses GROUP_CONCAT in SPARQL to aggregate all values of a
    multi-valued language-tagged property (like rdfs:label), returning them
    as a Python list of :class:`LangLiteral` objects that preserve language tags.

    Usage:
        class EntityLabels(Model):
            entity_iri: Annotated[str, SubjectField()]
            labels: Annotated[list[LangLiteral], LangStringList("rdfs:label")]

        # Returns all labels for an entity in a single list
        result = session.query(EntityLabels).filter_by(
            entity_iri="http://www.wikidata.org/entity/Q682"
        ).one()

        for label in result.labels:
            print(f"{label.value} ({label.lang})")
        # "Berlin" (en)
        # "Berlin" (de)
        # "Berlín" (es)

        # With limit for entities with many labels
        class EntityLimited(Model):
            entity_iri: Annotated[str, SubjectField()]
            labels: Annotated[
                list[LangLiteral], LangStringList("rdfs:label", limit=100)
            ]

    Args:
        predicate: The RDF predicate IRI for the language-tagged literals
        separator: Custom separator for GROUP_CONCAT (default: triple unit separator)
        limit: Maximum number of values to aggregate (default: None = unlimited).
            Use this to prevent memory issues when querying properties with millions
            of values. Only the first N matching values will be aggregated.
        **kwargs: Additional arguments passed to LangString

    Note:
        - Language tags are preserved via a parallel GROUP_CONCAT on LANG()
        - Empty collections are returned as empty lists [], not None
    """

    def get_group_concat_expr(
        self,
        inner_var: str,
        concat_var: str,
        escaped_sep: str,
        distinct: bool = False,
    ) -> str:
        """
        Generate GROUP_CONCAT that combines value and language tag.

        Format: "text1<RS>lang1<SEP>text2<RS>lang2" where RS=record separator.
        This ensures value-language pairs stay together during aggregation.
        """
        return _lang_group_concat_expr(inner_var, concat_var, escaped_sep, distinct)

    @override
    def _make_item(
        self,
        value: str,
        metadata: str | None,
    ) -> LangLiteral:
        """Create a LangLiteral from a (text, lang) pair."""
        return LangLiteral(value, metadata)

    def hydrate_values(
        self,
        values: list[str],
        convert_datatype: Callable[[str, str], Any] | None = None,
    ) -> list[LangLiteral]:
        """
        Parse combined value+language format into LangLiteral objects.

        Values are in format "text<RS>lang" where RS is record separator.
        """
        return self._hydrate_metadata_values(values)


class IRIList(CollectionFieldMixin, IRIField):
    """
    Collection field for aggregating multiple IRI references into a list.

    This field uses GROUP_CONCAT in SPARQL to aggregate all values of a
    multi-valued object property or IRI field, returning them as a Python
    list of IRI strings.

    Usage:
        class Person(Model):
            iri: Annotated[str, SubjectField()]
            rdf_type: Annotated[str, IRIField(RDF_TYPE, default="http://schema.org/Person")]
            friends: Annotated[list[str], IRIList("http://schema.org/knows")]

        # Returns a single Person with all friend IRIs in a list
        person = session.query(Person).filter_by(iri="http://example.org/alice").one()
        print(person.friends)
        # ["http://example.org/bob", "http://example.org/carol"]

        # With limit for properties with millions of values (e.g., Wikidata instances)
        class WikidataClass(Model):
            iri: Annotated[str, SubjectField()]
            instances: Annotated[list[str], IRIList(
                PropertyPath("^wdt:P31"),
                limit=1000  # Limit to 1000 instances
            )]

    Args:
        predicate: The RDF predicate IRI for the object property
        separator: Custom separator for GROUP_CONCAT (default: triple unit separator)
        limit: Maximum number of values to aggregate (default: None = unlimited).
            Use this to prevent memory issues when querying properties with millions
            of values. Only the first N matching values will be aggregated.
        **kwargs: Additional arguments passed to IRIField

    Note:
        Empty collections are returned as empty lists [], not None.
    """

    pass


class TypedLiteralList(CollectionFieldMixin, LiteralField):
    """
    Collection field for aggregating typed literals while preserving XSD datatypes.

    Unlike LiteralList which returns plain strings, TypedLiteralList preserves
    the XSD datatype IRI alongside each value, enabling proper Python type
    conversion (int, float, Decimal, bool, date, datetime).

    Returns list[TypedLiteral] where each item contains:
    - value: The converted Python value (int, float, Decimal, str, etc.)
    - datatype: The XSD datatype IRI or None if no datatype was present

    Usage:
        class Document(Model):
            iri: Annotated[str, SubjectField()]
            rdf_type: Annotated[str, IRIField(RDF_TYPE, default="http://example.org/Document")]
            page_counts: Annotated[
                list[TypedLiteral] | None,
                TypedLiteralList("http://example.org/pageCount")
            ] = None

        # Returns TypedLiteral objects with preserved datatypes
        doc = session.query(Document).filter_by(iri="http://example.org/doc1").one()
        for pc in doc.page_counts:
            print(f"{pc.value} ({pc.datatype})")
            # 42 (http://www.w3.org/2001/XMLSchema#integer)

        # With limit for properties with many values
        class LargeDoc(Model):
            iri: Annotated[str, SubjectField()]
            measurements: Annotated[
                list[TypedLiteral] | None,
                TypedLiteralList("ex:measurement", limit=500)
            ] = None

    Args:
        predicate: The RDF predicate IRI for the typed literal values
        separator: Custom separator for GROUP_CONCAT (default: triple unit separator)
        limit: Maximum number of values to aggregate (default: None = unlimited).
            Use this to prevent memory issues when querying properties with millions
            of values. Only the first N matching values will be aggregated.
        **kwargs: Additional arguments passed to LiteralField

    See Also:
        LiteralList: Use instead when you only need string values and don't care
            about XSD datatypes. LiteralList returns list[str] which is simpler
            to work with for plain text values like tags or keywords.

    Note:
        - Empty collections are returned as empty lists [], not None
        - Values are automatically converted to Python types based on XSD datatype
        - Unknown datatypes result in string values with the datatype preserved
    """

    def get_group_concat_expr(
        self,
        inner_var: str,
        concat_var: str,
        escaped_sep: str,
        distinct: bool = False,
    ) -> str:
        """
        Generate GROUP_CONCAT that combines value and datatype IRI.

        Format: "value1<GS>datatype1<SEP>value2<GS>datatype2" where GS=group separator.
        This ensures value-datatype pairs stay together during aggregation.
        """
        escaped_dtype_sep: str = _escape_sparql_string(VALUE_METADATA_SEPARATOR)
        distinct_kw: str = "DISTINCT " if distinct else ""
        return (
            f"(GROUP_CONCAT({distinct_kw}"
            f'CONCAT(STR(?{inner_var}), "{escaped_dtype_sep}", '
            f'COALESCE(STR(DATATYPE(?{inner_var})), "")); '
            f'separator="{escaped_sep}") AS ?{concat_var})'
        )

    @override
    def hydrate_value(
        self,
        rdf_value: Any,
        language: str | None = None,
        datatype: str | None = None,
    ) -> TypedLiteral:
        """
        Convert an RDF value to a TypedLiteral preserving datatype.

        TypedLiteralList fields return TypedLiteral objects to preserve
        the XSD datatype information from the SPARQL endpoint.

        Args:
            rdf_value: The RDF value to convert (typically rdflib Literal)
            language: Ignored for TypedLiteralList
            datatype: Optional datatype override (uses rdf_value's datatype
                if not provided)

        Returns:
            TypedLiteral with value and datatype preserved
        """
        value: Any = self._extract_raw_value(rdf_value)
        dtype: str | None = datatype
        if dtype is None and hasattr(rdf_value, "datatype") and rdf_value.datatype:
            dtype = str(rdf_value.datatype)
        return TypedLiteral(value=value, datatype=dtype)

    @override
    def _make_item(
        self,
        value: str | Any,
        metadata: str | None,
    ) -> TypedLiteral:
        """Create a TypedLiteral from a (value, datatype) pair."""
        return TypedLiteral(value=value, datatype=metadata)

    def hydrate_values(
        self,
        values: list[str],
        convert_datatype: Callable[[str, str], Any] | None = None,
    ) -> list[TypedLiteral]:
        """
        Parse combined value+datatype format into TypedLiteral objects.

        Values are in format "value<GS>datatype" where GS is group separator.
        Uses convert_datatype callback to convert raw values to Python types.
        """
        return self._hydrate_metadata_values(values, convert_datatype)


def is_collection_field(field_info: RDFFieldInfo) -> bool:
    """
    Check if a field is a collection field.

    Collection fields aggregate multiple RDF values into Python lists using
    GROUP_CONCAT in the generated SPARQL query.

    Args:
        field_info: RDF field information object to check

    Returns:
        True if the field is a collection field (LiteralList, LangStringList,
        or IRIList), False otherwise.

    Example:
        >>> from sparqlmojo import LiteralField, LiteralList, is_collection_field
        >>> is_collection_field(LiteralField("http://example.org/name"))
        False
        >>> is_collection_field(LiteralList("http://example.org/keywords"))
        True
    """
    return getattr(field_info, "is_collection", False)


__all__ = [
    "CollectionFieldMixin",
    "LiteralList",
    "LangStringList",
    "IRIList",
    "TypedLiteralList",
    "is_collection_field",
]
