"""
Declarative model implementation using Pydantic for validation.

Overview:
    - Model classes inherit from Pydantic BaseModel for validation
    - Models declare rdf_type and use custom Field descriptors
    - Fields know their predicate IRI and validate data types
    - Instances store values validated by Pydantic
    - Support for required fields and custom validators

Field Validation:
    Fields can enforce validation constraints through two mechanisms:
    1. Required fields: Use required=True to ensure field is not None
    2. Custom validators: Provide callable(s) that validate field values

    Validators should:
    - Accept the field value as the only parameter
    - Raise ValueError for validation failures
    - Be pure functions without side effects
    - Execute quickly (< 1ms recommended)

Basic Validation Example:
    >>> from typing import Annotated
    >>> from sparqlmojo import Model, LiteralField, validate_instance
    >>>
    >>> def validate_positive_age(value: int) -> None:
    ...     if value <= 0:
    ...         raise ValueError("Age must be positive")
    >>>
    >>> class Person(Model):
    ...     rdf_type = "http://schema.org/Person"
    ...     name: Annotated[str | None,
    ...         LiteralField("http://schema.org/name", required=True)] = None
    ...     age: Annotated[int | None,
    ...         LiteralField("http://schema.org/age",
    ...                     validator=validate_positive_age)] = None
    >>>
    >>> # Valid instance
    >>> person = Person(iri="http://example.org/john", name="John", age=30)
    >>> validate_instance(person)  # No exception raised
    >>>
    >>> # Missing required field
    >>> person = Person(iri="http://example.org/jane", name=None, age=25)
    >>> try:
    ...     validate_instance(person)
    ... except RequiredFieldError as e:
    ...     print(f"Error: {e}")  # "Required field 'name' is missing"
    >>>
    >>> # Invalid validator
    >>> person = Person(iri="http://example.org/bob", name="Bob", age=-5)
    >>> try:
    ...     validate_instance(person)
    ... except ValidationError as e:
    ...     print(f"Error: {e}")  # "Validation failed for 'age': Age must be positive"

Validator Composition:
    Multiple validators can be applied to a single field by passing a list.
    Validators are executed in order and all must pass.

    >>> def validate_min_length(value: str) -> None:
    ...     if len(value) < 3:
    ...         raise ValueError("Must be at least 3 characters")
    >>>
    >>> def validate_no_spaces(value: str) -> None:
    ...     if " " in value:
    ...         raise ValueError("Must not contain spaces")
    >>>
    >>> class User(Model):
    ...     rdf_type = "http://schema.org/User"
    ...     username: Annotated[str | None,
    ...         LiteralField("http://schema.org/username",
    ...                     required=True,
    ...                     validator=[validate_min_length, validate_no_spaces])] = None
    >>>
    >>> # Valid username
    >>> user = User(iri="http://example.org/alice", username="alice")
    >>> validate_instance(user)  # Passes both validators
    >>>
    >>> # Fails first validator
    >>> user = User(iri="http://example.org/ab", username="ab")
    >>> try:
    ...     validate_instance(user)
    ... except ValidationError as e:
    ...     print(f"Error: {e}")
    ...     # "Validation failed for 'username': Must be at least 3 characters"
    >>>
    >>> # Fails second validator
    >>> user = User(iri="http://example.org/alice_bob", username="alice bob")
    >>> try:
    ...     validate_instance(user)
    ... except ValidationError as e:
    ...     print(f"Error: {e}")
    ...     # "Validation failed for 'username': Must not contain spaces"

Session Integration:
    Validation is automatically triggered when adding or updating instances via Session:

    >>> from sparqlmojo import Session
    >>> session = Session()
    >>>
    >>> # Validation runs on add()
    >>> person = Person(iri="http://example.org/invalid", name=None, age=30)
    >>> try:
    ...     session.add(person)
    ... except RequiredFieldError as e:
    ...     print(f"Error: {e}")  # "Required field 'name' is missing"
    >>>
    >>> # Validation runs on update()
    >>> person = Person(iri="http://example.org/john", name="John", age=30)
    >>> person.mark_clean()
    >>> person.age = -5
    >>> try:
    ...     session.update(person)
    ... except ValidationError as e:
    ...     print(f"Error: {e}")  # "Validation failed for 'age': Age must be positive"
"""

import re
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import StrEnum
from typing import (
    TYPE_CHECKING,
    Annotated,
    Any,
    Callable,
    ClassVar,
    get_args,
    get_origin,
    override,
)

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from pydantic._internal._model_construction import (
    ModelMetaclass as PydanticModelMetaclass,
)

if TYPE_CHECKING:
    from ..engine.session import Session
    from .filtering import FieldFilter

# Import exceptions for type hints and potential future use
from ..exc.exceptions import UnknownFieldError
from .security import (
    DANGEROUS_PATH_KEYWORDS,
    escape_sparql_iri,
    escape_sparql_literal,
    validate_iri,
    validate_no_sparql_keywords,
)

