"""
Schema registry for managing ontology metadata and property information.

Features:
- PropertyInfo dataclass for storing property metadata
- Thread-safe registry with TTL caching
- Manual property registration
- SPARQL-based ontology fetching
- RDF file loading (Turtle, RDF/XML, etc.)
"""

import logging
import threading
import time
from dataclasses import dataclass, field
from types import MappingProxyType
from typing import Any, cast

from rdflib import Graph, Literal, Namespace, URIRef
from SPARQLWrapper import JSON, SPARQLWrapper

from .schema_validator import (
    _clear_global_registry,
    _set_global_registry,
    get_global_registry,
)

logger = logging.getLogger(__name__)

# Common RDF/OWL/RDFS namespaces
RDF = Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
RDFS = Namespace("http://www.w3.org/2000/01/rdf-schema#")
OWL = Namespace("http://www.w3.org/2002/07/owl#")
XSD = Namespace("http://www.w3.org/2001/XMLSchema#")


@dataclass(frozen=True)
class PropertyInfo:
    """
    Immutable container for RDF property metadata.

    Stores ontology information about a property including domain, range,
    inverse relationships, and OWL characteristics.

    Immutability is critical for thread-safe caching and preventing accidental
    modification of ontology metadata shared across multiple models and sessions.
    PropertyInfo instances can be safely cached and shared without defensive copying.

    Attributes:
        predicate_iri: The full IRI of the property
        domain: Frozen set of class IRIs that can be subjects of this property
        range_: Frozen set of class IRIs or datatypes that can be objects of
            this property
        inverse_of: IRI of the inverse property (from owl:inverseOf)
        is_functional: True if this is an owl:FunctionalProperty
        is_inverse_functional: True if this is an owl:InverseFunctionalProperty
        is_transitive: True if this is an owl:TransitiveProperty
        is_symmetric: True if this is an owl:SymmetricProperty
        subproperty_of: Frozen set of parent property IRIs (from rdfs:subPropertyOf)
        label: Immutable mapping of language codes to rdfs:label values
        comment: Immutable mapping of language codes to rdfs:comment values

    Example:
        >>> prop = PropertyInfo(
        ...     predicate_iri="http://schema.org/children",
        ...     inverse_of="http://schema.org/parent",
        ...     label=MappingProxyType({"en": "children"})
        ... )
    """

    predicate_iri: str
    domain: frozenset[str] = field(default_factory=frozenset)
    range_: frozenset[str] = field(default_factory=frozenset)
    inverse_of: str | None = None
    is_functional: bool = False
    is_inverse_functional: bool = False
    is_transitive: bool = False
    is_symmetric: bool = False
    subproperty_of: frozenset[str] = field(default_factory=frozenset)
    label: MappingProxyType[str, str] = field(
        default_factory=lambda: MappingProxyType({})
    )
    comment: MappingProxyType[str, str] = field(
        default_factory=lambda: MappingProxyType({})
    )


