#!/usr/bin/env python3
"""
core — the GENERAL CORE ENGINE (final).

One domain-agnostic discover→verify→adopt→compose engine. Capability GROWS from REAL data and
COMPOUNDS (composed skills are built from already-adopted skills — the "mul needs add" shape,
S013/S014), with the SOUND adoption rule from S035 (adopt iff the examples UNIQUELY determine one
candidate; else REFUSE). No hand-seeded curriculum, no fabricated data: every example is a real
(data → answer) pair the environment supplies. No model, deterministic, verify-or-refuse.

The engine knows NOTHING domain-specific. A `Domain` supplies a finite PRIMITIVE family (atomic
verified ops over its real data) and a `parse`. The engine supplies the COMBINATORS (general ways
to compose skills) and the registry + learning loop. Learnable abilities = a single primitive, or
a combinator over ALREADY-ADOPTED registry skills (so the library compounds).
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from typing import Callable


# ── a verified Skill (S014 ABI, generalized) ──────────────────────────────────────────
@dataclass
class Skill:
    name: str
    program: Callable          # data -> value (pure; data is the domain's parsed object)
    spec: dict                 # portable, JSON-able description (how to rebuild the program)
    deps: tuple = ()           # names of prerequisite adopted skills (compounding edges)
    provenance: str = ""


# ── general COMBINATORS over skills (domain-INDEPENDENT) ───────────────────────────────
# Each takes the VALUES produced by operand skills and returns a new value. Deterministic order.
_BINOPS = {
    "sub":     lambda a, b: a - b,
    "add":     lambda a, b: a + b,
    "gt":      lambda a, b: a > b,
    "ge":      lambda a, b: a >= b,
    "eq":      lambda a, b: a == b,
}


def _compose_program(op, prog_i, prog_j):
    f = _BINOPS[op]
    return lambda data: f(prog_i(data), prog_j(data))


# ── the verified-only Registry ─────────────────────────────────────────────────────────
class Registry:
    def __init__(self):
        self.skills: dict[str, Skill] = {}     # insertion-ordered (determinism)

    def names(self):
        return list(self.skills.keys())

    def add(self, skill: Skill):
        self.skills[skill.name] = skill

    def value(self, name, data):
        return self.skills[name].program(data)


# ── candidate enumeration: primitives ∪ combinator-compositions over the registry ──────
def _primitive_candidates(domain):
    """Every atomic primitive the domain exposes, as ('prim', spec, program)."""
    return [("prim", spec, prog) for (spec, prog) in domain.primitives()]


def _composition_candidates(reg: Registry):
    """Every combinator over an ORDERED PAIR of already-adopted registry skills, as
    ('comp', spec, program). This is the compounding search space: it is EMPTY until skills
    are adopted, so a composed skill is unreachable before its prerequisites exist."""
    out = []
    adopted = reg.names()
    for op in _BINOPS:
        for i in adopted:
            for j in adopted:
                if i == j and op in ("sub", "eq", "gt", "ge"):
                    continue                                  # trivial (0 / True) — skip
                prog_i, prog_j = reg.skills[i].program, reg.skills[j].program
                spec = {"kind": "comp", "op": op, "deps": [i, j]}
                out.append(("comp", spec, _compose_program(op, prog_i, prog_j)))
    return out


def _matches(program, example_pairs):
    try:
        return all(program(data) == ans for data, ans in example_pairs)
    except Exception:
        return False


def _signature(program, holdout_data):
    try:
        return tuple(program(d) for d in holdout_data)
    except Exception:
        return None


# ── learn = discover → verify (sound: unique-determination) → adopt ────────────────────
def learn(name, domain, example_pairs, holdout_data, reg: Registry):
    """
    example_pairs : [(data, answer)]  — REAL (parsed-source, environment-supplied answer).
    holdout_data  : [data]            — REAL parsed sources, no answers (informative-regime check).

    Sound rule (S035, generalized over primitives + compositions): adopt iff EXACTLY ONE candidate
    behavior is consistent with the examples. >1 → REFUSE underdetermined; 0 → REFUSE reach limit.
    Held-out agreement among multiple consistent candidates is NEVER sufficient (it can be vacuous).
    """
    candidates = _primitive_candidates(domain) + _composition_candidates(reg)
    consistent = [(kind, spec, prog) for (kind, spec, prog) in candidates if _matches(prog, example_pairs)]

    if not consistent:
        return {"adopted": False, "reason": "reach limit: no primitive or composition of adopted "
                                             "skills fits the examples", "n_consistent": 0}
    if len(consistent) > 1:
        # genuinely-distinct candidates that coincide on the examples → ambiguous. (Two specs that
        # are behaviorally identical on a discriminating holdout are de-duplicated below first.)
        sigs = {}
        for (kind, spec, prog) in consistent:
            sigs.setdefault(_signature(prog, holdout_data), []).append((kind, spec, prog))
        if len(sigs) > 1 or None in sigs:
            return {"adopted": False, "reason": f"underdetermined: examples consistent with "
                    f"{len(consistent)} candidates ({len(sigs)} behaviors) — need more discriminating "
                    f"examples", "n_consistent": len(consistent), "n_behaviors": len(sigs)}
        # all consistent candidates are behaviorally identical on the holdout: still ambiguous unless
        # the holdout is informative AND they are the SAME spec kind+op+deps. Conservatively REFUSE
        # when >1 DISTINCT spec remains (we cannot prove semantic identity from finite data).
        distinct = {json.dumps(spec, sort_keys=True) for (_, spec, _) in consistent}
        if len(distinct) > 1:
            return {"adopted": False, "reason": f"underdetermined: {len(consistent)} distinct candidates "
                    f"agree on holdout (vacuous) — need more discriminating examples",
                    "n_consistent": len(consistent), "n_behaviors": 1}

    # exactly one consistent candidate (or all identical-spec). Require a non-empty, DISCRIMINATING
    # holdout (the full candidate space must yield ≥2 distinct holdout signatures — informative regime).
    if not holdout_data:
        return {"adopted": False, "reason": "cannot verify: no held-out real data to confirm generalization"}
    full_sigs = {_signature(prog, holdout_data) for (_, _, prog) in candidates}
    full_sigs.discard(None)
    if len(full_sigs) < 2:
        return {"adopted": False, "reason": "uninformative holdout: does not discriminate any candidate"}

    kind, spec, prog = consistent[0]
    deps = tuple(spec["deps"]) if kind == "comp" else ()
    skill = Skill(name=name, program=prog, spec=spec, deps=deps,
                  provenance=(f"synthesized [{kind}] spec={spec}; uniquely determined by the examples "
                              f"(1 of {len(candidates)} candidates); confirmed on {len(holdout_data)} "
                              f"held-out real items"))
    reg.add(skill)
    return {"adopted": True, "name": name, "kind": kind, "spec": spec, "deps": deps,
            "n_consistent": len(consistent), "provenance": skill.provenance}


# ── persistence: the learned library is on-disk memory (capability = memory) ───────────
def save(reg: Registry, path):
    data = {n: {"spec": s.spec, "deps": list(s.deps), "provenance": s.provenance}
            for n, s in reg.skills.items()}
    with open(path, "w") as f:
        json.dump(data, f, indent=2, sort_keys=True)


def load(domain, path) -> Registry:
    """Rebuild the registry (programs) from the on-disk specs — programs are reconstructed, not
    pickled, so a fresh process recovers full capability from data alone."""
    reg = Registry()
    raw = json.loads(open(path).read())
    prim_by_spec = {json.dumps(spec, sort_keys=True): prog for (spec, prog) in domain.primitives()}
    # adopt in dependency order (insertion order of a sorted-by-deps pass)
    pending = dict(raw)
    progressed = True
    while pending and progressed:
        progressed = False
        for n in list(pending):
            spec = pending[n]["spec"]
            if spec.get("kind") == "comp":
                di, dj = spec["deps"]
                if di in reg.skills and dj in reg.skills:
                    prog = _compose_program(spec["op"], reg.skills[di].program, reg.skills[dj].program)
                    reg.add(Skill(n, prog, spec, tuple(spec["deps"]), pending[n]["provenance"]))
                    del pending[n]; progressed = True
            else:
                prog = prim_by_spec.get(json.dumps(spec, sort_keys=True))
                if prog is not None:
                    reg.add(Skill(n, prog, spec, (), pending[n]["provenance"]))
                    del pending[n]; progressed = True
    if pending:
        raise ValueError(f"cannot rebuild skills (missing primitives or broken deps): {list(pending)}")
    return reg
