"""
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 collections import deque
from dataclasses import dataclass, field
from enum import StrEnum
from types import MappingProxyType
from typing import Any, Callable, cast

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

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

logger = logging.getLogger(__name__)

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

# OWL namespace prefix for StrEnum
_OWL_NS = "http://www.w3.org/2002/07/owl#"


class OwlType(StrEnum):
    """OWL vocabulary terms used for property metadata and restrictions."""

    # Property types
    OBJECT_PROPERTY = f"{_OWL_NS}ObjectProperty"
    DATATYPE_PROPERTY = f"{_OWL_NS}DatatypeProperty"

    # Property characteristics
    FUNCTIONAL_PROPERTY = f"{_OWL_NS}FunctionalProperty"
    INVERSE_FUNCTIONAL_PROPERTY = f"{_OWL_NS}InverseFunctionalProperty"
    TRANSITIVE_PROPERTY = f"{_OWL_NS}TransitiveProperty"
    SYMMETRIC_PROPERTY = f"{_OWL_NS}SymmetricProperty"

    # Restriction predicates
    ON_PROPERTY = f"{_OWL_NS}onProperty"
    CARDINALITY = f"{_OWL_NS}cardinality"
    MAX_CARDINALITY = f"{_OWL_NS}maxCardinality"
    MIN_CARDINALITY = f"{_OWL_NS}minCardinality"

    # Other
    INVERSE_OF = f"{_OWL_NS}inverseOf"


@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
        max_cardinality: Maximum cardinality from owl:maxCardinality or owl:cardinality
        min_cardinality: Minimum cardinality from owl:minCardinality or owl:cardinality
        exact_cardinality: Exact cardinality from owl:cardinality

    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({})
    )
    max_cardinality: int | None = None
    min_cardinality: int | None = None
    exact_cardinality: int | None = None

    @property
    def is_single_valued(self) -> bool:
        """
        Return True if property allows at most one value.

        A property is single-valued if:
        - It is an owl:FunctionalProperty
        - It has owl:maxCardinality <= 1
        - It has owl:cardinality <= 1 (exact cardinality)
        """
        if self.is_functional:
            return True
        # Use <= 1 (not == 1) to handle cardinality 0, which means the property
        # is prohibited entirely - still not multi-valued
        if self.max_cardinality is not None and self.max_cardinality <= 1:
            return True
        if self.exact_cardinality is not None and self.exact_cardinality <= 1:
            return True
        return False


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,
        prefix_registry: PrefixRegistry | None = None,
    ) -> 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)
            prefix_registry: Optional prefix registry for expanding prefixed IRIs
        """
        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] = {}
        self._prefix_registry: PrefixRegistry | None = prefix_registry
        self._class_hierarchy: dict[str, frozenset[str]] = {}
        # Maps child class IRI → frozenset of direct parent IRIs (rdfs:subClassOf)

    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 register_class_parents(self, child_iri: str, parent_iris: set[str]) -> None:
        """
        Manually register direct parent classes for a class IRI.

        Merges with any existing parents already registered for the child.
        Can be called multiple times for the same child to accumulate parents.

        Args:
            child_iri: Full IRI of the child class
            parent_iris: Set of full IRIs of its direct parent classes

        Example:
            >>> registry.register_class_parents(
            ...     "http://example.org/Dog",
            ...     {"http://example.org/Animal"},
            ... )
        """
        with self._lock:
            existing: set[str] = set(self._class_hierarchy.get(child_iri, frozenset()))
            existing.update(parent_iris)
            self._class_hierarchy[child_iri] = frozenset(existing)
            logger.debug("Registered class parents: %s → %s", child_iri, parent_iris)

    def get_superclasses(self, uri: str, transitive: bool = True) -> frozenset[str]:
        """
        Return the superclasses of a class IRI.

        Args:
            uri: Full IRI of the class to look up
            transitive: If True (default), return all ancestors via BFS.
                        If False, return only direct parents.

        Returns:
            frozenset of ancestor class IRIs, or empty frozenset if none.
            The uri itself is never included in the result.

        Note:
            Class hierarchy is only populated via load_from_file() or
            register_class_parents(). SPARQL endpoint fetches do not
            retrieve class hierarchy.

        Example:
            >>> registry.register_class_parents("ex:Dog", {"ex:Animal"})
            >>> registry.register_class_parents("ex:Animal", {"ex:Thing"})
            >>> registry.get_superclasses("ex:Dog")
            frozenset({"ex:Animal", "ex:Thing"})
            >>> registry.get_superclasses("ex:Dog", transitive=False)
            frozenset({"ex:Animal"})
        """
        with self._lock:
            direct_parents: frozenset[str] = self._class_hierarchy.get(uri, frozenset())
            if not transitive:
                return direct_parents

            # BFS traversal — cycle-safe via visited set.
            # Seed visited with uri so it is never added to the result.
            visited: set[str] = {uri}
            ancestors: set[str] = set()
            queue: deque[str] = deque(direct_parents)
            while queue:
                current: str = queue.popleft()
                if current in visited:
                    continue
                visited.add(current)
                ancestors.add(current)
                for parent in self._class_hierarchy.get(current, frozenset()):
                    if parent not in visited:
                        queue.append(parent)

            return frozenset(ancestors)

    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:
            # Expand prefixed IRI if prefix registry is available.
            # Properties are always stored under their full IRIs because
            # register_property() stores prop_info.predicate_iri verbatim.
            # Expansion here normalises prefixed query inputs at read time;
            # register_property() intentionally does not also expand.
            if self._prefix_registry:
                predicate_iri = self._prefix_registry.expand(predicate_iri)
            # 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],
        max_cardinality: int | None = None,
        min_cardinality: int | None = None,
        exact_cardinality: int | None = None,
    ) -> 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
            max_cardinality: Maximum cardinality from owl:maxCardinality
            min_cardinality: Minimum cardinality from owl:minCardinality
            exact_cardinality: Exact cardinality from owl:cardinality

        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),
            max_cardinality=max_cardinality,
            min_cardinality=min_cardinality,
            exact_cardinality=exact_cardinality,
        )

    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(TURTLE)
            graph: Graph = cast(Graph, wrapper.query().convert())

            # Parse graph into PropertyInfo using the same logic as load_from_file
            prop_uri: URIRef = URIRef(predicate_iri)
            if (prop_uri, None, None) not in graph:
                return None
            return self._parse_graph_property(graph, prop_uri)

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

    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 (including custom subclasses)
        property_types: set[URIRef] = self._discover_property_types(graph)
        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),
            (URIRef(OwlType.INVERSE_OF), 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

            # Extract rdfs:subClassOf relationships for domain validation
            for child, parent in graph.subject_objects(RDFS.subClassOf):
                if isinstance(child, URIRef) and isinstance(parent, URIRef):
                    self.register_class_parents(str(child), {str(parent)})

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

        if activate:
            self.activate()

    def _aggregate_cardinality(
        self,
        graph: Graph,
        restriction: Any,
        predicate: URIRef,
        current: int | None,
        aggregator: Callable[[int, int], int],
    ) -> int | None:
        """
        Extract and aggregate cardinality values from a restriction.

        Args:
            graph: RDF graph containing restriction definitions
            restriction: The restriction node
            predicate: The cardinality predicate (maxCardinality, minCardinality, etc.)
            current: Current aggregated value (None if no value yet)
            aggregator: Function to combine values (min or max)

        Returns:
            Aggregated cardinality value or None
        """
        result: int | None = current
        for val in graph.objects(restriction, predicate):
            parsed: int | None = self._parse_cardinality_value(val)
            if parsed is not None:
                result = parsed if result is None else aggregator(result, parsed)
        return result

    def _extract_cardinality_from_restrictions(
        self, graph: Graph, prop_uri: URIRef
    ) -> tuple[int | None, int | None, int | None]:
        """
        Extract cardinality from owl:Restriction nodes referencing this property.

        Finds all owl:Restriction nodes with owl:onProperty pointing to this
        property and extracts cardinality values. When multiple restrictions
        exist, takes the most restrictive value (min of maxCardinality,
        max of minCardinality).

        Args:
            graph: RDF graph containing restriction definitions
            prop_uri: URI of the property to find restrictions for

        Returns:
            Tuple of (max_cardinality, min_cardinality, exact_cardinality)
        """
        max_cardinality: int | None = None
        min_cardinality: int | None = None
        exact_cardinality: int | None = None

        # Find all restriction nodes that reference this property
        # owl:Restriction nodes have owl:onProperty pointing to the property
        on_property: URIRef = URIRef(OwlType.ON_PROPERTY)
        max_card_pred: URIRef = URIRef(OwlType.MAX_CARDINALITY)
        min_card_pred: URIRef = URIRef(OwlType.MIN_CARDINALITY)
        exact_card_pred: URIRef = URIRef(OwlType.CARDINALITY)

        for restriction in graph.subjects(on_property, prop_uri):
            # Extract owl:maxCardinality (take minimum - most restrictive)
            max_cardinality = self._aggregate_cardinality(
                graph, restriction, max_card_pred, max_cardinality, min
            )
            # Extract owl:minCardinality (take maximum - most restrictive)
            min_cardinality = self._aggregate_cardinality(
                graph, restriction, min_card_pred, min_cardinality, max
            )
            # Extract owl:cardinality (exact cardinality - take minimum)
            exact_cardinality = self._aggregate_cardinality(
                graph, restriction, exact_card_pred, exact_cardinality, min
            )

        return max_cardinality, min_cardinality, exact_cardinality

    def _parse_cardinality_value(self, val: Any) -> int | None:
        """
        Parse cardinality value from RDF literal.

        Handles various XSD numeric types (xsd:integer, xsd:int, xsd:nonNegativeInteger,
        etc.) by using rdflib's Literal.toPython() method. Negative values are
        rejected as OWL cardinality must be non-negative.

        Args:
            val: RDF literal value (typically a Literal with numeric type)

        Returns:
            Integer cardinality value (>= 0), or None if parsing fails or negative
        """
        if isinstance(val, Literal):
            try:
                # Use rdflib's toPython() for automatic type conversion
                python_val: Any = val.toPython()
                if isinstance(python_val, int):
                    # OWL cardinality must be non-negative
                    return python_val if python_val >= 0 else None
                # Handle Decimal or float that represent whole numbers
                if isinstance(python_val, (float, int)):
                    int_val: int = int(python_val)
                    return int_val if int_val >= 0 else None
                # Fall back to string conversion
                int_val = int(str(val))
                return int_val if int_val >= 0 else None
            except (ValueError, TypeError):
                logger.debug("Failed to parse cardinality value: %s", val)
                return None
        return None

    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, URIRef(OwlType.INVERSE_OF)):
            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 = OwlType.FUNCTIONAL_PROPERTY in types
        is_inverse_functional: bool = OwlType.INVERSE_FUNCTIONAL_PROPERTY in types
        is_transitive: bool = OwlType.TRANSITIVE_PROPERTY in types
        is_symmetric: bool = OwlType.SYMMETRIC_PROPERTY 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)

        # Extract cardinality constraints from owl:Restriction nodes
        max_card: int | None
        min_card: int | None
        exact_card: int | None
        max_card, min_card, exact_card = self._extract_cardinality_from_restrictions(
            graph, prop_uri
        )

        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,
            max_cardinality=max_card,
            min_cardinality=min_card,
            exact_cardinality=exact_card,
        )

    def _discover_property_types(self, graph: Graph) -> set[URIRef]:
        """
        Discover all property types via rdfs:subClassOf traversal.

        Starts with base OWL/RDF property types and discovers any custom
        property types defined as subclasses in the loaded graph.

        Args:
            graph: RDF graph to search for property type hierarchies

        Returns:
            Set of URIRefs representing all discovered property types
        """
        base_types: list[URIRef] = [
            RDF.Property,
            URIRef(OwlType.OBJECT_PROPERTY),
            URIRef(OwlType.DATATYPE_PROPERTY),
            URIRef(OwlType.FUNCTIONAL_PROPERTY),
            URIRef(OwlType.INVERSE_FUNCTIONAL_PROPERTY),
            URIRef(OwlType.TRANSITIVE_PROPERTY),
            URIRef(OwlType.SYMMETRIC_PROPERTY),
        ]

        discovered: set[URIRef] = set()

        for base_type in base_types:
            discovered.add(base_type)
            for subclass in graph.transitive_subjects(RDFS.subClassOf, base_type):
                if subclass is not None and isinstance(subclass, URIRef):
                    discovered.add(subclass)

        return discovered

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

        Example:
            >>> registry.clear_cache()
        """
        with self._lock:
            self._properties.clear()
            self._cache_timestamps.clear()
            self._class_hierarchy.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