# Predicate marker for SubjectField (which doesn't represent a real predicate)
_SUBJECT_FIELD_PREDICATE_MARKER: str = "__subject__"


class XSD(StrEnum):
    """XSD datatype IRIs (full IRI form to avoid PREFIX dependency).

    Members are ``str`` instances so they can be passed directly as the
    ``datatype`` argument of :class:`LiteralField`.
    """

    STRING = "http://www.w3.org/2001/XMLSchema#string"
    INTEGER = "http://www.w3.org/2001/XMLSchema#integer"
    DECIMAL = "http://www.w3.org/2001/XMLSchema#decimal"
    FLOAT = "http://www.w3.org/2001/XMLSchema#float"
    DOUBLE = "http://www.w3.org/2001/XMLSchema#double"
    BOOLEAN = "http://www.w3.org/2001/XMLSchema#boolean"
    DATE = "http://www.w3.org/2001/XMLSchema#date"
    DATETIME = "http://www.w3.org/2001/XMLSchema#dateTime"


# Convenience aliases for backward compatibility and shorter usage
XSD_STRING: str = XSD.STRING
XSD_INTEGER: str = XSD.INTEGER
XSD_DECIMAL: str = XSD.DECIMAL
XSD_FLOAT: str = XSD.FLOAT
XSD_DOUBLE: str = XSD.DOUBLE
XSD_BOOLEAN: str = XSD.BOOLEAN
XSD_DATE: str = XSD.DATE
XSD_DATETIME: str = XSD.DATETIME


# Property path validation regex pattern (private; used only by PropertyPath)
_PROPERTY_PATH_ALLOWED_CHARS_PATTERN: str = (
    r"^[a-zA-Z0-9:\/\#\-\_\.\*\+\?\|\^\/\(\)\<\>\s]+$"
)


