"""UKO Query Interface — ACMS context strategy integration.

Provides :class:`UKOQueryInterface`, a service that allows ACMS context
strategies to query the UKO knowledge graph for resource classification
data.  Strategies use this interface to discover which ontology layer a
resource belongs to, what type it is, and what relationships it has.

The four UKO ontology layers (per spec §185):

| Layer | Name        | Description                                      |
|-------|-------------|--------------------------------------------------|
| 0     | Universal   | Foundation concepts (Resource, Container, etc.)  |
| 1     | Domain      | Software, documents, data schemas, infrastructure|
| 2     | Paradigm    | OO, functional, procedural, markdown             |
| 3     | Technology  | Python, TypeScript, Rust, Java, PostgreSQL       |

Based on ``docs/specification.md`` §185, §20568, §43924.

ISSUES CLOSED: #891
"""

from __future__ import annotations

from dataclasses import dataclass, field

import structlog

from cleveragents.domain.models.acms.index_backends import GraphIndexBackend

logger = structlog.get_logger(__name__)

__all__ = [
    "ClassificationResult",
    "UKOQueryInterface",
]

# ---------------------------------------------------------------------------
# Layer constants
# ---------------------------------------------------------------------------

#: Layer 0 — Universal foundation concepts.
LAYER_UNIVERSAL = 0
#: Layer 1 — Domain specializations (code, doc, data, infra).
LAYER_DOMAIN = 1
#: Layer 2 — Paradigm specializations (OO, functional, procedural).
LAYER_PARADIGM = 2
#: Layer 3 — Technology-specific (Python, TypeScript, Rust, Java).
LAYER_TECHNOLOGY = 3

# Predicate used to record the UKO layer of a resource.
_LAYER_PREDICATE = "uko:layer"
# Predicate used to record the primary type of a resource.
_TYPE_PREDICATE = "rdf:type"
# Predicate used to record the source resource ULID.
_SOURCE_PREDICATE = "uko:sourceResource"


# ---------------------------------------------------------------------------
# ClassificationResult
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class ClassificationResult:
    """Classification data for a single resource from the UKO graph.

    Attributes:
        resource_id: ULID of the classified resource.
        layer: Highest UKO layer populated for this resource (0-3).
        primary_type: Primary UKO type URI (e.g. ``uko-py:Module``).
        all_types: All UKO type URIs found for this resource.
        implicit_relations: Inferred relationships (subject, predicate,
            object) discovered during semantic analysis.
    """

    resource_id: str
    layer: int = LAYER_UNIVERSAL
    primary_type: str = ""
    all_types: tuple[str, ...] = field(default_factory=tuple)
    implicit_relations: tuple[tuple[str, str, str], ...] = field(default_factory=tuple)

    def __post_init__(self) -> None:
        if not self.resource_id or not self.resource_id.strip():
            raise ValueError("resource_id must be a non-empty string")
        if not (LAYER_UNIVERSAL <= self.layer <= LAYER_TECHNOLOGY):
            raise ValueError(
                f"layer must be between {LAYER_UNIVERSAL} and {LAYER_TECHNOLOGY}, "
                f"got {self.layer}"
            )


# ---------------------------------------------------------------------------
# UKOQueryInterface
# ---------------------------------------------------------------------------


