"""
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 typing import Any, cast

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

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.

    Attributes:
        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 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: Set of parent property IRIs (from rdfs:subPropertyOf)
        label: Dictionary mapping language codes to rdfs:label values
        comment: Dictionary mapping language codes to rdfs:comment values

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

    predicate_iri: str
    domain: set[str] = field(default_factory=set)
    range_: set[str] = field(default_factory=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] = field(default_factory=set)
    label: dict[str, str] = field(default_factory=dict)
    comment: dict[str, str] = field(default_factory=dict)


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 _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 PropertyInfo(
            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") -> 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.)

        Raises:
            Exception: If file cannot be parsed or read

        Example:
            >>> registry.load_from_file("schema.ttl", format="turtle")
            >>> registry.load_from_file("ontology.owl", format="xml")
        """
        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)

    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 PropertyInfo(
            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)
