#!/usr/bin/env python3
"""exp16 — cold-start / A4 generation (research/20 §D1 cold-start slice + §D2 A4). PAID (deepseek-v4-flash).

FROZEN in the pre-reg commit; RUN later in the DeepSeek off-peak valley (D39.7: peaks UTC 01-04 & 06-10
cost ×2 — this script REFUSES to run in a peak window unless --force). Generates, for the pre-registered
cold-start chapters [1,4,5,7,9] (14 chunks), fresh flash drafts WITHOUT glossary injection, in two configs:
  plain     — cold-start baseline (translator.md brief-filled, NO glossary block) -> spread + canon-recovery
  banknote  — plain + the banknote-v1 instruction (§B3)                            -> footnote channel + quality delta

Discipline: thinking ON (deepseek echo-mine guardrail — never disabled). LENGTH-GATE FIX (D39.9): a
finish_reason=='length' response is REJECTED/flagged, NOT accepted as valid (exp14 harness bug not
inherited). Every call persists raw usage+cost (D30.10) via the exp15 Spender; per-call gate + $10 hard cap.
"""
from __future__ import annotations

import argparse
import json
import sys
import time
from pathlib import Path

HERE = Path(__file__).resolve().parent
sys.path.append(str(HERE.parent / "exp15"))
import exp15_llm as LLM
import banknote as BN

BOOK = Path("/home/ubuntu/books/gu-zhenren")
RECORDS = BOOK / "rerun" / "records.json"
OUT = BOOK / "exp16" / "coldstart"
LEDGER = BOOK / "exp16" / "coldstart_costs.jsonl"

COLD_CHAPTERS = [1, 4, 5, 7, 9]     # FROZEN (greedy GT-cover: 52/57 entities, 14 chunks)
MODEL = "deepseek-v4-flash"
HARD_CAP = 10.0                      # exp16 paid budget (owner 17-18.07), separate from exp15
PER_CALL_CAP = 0.05                  # flash is cheap; a single chunk far below this

# ── FROZEN translator prompt: backend/prompts/translator.md (sha256 35d3ad6e...), brief from rerun/book.yaml,
#    glossary-injection block OMITTED (cold-start = new book, no bank). ──────────────────────────────
BRIEF = dict(source_lang="zh", target_lang="ru", genre="вебновелла", audience="взрослые читатели вебновелл",
             title="蛊真人", honorifics="keep", transcription="palladius", venuti="0.6", footnotes="minimal")

TRANSLATOR_SYSTEM = (
    'Ты — профессиональный литературный переводчик с языка «{source_lang}» на язык «{target_lang}».\n'
    'Жанр книги: {genre}. Аудитория: {audience}. Книга: «{title}».\n\n'
    'Правила перевода (translation brief):\n'
    '- Хонорифики: {honorifics}. Транскрипция имён и реалий: {transcription}.\n'
    '- Баланс форенизация/доместикация (0 — полная адаптация, 1 — сохранение чужого): {venuti}.\n'
    '- Сноски: {footnotes}.\n'
    '- Переводи ВСЕ предложения исходника: ничего не пропускай, не сокращай и не добавляй от себя.\n'
    '- Вёрстка — по нормам русского языка, НЕ копируй построчную разбивку исходника: реплики прямой речи '
    'оформляй с нового абзаца через тире «—».\n'
    '- Пиши живым литературным русским языком; избегай канцелярита и калек с исходного языка.\n\n'
    'Выведи ТОЛЬКО перевод, без служебных преамбул, комментариев, пояснений и markdown-заголовков.'
).format(**BRIEF)

TRANSLATOR_USER = "Переведи следующий фрагмент:\n\n{text}"
TRANSLATOR_MD_SHA = "35d3ad6e0656a7d1116fb8b034a4cea087d5c11ac6e043804185f94f15af5829"


def in_deepseek_peak() -> bool:
    """UTC 01:00-04:00 and 06:00-10:00 are the ×2 peak windows (D39.7). new/argless time is banned in
    workflow scripts but this is a plain script; use time.gmtime()."""
    h = time.gmtime().tm_hour
    return (1 <= h < 4) or (6 <= h < 10)


def load_cold_chunks():
    recs = json.load(open(RECORDS, encoding="utf-8"))
    out = [r for r in recs if r["chapter"] in COLD_CHAPTERS]
    out.sort(key=lambda r: (r["chapter"], r["chunk_idx"]))
    return out


