"""
Field type for multiple language versions of a string.

This module provides MultiLangString, a collection field that aggregates
language-tagged literals into a dict[str, str] mapping language codes to text.
"""

from typing import TYPE_CHECKING, Any, Callable, override

from .collection_fields import CollectionFieldMixin, _lang_group_concat_expr
from .constants import DEFAULT_COLLECTION_SEPARATOR, VALUE_METADATA_SEPARATOR
from .field_base import RDFFieldInfo
from .literal_fields import LiteralField, validate_language_tag
from .property_path import PropertyPath

if TYPE_CHECKING:
    from rdflib import Graph, URIRef


class MultiLangString(CollectionFieldMixin, LiteralField):
    """
    Field for multiple language versions of a string.
    Values are expected as a dict mapping language codes to text (e.g.,
    {'en': 'Hello', 'fr': 'Bonjour'}).

    In SELECT queries, uses GROUP_CONCAT aggregation (like LangStringList) to
    collect all language-tagged values, but hydrates to dict[str, str] instead
    of list[LangLiteral].

    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 LiteralField

    Raises:
        ValueError: If 'datatype' is passed (lang tags and datatypes are mutually
            exclusive in RDF), or if limit is not a positive integer
        TypeError: If limit is not an integer
    """

    def __init__(
        self,
        predicate: "str | PropertyPath",
        separator: str = DEFAULT_COLLECTION_SEPARATOR,
        limit: int | None = None,
        required: bool = False,
        default: Any = None,
        validator: Callable[[Any], None] | list[Callable[[Any], None]] | None = None,
        **kwargs: Any,
    ) -> None:
        # MultiLangString cannot have a datatype - lang tags and datatypes are
        # mutually exclusive in RDF
        if "datatype" in kwargs:
            raise ValueError(
                "MultiLangString does not support the 'datatype' parameter. "
                "Language tags and datatypes are mutually exclusive in RDF. "
                "Use LiteralField with datatype for typed literals."
            )
        super().__init__(
            predicate,
            separator=separator,
            limit=limit,
            required=required,
            default=default,
            validator=validator,
            **kwargs,
        )

    @override
    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.
        Delegates to _lang_group_concat_expr, shared with LangStringList.
        """
        return _lang_group_concat_expr(inner_var, concat_var, escaped_sep, distinct)

    @override
    def hydrate_values(  # type: ignore[override]
        self,
        values: list[str],
        convert_datatype: "Callable[[str, str], Any] | None" = None,
    ) -> "dict[str, str] | None":
        """
        Parse combined value+language format into dict[str, str].

        Values are in format "text<RS>lang" where RS is record separator.
        Returns None for empty input (matching dict[str, str] | None type).

        Note: Items without a language tag (no VALUE_METADATA_SEPARATOR or empty
        lang) are silently skipped, as a dict key requires a non-empty language
        code. This is intentional and differs from LangStringList which falls
        back to lang=None for unlabelled values.
        """
        if not values:
            return None
        result: dict[str, str] = {}
        for combined in values:
            if VALUE_METADATA_SEPARATOR in combined:
                text: str
                lang_str: str
                text, lang_str = combined.split(VALUE_METADATA_SEPARATOR, 1)
                if lang_str:
                    result[lang_str] = text
        return result if result else None

    @override
    def format_value(self, value: Any) -> list[str]:  # type: ignore[override]
        """
        Format value as a list of language-tagged SPARQL literals.

        Args:
            value: Dict mapping language codes to text

        Returns:
            List of formatted SPARQL literals with language tags

        Raises:
            ValueError: If value is not a dict or is empty
        """
        if not isinstance(value, dict):
            raise ValueError(
                "MultiLangString expects a dict mapping language codes to text, got "
                f"{type(value)}"
            )

        if not value:
            raise ValueError(
                "MultiLangString requires at least one language mapping. "
                "Got empty dict."
            )

        # Validate all language tags
        for lang in value.keys():
            validate_language_tag(lang)

        # Generate list of formatted values for each language
        result: list[str] = []
        for lang, text in value.items():
            escaped: str = self._escape_value(text)
            result.append(f'"{escaped}"@{lang}')

        return result

    @override
    def generate_triples(self, subject: str, predicate: str, value: Any) -> list[str]:
        """
        Generate multiple SPARQL triple strings for each language version.

        Args:
            subject: The formatted subject (e.g., "<http://example.org/person>")
            predicate: The formatted predicate (e.g., "<http://schema.org/name>")
            value: Dict mapping language codes to text

        Returns:
            List of complete triple strings, one per language.
        """
        formatted_values: list[str] = self.format_value(value)
        return [f"{subject} {predicate} {fv} ." for fv in formatted_values]

    @override
    def generate_triple_tuples(
        self, subject: str, predicate: str, value: Any
    ) -> list[tuple[str, str, str]]:
        """
        Generate (subject, predicate, object) tuples for each language version.

        Args:
            subject: The formatted subject
            predicate: The formatted predicate
            value: Dict mapping language codes to text

        Returns:
            List of (subject, predicate, object) tuples, one per language.
        """
        formatted_values: list[str] = self.format_value(value)
        return [(subject, predicate, fv) for fv in formatted_values]

    @override
    def collect_from_graph(  # type: ignore[override]
        self,
        graph: "Graph",
        subject: "URIRef",
        predicate: "URIRef",
        convert_fn: Callable[[Any, "RDFFieldInfo"], Any],
    ) -> "dict[str, str] | None":
        """
        Collect all language versions from graph into a dict.

        Iterates over all triples matching the subject and predicate,
        extracting values with language tags into a dict mapping
        language codes to text.

        Args:
            graph: The rdflib Graph to query
            subject: The subject URIRef to match
            predicate: The predicate URIRef for this field
            convert_fn: Not used - values are extracted directly as strings

        Returns:
            Dict mapping language codes to text values, or None if no
            language-tagged values found.
        """
        lang_dict: dict[str, str] = {}
        for _, _, obj in graph.triples((subject, predicate, None)):
            if hasattr(obj, "language") and obj.language:
                lang_dict[str(obj.language)] = str(obj)
        return lang_dict if lang_dict else None


__all__ = ["MultiLangString"]
