"""
Abstract base class for all RDF field types.

This module defines RDFFieldInfo, which provides common functionality
for field types like LiteralField, IRIField, and collection fields.
"""

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Callable

from ..security import validate_predicate_iri
from ..values import LangLiteral
from .property_path import PropertyPath

if TYPE_CHECKING:
    from rdflib import Graph, URIRef


class RDFFieldInfo(ABC):
    """Abstract base class for RDF field information with SPARQL formatting.

    Supports both simple predicates (IRI strings) and SPARQL property paths.
    When a PropertyPath is provided as the predicate, it is stored separately
    and the field's predicate attribute contains the SPARQL representation.

    Predicate Validation:
        String predicates are validated at field definition time. Namespace URIs
        (ending with '/' or '#') are rejected to prevent invalid SPARQL generation.
        Use complete predicate IRIs like 'http://schema.org/name' instead.
        PropertyPath objects are exempt as they have their own syntax rules.

    Example with simple predicate:
        >>> field = LiteralField("http://schema.org/name")
        >>> field.predicate
        'http://schema.org/name'
        >>> field.is_property_path()
        False

    Example with property path:
        >>> field = IRIField(PropertyPath("^<http://example.org/belongsTo>"))
        >>> field.predicate
        '^<http://example.org/belongsTo>'
        >>> field.is_property_path()
        True
        >>> field.get_property_path().to_sparql()
        '^<http://example.org/belongsTo>'
    """

    # Keyword arguments allowed for this class (excluding positional/named params).
    # Subclasses should define their own _ALLOWED_EXTRA_KWARGS to allow additional
    # kwargs. The validation logic in __init__() walks the MRO and accumulates all
    # allowed kwargs from the inheritance hierarchy, so subclasses automatically
    # inherit parent class allowances.
    #
    # Note: Named parameters declared explicitly in __init__ signatures (like
    # 'datatype', 'lang', 'range_', 'separator') are NOT subject to this validation
    # since they are consumed by the method signature before reaching **kwargs.
    # This attribute only controls what's allowed to pass through **kwargs.
    #
    # Example:
    #     class BaseField(RDFFieldInfo):
    #         _ALLOWED_EXTRA_KWARGS: frozenset[str] = frozenset({"base_param"})
    #
    #     class SubField(BaseField):
    #         _ALLOWED_EXTRA_KWARGS: frozenset[str] = frozenset({"sub_param"})
    #
    #     # SubField instances accept both "base_param" and "sub_param" via **kwargs
    _ALLOWED_EXTRA_KWARGS: frozenset[str] = frozenset()

    def __init__(
        self,
        predicate: "str | PropertyPath",
        required: bool = False,
        default: Any = None,
        validator: Callable[[Any], None] | list[Callable[[Any], None]] | None = None,
        **kwargs: Any,
    ):
        # Validate that only allowed keyword arguments are passed
        # Collect allowed kwargs from the entire class hierarchy
        allowed_kwargs: set[str] = set()
        for cls in type(self).__mro__:
            if hasattr(cls, "_ALLOWED_EXTRA_KWARGS"):
                allowed_kwargs.update(cls._ALLOWED_EXTRA_KWARGS)

        unknown_kwargs: set[str] = set(kwargs.keys()) - allowed_kwargs
        if unknown_kwargs:
            sorted_unknown: list[str] = sorted(unknown_kwargs)
            sorted_allowed: list[str] = sorted(allowed_kwargs) if allowed_kwargs else []
            allowed_msg: str = (
                f"Allowed extra kwargs: {sorted_allowed}"
                if sorted_allowed
                else "No extra keyword arguments are allowed for this field type"
            )
            raise TypeError(
                f"{type(self).__name__}() got unexpected keyword argument(s): "
                f"{sorted_unknown}. {allowed_msg}."
            )

        # Normalize validator(s) to a list
        if validator is None:
            self.validators: list[Callable[[Any], None]] = []
        elif callable(validator):
            self.validators = [validator]
        elif isinstance(validator, list):
            # Validate that all items in list are callable
            for i, v in enumerate(validator):
                if not callable(v):
                    raise TypeError(
                        f"validator[{i}] must be callable, got {type(v).__name__}"
                    )
            self.validators = list(validator)
        else:
            raise TypeError(
                f"validator must be callable or list of callables, "
                f"got {type(validator).__name__}"
            )

        # Handle PropertyPath vs string predicate
        # When a PropertyPath is provided, store it separately and use its SPARQL
        # representation as the predicate string. This enables field-level property
        # path support while maintaining backward compatibility with code that
        # expects predicate to be a string.
        self._property_path: PropertyPath | None = None
        if isinstance(predicate, PropertyPath):
            self._property_path = predicate
            self.predicate: str = predicate.to_sparql()
        else:
            self.predicate = predicate
            # Validate string predicates are not namespace-only URIs
            # (PropertyPath objects have their own syntax validation)
            validate_predicate_iri(predicate)

        self.required: bool = required
        self.field_name: str | None = None
        self.default: Any = default
        # Keep backward compatibility with single validator property
        self.validator: Callable[[Any], None] | None = (
            self.validators[0] if len(self.validators) == 1 else None
        )
        self.extra_kwargs: dict[str, Any] = kwargs

    def bind_name(self, name: str) -> None:
        """Bind the Python field name to this RDF field."""
        self.field_name = name

    def to_sparql_var(self) -> str:
        """Return the SPARQL variable name for this field."""
        return f"?{self.field_name}"

    def is_subject(self) -> bool:
        """Check if this field represents the RDF subject."""
        return getattr(self, "is_subject_field", False)

    def is_property_path(self) -> bool:
        """Check if this field uses a property path instead of a simple predicate.

        Returns:
            True if the field was defined with a PropertyPath, False otherwise.

        Example:
            >>> simple_field = LiteralField("http://schema.org/name")
            >>> simple_field.is_property_path()
            False
            >>> path_field = IRIField(PropertyPath("^<http://example.org/belongsTo>"))
            >>> path_field.is_property_path()
            True
        """
        return self._property_path is not None

    def get_property_path(self) -> PropertyPath | None:
        """Get the PropertyPath object if this field uses a property path.

        Returns:
            The PropertyPath object, or None if this field uses a simple predicate.

        Example:
            >>> field = IRIField(PropertyPath("^<http://example.org/belongsTo>"))
            >>> field.get_property_path().to_sparql()
            '^<http://example.org/belongsTo>'
        """
        return self._property_path

    @abstractmethod
    def _copy_kwargs(self) -> dict[str, Any]:
        """Return subclass-specific kwargs for copy().

        Subclasses must implement this to return a dictionary of keyword
        arguments that are specific to that subclass (e.g., datatype for
        LiteralField, lang for LangString).

        Returns:
            Dictionary of kwargs to pass to the constructor during copy().

        Raises:
            NotImplementedError: If not implemented by subclass.
        """
        raise NotImplementedError

    def copy(self) -> "RDFFieldInfo":
        """
        Create a deep copy of this field info.

        Returns:
            A new instance of the same field type with identical parameters.

        Example:
            >>> field = LiteralField("http://schema.org/name", default=None)
            >>> field.bind_name("original_name")
            >>> field_copy = field.copy()
            >>> field_copy.bind_name("copied_name")
            >>> assert field.field_name == "original_name"
            >>> assert field_copy.field_name == "copied_name"
        """
        # Use the PropertyPath object if present, otherwise use the predicate string
        predicate_arg: str | PropertyPath = (
            self._property_path if self._property_path is not None else self.predicate
        )
        return type(self)(
            predicate=predicate_arg,
            required=self.required,
            default=self.default,
            validator=self.validators,
            **self._copy_kwargs(),
            **self.extra_kwargs,
        )

    @abstractmethod
    def format_value(self, value: Any) -> str:
        """
        Format a value for SPARQL output (triple patterns, INSERT/DELETE).
        Subclasses must implement this to provide appropriate formatting.
        """
        ...

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

        This method handles the context-specific formatting needed for filters,
        where numeric values may need to stay unquoted for proper comparisons.
        Subclasses must implement this to provide appropriate formatting.
        """
        ...

    def generate_triples(self, subject: str, predicate: str, value: Any) -> list[str]:
        """
        Generate complete SPARQL triple strings for INSERT/DELETE operations.

        This method produces fully formatted triple strings ready for use in
        SPARQL INSERT DATA or DELETE DATA statements.

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

        Returns:
            List of complete triple strings (e.g., ["<s> <p> \"value\" ."])
            Most field types return a single-element list, but MultiLangString
            returns multiple triples (one per language).

        Example:
            >>> field = LiteralField("http://schema.org/name")
            >>> field.generate_triples("<s>", "<p>", "Alice")
            ['<s> <p> "Alice" .']
        """
        formatted_value: str = self.format_value(value)
        return [f"{subject} {predicate} {formatted_value} ."]

    def generate_triple_tuples(
        self, subject: str, predicate: str, value: Any
    ) -> list[tuple[str, str, str]]:
        """
        Generate (subject, predicate, object) tuples for staging operations.

        Unlike generate_triples() which returns complete triple strings,
        this method returns tuples that can be stored and later assembled
        into SPARQL statements.

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

        Returns:
            List of (subject, predicate, object) tuples.
            Most field types return a single tuple, but MultiLangString
            returns multiple tuples (one per language).

        Example:
            >>> field = LiteralField("http://schema.org/name")
            >>> field.generate_triple_tuples("<s>", "<p>", "Alice")
            [('<s>', '<p>', '"Alice"')]
        """
        formatted_value: str = self.format_value(value)
        return [(subject, predicate, formatted_value)]

    def _extract_raw_value(self, rdf_value: Any) -> Any:
        """
        Extract a Python value from an RDF value.

        Handles rdflib Literal (via toPython()), plain strings, and
        fallback to str() conversion. Centralises the type-dispatch
        logic that would otherwise be duplicated across field subclasses.

        Args:
            rdf_value: The RDF value to convert.

        Returns:
            Native Python value (int, str, datetime, etc.) or str.
        """
        if hasattr(rdf_value, "toPython"):
            return rdf_value.toPython()
        if isinstance(rdf_value, str):
            return rdf_value
        return str(rdf_value)

    def hydrate_value(
        self,
        rdf_value: Any,
        language: str | None = None,
        datatype: str | None = None,
    ) -> Any:
        """
        Convert an RDF value to the appropriate Python type.

        This method is used during result hydration to convert RDF values
        (from rdflib or SPARQL JSON results) into Python objects with
        proper type handling.

        When a language tag is present (either via the ``language`` parameter
        or via the rdflib Literal's ``.language`` attribute) the value is
        returned as a :class:`LangLiteral` so that the tag is preserved.

        Args:
            rdf_value: The RDF value to convert. Can be:
                - rdflib.Literal: Converts using toPython()
                - rdflib.URIRef: Converts to string
                - str: Returns as-is or with appropriate conversion
                - Any other type: Returns as-is
            language: Optional language tag (for language-tagged literals)
            datatype: Optional XSD datatype IRI (for typed literals)

        Returns:
            Converted Python value appropriate for this field type.
            - With language tag: LangLiteral with preserved language tag
            - Without language tag: toPython() result or string

        Example:
            >>> from rdflib import Literal
            >>> field = LiteralField("http://schema.org/name")
            >>> field.hydrate_value(Literal("Alice"))
            'Alice'
            >>> field.hydrate_value("hard", language="en")
            LangLiteral('hard', lang='en')
        """
        if language is not None:
            return LangLiteral(str(self._extract_raw_value(rdf_value)), language)

        if hasattr(rdf_value, "language") and rdf_value.language:
            return LangLiteral(str(rdf_value), str(rdf_value.language))

        return self._extract_raw_value(rdf_value)

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

        This method generates an expression to extract the language tag
        of a literal value in a SPARQL SELECT query. Most field types
        don't have language tags, so the base implementation returns None.

        LangString overrides this to return the LANG() expression needed
        to preserve language information in query results.

        Args:
            field_name: The name of the field (used to construct variable names)

        Returns:
            SPARQL expression like "(LANG(?name) AS ?name_lang)" for fields
            that support language tags, or None for other field types.

        Example:
            >>> field = LiteralField("http://schema.org/name")
            >>> field.get_lang_select_expression("name")
            None  # LiteralField has no language tag

            >>> field = LangString("http://schema.org/name")
            >>> field.get_lang_select_expression("name")
            '(LANG(?name) AS ?name_lang)'
        """
        return None

    def supports_language_filtering(self) -> bool:
        """
        Return whether this field supports language filtering via filter_lang().

        Default implementation returns False. LangString overrides to return True,
        enabling language-based filtering in SPARQL queries.

        Returns:
            True if the field supports filter_lang(), False otherwise.

        Example:
            >>> field = LiteralField("http://schema.org/name")
            >>> field.supports_language_filtering()
            False

            >>> field = LangString("http://schema.org/name")
            >>> field.supports_language_filtering()
            True
        """
        return False

    def get_construct_pattern(self, triple: str, var_name: str) -> str:
        """
        Generate OPTIONAL pattern for SPARQL CONSTRUCT WHERE clause.

        This method produces the WHERE clause pattern for retrieving field values
        in CONSTRUCT queries. The default implementation wraps the triple in a
        simple OPTIONAL pattern.

        LangString overrides this to add FILTER(lang(?var) = "xx") when a
        specific language is configured.

        Args:
            triple: The complete triple pattern (e.g., "<s> <p> ?name .")
            var_name: The variable name used in the triple (e.g., "name")

        Returns:
            OPTIONAL pattern string for the WHERE clause.

        Example:
            >>> field = LiteralField("http://schema.org/name")
            >>> field.get_construct_pattern("<s> <p> ?name .", "name")
            'OPTIONAL { <s> <p> ?name . }'

            >>> field = LangString("http://schema.org/name", lang="en")
            >>> field.get_construct_pattern("<s> <p> ?name .", "name")
            'OPTIONAL { <s> <p> ?name . FILTER(lang(?name) = "en") }'
        """
        return f"OPTIONAL {{ {triple} }}"

    def collect_from_graph(
        self,
        graph: "Graph",
        subject: "URIRef",
        predicate: "URIRef",
        convert_fn: Callable[[Any, "RDFFieldInfo"], Any],
    ) -> Any:
        """
        Collect field value(s) from an RDF graph.

        This method retrieves values for this field from the given graph by
        iterating over triples matching the subject and predicate. The default
        implementation returns the first matching value after conversion.

        LangString overrides to filter by the configured language tag.
        MultiLangString overrides to collect all language versions into a dict.

        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:
            The converted field value, or None if no matching triple found.
            MultiLangString returns dict[str, str] mapping language codes to text.

        Example:
            >>> from rdflib import Graph, URIRef, Literal
            >>> g = Graph()
            >>> g.add((URIRef("http://ex/s"), URIRef("http://ex/p"), Literal("hello")))
            >>> field = LiteralField("http://ex/p")
            >>> def convert(obj, f): return str(obj)
            >>> field.collect_from_graph(g, URIRef("http://ex/s"),
            ...                          URIRef("http://ex/p"), convert)
            'hello'
        """
        # Early return pattern: return first matching value immediately.
        # Base implementation for single-value fields (LiteralField, IRIField).
        # Subclasses override for different collection strategies:
        # - LangString: filters by language tag before returning
        # - MultiLangString: collects all values into a dict instead of early return
        for _, _, obj in graph.triples((subject, predicate, None)):
            return convert_fn(obj, self)
        # No matching triples found - return None to indicate missing value
        return None


__all__ = ["RDFFieldInfo"]