def gen_one(sp: LLM.Spender, source: str, config: str):
    system = TRANSLATOR_SYSTEM + (BN.BANKNOTE_INSTRUCTION if config == "banknote" else "")
    user = TRANSLATOR_USER.format(text=source)
    msgs = [{"role": "system", "content": system}, {"role": "user", "content": user}]
    in_tok = len(source) + len(system)   # rough; gate uses conservative estimate below
    ok, pred, why = sp.gate(MODEL, in_tok, LLM.MODELS[MODEL]["max_tokens"])
    if not ok:
        return {"err": f"gate: {why}", "pred": pred}
    r = LLM.call(MODEL, msgs, max_tokens=LLM.MODELS[MODEL]["max_tokens"])
    return r


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", choices=["plain", "banknote", "both"], default="both")
    ap.add_argument("--force", action="store_true", help="override the DeepSeek peak-window guard")
    ap.add_argument("--dry", action="store_true")
    a = ap.parse_args()

    if in_deepseek_peak() and not a.force:
        print(f"REFUSING: UTC hour {time.gmtime().tm_hour} is in a DeepSeek ×2 peak window (01-04/06-10). "
              f"Run in the valley or pass --force. (D39.7)")
        return

    chunks = load_cold_chunks()
    print(f"cold-start chunks: {len(chunks)} (chapters {COLD_CHAPTERS})")
    OUT.mkdir(parents=True, exist_ok=True)
    sp = LLM.Spender(str(LEDGER), per_call_cap=PER_CALL_CAP, hard_cap=HARD_CAP)
    print(f"spender: per_call_cap=${PER_CALL_CAP} hard_cap=${HARD_CAP} already_spent=${sp.total:.4f}")

    configs = ["plain", "banknote"] if a.config == "both" else [a.config]
    stats = {c: {"ok": 0, "length_reject": 0, "err": 0, "banknote_lines": 0, "parse_fail": 0, "truncated": 0}
             for c in configs}
    for config in configs:
        cdir = OUT / config
        cdir.mkdir(parents=True, exist_ok=True)
        for r in chunks:
            cid = f"{r['chapter']}.{r['chunk_idx']}"
            raw_fp = cdir / f"{cid}.raw.txt"
            if raw_fp.exists():
                continue
            if a.dry:
                print(f"  [dry] {config} {cid}")
                continue
            res = gen_one(sp, r["source"], config)
            rec = {"config": config, "id": cid, "model": MODEL, "usage": res.get("usage", {}),
                   "cost": res.get("cost", 0.0), "finish": res.get("finish"), "err": res.get("err"),
                   "latency_s": res.get("latency_s")}
            # TRUNCATION-GATE (D39.9 + self-review MAJOR): accept ONLY finish=='stop'. Reject/flag EVERY
            # other finish_reason (length, DeepSeek's insufficient_system_resource peak-overload cut,
            # content_filter, null/unknown) — a truncation via ANY reason must never be written to file.
            if res.get("err"):
                stats[config]["err"] += 1
                sp.record(rec); print(f"  ERR {config} {cid}: {res['err'][:70]}"); continue
            if res.get("finish") != "stop":
                stats[config]["length_reject"] += 1
                rec["rejected"] = f"finish={res.get('finish')!r}!=stop"
                sp.record(rec)
                print(f"  TRUNC-REJECT {config} {cid} finish={res.get('finish')!r} "
                      f"(comp={res['usage'].get('completion_tokens')})")
                continue
            sp.record(rec)
            text = res["text"]
            raw_fp.write_text(text, encoding="utf-8")
            if config == "banknote":
                clean, block = BN.split_banknote(text)
                ents, flags = BN.parse_banknote(block, truncated_generation=False)
                (cdir / f"{cid}.clean.txt").write_text(clean, encoding="utf-8")
                json.dump({"entries": ents, "flags": flags},
                          open(cdir / f"{cid}.banknote.json", "w"), ensure_ascii=False, indent=1)
                stats[config]["banknote_lines"] += flags["n_banknote_lines"]
                stats[config]["parse_fail"] += flags["banknote_parse_fail"]
                stats[config]["truncated"] += flags["banknote_truncated"]
            stats[config]["ok"] += 1
            print(f"  ok {config} {cid}: {len(text)}ch finish={res['finish']} ${res['cost']:.5f}")

    print(f"\n=== stats: {json.dumps(stats, ensure_ascii=False)} ===")
    print(f"=== total spend this run: ${sp.total:.4f} / cap ${HARD_CAP} ===")


if __name__ == "__main__":
    main()
