"""D — CLOSE (roadmap Step 5): idle deductive consolidation of derived facts.

The loop "learns from determined facts." Between turns, ``consolidate_once`` runs ONE
semi-naive layer of the member/subset deductive closure over the held self: for every
sound one-hop is-a inference whose conclusion is not yet realized, it VERIFIES the hop
with the sound+complete proof_chain ROBDD (reusing C's single verifier — no duplicate
proof logic) and writes the conclusion as a SPECULATIVE realized record carrying
derived-provenance (``generate.realize.realize_derived``). The next ``determine``
reaches the conclusion directly, and a later hop can chain off it — so the directly
answerable set climbs monotonically across idle ticks to the deductive-closure fixed
point, where a further tick is a no-op.

The only sound is-a rules are ``member ∘ subset → member`` and ``subset ∘ subset →
subset`` (Description-Logic subsumption). ``member ∘ member`` is NEVER an edge —
instance-of is not transitive ("Socrates is a man" + "man is a species" ⊬ "Socrates is
a species"). A consolidated ``member(s, t)`` is a member fact, extendable ONLY by a
subset edge, so the fallacy stays structurally unreachable across iterations.

wrong=0: every consolidated fact is the conclusion of a sound rule over realized
premises AND confirmed by the sound+complete decider (defence in depth). Honesty: a
fact derived from SPECULATIVE premises stays SPECULATIVE / as-told — the soundness of
the inference never upgrades the standing of the premises (COHERENT is never minted).
Boundary: SESSION memory (immediate, allowed), an extension of the ``generate.realize``
path — NOT reviewed/corpus learning and NOT coupled to proposals; the teaching/review
HITL path is untouched (no parallel learning path).

Determinism: a pure function of the current realized set. Candidates are consolidated
in sorted ``(predicate, subject, object)`` order, so the vault write order — and thus
the replayed structure — is independent of recall order. No clock, no LLM, no metric
call. Bounded by the same ``_SUBSUMPTION_SUBSET_FACT_BUDGET`` as the transitive search.
"""

from __future__ import annotations

from dataclasses import dataclass

from generate.determine.determine import (
    _SUBSUMPTION_SUBSET_FACT_BUDGET,
    Determined,
    _relational_transitive,
    _verify_subsumption,
)
from generate.meaning_graph.relational import TRANSITIVE_PREDICATES
from generate.realize import RealizedRecord, Realized, realize_derived, recall_realized
from session.context import SessionContext

#: Rule label stamped on the derived record's provenance, keyed by the conclusion's
#: predicate. ``member`` conclusions come from ``member ∘ subset``; ``subset``
#: conclusions from ``subset ∘ subset``. (``member ∘ member`` is not a rule.)
_RULE_FOR_PREDICATE = {"member": "member_subset", "subset": "subset_subset"}


@dataclass(frozen=True, slots=True)
class ConsolidationResult:
    """Outcome of one idle consolidation layer.

    ``considered`` — one-hop conclusions not already realized (this tick's frontier).
    ``consolidated`` — conclusions newly written as derived realized records.
    ``redundant`` — verified conclusions that dedup-hit an existing record (defensive;
    the frontier already excludes present facts).
    ``at_fixed_point`` — True iff nothing new was consolidated (the closure is
    saturated; re-running is a no-op).
    ``budget_exceeded`` — True iff the realized subset-fact count exceeded the bound, so
    the layer safely declined (a coverage backstop, never an unsound write).
    """

    considered: int
    consolidated: int
    redundant: int
    at_fixed_point: bool
    budget_exceeded: bool = False


def _one_hop_candidates(
    member_facts: tuple[RealizedRecord, ...],
    subset_facts: tuple[RealizedRecord, ...],
) -> list[tuple[str, str, str, RealizedRecord | None, tuple[RealizedRecord, ...]]]:
    """Enumerate the sound one-hop conclusions NOT already realized.

    Returns ``(predicate, subject, object, member_fact, subset_path)`` tuples ready for
    ``_verify_subsumption``. A ``member`` candidate carries its seed member fact and a
    one-edge subset path; a ``subset`` candidate carries a two-edge subset path and no
    member fact. Reflexive conclusions (subject == object) are excluded — they are not
    real subsumption claims.
    """
    existing: set[tuple[str, str, str]] = set()
    for f in member_facts:
        if len(f.relation_arguments) == 2:
            existing.add(("member", f.relation_arguments[0], f.relation_arguments[1]))
    for f in subset_facts:
        if len(f.relation_arguments) == 2:
            existing.add(("subset", f.relation_arguments[0], f.relation_arguments[1]))

    # subset adjacency: class → [(superclass, fact)] — the subclass-of edges.
    supers: dict[str, list[tuple[str, RealizedRecord]]] = {}
    for f in subset_facts:
        if len(f.relation_arguments) == 2:
            supers.setdefault(f.relation_arguments[0], []).append(
                (f.relation_arguments[1], f)
            )

    out: list[
        tuple[str, str, str, RealizedRecord | None, tuple[RealizedRecord, ...]]
    ] = []

    # member ∘ subset → member: member(s, b) + subset(b, t) ⊢ member(s, t).
    for m in member_facts:
        if len(m.relation_arguments) != 2:
            continue
        s, b = m.relation_arguments
        for t, sub_fact in supers.get(b, ()):
            if s == t:
                continue
            if ("member", s, t) not in existing:
                out.append(("member", s, t, m, (sub_fact,)))

    # subset ∘ subset → subset: subset(a, b) + subset(b, t) ⊢ subset(a, t).
    for f in subset_facts:
        if len(f.relation_arguments) != 2:
            continue
        a, b = f.relation_arguments
        for t, sub_fact in supers.get(b, ()):
            if a == t:
                continue
            if ("subset", a, t) not in existing:
                out.append(("subset", a, t, None, (f, sub_fact)))

    return out


