"""
Schema-aware model processing.

When a global SchemaRegistry is set, all models automatically receive:
- Inverse discovery: InverseFields use named predicates from owl:inverseOf
- Validation: Fields are checked against ontology constraints

This follows "Convention over Configuration" - schema features are enabled
by default when a registry exists. Models can opt out via Meta.schema_aware = False.

Validates:
- Domain constraints (property domain matches model's rdf_type)
- Range constraints (Python type matches XSD range)
- Cardinality (functional properties use single-value fields)
- Property existence (predicate is defined in ontology)
"""

from __future__ import annotations

import warnings
from dataclasses import dataclass
from datetime import date, datetime
from decimal import Decimal
from typing import TYPE_CHECKING, Any, Literal, get_args, get_origin

from ..exc.exceptions import (
    CardinalityViolationError,
    DomainConstraintError,
    RangeTypeMismatchError,
    UnknownPropertyWarning,
)

if TYPE_CHECKING:
    from .model import Model, RDFFieldInfo
    from .schema_registry import PropertyInfo, SchemaRegistry

# Warning stacklevel constants
# These indicate how many frames up the call stack to report as the warning source.
# Call chain for _handle_policy:
#   warn (1) -> _handle_policy (2) -> _validate_field (3) -> _validate_model (4)
_STACKLEVEL_HANDLE_POLICY: int = 4
# Call chain for _handle_policy_with_exception (called from _validate_* helpers):
#   warn (1) -> _handle_policy_with_exception (2) -> _validate_* (3)
#   -> _validate_field (4) -> _validate_model (5)
_STACKLEVEL_HANDLE_POLICY_WITH_EXCEPTION: int = 5

# XSD to Python type mapping
XSD_TO_PYTHON: dict[str, set[type]] = {
    "http://www.w3.org/2001/XMLSchema#string": {str},
    "http://www.w3.org/2001/XMLSchema#integer": {int},
    "http://www.w3.org/2001/XMLSchema#decimal": {float, int, Decimal},
    "http://www.w3.org/2001/XMLSchema#float": {float},
    "http://www.w3.org/2001/XMLSchema#double": {float},
    "http://www.w3.org/2001/XMLSchema#boolean": {bool},
    "http://www.w3.org/2001/XMLSchema#date": {date},
    "http://www.w3.org/2001/XMLSchema#dateTime": {datetime},
}

PolicyType = Literal["ignore", "warn", "error"]

# Module-level registry and pending models queue
_global_registry: SchemaRegistry | None = None
_pending_models: list[type[Model]] = []


@dataclass
class SchemaValidationConfig:
    """Configuration for schema validation behavior."""

    enabled: bool = True
    strict_mode: bool = False
    unknown_property_policy: PolicyType = "warn"
    domain_mismatch_policy: PolicyType = "error"
    range_mismatch_policy: PolicyType = "error"
    cardinality_mismatch_policy: PolicyType = "warn"


def _set_global_registry(registry: SchemaRegistry) -> None:
    """
    Internal: Set global registry and process all pending models.

    Use SchemaRegistry.activate() instead of calling this directly.
    """
    global _global_registry
    _global_registry = registry

    for model_cls in _pending_models:
        _process_model_with_schema(model_cls, registry)
    _pending_models.clear()


def get_global_registry() -> SchemaRegistry | None:
    """Get the current global schema registry."""
    return _global_registry


def _clear_global_registry() -> None:
    """Internal: Clear the global registry. Use SchemaRegistry.deactivate()."""
    global _global_registry
    _global_registry = None
    _pending_models.clear()


def clear_global_registry() -> None:
    """
    Clear the global registry.

    This is a convenience function mainly for testing.
    Prefer using registry.deactivate() when possible.
    """
    _clear_global_registry()


def register_model(model_cls: type[Model]) -> None:
    """
    Register model for schema processing.

    If a global registry exists, the model is processed immediately.
    Otherwise, it's queued for processing when registry.activate() is called.
    """
    if _global_registry is not None:
        _process_model_with_schema(model_cls, _global_registry)
    else:
        _pending_models.append(model_cls)


