#!/usr/bin/env python3
"""
teach.py — thin runner for the vendored S053 topic-agnostic skill-birth engine.

Takes ONE JSON file describing an external task family and runs the REAL birth
loop (skill_birth.SkillBirthEngine). This script wires and converts ONLY:
- it verifies the bundled node_core files against the engine contract's pinned
  SHA-256 baseline (interfaces.NODE_CORE_SHAS) and REFUSES to run on mismatch
  (a pin that does not verify means the wiring is wrong by definition);
- it converts the JSON family into the frozen interfaces.TaskFamily shape;
- the checker it supplies is a pure LOOKUP over the labeled pairs YOU provided
  (an answer is correct iff it matches what you labeled for that exact input)
  — no rule, threshold, or domain logic is computed here;
- every decision (authored standard, ADOPT + learned rule, or constructive
  refusal) comes verbatim from SkillBirthEngine.birth().

JSON family shape (see GUIDE.md "Teach it a new skill"):
  {
    "name": "reporting label (never read by the engine's decision path)",
    "description": "for humans; the engine never reads it",
    "examples":  [{"input": <JSON value>, "expected": <JSON value>}, ...],
    "holdout":   [ same shape ]            (optional; engine authors its own
                                            split from examples when omitted),
    "probe_pool": [<JSON input value>, ...] (optional; candidate discriminating
                                            inputs a refusal may name)
  }

Exit codes: 0 = the engine decided (ADOPT or typed refusal — refusal is a valid
decision); 2 = usage; 3 = unreadable/malformed input file; 4 = sha-pin mismatch.
"""
import hashlib
import json
import os
import sys

_HERE = os.path.dirname(os.path.abspath(__file__))
_AI_NODE_ROOT = os.path.dirname(_HERE)
if _HERE not in sys.path:
    sys.path.insert(0, _HERE)

import interfaces as IF   # noqa: E402  — the frozen S053 contract (read-only)
import skill_birth as SB  # noqa: E402  — the real engine (read-only)


def verify_node_core_pins() -> None:
    """G11: every bundled node_core file must match the engine contract's pinned sha.
    Hard-stop on any mismatch — never weakened, never skipped."""
    mismatches = []
    for rel, pinned in sorted(IF.NODE_CORE_SHAS.items()):
        path = os.path.join(_AI_NODE_ROOT, rel)
        if not os.path.exists(path):
            mismatches.append(f"{rel}: MISSING (pinned {pinned[:16]}...)")
            continue
        actual = hashlib.sha256(open(path, "rb").read()).hexdigest()
        if actual != pinned:
            mismatches.append(f"{rel}: sha {actual[:16]}... != pinned {pinned[:16]}...")
    if mismatches:
        print("SHA-PIN VERIFICATION FAILED — refusing to run the birth engine:", file=sys.stderr)
        for m in mismatches:
            print(f"  {m}", file=sys.stderr)
        raise SystemExit(4)
    print(f"node_core sha pins verified: {len(IF.NODE_CORE_SHAS)}/{len(IF.NODE_CORE_SHAS)} "
          f"bundled files match interfaces.NODE_CORE_SHAS (G11)")


def load_family(path: str) -> IF.TaskFamily:
    with open(path, encoding="utf-8") as f:
        doc = json.load(f)
    if not isinstance(doc, dict):
        raise ValueError("family JSON must be an object")
    name = doc.get("name")
    if not isinstance(name, str) or not name.strip():
        raise ValueError("family JSON needs a non-empty string 'name'")
    raw_examples = doc.get("examples")
    if not isinstance(raw_examples, list) or not raw_examples:
        raise ValueError("family JSON needs a non-empty 'examples' list")

    def pairs(raw, what):
        out = []
        for i, e in enumerate(raw):
            if not isinstance(e, dict) or "input" not in e or "expected" not in e:
                raise ValueError(f"{what}[{i}] must be an object with 'input' and 'expected'")
            out.append((e["input"], e["expected"]))
        return out

    examples = pairs(raw_examples, "examples")
    holdout = pairs(doc.get("holdout", []), "holdout") if doc.get("holdout") else []
    probe_pool = doc.get("probe_pool", [])
    if not isinstance(probe_pool, list):
        raise ValueError("'probe_pool' must be a list of input values")

    # The checker: a pure lookup over YOUR labeled pairs. Wire-only — this file
    # computes no rule; correctness is defined as "matches the label you gave".
    labeled = {SB.canon(x): y for x, y in examples + holdout}

    def checker(x, a) -> bool:
        cx = SB.canon(x)
        return cx in labeled and SB.canonical_eq(labeled[cx], a)

    return IF.TaskFamily(
        name=name,
        expect=IF.ExpectTag.ADOPT,   # harness-only field; the engine's decision never reads it
        examples=examples,
        holdout=holdout,
        adversarial_seeds=[],        # would need labels; not part of the JSON teach surface
        negatives=[],                # negatives are callables; not expressible in JSON
        probe_pool=probe_pool,
        checker=checker,
    )


def main() -> None:
    if len(sys.argv) != 2:
        print("usage: teach.py <family.json>", file=sys.stderr)
        raise SystemExit(2)
    verify_node_core_pins()
    try:
        fam = load_family(sys.argv[1])
    except (OSError, ValueError, json.JSONDecodeError) as e:
        print(f"teach.py: cannot load family JSON: {e}", file=sys.stderr)
        raise SystemExit(3)

    engine = SB.SkillBirthEngine()
    outcome = engine.birth(fam, fam.name)

    print()
    print(f"family: {outcome.family}")
    print(f"named skill: {outcome.hypothesis.skill_name} "
          f"({outcome.hypothesis.in_shape} -> {outcome.hypothesis.out_shape}, "
          f"n_train={outcome.hypothesis.n_train})")
    print()
    print("AUTHORED STANDARD (written by the engine BEFORE deciding):")
    print(f"  {outcome.standard.plain_english}")
    print(f"  success rule: {outcome.standard.success_rule}")
    print()
    if outcome.status == IF.BirthStatus.ADOPTED and outcome.adopted is not None:
        a = outcome.adopted
        print(f"DECISION: ADOPT")
        print(f"  learned rule: op={a.spec.op} params={json.dumps(a.spec.params, sort_keys=True)}")
        print(f"  provenance: {a.provenance}")
    elif outcome.refusal is not None:
        r = outcome.refusal
        print(f"DECISION: REFUSE ({r.kind.value})")
        print(f"  {r.plain_english}")
        if r.missing_input is not None:
            print(f"  missing labeled input: {json.dumps(r.missing_input, sort_keys=True)}")
        if r.disagreeing_values:
            print(f"  surviving rules disagree there: {list(r.disagreeing_values)}")
    print()
    c = outcome.counts
    print(f"accounting: proposals={c.proposals} train_checks={c.train_checks} "
          f"holdout_checks={c.holdout_checks} survivors={c.survivors} "
          f"adoptions={c.adoptions} refusals={c.refusals}")


if __name__ == "__main__":
    main()