class PropertyPath:
    """
    Represents a SPARQL property path for relationship traversal.

    Property paths allow traversing multiple relationship levels and using
    path operators like zero-or-more (*), one-or-more (+), zero-or-one (?),
    alternative paths (|), and inverse paths (^).

    **IRI Expansion**: Property paths are NOT automatically expanded by the
    session's prefix registry. You must use the exact form you want in the
    generated SPARQL query:
    - Short-form: `PropertyPath('schema:knows+')` uses prefix as-is
    - Full IRI: `PropertyPath('<http://schema.org/knows>+')` uses full IRI

    Examples:
        - 'schema:knows+' - one or more knows relationships (transitive)
        - 'org:reportsTo*' - zero or more reportsTo relationships
        - 'schema:parent|schema:guardian' - alternative paths
        - '^schema:child' - inverse path (find parents)
        - '<http://schema.org/knows>+' - full IRI with transitive operator
    """

    OPERATORS: frozenset[str] = frozenset({"^", "*", "+", "?", "|"})

    def __init__(self, path: str):
        """
        Initialize property path.

        Args:
            path: SPARQL property path string (e.g., 'schema:knows+', 'org:reportsTo*')

        Raises:
            ValueError: If path is empty or contains invalid characters
            SPARQLInjectionError: If path contains dangerous SPARQL keywords
        """
        if not path or not path.strip():
            raise ValueError("Property path cannot be empty")

        # Validate for SPARQL injection
        self._validate_property_path_security(path)

        self._path = path

    @property
    def path(self) -> str:
        """Get the property path string (read-only)."""
        return self._path

    def _validate_property_path_security(self, path: str) -> None:
        """
        Validate property path for security.

        Allows valid property path operators but rejects dangerous SPARQL keywords.

        Args:
            path: Property path string to validate

        Raises:
            ValueError: If path contains invalid characters
            SPARQLInjectionError: If path contains dangerous SPARQL keywords
        """
        self._validate_no_dangerous_keywords(path)
        self._validate_allowed_characters(path)

    def _validate_no_dangerous_keywords(self, path: str) -> None:
        """
        Check for dangerous SPARQL keywords in property path.

        Uses word boundary matching to avoid false positives with legitimate IRIs
        that contain keywords as substrings.

        Args:
            path: Property path string to validate

        Raises:
            SPARQLInjectionError: If dangerous SPARQL keywords are detected
        """
        validate_no_sparql_keywords(path, DANGEROUS_PATH_KEYWORDS, "Property path")

    def _validate_allowed_characters(self, path: str) -> None:
        """
        Validate property path character set.

        Property paths should contain:
        - IRI characters (letters, numbers, :, /, #, -, _, .)
        - Angle brackets for full IRIs (<, >)
        - Property path operators (*, +, ?, |, ^, /, (, ))
        - Limited whitespace

        Args:
            path: Property path string to validate

        Raises:
            ValueError: If path contains invalid characters
        """
        if not re.match(_PROPERTY_PATH_ALLOWED_CHARS_PATTERN, path):
            raise ValueError(
                f"Property path contains invalid characters: {path}. "
                f"Allowed: IRI characters, *, +, ?, |, ^, /, (), "
                f"and limited whitespace."
            )

    def to_sparql(self) -> str:
        """
        Return SPARQL representation of property path.

        Returns:
            SPARQL property path string ready for use in queries
        """
        return self.path

    @staticmethod
    def format_predicate(predicate: str) -> str:
        """
        Format a predicate IRI for use in a property path expression.

        This function wraps full IRIs in angle brackets while leaving
        prefixed forms (e.g., schema:knows) unchanged.

        Rules:
        - Full IRI (contains "://"): wrap in <...>
        - Already wrapped (starts with "<"): return as-is
        - Prefixed form (contains ":"): return as-is

        Args:
            predicate: The predicate IRI to format

        Returns:
            Formatted predicate string suitable for property path expressions

        Examples:
            >>> PropertyPath.format_predicate("http://schema.org/knows")
            '<http://schema.org/knows>'
            >>> PropertyPath.format_predicate("schema:knows")
            'schema:knows'
            >>> PropertyPath.format_predicate("<http://schema.org/knows>")
            '<http://schema.org/knows>'
        """
        # Already wrapped in angle brackets
        if predicate.startswith("<") and predicate.endswith(">"):
            return predicate

        # Full IRI (contains "://") - needs wrapping
        if "://" in predicate:
            escaped: str = escape_sparql_iri(predicate)
            return f"<{escaped}>"

        # Prefixed form or other - return as-is
        return predicate

    @staticmethod
    def has_operators(predicate: str) -> bool:
        """
        Check if a predicate string contains property path operators.

        Property path operators (^, *, +, ?, |) should not be wrapped in angle
        brackets. This function detects their presence to avoid double-wrapping.

        Args:
            predicate: The predicate string to check

        Returns:
            True if the predicate contains any property path operators
        """
        return any(op in predicate for op in PropertyPath.OPERATORS)


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

    # 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,
        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__}"
            )

        # Store original predicate (may be short-form)
        self.predicate: str = 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 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"
        """
        return type(self)(
            predicate=self.predicate,
            required=self.required,
            default=self.default,
            validator=self.validators,
            **self.extra_kwargs,
        )

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


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,
        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 format_value(self, value: Any) -> str:
        """Format value as a quoted SPARQL literal with escaping."""
        escaped: str = self._escape_value(value)
        if self.datatype:
            return f'"{escaped}"^^<{self.datatype}>'
        return f'"{escaped}"'

    @override
    def copy(self) -> "LiteralField":
        """Create a deep copy preserving the datatype attribute."""
        field: LiteralField = type(self)(
            predicate=self.predicate,
            datatype=self.datatype,
            required=self.required,
            default=self.default,
            validator=self.validators,
            **self.extra_kwargs,
        )
        return field


@dataclass(frozen=True)
class LangLiteral:
    """
    A language-tagged literal value from RDF.

    This dataclass represents an RDF literal with an associated language tag.
    It is returned when querying LangString fields to preserve the language
    information from the SPARQL endpoint.

    Attributes:
        text: The string value of the literal.
        lang: The BCP 47 language tag (e.g., 'en', 'yi', 'zh-Hans'), or None
              if no language tag was present.

    Example:
        >>> literal = LangLiteral("Hello", "en")
        >>> print(literal.text)   # "Hello"
        >>> print(literal.lang)   # "en"
        >>> print(str(literal))   # "Hello"
    """

    text: str
    lang: str | None = None

    def __str__(self) -> str:
        """Return the text value when converting to string."""
        return self.text

    def __repr__(self) -> str:
        """Return a detailed representation including language tag."""
        if self.lang:
            return f"LangLiteral({self.text!r}, lang={self.lang!r})"
        return f"LangLiteral({self.text!r})"


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,
        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 format_value(self, value: Any) -> str:
        """Format value as a language-tagged SPARQL literal."""
        escaped: str = self._escape_value(value)
        if self.lang:
            return f'"{escaped}"@{self.lang}'
        return f'"{escaped}"'

    @override
    def copy(self) -> "LangString":
        """Create a deep copy preserving the lang attribute."""
        field: LangString = type(self)(
            predicate=self.predicate,
            lang=self.lang,
            required=self.required,
            default=self.default,
            validator=self.validators,
            **self.extra_kwargs,
        )
        return field


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,
        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


class IRIField(RDFFieldInfo):
    """
    Field for IRI references to other resources.
    Values are serialized with angle brackets in SPARQL.
    """

    @override
    def format_value(self, value: Any) -> str:
        """Format value as a SPARQL IRI reference with angle brackets."""
        escaped: str = escape_sparql_iri(str(value))
        return f"<{escaped}>"


class ObjectPropertyField(IRIField):
    """
    Field for object properties linking to other RDF resources.

    Inherits from IRIField to automatically handle IRI formatting in SPARQL
    queries. This ensures filter operations produce correct IRI syntax
    (``<http://...>``) rather than string literals (``"http://..."``), which is
    essential for SPARQL query correctness when filtering on object properties.

    The inheritance relationship also simplifies type checking: any code that
    checks ``isinstance(field, IRIField)`` will correctly identify
    ObjectPropertyField as an IRI-valued field, enabling consistent handling
    across all IRI field types.

    The range_ parameter specifies the RDF class (rdfs:range) that objects
    of this property should have. For example, if 'knows' links Person to Person,
    range_="http://schema.org/Person". This enables:

    - Type validation in future implementations
    - Automatic type constraints in SPARQL queries
    - Schema documentation and introspection

    Currently range_ is stored for future use but not enforced.

    Example:
        >>> class Person(Model):
        ...     rdf_type = "http://schema.org/Person"
        ...     knows: Annotated[str | None, ObjectPropertyField(
        ...         "http://schema.org/knows",
        ...         range_="http://schema.org/Person"
        ...     )] = None
        >>>
        >>> # Filtering produces correct IRI syntax
        >>> query = session.query(Person).filter_by(
        ...     knows="http://example.org/bob"
        ... )
        >>> # Generated SPARQL: FILTER(?knows = <http://example.org/bob>)
    """

    def __init__(
        self,
        predicate: str,
        range_: 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,
        )
        self.range: str | None = range_

    # format_value() is inherited from IRIField


class SubjectField(IRIField):
    """
    Field that maps to the RDF subject (the ?s variable in SPARQL).

    Inherits from IRIField to automatically handle IRI formatting in SPARQL
    queries. This ensures that when filtering by subject IRI, the value is
    correctly formatted with angle brackets (``<http://...>``) rather than
    as a string literal. The inheritance also enables consistent type checking:
    ``isinstance(field, IRIField)`` correctly identifies SubjectField as an
    IRI-valued field.

    Unlike other fields, SubjectField doesn't represent a predicate relationship.
    It represents the subject IRI itself, allowing you to filter and access
    the subject IRI as a named field in your model.

    This is particularly useful for modeling property relationships (e.g., rdfs:label)
    where you need to query for ``?subject <property> ?value`` patterns and filter
    by the subject.

    Usage:
        >>> class Label(Model):
        ...     rdf_type = None  # Property relationship, not a typed entity
        ...     entity_iri: Annotated[str, SubjectField()]
        ...     text: Annotated[str, LangString("rdfs:label")]
        >>>
        >>> # Filter by subject IRI - uses VALUES clause for efficiency
        >>> labels = session.query(Label).filter_by(
        ...     entity_iri="http://www.wikidata.org/entity/Q682"
        ... ).all()

    Note:
        - Each model can have at most one SubjectField
        - SubjectField does not generate a triple pattern in SPARQL
        - Values are populated from the ?s binding in query results
        - When used with filter_by(), generates a VALUES clause rather than FILTER
          for better query performance

    See Also:
        - :class:`LiteralList`, :class:`LangStringList`, :class:`IRIList` for
          collection fields that aggregate multiple values into lists
    """

    # SubjectField allows 'description' as an extra kwarg for documentation
    _ALLOWED_EXTRA_KWARGS: frozenset[str] = frozenset({"description"})

    def __init__(
        self,
        *,
        required: bool = False,
        default: Any = None,
        validator: Callable[[Any], None] | list[Callable[[Any], None]] | None = None,
        description: str | None = None,
        **kwargs: Any,
    ):
        """
        Initialize a SubjectField.

        Args:
            required: Whether this field is required (default: False)
            default: Default value if not provided (default: None)
            validator: Optional validation function(s)
            description: Human-readable description of this field
            **kwargs: Additional keyword arguments passed to parent
        """
        # SubjectField has no predicate - it IS the subject
        # Use special marker that won't conflict with real predicates
        super().__init__(
            predicate=_SUBJECT_FIELD_PREDICATE_MARKER,
            required=required,
            default=default,
            validator=validator,
            description=description,
            **kwargs,
        )
        self.is_subject_field: bool = True

    # format_value() is inherited from IRIField


# =============================================================================
# Collection Field Types
# =============================================================================

# Default separator for GROUP_CONCAT aggregation
# Uses three ASCII Unit Separator characters (\u001F) which are:
# 1. Invisible in output (easier to work with than visible separators)
# 2. Guaranteed not to appear in natural text (control character)
# 3. Used in sequence of three to further reduce collision risk
DEFAULT_COLLECTION_SEPARATOR: str = "\u001f\u001f\u001f"

# Separator between value and language tag within each LangStringList item
# Uses ASCII Record Separator (\u001E) to combine "text" + separator + "lang"
# This ensures value and language stay paired during GROUP_CONCAT aggregation
LANG_VALUE_SEPARATOR: str = "\u001e"

# Suffix for GROUP_CONCAT result variables (e.g., "labels" -> "labels_concat")
COLLECTION_CONCAT_SUFFIX: str = "_concat"

# Suffix for SAMPLE() result variables in collection queries
# Avoids SPARQL variable scope conflicts (e.g., "description" -> "description_sample")
SAMPLE_VARIABLE_SUFFIX: str = "_sample"


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)

    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):
        ...     rdf_type = None
        ...     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

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

        Args:
            predicate: The RDF predicate IRI for this field
            separator: String used for GROUP_CONCAT (default: triple unit separator)
            **kwargs: Additional arguments passed to parent field class
        """
        self.separator = separator
        super().__init__(predicate, **kwargs)  # type: ignore[call-arg]


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):
            rdf_type = "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"]

    Args:
        predicate: The RDF predicate IRI for the literal values
        separator: Custom separator for GROUP_CONCAT (default: triple unit separator)
        **kwargs: Additional arguments passed to LiteralField

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

    pass


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):
            rdf_type = None
            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.text} ({label.lang})")
        # "Berlin" (en)
        # "Berlin" (de)
        # "Berlín" (es)

    Args:
        predicate: The RDF predicate IRI for the language-tagged literals
        separator: Custom separator for GROUP_CONCAT (default: triple unit separator)
        **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
    """

    pass


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):
            rdf_type = "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"]

    Args:
        predicate: The RDF predicate IRI for the object property
        separator: Custom separator for GROUP_CONCAT (default: triple unit separator)
        **kwargs: Additional arguments passed to IRIField

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

    pass


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)


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 = 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')"
        )


def validate_instance(instance: "Model") -> None:
    """
    Validate all fields on a model instance.

    Checks:
    - Required fields are present and not None
    - Custom validators pass for fields that have them

    Args:
        instance: Model instance to validate

    Raises:
        ValidationError: If validation fails with detailed error messages
        RequiredFieldError: If required field is missing

    Example:
        >>> class Person(Model):
        ...     name: Annotated[str,
        ...         LiteralField("http://schema.org/name", required=True)]
        ...     age: Annotated[int,
        ...         LiteralField("http://schema.org/age",
        ...                     validator=lambda x: x >= 0)]
        >>>
        >>> person = Person(name="John", age=-5)
        >>> validate_instance(person)  # Raises ValidationError for negative age
    """
    from ..exc.exceptions import RequiredFieldError, ValidationError

    errors: list[str] = []

    # Get all RDF fields for this model class
    rdf_fields = getattr(instance.__class__, "_rdf_fields", {})

    for field_name, field in rdf_fields.items():
        # Get field value once for both checks
        field_value = getattr(instance, field_name, None)

        # Check required fields
        if field.required and field_value is None:
            errors.append(f"Required field '{field_name}' is missing")

        # Check custom validators (only if value is present)
        if field.validators and field_value is not None:
            for validator_func in field.validators:
                try:
                    validator_func(field_value)
                except ValueError as e:
                    # Expected validation errors
                    errors.append(f"Validation failed for '{field_name}': {e}")
                except Exception as e:
                    # Unexpected errors from buggy validators - re-raise with context
                    from ..exc.exceptions import ValidationError

                    raise ValidationError(
                        f"Validator function for '{field_name}' raised "
                        f"unexpected error: {type(e).__name__}: {e}"
                    ) from e

    if errors:
        # Use RequiredFieldError for single required field error
        if len(errors) == 1 and "Required field" in errors[0]:
            raise RequiredFieldError(errors[0])
        # Use ValidationError for multiple errors or validator errors
        else:
            raise ValidationError("\n".join(errors))


def _register_subject_field(
    cls_name: str, field_name: str, subject_field_name: str | None
) -> str:
    """
    Register a SubjectField and validate uniqueness.

    Args:
        cls_name: Name of the model class being defined
        field_name: Name of the field being registered
        subject_field_name: Name of previously registered SubjectField, if any

    Returns:
        The field_name (for assignment to subject_field_name variable)

    Raises:
        ValueError: If multiple SubjectFields are declared
    """
    if subject_field_name is not None:
        raise ValueError(
            f"Model {cls_name} cannot have multiple "
            f"SubjectFields: {subject_field_name}, {field_name}"
        )
    return field_name


class ModelMetaclass(PydanticModelMetaclass):
    """
    Custom metaclass that extends Pydantic's ModelMetaclass.

    Enables field-level filtering syntax: Person.name == "Alice"

    By intercepting class attribute access, this metaclass returns FieldFilter
    objects for RDF fields, allowing intuitive filter expressions like:
        query.filter(Person.name == "Alice")
        query.filter(Product.price > 100)
    """

    def __getattr__(cls, name: str) -> "FieldFilter":  # noqa: N805
        """
        Intercept attribute access on Model classes.

        Returns FieldFilter for RDF fields, enabling syntax like:
            Person.name == "Alice"

        Args:
            name: Attribute name being accessed

        Returns:
            FieldFilter object for RDF fields

        Raises:
            AttributeError: If attribute is not an RDF field
        """
        # Check if this is an RDF field
        rdf_fields = getattr(cls, "_rdf_fields", {})
        if name in rdf_fields:
            from .filtering import FieldFilter

            return FieldFilter(name)

        # Not an RDF field - raise AttributeError
        raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")


class Model(BaseModel, metaclass=ModelMetaclass):
    """
    Base class for RDF models with Pydantic validation.

    Models should declare:
    - rdf_type: The RDF class IRI (as ClassVar)
    - Fields using LiteralField, IRIField, or ObjectPropertyField with type annotations

    Features:
        - Automatic IRI validation (can be disabled with validate_iris = False)
        - Dirty tracking for field modifications
        - SPARQL triple pattern generation
        - Pydantic type validation and coercion
        - Field-level validation with required fields and custom validators

    Field Validation:
        Fields support required enforcement and custom validators.
        See module-level documentation for detailed validation examples including:
        - Required field enforcement
        - Custom validator functions
        - Validator composition (multiple validators per field)
        - Session integration (automatic validation on add/update)

    Raises:
        UnknownFieldError: When unknown fields are provided
        IRIError: When IRI validation fails (if validate_iris is True)
        RequiredFieldError: When required fields are missing
        ValidationError: When custom validators fail

    Example:
        >>> class Person(Model):
        ...     rdf_type = "http://schema.org/Person"
        ...     name: Annotated[str, LiteralField("http://schema.org/name")]
        ...     age: Annotated[int, LiteralField("http://schema.org/age")]

        >>> # Valid IRI - works fine
        >>> person = Person(iri="http://example.org/john", name="John", age=30)

        >>> # Invalid IRI - raises IRIError
        >>> try:
        ...     person = Person(iri="invalid iri with spaces", name="John", age=30)
        ... except IRIError as e:
        ...     print(f"Validation failed: {e}")

        >>> # Disable validation for performance
        >>> class FastPerson(Model):
        ...     rdf_type = "http://schema.org/Person"
        ...     name: Annotated[str, LiteralField("http://schema.org/name")]
        ...     validate_iris = False  # Disable IRI validation

        >>> person = FastPerson(iri="any string works now", name="John")
    """

    model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")

    # Class variables
    validate_iris: ClassVar[bool] = True
    rdf_type: ClassVar[str | None] = None
    _rdf_fields: ClassVar[dict[str, RDFFieldInfo]] = {}
    _subject_field_name: ClassVar[str | None] = None

    iri: str | None = None

    @classmethod
    def _setup_pydantic_field(cls, field_name: str, field_info: RDFFieldInfo) -> None:
        """
        Set up Pydantic Field attribute for a field.

        Args:
            field_name: Name of the field to set up
            field_info: RDF field information containing default and kwargs

        This ensures proper default values are set instead of FieldFilter objects.
        """
        setattr(
            cls,
            field_name,
            Field(
                default=field_info.default,
                **field_info.extra_kwargs,
            ),
        )

    @classmethod
    def _inherit_parent_fields(
        cls,
        rdf_fields: dict[str, RDFFieldInfo],
        subject_field_name: str | None,
        annotations: dict[str, Any],
    ) -> tuple[dict[str, RDFFieldInfo], str | None]:
        """
        Inherit RDF fields from parent Model classes.

        Args:
            rdf_fields: Dictionary to populate with inherited fields
            subject_field_name: Current SubjectField name (if any)
            annotations: Annotations defined on this class (to avoid
                inheriting overridden fields)

        Returns:
            Tuple of (updated rdf_fields dict, updated subject_field_name)

        This method walks the MRO to collect fields from parent Model classes,
        avoiding duplication and respecting field overrides in child classes.
        """
        for base_class in cls.__mro__:
            # Skip current class (process separately) and base Model class
            if base_class == cls or base_class == Model:
                continue

            # Check if base class is a Model subclass and has RDF fields
            if issubclass(base_class, Model) and hasattr(base_class, "_rdf_fields"):
                # Inherit all RDF fields from parent
                for field_name, field_info in base_class._rdf_fields.items():
                    # Only inherit if not already processed and not overridden in child
                    if field_name not in rdf_fields and field_name not in annotations:
                        # Create a copy of the field info to avoid mutation issues
                        field_copy = field_info.copy()
                        field_copy.bind_name(field_name)
                        rdf_fields[field_name] = field_copy

                        # Set up Pydantic Field attribute for inherited field
                        cls._setup_pydantic_field(field_name, field_copy)

                        # Check if this is a SubjectField
                        if isinstance(field_copy, SubjectField):
                            subject_field_name = _register_subject_field(
                                cls.__name__, field_name, subject_field_name
                            )

        return rdf_fields, subject_field_name

    @classmethod
    def _process_class_annotations(
        cls,
        rdf_fields: dict[str, RDFFieldInfo],
        subject_field_name: str | None,
        annotations: dict[str, Any],
    ) -> tuple[dict[str, RDFFieldInfo], str | None]:
        """
        Process fields defined directly on this class via annotations.

        Args:
            rdf_fields: Dictionary to populate with class fields
            subject_field_name: Current SubjectField name (if any)
            annotations: Type annotations defined on this class

        Returns:
            Tuple of (updated rdf_fields dict, updated subject_field_name)

        This method extracts RDFFieldInfo from Annotated type hints and
        registers them with the appropriate metadata.
        """
        for field_name in annotations:
            # Check if field descriptor is in Annotated type metadata
            annotation = annotations[field_name]
            if get_origin(annotation) is Annotated:
                # Extract metadata from Annotated type
                metadata = get_args(annotation)
                if len(metadata) >= 2:
                    # The second argument should be the field descriptor
                    field_descriptor = metadata[1]
                    if isinstance(field_descriptor, RDFFieldInfo):
                        field_descriptor.bind_name(field_name)

                        # Check if this is a SubjectField
                        if isinstance(field_descriptor, SubjectField):
                            subject_field_name = _register_subject_field(
                                cls.__name__, field_name, subject_field_name
                            )

                        rdf_fields[field_name] = field_descriptor
                        cls._setup_pydantic_field(field_name, field_descriptor)

        return rdf_fields, subject_field_name

    def __init_subclass__(cls, **kwargs: Any) -> None:
        """
        Extract RDF field metadata when subclass is created.

        This method processes both inherited fields from parent Model classes
        and fields defined directly on the subclass. Inherited fields are copied
        to avoid mutation issues, and Pydantic Field descriptors are set up to
        ensure proper default values.

        Field Inheritance Behavior:
        - Fields from parent Model classes are inherited automatically
        - Child classes can override parent fields by redefining them
        - Inherited fields maintain their predicates, defaults, and validators
        - SubjectFields are inherited and validated (only one per class hierarchy)

        Example:
            >>> class Person(Model):
            ...     name: Annotated[str | None, LiteralField(..., default=None)]
            >>>
            >>> class Employee(Person):
            ...     employee_id: Annotated[str | None, LiteralField(..., default=None)]
            >>>
            >>> emp = Employee(name="Alice")
            >>> emp.name  # "Alice" (set value)
            >>> emp.employee_id  # None (inherited default)
        """
        super().__init_subclass__(**kwargs)

        rdf_fields: dict[str, RDFFieldInfo] = {}
        subject_field_name: str | None = None
        annotations = getattr(cls, "__annotations__", {})

        # Inherit fields from parent classes
        rdf_fields, subject_field_name = cls._inherit_parent_fields(
            rdf_fields, subject_field_name, annotations
        )

        # Process fields defined on this class
        rdf_fields, subject_field_name = cls._process_class_annotations(
            rdf_fields, subject_field_name, annotations
        )

        cls._rdf_fields = rdf_fields
        cls._subject_field_name = subject_field_name

    @model_validator(mode="before")
    @classmethod
    def check_unknown_fields(cls, data: Any) -> Any:
        """Validate that all provided fields are known RDF fields."""
        if isinstance(data, dict):
            rdf_fields = getattr(cls, "_rdf_fields", {})
            for field_name in data.keys():
                if field_name not in rdf_fields and field_name != "iri":
                    raise UnknownFieldError(cls, field_name)
        return data

    @field_validator("iri")
    @classmethod
    def validate_iri_field(cls, v: str | None) -> str | None:
        """Validate IRI format if validation is enabled."""
        if v is not None and cls.validate_iris:
            validate_iri(v)
        return v

    def __init__(self, **data: Any) -> None:
        """Initialize model instance with dirty tracking."""
        # Pydantic validation happens automatically before this
        super().__init__(**data)

        # Initialize dirty tracking
        self._original_data: dict[str, Any] = {}
        self._dirty_fields: set[str] = set()

        # Store original state of all fields
        rdf_fields = getattr(self.__class__, "_rdf_fields", {})
        for field_name in rdf_fields.keys():
            if hasattr(self, field_name):
                # Get value from __dict__ to avoid triggering metaclass
                value = self.__dict__.get(field_name)
                self._original_data[field_name] = value
            else:
                # Field not set on instance, use default from field info
                field_info = rdf_fields[field_name]
                self._original_data[field_name] = field_info.default

    @classmethod
    def _get_field_filter(cls, field_name: str) -> "FieldFilter":
        """
        Get a FieldFilter object for the specified field.

        This method enables field-level filtering syntax like:
            Person.name == "Alice"

        Args:
            field_name: Name of the field to filter on

        Returns:
            FieldFilter object for the specified field

        Raises:
            UnknownFieldError: If the field doesn't exist in the model
        """
        from .filtering import FieldFilter

        # Check if field exists in model
        rdf_fields: dict[str, RDFFieldInfo] = getattr(cls, "_rdf_fields", {})
        if field_name not in rdf_fields:
            raise UnknownFieldError(cls, field_name)

        return FieldFilter(field_name)

    @classmethod
    def _field_accessor(cls, name: str) -> "FieldFilter":
        """
        Class method to access fields for filtering.

        This method enables the intuitive filtering syntax:
            Person.name == "Alice"

        Args:
            name: Attribute name being accessed

        Returns:
            FieldFilter object for the specified field

        Raises:
            AttributeError: If the attribute is not a known RDF field
        """
        try:
            return cls._get_field_filter(name)
        except UnknownFieldError:
            # If not a field, raise standard AttributeError
            raise AttributeError(
                f"type object '{cls.__name__}' has no attribute '{name}'"
            )

    def __setattr__(self, key: str, value: Any) -> None:
        """
        Override attribute setting to track changes for dirty tracking.

        Args:
            key: Attribute name
            value: New value
        """
        # Handle special attributes and private attributes normally
        if key in ("_original_data", "_dirty_fields") or key.startswith("_"):
            object.__setattr__(self, key, value)
            return

        # Check if this is an RDF field
        rdf_fields = getattr(self.__class__, "_rdf_fields", {})
        if key in rdf_fields:
            # Check if value has actually changed
            current_value = getattr(self, key, None)
            if current_value != value:
                # Mark as dirty if not already marked
                if key not in self._dirty_fields:
                    self._dirty_fields.add(key)
            # Set the value
            object.__setattr__(self, key, value)
        else:
            # For non-RDF fields, use normal attribute setting
            object.__setattr__(self, key, value)

    def mark_clean(self) -> None:
        """Mark all fields as clean (not modified)."""
        # Update original data to current values
        rdf_fields = getattr(self.__class__, "_rdf_fields", {})
        for field_name in rdf_fields.keys():
            if hasattr(self, field_name):
                self._original_data[field_name] = getattr(self, field_name)
        self._dirty_fields.clear()

    def is_dirty(self) -> bool:
        """Check if any fields have been modified."""
        return len(self._dirty_fields) > 0

    def get_changes(self) -> dict[str, tuple[Any, Any]]:
        """Get dictionary of changed fields with (old, new) values."""
        changes: dict[str, tuple[Any, Any]] = {}
        for field_name in self._dirty_fields:
            old_value = self._original_data.get(field_name)
            new_value = getattr(self, field_name, None)
            changes[field_name] = (old_value, new_value)
        return changes

    def triple_patterns(
        self, subject_var: str = "?s", session: "Session | None" = None
    ) -> list[str]:
        """
        Generate SPARQL triple patterns for this model instance.

        Args:
            subject_var: The SPARQL variable to use for the subject
            session: Optional session with prefix registry for IRI expansion

        Returns:
            List of SPARQL triple pattern strings

        Raises:
            AttributeError: If required RDF fields are missing from the model class
        """
        clauses: list[str] = []

        # Type triple
        if self.rdf_type:
            cls_iri: str = (
                self.rdf_type
                if isinstance(self.rdf_type, str)
                else self.rdf_type.predicate
            )
            # Expand short-form IRI if session is provided
            if session:
                cls_iri = self.expand_iri(cls_iri, session)
            clauses.append(f"{subject_var} a {cls_iri} .")

        # Field triples
        rdf_fields = getattr(self.__class__, "_rdf_fields", {})
        for field_name, field_info in rdf_fields.items():
            value = getattr(self, field_name, None)
            var: str = f"?{field_name}"

            # Expand predicate IRI if session is provided
            predicate: str = field_info.predicate
            if session:
                predicate = self.expand_iri(predicate, session)

            if value is not None:
                # Handle MultiLangString which returns multiple values
                if isinstance(field_info, MultiLangString):
                    # Get list of formatted values for each language
                    formatted_values: list[str] = field_info.format_value(value)
                    for formatted_val in formatted_values:
                        clauses.append(f"{subject_var} {predicate} {formatted_val} .")
                else:
                    # Regular single-value field - use polymorphic formatting
                    formatted_value: str = field_info.format_value(value)
                    clauses.append(f"{subject_var} {predicate} {formatted_value} .")
            else:
                # Optional fetch pattern
                clauses.append(f"OPTIONAL {{ {subject_var} {predicate} {var} . }}")

        return clauses

    def expand_iri(self, iri: str, session: "Session | None" = None) -> str:
        """Expand a short-form IRI using session's prefix registry if available.

        Args:
            iri: The IRI to expand (may be short-form like 'schema:Person')
            session: Optional session with prefix registry

        Returns:
            Expanded IRI (full IRI)
        """
        if not iri:
            return iri

        # If session is provided and has expand_iri method, use it
        if session and hasattr(session, "expand_iri"):
            try:
                return session.expand_iri(iri)
            except (ValueError, AttributeError):
                # If expansion fails, return original IRI
                return iri

        # No session or expansion failed, return original
        return iri

    @classmethod
    def subject_var(cls) -> str:
        """Return the default SPARQL subject variable."""
        return "?s"