class UKOQueryInterface:
    """Service for querying UKO classification data from the graph backend.

    Provides ACMS context strategies with a typed interface to the UKO
    knowledge graph.  Strategies use this to discover resource types,
    ontology layers, and implicit relationships without needing to
    construct raw SPARQL queries.

    Based on ``docs/specification.md`` §20568 — UKO classification
    persists across sessions and feeds the ACMS context pipeline.

    Args:
        graph_backend: The graph index backend to query.
        project: Namespaced project name (e.g. ``"local/my-app"``).
    """

    def __init__(
        self,
        graph_backend: GraphIndexBackend,
        project: str,
    ) -> None:
        if not isinstance(graph_backend, GraphIndexBackend):
            raise TypeError(
                "graph_backend must satisfy GraphIndexBackend, "
                f"got {type(graph_backend).__name__}"
            )
        if not project or not project.strip():
            raise ValueError("project must be a non-empty string")
        self._graph_backend = graph_backend
        self._project = project
        logger.debug(
            "uko_query_interface.initialized",
            project=project,
        )

    @property
    def project(self) -> str:
        """The namespaced project name."""
        return self._project

    def classify_resource(self, resource_id: str) -> ClassificationResult:
        """Classify a resource by querying its UKO triples.

        Queries the graph backend for all triples associated with the
        given resource and extracts type, layer, and relationship data.

        Args:
            resource_id: ULID of the resource to classify.

        Returns:
            :class:`ClassificationResult` with classification data.
            Returns a default result (layer 0, no types) if the resource
            has not been indexed.

        Raises:
            ValueError: If *resource_id* is empty or whitespace-only.
        """
        if not resource_id or not resource_id.strip():
            raise ValueError("resource_id must be a non-empty string")

        log = logger.bind(resource_id=resource_id, project=self._project)
        log.debug("uko_query_interface.classify_resource")

        resource_uri = f"uko://resource/{resource_id}"

        # Query all triples where this resource is the source.
        try:
            bindings = self._graph_backend.query(
                self._project,
                f"SELECT ?s ?p ?o WHERE {{ ?s uko:sourceResource <{resource_uri}> }}",
            )
        except Exception as exc:
            log.warning(
                "uko_query_interface.query_failed",
                error=str(exc),
            )
            return ClassificationResult(resource_id=resource_id)

        # Extract types and layer from bindings.
        types: list[str] = []
        max_layer = LAYER_UNIVERSAL
        implicit_relations: list[tuple[str, str, str]] = []

        for binding in bindings:
            predicate = binding.get("p", "")
            obj = binding.get("o", "")
            subject = binding.get("s", "")

            if predicate == _TYPE_PREDICATE and obj:
                types.append(obj)
                # Infer layer from type URI prefix.
                layer = _infer_layer_from_uri(obj)
                if layer > max_layer:
                    max_layer = layer

            elif predicate == _LAYER_PREDICATE and obj:
                try:
                    layer_val = int(obj)
                    if (
                        LAYER_UNIVERSAL <= layer_val <= LAYER_TECHNOLOGY
                        and layer_val > max_layer
                    ):
                        max_layer = layer_val
                except ValueError:
                    pass

            elif predicate.startswith("uko:implicit") and subject and obj:
                implicit_relations.append((subject, predicate, obj))

        primary_type = types[0] if types else ""

        result = ClassificationResult(
            resource_id=resource_id,
            layer=max_layer,
            primary_type=primary_type,
            all_types=tuple(types),
            implicit_relations=tuple(implicit_relations),
        )

        log.debug(
            "uko_query_interface.classify_resource.done",
            layer=max_layer,
            type_count=len(types),
        )
        return result

    def get_resources_by_layer(self, layer: int) -> list[str]:
        """Return resource IDs classified at the given ontology layer.

        Args:
            layer: UKO layer number (0-3).

        Returns:
            List of resource ULIDs at the given layer.

        Raises:
            ValueError: If *layer* is outside 0-3.
        """
        if not (LAYER_UNIVERSAL <= layer <= LAYER_TECHNOLOGY):
            raise ValueError(
                f"layer must be between {LAYER_UNIVERSAL} and {LAYER_TECHNOLOGY}, "
                f"got {layer}"
            )

        log = logger.bind(layer=layer, project=self._project)
        log.debug("uko_query_interface.get_resources_by_layer")

        try:
            bindings = self._graph_backend.query(
                self._project,
                f'SELECT ?s ?o WHERE {{ ?s {_LAYER_PREDICATE} "{layer}" }}',
            )
        except Exception as exc:
            log.warning(
                "uko_query_interface.layer_query_failed",
                error=str(exc),
            )
            return []

        resource_ids: list[str] = []
        for binding in bindings:
            obj = binding.get("o", "")
            # Extract resource ID from uko://resource/<id> URI.
            if obj.startswith("uko://resource/"):
                resource_ids.append(obj[len("uko://resource/") :])

        log.debug(
            "uko_query_interface.get_resources_by_layer.done",
            count=len(resource_ids),
        )
        return resource_ids

    def get_implicit_relations(
        self,
        resource_id: str,
    ) -> list[tuple[str, str, str]]:
        """Return implicit relationships inferred for a resource.

        Args:
            resource_id: ULID of the resource.

        Returns:
            List of (subject, predicate, object) tuples for implicit
            relationships.

        Raises:
            ValueError: If *resource_id* is empty or whitespace-only.
        """
        if not resource_id or not resource_id.strip():
            raise ValueError("resource_id must be a non-empty string")

        result = self.classify_resource(resource_id)
        return list(result.implicit_relations)


