"""E1 trainer (PLAN_SELFPACED): halting-gate merge on MBPP prompt-only loop.

Same data/recipe as train_merge_code.py (STaR-verified code supervision,
prompt-only loop_mask), but NO difficulty->depth curriculum: every batch
mixes difficulties and runs the soft-halting loop to k_max — the GATE must
learn the allocation the curriculum used to hand-code.

Loss: CE(code tokens | s_mix) + lam * mean(E[iters]).
"""

import argparse
import json
import os
import math
import random
import sys
import time
from pathlib import Path

import torch
import torch.nn.functional as F

from halting_common import HaltingMergeAdapter, halting_loop_logits
from loop_common import BandLooper
from prep_mbpp import DIRECT_SUFFIX, mbpp_prompt
from train_merge_code import build_code_batch, MAX_TOK

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from jlens.core import load_model  # noqa: E402

OUT = Path(os.environ.get("LOOP_OUT",
    Path(__file__).resolve().parent.parent / "results-loop"))
STEPS = 600
BATCH = 4
WARMUP = 20

ap = argparse.ArgumentParser()
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--lam", type=float, default=1e-3,
                help="compute penalty weight on E[iterations]")
ap.add_argument("--kmax", type=int, default=4)
ap.add_argument("--lr", type=float, default=1e-3)
ap.add_argument("--lam-warmup", type=int, default=100,
                help="steps before the penalty ramps in (anti-collapse)")
ARGS = ap.parse_args()
LR = ARGS.lr
TAG = (f"gate_l{ARGS.lam:g}" + (f"_s{ARGS.seed}" if ARGS.seed else ""))


def lr_at(step):
    if step < WARMUP:
        return LR * (step + 1) / WARMUP
    t = (step - WARMUP) / max(1, STEPS - WARMUP)
    return 1e-4 + 0.5 * (LR - 1e-4) * (1 + math.cos(math.pi * t))


def lam_at(step):
    return ARGS.lam * min(1.0, max(0.0, (step - ARGS.lam_warmup) / 100))


def main():
    rng = random.Random(ARGS.seed)
    torch.manual_seed(ARGS.seed)
    data = json.load(open(OUT / "mbpp_data.json"))
    model, tok = load_model(dtype=torch.bfloat16)
    for p in model.parameters():
        p.requires_grad_(False)
    looper = BandLooper(model)
    adapter = HaltingMergeAdapter(
        d=model.config.get_text_config().hidden_size).cuda()
    opt = torch.optim.AdamW(adapter.parameters(), lr=LR, weight_decay=0.01)

    train = [it for it in data if it["split"] == "train"
             and it["label"] != "drop" and it["sol_code"]]
    train = [it for it in train
             if len(tok(mbpp_prompt(tok, it, DIRECT_SUFFIX))["input_ids"])
             + len(tok(it["sol_code"])["input_ids"]) + 12 <= MAX_TOK]
    val = train[:24]
    pool = train[24:]
    print(f"pool={len(pool)} val={len(val)}  lam={ARGS.lam} kmax={ARGS.kmax}",
          flush=True)

    log = []
    t0 = time.time()
    for step in range(STEPS):
        batch = rng.sample(pool, BATCH)
        ids, msk, lab, lmask = build_code_batch(tok, batch)
        # last prompt position = last position where label == -100 & mask==1
        pl = (lmask.long().cumsum(-1).argmax(-1))
        for g in opt.param_groups:
            g["lr"] = lr_at(step)
        logits, q, ei = halting_loop_logits(
            looper, adapter, ids, ARGS.kmax, msk, pl, loop_mask=lmask)
        ce = F.cross_entropy(logits[:, :-1].flatten(0, 1).float(),
                             lab[:, 1:].flatten(), ignore_index=-100)
        loss = ce + lam_at(step) * ei.mean()
        opt.zero_grad(set_to_none=True)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(adapter.parameters(), 1.0)
        opt.step()
        hard_b = [it["label"] == "hard" for it in batch]
        log.append({"step": step, "ce": ce.item(), "ei": ei.mean().item(),
                    "ei_hard": (ei[torch.tensor(hard_b)].mean().item()
                                if any(hard_b) else None),
                    "ei_easy": (ei[~torch.tensor(hard_b)].mean().item()
                                if not all(hard_b) else None)})
        if step % 10 == 0:
            r = log[-1]
            print(f"step {step:4d}  ce={r['ce']:.4f}  E[k]={r['ei']:.2f}  "
                  f"(hard {r['ei_hard']} / easy {r['ei_easy']})  "
                  f"({(time.time()-t0)/(step+1):.1f}s/step)", flush=True)
        if step % 100 == 99 or step == STEPS - 1:
            torch.save(adapter.state_dict(),
                       OUT / f"adapter_{TAG}_e{step+1}.pt")
            json.dump(log, open(OUT / f"train_{TAG}_log.json", "w"))
    print("done", flush=True)


if __name__ == "__main__":
    main()
