"""
Field types for RDF literals.

This module provides field types for literal values including:
- LiteralField: Basic literals (strings, numbers, dates, booleans)
- LangString: Language-tagged literals
- MultiLangString: Multiple language versions of a string
"""

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

from ..security import escape_sparql_literal, validate_iri
from ..values import LangLiteral, Literal, TypedLiteral
from .constants import LANG_VARIABLE_SUFFIX
from .field_base import RDFFieldInfo
from .property_path import PropertyPath

if TYPE_CHECKING:
    from rdflib import Graph, URIRef


def validate_language_tag(lang: str) -> None:
    """
    Validate BCP 47 language tag format.

    Args:
        lang: Language tag to validate (e.g., 'en', 'en-US', 'zh-Hans')

    Raises:
        ValueError: If language tag is invalid

    BCP 47 format (simplified):
        - Primary language subtag: 2-3 lowercase letters (e.g., 'en', 'fra')
        - Optional script subtag: 4 letters, first uppercase (e.g., 'Hans', 'Latn')
        - Optional region subtag: 2 uppercase letters or 3 digits (e.g., 'US', '419')

    Examples:
        - 'en' - English
        - 'en-US' - English (United States)
        - 'zh-Hans' - Chinese (Simplified script)
        - 'zh-Hans-CN' - Chinese (Simplified, China)
    """
    if not lang or not lang.strip():
        raise ValueError("Language tag cannot be empty")

    # BCP 47 simplified pattern:
    # - Primary: 2-3 lowercase letters
    # - Optional script: hyphen + 4 letters (first capital)
    # - Optional region: hyphen + 2 uppercase letters OR 3 digits
    pattern: str = r"^[a-z]{2,3}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?$"

    if not re.match(pattern, lang):
        raise ValueError(
            f"Invalid language tag '{lang}'. Expected BCP 47 format "
            f"(e.g., 'en', 'en-US', 'zh-Hans', 'zh-Hans-CN')"
        )


class LiteralField(RDFFieldInfo):
    """
    Field for RDF literals (strings, numbers, dates, booleans, etc.).
    Values are serialized with quotes in SPARQL.

    When ``datatype`` is set, formatted values include an explicit XSD
    datatype suffix (e.g., ``"value"^^<http://www.w3.org/2001/XMLSchema#string>``).
    """

    def __init__(
        self,
        predicate: "str | PropertyPath",
        datatype: str | None = None,
        required: bool = False,
        default: Any = None,
        validator: Callable[[Any], None] | list[Callable[[Any], None]] | None = None,
        **kwargs: Any,
    ):
        super().__init__(
            predicate=predicate,
            required=required,
            default=default,
            validator=validator,
            **kwargs,
        )
        # Validate datatype IRI to prevent injection attacks
        if datatype is not None:
            validate_iri(datatype, strict=True)
        self.datatype: str | None = datatype

    def _escape_value(self, value: Any) -> str:
        """
        Escape value for use in SPARQL literal.

        Args:
            value: Value to escape

        Returns:
            Escaped string safe for SPARQL
        """
        return escape_sparql_literal(str(value))

    @override
    def _copy_kwargs(self) -> dict[str, Any]:
        """Return datatype for copy()."""
        return {"datatype": self.datatype}

    @override
    def format_value(self, value: Any) -> str:
        """Format value as a quoted SPARQL literal with escaping.

        If value already has to_sparql(), delegates to it.
        Otherwise auto-wraps in TypedLiteral (if field has datatype) or Literal.
        """
        # Value types know how to format themselves
        if hasattr(value, "to_sparql"):
            result: str = value.to_sparql()
            return result

        # Auto-wrap plain values
        if self.datatype:
            return TypedLiteral(value, self.datatype).to_sparql()
        return Literal(value).to_sparql()

    @override
    def format_value_for_filter(self, value: Any) -> str:
        """Format value for SPARQL FILTER expressions.

        If value already has to_sparql_filter(), delegates to it.
        Otherwise auto-wraps in TypedLiteral (if field has datatype) or Literal.
        Numbers stay unquoted for proper numeric comparisons.
        """
        # Value types know how to format themselves for filters
        if hasattr(value, "to_sparql_filter"):
            result: str = value.to_sparql_filter()
            return result

        # Auto-wrap plain values
        if self.datatype:
            return TypedLiteral(value, self.datatype).to_sparql_filter()
        return Literal(value).to_sparql_filter()