class SchemaRegistry:
    """
    Thread-safe registry for managing ontology metadata.

    Provides caching and lazy loading of property information from:
    - Manual registration
    - SPARQL endpoints
    - RDF files (Turtle, RDF/XML, etc.)

    Example:
        >>> registry = SchemaRegistry(endpoint="http://example.org/sparql")
        >>> # Manual registration
        >>> prop = PropertyInfo(
        ...     predicate_iri="http://schema.org/children",
        ...     inverse_of="http://schema.org/parent"
        ... )
        >>> registry.register_property(prop)
        >>>
        >>> # Lazy fetch from endpoint
        >>> info = registry.get_property("http://schema.org/name")
        >>>
        >>> # Load from file
        >>> registry.load_from_file("ontology.ttl", format="turtle")
    """

    def __init__(self, endpoint: str | None = None, cache_ttl: int = 3600) -> None:
        """
        Initialize schema registry.

        Args:
            endpoint: Optional SPARQL endpoint URL for fetching metadata
            cache_ttl: Time-to-live for cached entries in seconds (default: 1 hour)
        """
        self._properties: dict[str, PropertyInfo] = {}
        self._endpoint: str | None = endpoint
        self._cache_ttl: int = cache_ttl
        self._lock: threading.RLock = threading.RLock()
        self._cache_timestamps: dict[str, float] = {}

    def register_property(self, prop_info: PropertyInfo) -> None:
        """
        Manually register property metadata.

        Args:
            prop_info: PropertyInfo instance to register

        Example:
            >>> prop = PropertyInfo(
            ...     predicate_iri="http://example.org/knows",
            ...     is_symmetric=True
            ... )
            >>> registry.register_property(prop)
        """
        with self._lock:
            self._properties[prop_info.predicate_iri] = prop_info
            self._cache_timestamps[prop_info.predicate_iri] = time.time()
            logger.debug("Registered property: %s", prop_info.predicate_iri)

    def get_property(self, predicate_iri: str) -> PropertyInfo | None:
        """
        Get property metadata, fetching from endpoint if needed.

        Checks cache first. If not found or expired, fetches from SPARQL
        endpoint (if configured).

        Args:
            predicate_iri: Full IRI of the property

        Returns:
            PropertyInfo if found, None otherwise

        Example:
            >>> info = registry.get_property("http://schema.org/name")
            >>> if info and info.inverse_of:
            ...     print(f"Inverse: {info.inverse_of}")
        """
        with self._lock:
            # Check cache and TTL
            if predicate_iri in self._properties:
                timestamp: float = self._cache_timestamps.get(predicate_iri, 0)
                age: float = time.time() - timestamp
                if age < self._cache_ttl:
                    logger.debug("Cache hit for property: %s", predicate_iri)
                    return self._properties[predicate_iri]
                else:
                    logger.debug("Cache expired for property: %s", predicate_iri)

            # Fetch from SPARQL if endpoint configured
            if self._endpoint:
                logger.debug("Fetching property from endpoint: %s", predicate_iri)
                prop_info: PropertyInfo | None = self._fetch_from_sparql(predicate_iri)
                if prop_info:
                    self._properties[predicate_iri] = prop_info
                    self._cache_timestamps[predicate_iri] = time.time()
                    return prop_info

            # Not found
            return None

    def _create_property_info(
        self,
        predicate_iri: str,
        domain: set[str],
        range_: set[str],
        inverse_of: str | None,
        is_functional: bool,
        is_inverse_functional: bool,
        is_transitive: bool,
        is_symmetric: bool,
        subproperty_of: set[str],
        label: dict[str, str],
        comment: dict[str, str],
    ) -> PropertyInfo:
        """
        Create PropertyInfo from mutable collections.

        Converts mutable sets and dicts to their immutable counterparts
        (frozenset, MappingProxyType) for thread-safe caching.

        Args:
            predicate_iri: The full IRI of the property
            domain: Set of class IRIs that can be subjects of this property
            range_: Set of class IRIs or datatypes that can be objects
            inverse_of: IRI of the inverse property or None
            is_functional: True if this is an owl:FunctionalProperty
            is_inverse_functional: True if this is an owl:InverseFunctionalProperty
            is_transitive: True if this is an owl:TransitiveProperty
            is_symmetric: True if this is an owl:SymmetricProperty
            subproperty_of: Set of parent property IRIs
            label: Mapping of language codes to rdfs:label values
            comment: Mapping of language codes to rdfs:comment values

        Returns:
            Immutable PropertyInfo instance
        """
        return PropertyInfo(
            predicate_iri=predicate_iri,
            domain=frozenset(domain),
            range_=frozenset(range_),
            inverse_of=inverse_of,
            is_functional=is_functional,
            is_inverse_functional=is_inverse_functional,
            is_transitive=is_transitive,
            is_symmetric=is_symmetric,
            subproperty_of=frozenset(subproperty_of),
            label=MappingProxyType(label),
            comment=MappingProxyType(comment),
        )

    def _fetch_from_sparql(self, predicate_iri: str) -> PropertyInfo | None:
        """
        Fetch property metadata from SPARQL endpoint.

        Args:
            predicate_iri: Full IRI of the property

        Returns:
            PropertyInfo if found, None if not found or on error
        """
        if not self._endpoint:
            return None

        # Build CONSTRUCT query for property metadata
        query: str = f"""
        PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
        PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
        PREFIX owl: <http://www.w3.org/2002/07/owl#>

        CONSTRUCT {{
          <{predicate_iri}> ?p ?o .
        }}
        WHERE {{
          <{predicate_iri}> ?p ?o .
        }}
        """

        try:
            wrapper: SPARQLWrapper = SPARQLWrapper(self._endpoint)
            wrapper.setQuery(query)
            wrapper.setReturnFormat(JSON)
            results: dict[str, Any] = cast(dict[str, Any], wrapper.query().convert())

            # Parse results into PropertyInfo
            return self._parse_sparql_results(predicate_iri, results)

        except Exception as e:
            logger.warning(
                "Failed to fetch property %s from endpoint: %s", predicate_iri, e
            )
            return None

    def _parse_sparql_results(
        self, predicate_iri: str, results: dict[str, Any]
    ) -> PropertyInfo | None:
        """
        Parse SPARQL JSON results into PropertyInfo.

        Args:
            predicate_iri: The property IRI being queried
            results: SPARQL JSON results

        Returns:
            PropertyInfo or None if no results
        """
        bindings: list[dict[str, Any]] = results.get("results", {}).get("bindings", [])
        if not bindings:
            return None

        # Initialize collections
        domain: set[str] = set()
        range_: set[str] = set()
        inverse_of: str | None = None
        is_functional: bool = False
        is_inverse_functional: bool = False
        is_transitive: bool = False
        is_symmetric: bool = False
        subproperty_of: set[str] = set()
        label: dict[str, str] = {}
        comment: dict[str, str] = {}

        # Process each binding
        for binding in bindings:
            p: str = binding.get("p", {}).get("value", "")
            o: dict[str, str] = binding.get("o", {})
            o_value: str = o.get("value", "")
            o_lang: str | None = o.get("xml:lang")

            # Extract metadata based on predicate
            if p == str(RDFS.domain):
                domain.add(o_value)
            elif p == str(RDFS.range):
                range_.add(o_value)
            elif p == str(OWL.inverseOf):
                inverse_of = o_value
            elif p == str(RDF.type):
                if o_value == str(OWL.FunctionalProperty):
                    is_functional = True
                elif o_value == str(OWL.InverseFunctionalProperty):
                    is_inverse_functional = True
                elif o_value == str(OWL.TransitiveProperty):
                    is_transitive = True
                elif o_value == str(OWL.SymmetricProperty):
                    is_symmetric = True
            elif p == str(RDFS.subPropertyOf):
                subproperty_of.add(o_value)
            elif p == str(RDFS.label):
                if o_lang:
                    label[o_lang] = o_value
                else:
                    label[""] = o_value
            elif p == str(RDFS.comment):
                if o_lang:
                    comment[o_lang] = o_value
                else:
                    comment[""] = o_value

        return self._create_property_info(
            predicate_iri=predicate_iri,
            domain=domain,
            range_=range_,
            inverse_of=inverse_of,
            is_functional=is_functional,
            is_inverse_functional=is_inverse_functional,
            is_transitive=is_transitive,
            is_symmetric=is_symmetric,
            subproperty_of=subproperty_of,
            label=label,
            comment=comment,
        )

    def load_from_file(
        self, path: str, format: str = "turtle", *, activate: bool = False
    ) -> None:
        """
        Load ontology from RDF file.

        Parses the file using rdflib and extracts property metadata.

        Args:
            path: Path to RDF file
            format: RDF format (turtle, xml, n3, etc.)
            activate: If True, activate this registry as the global registry
                after loading. This enables schema features for all models.

        Raises:
            Exception: If file cannot be parsed or read

        Example:
            >>> # Load and activate in one step
            >>> registry = SchemaRegistry()
            >>> registry.load_from_file("schema.ttl", activate=True)

            >>> # Or load without activating
            >>> registry.load_from_file("schema.ttl")
            >>> registry.activate()  # Activate later
        """
        logger.info("Loading ontology from file: %s (format: %s)", path, format)

        graph: Graph = Graph()

        # Parse RDF file with explicit error handling
        try:
            graph.parse(path, format=format)
        except FileNotFoundError as e:
            logger.error("Ontology file not found: %s", path)
            raise FileNotFoundError(f"Ontology file not found: {path}") from e
        except Exception as e:
            logger.error("Failed to parse ontology file %s: %s", path, e)
            raise ValueError(
                f"Failed to parse ontology file {path} (format={format}): {e}"
            ) from e

        # Extract all properties (subjects of property-related predicates)
        properties: set[str] = set()

        # Find all resources that are properties by type
        property_types = [
            RDF.Property,
            OWL.ObjectProperty,
            OWL.DatatypeProperty,
            OWL.FunctionalProperty,
            OWL.InverseFunctionalProperty,
            OWL.TransitiveProperty,
            OWL.SymmetricProperty,
        ]
        for prop_type in property_types:
            for s in graph.subjects(RDF.type, prop_type):
                properties.add(str(s))

        # Also find properties that have domain, range, or inverse definitions
        property_predicates = [
            (RDFS.domain, None),
            (RDFS.range, None),
            (OWL.inverseOf, None),
        ]
        for predicate, obj in property_predicates:
            for s in graph.subjects(predicate, obj):
                properties.add(str(s))

        # Parse each property
        count: int = 0
        with self._lock:
            for prop_iri in properties:
                prop_info: PropertyInfo = self._parse_graph_property(
                    graph, URIRef(prop_iri)
                )
                self._properties[prop_iri] = prop_info
                self._cache_timestamps[prop_iri] = time.time()
                count += 1

        logger.info("Loaded %d properties from %s", count, path)

        if activate:
            self.activate()

    def _parse_graph_property(self, graph: Graph, prop_uri: URIRef) -> PropertyInfo:
        """
        Parse property metadata from rdflib Graph.

        Args:
            graph: RDF graph containing property definitions
            prop_uri: URI of the property to parse

        Returns:
            PropertyInfo instance
        """
        predicate_iri: str = str(prop_uri)

        # Extract metadata
        domain: set[str] = {str(o) for o in graph.objects(prop_uri, RDFS.domain)}
        range_: set[str] = {str(o) for o in graph.objects(prop_uri, RDFS.range)}

        inverse_of: str | None = None
        for o in graph.objects(prop_uri, OWL.inverseOf):
            inverse_of = str(o)
            break  # Take first inverse

        # Check property types
        types: set[str] = {str(t) for t in graph.objects(prop_uri, RDF.type)}
        is_functional: bool = str(OWL.FunctionalProperty) in types
        is_inverse_functional: bool = str(OWL.InverseFunctionalProperty) in types
        is_transitive: bool = str(OWL.TransitiveProperty) in types
        is_symmetric: bool = str(OWL.SymmetricProperty) in types

        subproperty_of: set[str] = {
            str(o) for o in graph.objects(prop_uri, RDFS.subPropertyOf)
        }

        # Extract labels and comments with language tags
        label: dict[str, str] = {}
        for o in graph.objects(prop_uri, RDFS.label):
            if isinstance(o, Literal):
                lang: str = o.language or ""
                label[lang] = str(o)

        comment: dict[str, str] = {}
        for o in graph.objects(prop_uri, RDFS.comment):
            if isinstance(o, Literal):
                lang = o.language or ""
                comment[lang] = str(o)

        return self._create_property_info(
            predicate_iri=predicate_iri,
            domain=domain,
            range_=range_,
            inverse_of=inverse_of,
            is_functional=is_functional,
            is_inverse_functional=is_inverse_functional,
            is_transitive=is_transitive,
            is_symmetric=is_symmetric,
            subproperty_of=subproperty_of,
            label=label,
            comment=comment,
        )

    def clear_cache(self) -> None:
        """
        Clear all cached property information.

        Example:
            >>> registry.clear_cache()
        """
        with self._lock:
            self._properties.clear()
            self._cache_timestamps.clear()
            logger.debug("Cleared schema registry cache")

    def get_all_properties(self) -> dict[str, PropertyInfo]:
        """
        Get all cached property information.

        Returns:
            Dictionary mapping property IRIs to PropertyInfo instances

        Example:
            >>> props = registry.get_all_properties()
            >>> for iri, info in props.items():
            ...     print(f"{iri}: {info.label}")
        """
        with self._lock:
            return dict(self._properties)

    def activate(self) -> None:
        """
        Activate this registry as the global schema registry.

        When activated, all models automatically receive:
        - Inverse discovery for InverseFields
        - Validation against ontology constraints

        Models can opt out via Meta.schema_aware = False.

        Example:
            >>> registry = SchemaRegistry()
            >>> registry.load_from_file("schema.ttl")
            >>> registry.activate()  # Now all models use this registry
        """
        _set_global_registry(self)

    def deactivate(self) -> None:
        """
        Deactivate this registry if it is the current global registry.

        After deactivation, models will no longer receive schema features
        automatically.

        Example:
            >>> registry.deactivate()
        """
        if get_global_registry() is self:
            _clear_global_registry()

    @property
    def is_active(self) -> bool:
        """
        Check if this registry is the current global registry.

        Returns:
            True if this registry is active, False otherwise.
        """
        return get_global_registry() is self
