"""STaR-style difficulty labeling of GSM8K with the frozen base model.

Two passes per item with frozen gemma-4-E2B-it:
  direct: answer with no CoT  -> correct = "easy"
  cot:    step-by-step        -> correct (and direct wrong) = "hard"
Items the model cannot solve even with CoT are dropped from training
(unreachable supervision) but kept in the test split for eval.

Output: results-loop/star_data.json
"""

import json
import os
import random
import time
from pathlib import Path

import torch
from datasets import load_dataset

from loop_common import (COT_SUFFIX, DIRECT_SUFFIX, chat_prompt, gold_answer,
                         last_number, num_eq)
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from jlens.core import load_model  # noqa: E402

N_TRAIN, N_TEST = 1024, 256
OUT = Path(os.environ.get("LOOP_OUT",
    Path(__file__).resolve().parent.parent / "results-loop"))
OUT.mkdir(exist_ok=True)


@torch.no_grad()
def batch_generate(model, tok, prompts, max_new_tokens, batch_size):
    outs = []
    for i in range(0, len(prompts), batch_size):
        chunk = prompts[i : i + batch_size]
        enc = tok(chunk, return_tensors="pt", padding=True,
                  add_special_tokens=False).to("cuda")
        gen = model.generate(**enc, max_new_tokens=max_new_tokens,
                             do_sample=False,
                             pad_token_id=tok.pad_token_id or 0)
        for j in range(len(chunk)):
            outs.append(tok.decode(gen[j, enc["input_ids"].shape[1]:],
                                   skip_special_tokens=True))
        print(f"  {min(i+batch_size, len(prompts))}/{len(prompts)}", flush=True)
    return outs


def main():
    model, tok = load_model(dtype=torch.bfloat16)
    tok.padding_side = "left"
    ds = load_dataset("openai/gsm8k", "main")

    rng = random.Random(0)
    tr_idx = rng.sample(range(len(ds["train"])), N_TRAIN)
    te_idx = rng.sample(range(len(ds["test"])), N_TEST)

    items = []
    for split, idxs in (("train", tr_idx), ("test", te_idx)):
        for i in idxs:
            row = ds[split][i]
            items.append({"split": split, "idx": i, "question": row["question"],
                          "gold": gold_answer(row["answer"])})

    t0 = time.time()
    print(f"direct pass ({len(items)} items)", flush=True)
    direct = batch_generate(
        model, tok, [chat_prompt(tok, it["question"], DIRECT_SUFFIX) for it in items],
        max_new_tokens=10, batch_size=64)
    print(f"direct pass done in {time.time()-t0:.0f}s", flush=True)

    t0 = time.time()
    print("cot pass", flush=True)
    cot = batch_generate(
        model, tok, [chat_prompt(tok, it["question"], COT_SUFFIX) for it in items],
        max_new_tokens=320, batch_size=32)
    print(f"cot pass done in {time.time()-t0:.0f}s", flush=True)

    for it, d, c in zip(items, direct, cot):
        it["direct_pred"] = last_number(d)
        it["direct_ok"] = num_eq(it["direct_pred"], it["gold"])
        it["cot_pred"] = last_number(c)
        it["cot_ok"] = num_eq(it["cot_pred"], it["gold"])
        it["label"] = ("easy" if it["direct_ok"]
                       else "hard" if it["cot_ok"] else "drop")

    for split in ("train", "test"):
        sub = [it for it in items if it["split"] == split]
        n = len(sub)
        print(f"{split}: n={n}  direct_acc={sum(i['direct_ok'] for i in sub)/n:.3f}  "
              f"cot_acc={sum(i['cot_ok'] for i in sub)/n:.3f}  "
              f"easy={sum(i['label']=='easy' for i in sub)}  "
              f"hard={sum(i['label']=='hard' for i in sub)}  "
              f"drop={sum(i['label']=='drop' for i in sub)}", flush=True)

    with open(OUT / "star_data.json", "w") as f:
        json.dump(items, f, indent=1)
    print("wrote", OUT / "star_data.json")


if __name__ == "__main__":
    main()