def _process_model_with_schema(
    model_cls: type[Model],
    registry: SchemaRegistry,
) -> None:
    """
    Apply all schema features to a model.

    This includes:
    1. Discovering inverse predicates for InverseFields
    2. Validating fields against schema constraints
    """
    # Check for opt-out
    if _is_schema_disabled(model_cls):
        return

    # 1. Discover inverses for InverseFields
    _discover_inverses_for_model(model_cls, registry)

    # 2. Validate fields against schema
    _validate_model(model_cls, registry)


def _is_schema_disabled(model_cls: type[Model]) -> bool:
    """Check if model has opted out of schema features via Meta.schema_aware."""
    meta: Any = getattr(model_cls, "Meta", None)
    if meta is None:
        return False
    # Explicit opt-out: schema_aware = False
    return getattr(meta, "schema_aware", True) is False


def _discover_inverses_for_model(
    model_cls: type[Model],
    registry: SchemaRegistry,
) -> None:
    """Discover inverse predicates for all InverseFields in model."""
    from .model import InverseField

    rdf_fields: dict[str, Any] = getattr(model_cls, "_rdf_fields", {})
    for field_info in rdf_fields.values():
        if isinstance(field_info, InverseField):
            field_info.discover_inverse_from_schema(registry)


def validate_model_against_schema(
    model_cls: type[Model],
    registry: SchemaRegistry,
    config: SchemaValidationConfig | None = None,
) -> list[str]:
    """
    Validate all fields in a model against schema constraints.

    Returns list of warning messages.
    Raises SchemaValidationError subclasses for errors.
    """
    return _validate_model(model_cls, registry, config)


def _validate_model(
    model_cls: type[Model],
    registry: SchemaRegistry,
    config: SchemaValidationConfig | None = None,
) -> list[str]:
    """Internal validation implementation."""
    from .model import SubjectField

    if config is None:
        config = _get_validation_config(model_cls)

    warnings_list: list[str] = []
    rdf_fields: dict[str, Any] = getattr(model_cls, "_rdf_fields", {})

    for field_name, field_info in rdf_fields.items():
        # Skip SubjectField
        if isinstance(field_info, SubjectField):
            continue

        # Skip fields using rdf:type predicate - it's a built-in RDF predicate
        if _is_rdf_type_field(field_info):
            continue

        field_warnings = _validate_field(
            model_cls, field_name, field_info, registry, config
        )
        warnings_list.extend(field_warnings)

    return warnings_list


def _get_validation_config(model_cls: type[Model]) -> SchemaValidationConfig:
    """Extract validation config from model's Meta class."""
    meta: Any = getattr(model_cls, "Meta", None)
    if meta is None:
        return SchemaValidationConfig()

    return SchemaValidationConfig(
        enabled=True,  # Always enabled when we reach this point
        strict_mode=getattr(meta, "strict_mode", False),
        unknown_property_policy=getattr(meta, "unknown_property_policy", "warn"),
        domain_mismatch_policy=getattr(meta, "domain_mismatch_policy", "error"),
        range_mismatch_policy=getattr(meta, "range_mismatch_policy", "error"),
        cardinality_mismatch_policy=getattr(
            meta, "cardinality_mismatch_policy", "warn"
        ),
    )


def _validate_field(
    model_cls: type[Model],
    field_name: str,
    field_info: RDFFieldInfo,
    registry: SchemaRegistry,
    config: SchemaValidationConfig,
) -> list[str]:
    """Validate a single field against schema constraints."""
    warnings_list: list[str] = []
    predicate, is_inverse = _get_predicate_info(field_info)

    if not predicate:
        return warnings_list

    # Get property info from registry
    prop_info: PropertyInfo | None = registry.get_property(predicate)

    # Check property existence
    if prop_info is None:
        msg: str = f"Property '{predicate}' on field '{field_name}' not found in schema"
        warnings_list.extend(_handle_policy(config.unknown_property_policy, msg))
        return warnings_list

    # Validate each constraint type
    # Skip domain validation for inverse paths - the model is in the object position
    if not is_inverse:
        _validate_domain(
            model_cls, field_name, predicate, prop_info, config, warnings_list, registry
        )
    _validate_range(model_cls, field_name, predicate, prop_info, config, warnings_list)
    _validate_cardinality(
        field_name, predicate, field_info, prop_info, config, warnings_list
    )

    return warnings_list


