#!/usr/bin/env python3
"""
dual_core — the DUAL ENGINE substrate: a fluid candidate-PROPOSER tier in front of
the exact verifier (right-or-refuse), over the recursion-synthesis space (para2, S042).

Two tiers:
  Tier 1 (exact, unchanged): verify a candidate program on examples + validation +
          held-out; adopt iff it matches everywhere. 0 confident errors by construction.
  Tier 2 (fluid): a PROPOSER orders the candidate space so the verifier finds a
          verifying program in fewer evaluations. A bad proposal costs an evaluation,
          NEVER a wrong answer (the exact gate is unchanged).

Metric: evaluations-to-find = position, in the proposer's order, of the first candidate
that fully verifies (examples ∧ validation ∧ held-out). Lower is better. Correctness is
identical across proposers (same exact gate); only speed (evaluations) differs. An
unreachable target makes every proposer scan the whole space and ABSTAIN (0 false adopt).

Reuses the SEALED para2 candidate space + verifier substrate from reach_probe_t6 (S042),
loaded read-only by file path. Deterministic, zero RNG except seeded random-proposer floor.
"""
from __future__ import annotations
import importlib.util
import os
import random

HERE = os.path.dirname(os.path.abspath(__file__))
EXP = os.path.dirname(HERE)

# ── load the sealed reach_probe_t6 (S042) substrate read-only, by file path ──
_T6_PATH = os.path.join(EXP, "reach_probe_t6", "run.py")
_spec = importlib.util.spec_from_file_location("t6_run", _T6_PATH)
t6 = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(t6)   # self-wires emergent_recursion as base_run

para2 = t6.para2
STEP1 = t6.STEP1
BASE_VAL = t6.BASE_VAL
U_EX = t6.U_EX        # 5 example inputs
U_VAL = t6.U_VAL      # 3 validation inputs
U_HOLD = t6.U_HOLD    # 6 held-out inputs (len 6-11, disjoint)

CANDS = t6.step2_candidates()   # [(desc, step2_fn)], the full para2 candidate space
N = len(CANDS)


def prog_of(step2_fn):
    """A candidate's program: para2 with that step2, fixed content-free step1/base."""
    return lambda xs: para2(step2_fn, STEP1, BASE_VAL, xs)


def make_target(step2_fn):
    """A target oracle = the function some candidate step2 computes (guaranteed reachable)."""
    return prog_of(step2_fn)


def verifies(step2_fn, oracle):
    """Tier-1 exact gate: candidate matches oracle on examples ∧ validation ∧ held-out."""
    prog = prog_of(step2_fn)
    try:
        if not all(prog(i) == oracle(i) for i in U_EX):
            return False
        if not all(prog(i) == oracle(i) for i in U_VAL):
            return False
        return all(prog(i) == oracle(i) for i in U_HOLD)
    except Exception:
        return False


def verifier_indices(oracle):
    """Enumeration indices of ALL candidates that fully verify against the oracle.
    One pass over the space (early-exit on examples makes most candidates cheap)."""
    out = []
    for idx, (desc, fn) in enumerate(CANDS):
        if verifies(fn, oracle):
            out.append(idx)
    return out


def evals_to_find(order_indices, verifier_set):
    """Position (1-based) in `order_indices` of the first index in verifier_set.
    Returns (evals, found_bool). If no verifier in the order → scanned all, ABSTAIN."""
    vset = set(verifier_set)
    for pos, idx in enumerate(order_indices, start=1):
        if idx in vset:
            return pos, True
    return len(order_indices), False


# ── proposers (Tier 2) ─────────────────────────────────────────────────────────────────
def order_enumeration():
    """Fixed enumeration order (the natural scan; no proposer)."""
    return list(range(N))


def order_random(seed):
    """Dumb proposer: uniformly random order (the floor)."""
    r = random.Random(seed)
    idx = list(range(N))
    r.shuffle(idx)
    return idx


def order_structural():
    """A TARGET-BLIND structural prior (the naive heuristic): prefer depth-2 pair-shaped
    recursive programs, demote nil/cond. Same order for every target — kept only to show that
    a target-blind prior does NOT reliably help (it can't use the task)."""
    def score(desc):
        return (2 * (desc.count("cons(") >= 2) + ("h1" in desc) + ("h2" in desc)
                + ("rec" in desc) - ("nil" in desc) - ("cond(" in desc))
    return sorted(range(N), key=lambda i: (-score(CANDS[i][0]), i))


HEURISTIC_PROBE_K = 2   # examples the cheap proposer is allowed to probe (gate uses 14 inputs)


def order_heuristic(oracle, k=HEURISTIC_PROBE_K):
    """The fluid HEURISTIC proposer (target-CONDITIONAL): rank candidates by how many of the
    first k example inputs they reproduce — a CHEAP signal (k probes) vs the exact gate's
    14-input check. Candidates that fit the few cheap examples are tried by the verifier first;
    the exact gate still confirms all 14. Deterministic; tie-break enumeration index."""
    probe_in = U_EX[:k]
    probe_out = [oracle(i) for i in probe_in]

    def score(fn):
        prog = prog_of(fn)
        try:
            return sum(1 for i, o in zip(probe_in, probe_out) if prog(i) == o)
        except Exception:
            return -1
    return sorted(range(N), key=lambda idx: (-score(CANDS[idx][1]), idx))
