#!/usr/bin/env python3
"""
loop — the generic self-learning loop (S045), extracted CLEAN (no domain code, no deps).

`learn_unique` is domain-agnostic: it is parameterized by a candidate list, a metric function, and
example/holdout units, and knows nothing about what any of them mean. Adopt iff the examples UNIQUELY
determine one candidate, confirmed on a discriminating holdout; else refuse (reach limit /
underdetermined / cannot-verify / uninformative-holdout). Pure function; stdlib-free.

This is the simple loop most skills use; `engine.py` carries the richer S036 compositional engine
(primitives + combinators + Registry + compose). Both are universal; a skill uses whichever fits.
"""
from __future__ import annotations


def consistent_set(all_comps, metric_fn, example_pairs):
    """Every candidate that reproduces ALL example answers (the candidates still in play)."""
    return [c for c in all_comps if all(metric_fn(u, c) == a for u, a in example_pairs)]


def learn_unique(all_comps, metric_fn, example_pairs, holdout_units):
    """
    example_pairs : [(unit, answer)]  — real units + the answer the environment supplies.
    holdout_units : [unit]            — real units whose answers the synthesis never sees.
      no candidate fits examples              -> REFUSE (reach limit)
      >1 candidate fits examples (ambiguous)  -> REFUSE (underdetermined)
      exactly one, discriminating holdout     -> ADOPT that unique candidate
    """
    consistent = [c for c in all_comps
                  if all(metric_fn(u, c) == a for u, a in example_pairs)]
    if not consistent:
        return {"adopted": False, "reason": "reach limit: no candidate of the current family fits the examples"}
    if len(consistent) > 1:
        return {"adopted": False,
                "reason": f"underdetermined: examples consistent with {len(consistent)} candidates "
                          f"(need more discriminating examples)",
                "n_consistent": len(consistent)}
    if not holdout_units:
        return {"adopted": False, "reason": "cannot verify: no held-out units to confirm generalization"}
    full_sigs = {tuple(metric_fn(u, c) for u in holdout_units) for c in all_comps}
    if len(full_sigs) < 2:
        return {"adopted": False, "reason": "uninformative holdout: does not discriminate any candidate"}
    return {"adopted": True, "comp": consistent[0], "n_consistent": 1,
            "provenance": f"synthesized: {consistent[0]}; uniquely determined by the examples "
                          f"(1 of {len(all_comps)} consistent); confirmed on {len(holdout_units)} held-out units"}