def _get_model_rdf_types(model_cls: type[Model]) -> set[str]:
    """
    Get all rdf:type values from a model class.

    Finds all fields that use the rdf:type predicate and returns their
    default values. A model can have multiple types.

    Returns:
        Set of rdf:type IRIs, or empty set if none defined.
    """
    rdf_fields: dict[str, Any] = getattr(model_cls, "_rdf_fields", {})
    rdf_types: set[str] = set()

    for field_info in rdf_fields.values():
        if _is_rdf_type_field(field_info):
            default: Any = getattr(field_info, "default", None)
            if isinstance(default, str):
                rdf_types.add(default)

    return rdf_types


def _validate_domain(
    model_cls: type[Model],
    field_name: str,
    predicate: str,
    prop_info: PropertyInfo,
    config: SchemaValidationConfig,
    warnings_list: list[str],
    registry: SchemaRegistry,
) -> None:
    """Validate that at least one of model's rdf:types matches property domain.

    Follows rdfs:subClassOf chains: if the model's rdf_type is a subclass of a
    class in the property's declared domain, validation passes.
    """
    model_rdf_types: set[str] = _get_model_rdf_types(model_cls)
    if not prop_info.domain or not model_rdf_types:
        return

    # Check direct match first
    if model_rdf_types.intersection(prop_info.domain):
        return

    # Check rdfs:subClassOf chain for each rdf_type (#73)
    for rdf_type in model_rdf_types:
        ancestors: frozenset[str] = registry.get_superclasses(rdf_type, transitive=True)
        if ancestors.intersection(prop_info.domain):
            return

    types_str: str = ", ".join(sorted(model_rdf_types))
    msg: str = (
        f"Domain constraint violation on field '{field_name}': "
        f"predicate expects {prop_info.domain}, model has '{{{types_str}}}'"
    )
    _handle_policy_with_exception(
        config.domain_mismatch_policy,
        msg,
        DomainConstraintError(field_name, predicate, types_str, prop_info.domain),
        warnings_list,
    )


def _validate_range(
    model_cls: type[Model],
    field_name: str,
    predicate: str,
    prop_info: PropertyInfo,
    config: SchemaValidationConfig,
    warnings_list: list[str],
) -> None:
    """Validate that field's Python type matches property range constraint."""
    if not prop_info.range_:
        return

    python_type: type | None = _get_field_python_type(model_cls, field_name)
    if python_type and not _is_range_compatible(python_type, prop_info.range_):
        msg: str = (
            f"Range mismatch on field '{field_name}': "
            f"type '{python_type}' vs range {prop_info.range_}"
        )
        _handle_policy_with_exception(
            config.range_mismatch_policy,
            msg,
            RangeTypeMismatchError(
                field_name, predicate, str(python_type), prop_info.range_
            ),
            warnings_list,
        )


def _validate_cardinality(
    field_name: str,
    predicate: str,
    field_info: RDFFieldInfo,
    prop_info: PropertyInfo,
    config: SchemaValidationConfig,
    warnings_list: list[str],
) -> None:
    """Validate that collection fields match property cardinality constraints."""
    from .model import is_collection_field

    is_collection: bool = is_collection_field(field_info)
    if prop_info.is_single_valued and is_collection:
        # Build descriptive constraint info
        constraint_type: str = _get_cardinality_constraint_type(prop_info)
        msg: str = (
            f"Cardinality violation on field '{field_name}': "
            f"{constraint_type} with collection field"
        )
        _handle_policy_with_exception(
            config.cardinality_mismatch_policy,
            msg,
            CardinalityViolationError(field_name, predicate, True, True),
            warnings_list,
        )


def _get_cardinality_constraint_type(prop_info: PropertyInfo) -> str:
    """Get a human-readable description of the cardinality constraint."""
    if prop_info.is_functional:
        return "owl:FunctionalProperty"
    if prop_info.exact_cardinality is not None:
        return f"owl:cardinality {prop_info.exact_cardinality}"
    if prop_info.max_cardinality is not None:
        return f"owl:maxCardinality {prop_info.max_cardinality}"
    return "single-valued property"


def _is_rdf_type_field(field_info: RDFFieldInfo) -> bool:
    """
    Check if field uses the rdf:type predicate.

    rdf:type is a built-in RDF predicate and should not be validated
    against user ontologies.
    """
    predicate_iri, _ = _get_predicate_info(field_info)
    return predicate_iri == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"