# ---------------------------------------------------------------------------
# Layer inference helpers
# ---------------------------------------------------------------------------

# URI prefix → layer mapping (mirrors UKOLoader._LAYER_IRI_PREFIXES).
_LAYER_URI_PREFIXES: dict[str, int] = {
    "https://cleveragents.ai/ontology/uko#": LAYER_UNIVERSAL,
    "https://cleveragents.ai/ontology/uko/code#": LAYER_DOMAIN,
    "https://cleveragents.ai/ontology/uko/doc#": LAYER_DOMAIN,
    "https://cleveragents.ai/ontology/uko/data#": LAYER_DOMAIN,
    "https://cleveragents.ai/ontology/uko/infra#": LAYER_DOMAIN,
    "https://cleveragents.ai/ontology/uko/oo#": LAYER_PARADIGM,
    "https://cleveragents.ai/ontology/uko/func#": LAYER_PARADIGM,
    "https://cleveragents.ai/ontology/uko/proc#": LAYER_PARADIGM,
    "https://cleveragents.ai/ontology/uko/py#": LAYER_TECHNOLOGY,
    "https://cleveragents.ai/ontology/uko/ts#": LAYER_TECHNOLOGY,
    "https://cleveragents.ai/ontology/uko/rs#": LAYER_TECHNOLOGY,
    "https://cleveragents.ai/ontology/uko/java#": LAYER_TECHNOLOGY,
}

# Prefixed name → layer mapping.
_LAYER_PREFIXED_PREFIXES: dict[str, int] = {
    "uko:": LAYER_UNIVERSAL,
    "uko-code:": LAYER_DOMAIN,
    "uko-doc:": LAYER_DOMAIN,
    "uko-data:": LAYER_DOMAIN,
    "uko-infra:": LAYER_DOMAIN,
    "uko-oo:": LAYER_PARADIGM,
    "uko-func:": LAYER_PARADIGM,
    "uko-proc:": LAYER_PARADIGM,
    "uko-py:": LAYER_TECHNOLOGY,
    "uko-ts:": LAYER_TECHNOLOGY,
    "uko-rs:": LAYER_TECHNOLOGY,
    "uko-java:": LAYER_TECHNOLOGY,
}


def _infer_layer_from_uri(uri: str) -> int:
    """Infer the UKO layer from a type URI or prefixed name.

    Returns the layer number (0-3), defaulting to 0 (universal) if
    the URI does not match any known prefix.
    """
    # Check full IRIs first.
    for prefix, layer in _LAYER_URI_PREFIXES.items():
        if uri.startswith(prefix):
            return layer
    # Check prefixed names.
    for prefix, layer in _LAYER_PREFIXED_PREFIXES.items():
        if uri.startswith(prefix):
            return layer
    return LAYER_UNIVERSAL