def _relational_transitive_one_hop_candidates(
    ctx: SessionContext,
) -> list[tuple[str, str, str]]:
    """Direct 2-hop candidates for declared TRANSITIVE_PREDICATES only.

    For each p in TRANSITIVE_PREDICATES, scan realized p-edges for p(a,b) + p(b,c)
    where p(a,c) is not yet realized and a != c. Returns sorted list of (p, a, c).
    Same-predicate only (no inverse, no symmetric, no cross-predicate composition).
    Reflexives excluded by construction. Collection is cheap; the caller still
    invokes the full _relational_transitive verifier (search+proof_chain) before any
    realize_derived — candidate shape is never trusted alone (wrong=0 defence).
    """
    out: list[tuple[str, str, str]] = []
    for p in TRANSITIVE_PREDICATES:
        facts = recall_realized(ctx, predicate=p)
        have: set[tuple[str, str]] = {
            (f.relation_arguments[0], f.relation_arguments[1])
            for f in facts if len(f.relation_arguments) == 2
        }
        adj: dict[str, list[str]] = {}
        for f in facts:
            if len(f.relation_arguments) == 2:
                a, b = f.relation_arguments
                adj.setdefault(a, []).append(b)
        for a, mids in adj.items():
            for b in mids:
                for c in adj.get(b, ()):
                    if a == c:
                        continue
                    if (a, c) not in have:
                        out.append((p, a, c))
    out.sort(key=lambda t: (t[0], t[1], t[2]))
    return out


def consolidate_once(
    ctx: SessionContext, *, fact_budget: int = _SUBSUMPTION_SUBSET_FACT_BUDGET
) -> ConsolidationResult:
    """Consolidate one semi-naive layer of the member/subset deductive closure.

    Reads the realized member + subset facts present NOW, derives every sound one-hop
    conclusion not yet realized, proof_chain-verifies each, and writes the verified ones
    as derived realized records. Facts written this tick are NOT re-read this tick — the
    next layer derives off them next tick (semi-naive evaluation → monotone climb).
    """
    member_facts = recall_realized(ctx, predicate="member")
    subset_facts = recall_realized(ctx, predicate="subset")
    if len(subset_facts) > fact_budget:
        # Bounded — a safe coverage decline, never an unsound write.
        return ConsolidationResult(
            considered=0,
            consolidated=0,
            redundant=0,
            at_fixed_point=False,
            budget_exceeded=True,
        )

    is_a_cands = _one_hop_candidates(member_facts, subset_facts)
    rel_cands = _relational_transitive_one_hop_candidates(ctx)
    # Unify for deterministic global sort by (predicate, subject, object).
    # Relational cands use dummy member/subset slots (dispatch below ignores them).
    candidates = is_a_cands + [(p, s, o, None, ()) for (p, s, o) in rel_cands]
    candidates.sort(key=lambda c: (c[0], c[1], c[2]))

    consolidated = 0
    redundant = 0
    for predicate, subject, obj, member_fact, subset_path in candidates:
        if predicate in TRANSITIVE_PREDICATES:
            # Relational strict-order: reuse the Phase C sound verifier
            # (BFS reachability over p-edges + proof_chain ROBDD). Must return
            # Determined with rule="transitive" or we refuse the write.
            det = _relational_transitive(ctx, predicate, subject, obj)
            if not isinstance(det, Determined):
                continue
            premise_keys = tuple(g.structure_key for g in det.grounds)
            outcome = realize_derived(
                ctx,
                predicate=predicate,
                subject=subject,
                obj=obj,
                rule="transitive",
                premise_structure_keys=premise_keys,
            )
            if isinstance(outcome, Realized):
                if outcome.created:
                    consolidated += 1
                else:
                    redundant += 1
            continue

        # is-a (member/subset) — unchanged from baseline
        verdict = _verify_subsumption(
            predicate, subject, obj, member_fact=member_fact, subset_path=subset_path
        )
        if not isinstance(verdict, Determined):
            # The sound+complete decider did not confirm the hop — never write
            # (defence in depth: a candidate-construction bug cannot consolidate a
            # wrong fact).
            continue
        premise_keys = tuple(g.structure_key for g in verdict.grounds)
        outcome = realize_derived(
            ctx,
            predicate=predicate,
            subject=subject,
            obj=obj,
            rule=_RULE_FOR_PREDICATE[predicate],
            premise_structure_keys=premise_keys,
        )
        if isinstance(outcome, Realized):
            if outcome.created:
                consolidated += 1
            else:
                redundant += 1

    return ConsolidationResult(
        considered=len(candidates),
        consolidated=consolidated,
        redundant=redundant,
        at_fixed_point=consolidated == 0,
    )


__all__ = ["ConsolidationResult", "consolidate_once"]