def _get_predicate_info(field_info: RDFFieldInfo) -> tuple[str, bool]:
    """
    Extract predicate IRI and inverse flag from field info.

    Returns:
        Tuple of (predicate_iri, is_inverse) where is_inverse is True
        if the field uses an inverse property path (^predicate).
    """
    predicate: Any = getattr(field_info, "predicate", None)
    if predicate is None:
        return ("", False)
    # Handle PropertyPath objects
    if hasattr(predicate, "sparql"):
        sparql_repr: str = predicate.sparql
        return _extract_iri_from_path(sparql_repr)
    # Handle string predicates (may also be inverse paths)
    return _extract_iri_from_path(str(predicate))


def _extract_iri_from_path(sparql_repr: str) -> tuple[str, bool]:
    """
    Extract base IRI and inverse flag from a SPARQL path expression.

    Handles inverse paths like ^<http://example.org/pred> by extracting
    the base predicate IRI and returning is_inverse=True.

    Returns:
        Tuple of (predicate_iri, is_inverse).
    """
    # For inverse paths like ^<iri>, extract the base predicate
    if sparql_repr.startswith("^<") and sparql_repr.endswith(">"):
        return (sparql_repr[2:-1], True)
    # For inverse paths without angle brackets
    if sparql_repr.startswith("^"):
        return (sparql_repr[1:].strip("<>"), True)
    # Regular predicate - just strip angle brackets
    return (sparql_repr.strip("<>"), False)


def _get_field_python_type(model_cls: type[Model], field_name: str) -> type | None:
    """Extract Python type from model field annotation."""
    annotations: dict[str, Any] = getattr(model_cls, "__annotations__", {})
    annotation: Any = annotations.get(field_name)
    if annotation is None:
        return None

    # Unwrap Annotated and Optional/Union wrappers
    unwrapped: Any = _unwrap_annotated(annotation)
    unwrapped = _unwrap_optional(unwrapped)

    return unwrapped if isinstance(unwrapped, type) else None


def _unwrap_annotated(annotation: Any) -> Any:
    """
    Unwrap Annotated[T, ...] to get the base type T.

    Returns the annotation unchanged if not an Annotated type.
    """
    from typing import Annotated

    origin: Any = get_origin(annotation)
    if origin is Annotated:
        args: tuple[Any, ...] = get_args(annotation)
        if args:
            return args[0]
    return annotation


def _unwrap_optional(annotation: Any) -> Any:
    """
    Unwrap Optional[T] or Union[T, None] to get the base type T.

    Handles both typing.Union and PEP 604 pipe syntax (T | None).
    Returns the annotation unchanged if not an Optional/Union type.
    """
    import types

    origin: Any = get_origin(annotation)

    # Handle Optional/Union types (both typing.Union and PEP 604 | syntax)
    if origin is not None or isinstance(annotation, types.UnionType):
        args: tuple[Any, ...] = get_args(annotation)
        # Filter out None type
        non_none: list[Any] = [a for a in args if a not in (type(None), None)]
        if non_none:
            return non_none[0]

    return annotation


def _is_range_compatible(
    python_type: type, xsd_ranges: frozenset[str] | set[str]
) -> bool:
    """Check if Python type is compatible with any XSD range."""
    for xsd_iri in xsd_ranges:
        compatible_types = XSD_TO_PYTHON.get(xsd_iri)
        if compatible_types and python_type in compatible_types:
            return True
        # Object properties - IRI ranges are compatible with str
        if not xsd_iri.startswith("http://www.w3.org/2001/XMLSchema#"):
            if python_type is str:
                return True
    return False


def _handle_policy(policy: PolicyType, message: str) -> list[str]:
    """Handle policy action, returning warnings list."""
    if policy == "error":
        raise ValueError(message)
    elif policy == "warn":
        warnings.warn(
            message, UnknownPropertyWarning, stacklevel=_STACKLEVEL_HANDLE_POLICY
        )
        return [message]
    return []


def _handle_policy_with_exception(
    policy: PolicyType,
    message: str,
    exception: Exception,
    warnings_list: list[str],
) -> None:
    """Handle policy action with specific exception type."""
    if policy == "error":
        raise exception
    elif policy == "warn":
        warnings.warn(
            message,
            UnknownPropertyWarning,
            stacklevel=_STACKLEVEL_HANDLE_POLICY_WITH_EXCEPTION,
        )
        warnings_list.append(message)