class LangString(LiteralField):
    """
    Field for RDF language-tagged literals.
    Values are serialized with language tags in SPARQL (e.g., "Hello"@en).
    """

    def __init__(
        self,
        predicate: "str | PropertyPath",
        lang: str | None = None,
        required: bool = False,
        default: Any = None,
        validator: Callable[[Any], None] | list[Callable[[Any], None]] | None = None,
        **kwargs: Any,
    ):
        super().__init__(
            predicate=predicate,
            required=required,
            default=default,
            validator=validator,
            **kwargs,
        )
        # Validate language tag if provided
        if lang is not None:
            validate_language_tag(lang)
        self.lang: str | None = lang

    @override
    def _copy_kwargs(self) -> dict[str, Any]:
        """Return lang for copy(), merged with parent kwargs."""
        return {**super()._copy_kwargs(), "lang": self.lang}

    @override
    def format_value(self, value: Any) -> str:
        """Format value as a language-tagged SPARQL literal.

        If value already has to_sparql(), delegates to it.
        Otherwise auto-wraps in LangLiteral.
        """
        # Value types know how to format themselves
        if hasattr(value, "to_sparql"):
            result: str = value.to_sparql()
            return result

        # Auto-wrap plain values
        return LangLiteral(value, self.lang).to_sparql()

    @override
    def format_value_for_filter(self, value: Any) -> str:
        """Format value for SPARQL FILTER expressions.

        If value already has to_sparql_filter(), delegates to it.
        Otherwise auto-wraps in LangLiteral.
        """
        # Value types know how to format themselves for filters
        if hasattr(value, "to_sparql_filter"):
            result: str = value.to_sparql_filter()
            return result

        # Auto-wrap plain values
        return LangLiteral(value, self.lang).to_sparql_filter()

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

        LangString fields return LangLiteral objects to preserve the
        language information from the SPARQL endpoint.

        Args:
            rdf_value: The RDF value to convert
            language: Optional language tag override
            datatype: Ignored for LangString fields

        Returns:
            LangLiteral with text and language tag preserved

        Example:
            >>> from rdflib import Literal
            >>> field = LangString("http://schema.org/name")
            >>> literal = Literal("Hello", lang="en")
            >>> field.hydrate_value(literal)
            LangLiteral('Hello', lang='en')
        """
        # Extract text value
        if hasattr(rdf_value, "toPython"):
            text: str = str(rdf_value)
        elif isinstance(rdf_value, str):
            text = rdf_value
        else:
            text = str(rdf_value)

        # Extract language tag from rdflib Literal if not provided
        lang: str | None = language
        if lang is None and hasattr(rdf_value, "language") and rdf_value.language:
            lang = str(rdf_value.language)

        return LangLiteral(text, lang)

    @override
    def get_lang_select_expression(self, field_name: str) -> str:
        """
        Generate SPARQL LANG() expression for SELECT clause.

        LangString fields return a LANG() expression to extract the
        language tag from the literal value in query results.

        Args:
            field_name: The name of the field

        Returns:
            SPARQL expression like "(LANG(?name) AS ?name_lang)"

        Example:
            >>> field = LangString("http://schema.org/name")
            >>> field.get_lang_select_expression("name")
            '(LANG(?name) AS ?name_lang)'
        """
        lang_var: str = f"{field_name}{LANG_VARIABLE_SUFFIX}"
        return f"(LANG(?{field_name}) AS ?{lang_var})"

    @override
    def supports_language_filtering(self) -> bool:
        """
        Return True since LangString fields support language filtering.

        Returns:
            True, enabling filter_lang() to be used with this field.
        """
        return True

    @override
    def get_construct_pattern(self, triple: str, var_name: str) -> str:
        """
        Generate OPTIONAL pattern with language filter for CONSTRUCT queries.

        When a specific language is configured, adds a FILTER clause to
        restrict results to that language tag.

        Args:
            triple: The complete triple pattern
            var_name: The variable name used in the triple

        Returns:
            OPTIONAL pattern with FILTER for language-tagged fields.
        """
        if self.lang:
            return f'OPTIONAL {{ {triple} FILTER(lang(?{var_name}) = "{self.lang}") }}'
        return super().get_construct_pattern(triple, var_name)

    @override
    def collect_from_graph(
        self,
        graph: "Graph",
        subject: "URIRef",
        predicate: "URIRef",
        convert_fn: Callable[[Any, "RDFFieldInfo"], Any],
    ) -> Any:
        """
        Collect field value from graph, filtering by configured language tag.

        When a specific language is configured, only returns values with
        matching language tags. Otherwise, delegates to base implementation.

        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

        Returns:
            The converted field value matching the language, or None if not found.
        """
        if self.lang:
            for _, _, obj in graph.triples((subject, predicate, None)):
                if hasattr(obj, "language") and obj.language == self.lang:
                    return convert_fn(obj, self)
            return None
        return super().collect_from_graph(graph, subject, predicate, convert_fn)


class MultiLangString(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'}).
    """

    def __init__(
        self,
        predicate: "str | PropertyPath",
        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=predicate,
            required=required,
            default=default,
            validator=validator,
            **kwargs,
        )

    @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.

        MultiLangString values are dicts mapping language codes to text,
        so each entry generates a separate triple.

        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.

        Example:
            >>> field = MultiLangString("http://schema.org/name")
            >>> field.generate_triples("<http://ex/p>", "<http://schema.org/name>",
            ...                        {"en": "Hello", "fr": "Bonjour"})
            ['<http://ex/p> <http://schema.org/name> "Hello"@en .',
             '<http://ex/p> <http://schema.org/name> "Bonjour"@fr .']
        """
        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(
        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__ = [
    "LiteralField",
    "LangString",
    "MultiLangString",
    "validate_language_tag",
]
