"""UKO Indexer internal helpers — backend indexing and lifecycle hooks.

Extracted from ``uko_indexer.py`` to comply with the 500-line file limit
(CONTRIBUTING.md).  All functions in this module are implementation
details; callers should use :class:`UKOIndexer` directly.
"""

from __future__ import annotations

from datetime import datetime

import structlog

from cleveragents.domain.models.acms.analyzers import UKOTriple
from cleveragents.domain.models.acms.index_backends import (
    GraphIndexBackend,
    TextIndexBackend,
    VectorIndexBackend,
)
from cleveragents.domain.models.acms.provenance import (
    IndexResult,
    ProvenancedTriple,
    ProvenanceMetadata,
)
from cleveragents.domain.models.core.resource import Resource

from .uko_indexer_protocols import IndexLifecycleHook
from .uko_inference import infer_implicit_relations
from .uko_query_interface import _infer_layer_from_uri

logger = structlog.get_logger(__name__)


# ---------------------------------------------------------------------------
# Lifecycle hook helpers
# ---------------------------------------------------------------------------


def fire_on_indexed(
    hook: IndexLifecycleHook,
    result: IndexResult,
) -> None:
    """Fire *on_indexed* lifecycle hook, guarding against failures."""
    try:
        hook.on_indexed(result)
    except Exception:
        logger.warning(
            "indexer.lifecycle_hook_error",
            hook="on_indexed",
            resource_id=result.resource_id,
            exc_info=True,
        )


def fire_on_removed(
    hook: IndexLifecycleHook,
    resource_id: str,
    project: str,
) -> None:
    """Fire *on_removed* lifecycle hook, guarding against failures."""
    try:
        hook.on_removed(resource_id, project)
    except Exception:
        logger.warning(
            "indexer.lifecycle_hook_error",
            hook="on_removed",
            resource_id=resource_id,
            exc_info=True,
        )


def fire_on_error(
    hook: IndexLifecycleHook,
    resource_id: str,
    error: str,
) -> None:
    """Fire *on_error* lifecycle hook, guarding against failures."""
    try:
        hook.on_error(resource_id, error)
    except Exception:
        logger.warning(
            "indexer.lifecycle_hook_error",
            hook="on_error",
            resource_id=resource_id,
            exc_info=True,
        )


# ---------------------------------------------------------------------------
# Provenance
# ---------------------------------------------------------------------------


def attach_provenance(
    triples: list[UKOTriple],
    resource: Resource,
    timestamp: datetime,
) -> list[ProvenancedTriple]:
    """Wrap each triple with provenance metadata."""
    provenance = ProvenanceMetadata(
        source_resource=resource.resource_id,
        source_path=resource.location or "",
        valid_from=timestamp,
        is_current=True,
    )
    return [ProvenancedTriple(triple=t, provenance=provenance) for t in triples]


# ---------------------------------------------------------------------------
# Backend indexing helpers
# ---------------------------------------------------------------------------


def _build_layer_triples(
    triples: list[UKOTriple],
    resource_uri: str,
) -> list[UKOTriple]:
    """Build layer-annotation triples for the four UKO ontology layers.

    Inspects the type URIs in *triples* to determine which layers are
    populated, then emits ``uko:layer`` triples for each populated layer.

    The four layers (per spec §185):
    - Layer 0: Universal foundation (``uko:`` prefix)
    - Layer 1: Domain specializations (``uko-code:``, ``uko-doc:``, etc.)
    - Layer 2: Paradigm specializations (``uko-oo:``, ``uko-func:``, etc.)
    - Layer 3: Technology-specific (``uko-py:``, ``uko-ts:``, etc.)

    Args:
        triples: UKO triples produced by a domain analyzer.
        resource_uri: URI of the resource being indexed.

    Returns:
        List of ``uko:layer`` triples for each populated layer.
    """
    populated_layers: set[int] = set()

    for triple in triples:
        if triple.predicate in ("rdf:type", "a"):
            obj = triple.object_uri or triple.object_value
            if obj:
                layer = _infer_layer_from_uri(obj)
                populated_layers.add(layer)

    # Always include layer 0 (universal) if any triples exist.
    if triples:
        populated_layers.add(0)

    layer_triples: list[UKOTriple] = []
    for layer in sorted(populated_layers):
        layer_triples.append(
            UKOTriple(
                subject_uri=resource_uri,
                predicate="uko:layer",
                object_value=str(layer),
            )
        )

    return layer_triples


