"""
Model base class, metaclass, and validation utilities.

This module provides the core Model base class that combines Pydantic
validation with RDF field metadata extraction.
"""

from typing import TYPE_CHECKING, Annotated, Any, ClassVar, get_args, get_origin

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

from ...exc.exceptions import UnknownFieldError
from ..schema_validator import register_model
from ..security import validate_iri
from .collection_fields import is_collection_field
from .field_base import RDFFieldInfo
from .iri_fields import SubjectField

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


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"

        For collection fields (LiteralList, IRIList, etc.), returns
        CollectionFieldFilter which has contains() for membership checks.

        Args:
            name: Attribute name being accessed

        Returns:
            FieldFilter for regular fields, CollectionFieldFilter for collections

        Raises:
            AttributeError: If attribute is not an RDF field
        """
        # Check if this is an RDF field
        rdf_fields: dict[str, RDFFieldInfo] = getattr(cls, "_rdf_fields", {})
        if name in rdf_fields:
            field_info: RDFFieldInfo = rdf_fields[name]

            # Return appropriate filter type based on field type
            if is_collection_field(field_info):
                from ..filtering import CollectionFieldFilter

                return CollectionFieldFilter(name)
            else:
                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:
    - Fields using LiteralField, IRIField, or ObjectPropertyField
    - Optional: rdf_type using IRIField(RDF_TYPE, default="...")

    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):
        ...     iri: Annotated[str, SubjectField()]
        ...     rdf_type: Annotated[str, IRIField(RDF_TYPE, default="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):
        ...     iri: Annotated[str, SubjectField()]
        ...     rdf_type: Annotated[str, IRIField(RDF_TYPE, default="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_fields: ClassVar[dict[str, RDFFieldInfo]] = {}
    _subject_field_name: ClassVar[str | None] = None

    # Note: Models must declare a SubjectField for their IRI.
    # Example: iri: Annotated[str, SubjectField()]

    @property
    def subject_iri(self) -> str | None:
        """Get the subject IRI from the model's SubjectField.

        This provides a consistent way to access the subject IRI regardless
        of what the SubjectField is named in the model.

        Returns:
            The subject IRI value, or None if not set.

        Raises:
            ValueError: If the model has no SubjectField declared.
        """
        if not self.__class__._subject_field_name:
            raise ValueError(
                f"Model {self.__class__.__name__} has no SubjectField declared. "
                "Add a SubjectField to your model, e.g.: "
                "iri: Annotated[str, SubjectField()]"
            )
        return getattr(self, self.__class__._subject_field_name, None)

    @subject_iri.setter
    def subject_iri(self, value: str | None) -> None:
        """Set the subject IRI via the model's SubjectField."""
        if not self.__class__._subject_field_name:
            raise ValueError(
                f"Model {self.__class__.__name__} has no SubjectField declared."
            )
        setattr(self, self.__class__._subject_field_name, value)

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

        # Register model for schema processing (validation + inverse discovery)
        # Processing happens when registry.activate() is called
        register_model(cls)

    @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:
                    raise UnknownFieldError(cls, field_name)
        return data

    @model_validator(mode="after")
    def validate_subject_iri(self) -> "Model":
        """Validate SubjectField IRI format if validation is enabled."""
        if self.__class__._subject_field_name and self.__class__.validate_iris:
            iri_value: str | None = getattr(
                self, self.__class__._subject_field_name, None
            )
            if iri_value is not None:
                validate_iri(iri_value)
        return self

    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] = []

        # Field triples
        rdf_fields = getattr(self.__class__, "_rdf_fields", {})
        for field_name, field_info in rdf_fields.items():
            # Skip SubjectField - represents the subject, not a predicate-object
            if isinstance(field_info, SubjectField):
                continue

            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:
                # Use polymorphic generate_triples() - handles both single and
                # multi-value fields (e.g., MultiLangString returns multiple triples)
                triples: list[str] = field_info.generate_triples(
                    subject_var, predicate, value
                )
                clauses.extend(triples)
            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"


__all__ = [
    "Model",
    "ModelMetaclass",
    "validate_instance",
]
