"""UKO Implicit Relationship Inference — semantic analysis during indexing.

Provides :class:`UKOInferenceEngine`, which analyses UKO triples produced
by domain analyzers and infers additional implicit relationships based on
semantic patterns.

Implicit relationships are inferred from:

1. **Co-occurrence**: Two subjects that share the same type are related
   via ``uko:implicitSiblingOf``.
2. **Containment**: A subject whose URI is a prefix of another subject's
   URI is related via ``uko:implicitContains``.
3. **Dependency**: A subject that references another subject's URI in its
   object values is related via ``uko:implicitDependsOn``.

These inferred triples are stored alongside the original triples in the
graph backend with a confidence score below 1.0 to distinguish them from
deterministic extractions.

Based on ``docs/specification.md`` §185 — "Semantically aware — implicit
relationships are inferred from content analysis."

ISSUES CLOSED: #891
"""

from __future__ import annotations

import structlog

from cleveragents.domain.models.acms.analyzers import UKOTriple

logger = structlog.get_logger(__name__)

__all__ = [
    "UKOInferenceEngine",
    "infer_implicit_relations",
]

# Confidence score for inferred (implicit) relationships.
_IMPLICIT_CONFIDENCE = 0.7

# Predicates for implicit relationships.
_PRED_SIBLING = "uko:implicitSiblingOf"
_PRED_CONTAINS = "uko:implicitContains"
_PRED_DEPENDS_ON = "uko:implicitDependsOn"


class UKOInferenceEngine:
    """Infers implicit relationships from a set of UKO triples.

    Analyses the triples produced by domain analyzers and generates
    additional triples representing inferred semantic relationships.
    These are stored with a confidence score of 0.7 to distinguish
    them from deterministic extractions.

    Based on ``docs/specification.md`` §185 — implicit relationship
    inference from content analysis.
    """

    def infer(
        self,
        triples: list[UKOTriple],
    ) -> list[UKOTriple]:
        """Infer implicit relationships from *triples*.

        Args:
            triples: List of UKO triples produced by a domain analyzer.

        Returns:
            List of additional inferred triples.  May be empty if no
            implicit relationships are detected.
        """
        if not triples:
            return []

        inferred: list[UKOTriple] = []

        # Build subject → types mapping for co-occurrence analysis.
        subject_types: dict[str, list[str]] = {}
        for triple in triples:
            if triple.predicate in ("rdf:type", "a"):
                obj = triple.object_uri or triple.object_value
                if obj:
                    subject_types.setdefault(triple.subject_uri, []).append(obj)

        # Build subject → object_uris mapping for dependency analysis.
        subject_refs: dict[str, set[str]] = {}
        for triple in triples:
            if triple.object_uri:
                subject_refs.setdefault(triple.subject_uri, set()).add(
                    triple.object_uri
                )

        subjects = list(subject_types.keys())

        # 1. Co-occurrence: subjects sharing the same type are siblings.
        type_to_subjects: dict[str, list[str]] = {}
        for subj, types in subject_types.items():
            for t in types:
                type_to_subjects.setdefault(t, []).append(subj)

        for _type_uri, type_subjects in type_to_subjects.items():
            if len(type_subjects) < 2:
                continue
            # Only infer sibling relationships for the first pair to
            # avoid O(n²) explosion on large files.
            s1, s2 = type_subjects[0], type_subjects[1]
            if s1 != s2:
                inferred.append(
                    UKOTriple(
                        subject_uri=s1,
                        predicate=_PRED_SIBLING,
                        object_uri=s2,
                        confidence=_IMPLICIT_CONFIDENCE,
                    )
                )

        # 2. Containment: subject URI is a prefix of another subject URI.
        for i, s1 in enumerate(subjects):
            for s2 in subjects[i + 1 :]:
                if s2.startswith(s1 + "/") or s2.startswith(s1 + "#"):
                    inferred.append(
                        UKOTriple(
                            subject_uri=s1,
                            predicate=_PRED_CONTAINS,
                            object_uri=s2,
                            confidence=_IMPLICIT_CONFIDENCE,
                        )
                    )
                elif s1.startswith(s2 + "/") or s1.startswith(s2 + "#"):
                    inferred.append(
                        UKOTriple(
                            subject_uri=s2,
                            predicate=_PRED_CONTAINS,
                            object_uri=s1,
                            confidence=_IMPLICIT_CONFIDENCE,
                        )
                    )

        # 3. Dependency: subject references another subject's URI.
        subject_set = set(subjects)
        for subj, refs in subject_refs.items():
            for ref in refs:
                if ref in subject_set and ref != subj:
                    inferred.append(
                        UKOTriple(
                            subject_uri=subj,
                            predicate=_PRED_DEPENDS_ON,
                            object_uri=ref,
                            confidence=_IMPLICIT_CONFIDENCE,
                        )
                    )

        logger.debug(
            "uko_inference.inferred",
            input_count=len(triples),
            inferred_count=len(inferred),
        )
        return inferred


def infer_implicit_relations(
    triples: list[UKOTriple],
) -> list[UKOTriple]:
    """Module-level convenience wrapper for :class:`UKOInferenceEngine`.

    Args:
        triples: List of UKO triples to analyse.

    Returns:
        List of inferred implicit relationship triples.
    """
    engine = UKOInferenceEngine()
    return engine.infer(triples)