def index_graph(
    graph_backend: GraphIndexBackend,
    project: str,
    resource: Resource,
    provenanced: list[ProvenancedTriple],
    errors: list[str],
    resource_uri: str,
) -> tuple[int, set[str]]:
    """Store provenanced triples in the graph backend.

    Also runs implicit relationship inference and stores layer-annotation
    triples for the four UKO ontology layers.

    Returns:
        A ``(stored_count, subjects)`` tuple.
    """
    # Run implicit relationship inference on the original triples.
    # Only include triples with valid objects (same filter as the main loop).
    original_triples = [
        pt.triple
        for pt in provenanced
        if pt.triple.object_uri or pt.triple.object_value
    ]
    inferred = infer_implicit_relations(original_triples)

    # Build layer-annotation triples (only when there are valid triples).
    layer_triples = (
        _build_layer_triples(original_triples, resource_uri) if original_triples else []
    )

    # Build the full list to store: original + inferred + layer triples.
    # Do NOT mutate the input list — create a new list.
    # Track the boundary between original and augmented triples so we
    # can count only original triples in the returned ``stored`` count.
    all_provenanced: list[ProvenancedTriple] = list(provenanced)
    original_count = len(all_provenanced)
    if provenanced and original_triples:
        base_prov = provenanced[0].provenance
        for t in inferred + layer_triples:
            all_provenanced.append(ProvenancedTriple(triple=t, provenance=base_prov))

    subjects: set[str] = set()
    stored = 0
    for idx_pt, pt in enumerate(all_provenanced):
        is_original = idx_pt < original_count
        t = pt.triple
        obj = t.object_uri if t.object_uri else t.object_value
        if not obj:
            continue
        try:
            graph_backend.add_triple(
                project,
                t.subject_uri,
                t.predicate,
                obj,
            )
        except Exception as exc:
            errors.append(f"Graph backend error: {type(exc).__name__}")
            logger.warning(
                "indexer.graph_error",
                project=project,
                subject=t.subject_uri,
                error=str(exc),
            )
            continue

        # Data triple succeeded — count it (original triples only) and track subject.
        if is_original:
            stored += 1
        subjects.add(t.subject_uri)

        # When both object_uri and object_value are set (e.g. a
        # URI-identified resource with a literal label), store the
        # literal as a separate ``rdfs:label`` triple so no data is
        # silently discarded.  Track the object_uri as a subject so
        # it's cleaned up on resource removal.
        if t.object_uri and t.object_value:
            try:
                graph_backend.add_triple(
                    project,
                    t.object_uri,
                    "rdfs:label",
                    t.object_value,
                )
                subjects.add(t.object_uri)
            except Exception as exc:
                errors.append(f"Graph label error: {type(exc).__name__}")

        # Best-effort provenance metadata — failures do not affect
        # the stored count since the data triple already persisted.
        # Per spec Provenance Contract: sourceResource, sourcePath,
        # sourceRange, validFrom, isCurrent, and confidence.
        prov = pt.provenance
        prov_triples: list[tuple[str, str]] = [
            ("uko:sourceResource", resource_uri),
        ]
        if prov.source_path:
            prov_triples.append(("uko:sourcePath", prov.source_path))
        if prov.source_range:
            prov_triples.append(("uko:sourceRange", prov.source_range))
        prov_triples.append(("uko:validFrom", prov.valid_from.isoformat()))
        prov_triples.append(("uko:isCurrent", str(prov.is_current).lower()))
        if t.confidence < 1.0:
            prov_triples.append(("uko:confidence", str(t.confidence)))
        for pred, obj_val in prov_triples:
            try:
                graph_backend.add_triple(
                    project,
                    t.subject_uri,
                    pred,
                    obj_val,
                )
            except Exception as exc:
                errors.append(f"Graph provenance error ({pred}): {type(exc).__name__}")
    return stored, subjects


def index_text(
    text_backend: TextIndexBackend | None,
    project: str,
    resource: Resource,
    content: str,
    errors: list[str],
    resource_uri: str,
) -> int:
    """Index resource content in the text backend.

    Returns:
        Number of documents indexed (0 or 1).
    """
    if text_backend is None:
        return 0
    try:
        text_backend.index_document(
            project,
            resource_uri,
            content,
            {
                "resource_id": resource.resource_id,
                "location": resource.location or "",
                "resource_type": resource.resource_type_name,
            },
        )
        return 1
    except Exception as exc:
        error_msg = f"Text backend error: {type(exc).__name__}"
        errors.append(error_msg)
        logger.warning(
            "indexer.text_error",
            project=project,
            resource_id=resource.resource_id,
            error=str(exc),
        )
        return 0


def index_vector(
    vector_backend: VectorIndexBackend | None,
    project: str,
    resource: Resource,
    content: str,
    errors: list[str],
    resource_uri: str,
) -> int:
    """Index resource embedding in the vector backend.

    Returns:
        Number of embeddings indexed (0 or 1).
    """
    if vector_backend is None:
        return 0
    # TODO(#578): integrate real embedding model — placeholder vector
    # avoids leaking content size metadata by using a constant.
    # See docs/reference/uko_indexer.md § Known Limitations.
    placeholder_embedding = [1.0]
    try:
        vector_backend.index_embedding(
            project,
            resource_uri,
            placeholder_embedding,
            {
                "resource_id": resource.resource_id,
                "location": resource.location or "",
                "resource_type": resource.resource_type_name,
            },
        )
        return 1
    except Exception as exc:
        error_msg = f"Vector backend error: {type(exc).__name__}"
        errors.append(error_msg)
        logger.warning(
            "indexer.vector_error",
            project=project,
            resource_id=resource.resource_id,
            error=str(exc),
        )
        return 0


# ---------------------------------------------------------------------------
# Module exports
# ---------------------------------------------------------------------------

__all__: list[str] = [
    "_build_layer_triples",
    "attach_provenance",
    "fire_on_error",
    "fire_on_indexed",
    "fire_on_removed",
    "index_graph",
    "index_text",
    "index_vector",
]
