#!/usr/bin/env python3
"""Build one ~20-minute themed episode script from a collect_day.py material file.

Segments: cold open -> radar briefing -> day anchor (-> Friday zeitgeist) -> outro.
Each segment is written by the chosen writer with its own word budget, then
concatenated into one script file + .meta.json in the exact format
tts_vibevoice.py already renders.

Writers:
  local  — gemma on :8000 (the production default)
  claude — `claude -p` (claude-sonnet-5), no tools; same prompts, same material

Usage: build_episode.py --day thu|fri|sat --writer local|claude \
           --material material.json --script-out script.txt
"""
import argparse, json, os, re, subprocess, sys, urllib.request
from datetime import datetime, timezone

HERE = os.path.dirname(os.path.abspath(__file__))
LLM_URL = "http://localhost:8000/v1/chat/completions"
LLM = "gemma4-26b-a4b"
CLAUDE_MODEL = "claude-sonnet-5"

SPEECH_RULES = """SPEECH-SAFETY RULES (this text goes directly to a text-to-speech engine):
- Plain spoken prose ONLY. No markdown, no asterisks, no headers, no bullet lists.
- Never write citation brackets like [1]; name authors naturally or say "one earlier study".
- Never write URLs, arXiv IDs, file paths, or version strings.
- No mathematical notation or symbols: say "F one score" not "F1", "about eighty-seven
  percent" not "87%". Expand acronyms on first use. Numbers under twenty as words.
- Never attempt to pronounce Lakota or other Indigenous-language words; describe them
  ("the Lakota word for plums") or spell them letter by letter, naming diacritics.
  Machine pronunciation of these words teaches learners errors.
This is ONE SEGMENT of a continuous episode: no greeting, no sign-off, no
"welcome back" — open with at most a one-sentence transition, unless the segment
instructions say otherwise.
- Never narrate the show's structure or your own process: no meta commentary
  about the script, the sources, or what you are about to do; no filler
  signposts like "let's dive in" or "that wraps up this segment". Transition
  through content, not announcements."""

LISTENER = """The listener builds community-sovereign language AI for Lakota (OCR of
legacy texts, RAG grounded in community-approved books, per-stage evaluation,
community-governed data) and listens while driving."""

DIALOGUE_RULES = """This episode has TWO hosts in conversation:
- Speaker 1 leads: frames each topic, carries the narrative, addresses Mason directly.
- Speaker 2 is the analyst: asks the questions a sharp listener would ask, adds
  caveats and context, pushes back when a claim is thin.
FORMAT: every turn is ONE line starting with exactly "Speaker 1:" or "Speaker 2:"
followed by that host's words. Never put a speaker tag mid-line. No names, no
stage directions, no other formatting.
Make it a real conversation — reactions, short interjections, genuine questions,
occasional disagreement — never two alternating monologues. Turns should vary in
length; most under sixty words."""

DAY_META = {
    "thu": ("Roundup — Native language tech & data sovereignty", "weekly Native research and language-tech roundup"),
    "fri": ("GeoAI deep-dive + AI zeitgeist", "a geospatial AI deep-dive plus the week in general AI"),
    "sat": ("Week in review", "synthesis of everything the radar kept this week"),
}


def write_local(prompt, budget_words):
    body = {"model": LLM, "temperature": 0.3, "max_tokens": int(budget_words * 2.2) + 300,
            "messages": [{"role": "user", "content": prompt}]}
    req = urllib.request.Request(LLM_URL, data=json.dumps(body).encode(),
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=600) as r:
        return json.load(r)["choices"][0]["message"]["content"].strip()


def write_claude(prompt, budget_words):
    # tools off + neutral cwd: the writer must not explore the repo or comment on
    # its process — sentinel markers make extraction immune to any chatter around it
    prompt += ("\n\nWrite the segment between two marker lines: a line containing "
               "exactly <<<SCRIPT>>> before it and a line containing exactly "
               "<<<END>>> after it. Nothing but spoken prose between the markers.")
    r = subprocess.run(
        ["claude", "-p", "--model", CLAUDE_MODEL, "--disallowedTools",
         "Read,Write,Edit,Bash,Glob,Grep,WebSearch,WebFetch,Agent,TodoWrite"],
        input=prompt, capture_output=True, text=True, timeout=900, cwd="/tmp")
    if r.returncode != 0:
        raise RuntimeError(f"claude -p failed: {r.stderr[:400]}")
    m = re.search(r"<<<SCRIPT>>>\s*(.*?)\s*<<<END>>>", r.stdout, re.S)
    if not m:
        raise RuntimeError(f"claude -p output missing markers: {r.stdout[:300]}")
    return m.group(1).strip()


def items_block(items, cap=2500):
    out = []
    for it in items:
        why = it.get("why") or it.get("summary", "")
        out.append(f"- {it['title']} ({', '.join(it.get('authors', []))[:100]}): {why[:400]}")
    return "\n".join(out)[:cap * 10]


def segments_for(mat, speakers=1):
    """Return [(name, budget_words, prompt), ...] in episode order."""
    day, date = mat["day"], mat["date"]
    title, blurb = DAY_META[day]
    rules = SPEECH_RULES
    if speakers == 2:
        rules += ("\n\n" + DIALOGUE_RULES +
                  "\nWhen a segment says to start with a specific phrase, Speaker 1 says it.")
    segs = []

    anchor_names = {"thu": "the weekly Native language-tech roundup",
                    "fri": "a geospatial AI deep-dive, then the week in general AI",
                    "sat": "the week in review"}
    segs.append(("cold_open", 120, f"""{rules}

{LISTENER}

Write the COLD OPEN of today's episode, about 120 words. Exception to the
transition rule: start with exactly "Hey Mason!" then the date in natural
speech ({date}), then preview what's coming: a quick radar briefing over the
newest kept research, and then {anchor_names[day]}. Energetic but not cheesy.
End by leading into the radar briefing."""))

    segs.append(("radar", 550, f"""{rules}

{LISTENER}

Write the RADAR BRIEFING segment, about 550 words. These are the items the
research filter kept in the last few days. Group them by theme rather than
reading a list; for each item or theme, one or two sentences on what it is and
why it matters to the listener's work. If items are weak or repetitive,
prioritize ruthlessly and say less. If there are no items below, say the radar
was quiet and move on in two sentences.

ITEMS:
{items_block(mat['radar']) or '(none)'}"""))

    if day == "thu":
        segs.append(("roundup", 1900, f"""{rules}

{LISTENER}

Write the main segment, about 1900 words: the WEEKLY ROUNDUP of Native and
Indigenous language technology and data-sovereignty news. For each story: what
happened, who is doing it, and why it matters through a community-sovereignty
lens — what it says about who controls language data, what peer nations'
approaches suggest for Lakota work. These are news reports, not papers: be
honest about what is announced versus demonstrated. Prioritize; drop weak items
rather than padding. Weave stories into a narrative where they connect.

STORIES:
{json.dumps(mat['news'], indent=1)[:14000]}"""))

    elif day == "fri":
        g = mat["geoai"]
        segs.append(("geoai", 1350, f"""{rules}

The listener is also a geospatial practitioner: he builds visualizations and
tools with Sentinel-2 and other satellite imagery, and runs local models on his
own hardware.

Write the main segment, about 1350 words: a DEEP-DIVE on one geospatial AI
paper. Cover: the problem and why it exists; how the method works mechanically;
the handful of findings that matter, each with a sentence of context; honest
limitations; and close with what the listener should actually try with his own
imagery work, and what to ignore. Never recite more than two numbers in a row.

PAPER TITLE: {g['paper']['title']}
WHY PICKED: {g['reason']}
PAPER TEXT (truncated):
{g['text'][:30000]}"""))
        segs.append(("zeitgeist", 650, f"""{rules}

{LISTENER}

Write the AI ZEITGEIST segment, about 650 words: the week in general AI, for
someone who runs local models and builds language AI but skips the hype cycle.
Open with a one-sentence transition from the paper deep-dive that came before.
Pick only the developments that actually matter and frame each one: what does
this mean for local inference, small models, evaluation, or data governance.
Skepticism welcome where earned.

DEVELOPMENTS:
{json.dumps(mat['zeitgeist'], indent=1)[:10000]}"""))

    elif day == "sat":
        segs.append(("review", 1900, f"""{rules}

{LISTENER}

Write the main segment, about 1900 words: the WEEK IN REVIEW. Synthesize
everything the research radar kept this week — this is the one segment that
connects things instead of reporting them. What themes emerged across papers?
Which items relate to or contradict each other? What actually moved this week
in the listener's areas (low-resource NLP, RAG evaluation, OCR, data
sovereignty), and what should he carry into next week? Refer back to episodes
already covered this week (listed below) briefly rather than re-explaining
them. If the week was thin, say so and go deeper on fewer items.

THIS WEEK'S KEPT ITEMS:
{items_block(mat['week'], cap=1600)}

EPISODES ALREADY COVERED THIS WEEK:
{json.dumps(mat.get('episodes_this_week', []))}"""))

    covered = {"thu": [n["title"] for n in mat.get("news", [])],
               "fri": [mat["geoai"]["paper"]["title"]] if "geoai" in mat else [],
               "sat": [w["title"] for w in mat.get("week", [])[:8]]}[day]
    segs.append(("outro", 80, f"""{rules}

Write the OUTRO, about 80 words. Exception to the transition rule: this closes
the episode. One or two sentences wrapping up today ({blurb}), a light sign-off
addressed to Mason. No URLs, no "subscribe" boilerplate. You have all the
context you need — never ask for more. Today's episode covered a radar briefing
plus: {'; '.join(covered)[:600]}"""))
    return segs


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--day", required=True, choices=["thu", "fri", "sat"])
    ap.add_argument("--writer", required=True, choices=["local", "claude"])
    ap.add_argument("--material", required=True)
    ap.add_argument("--script-out", required=True)
    ap.add_argument("--speakers", type=int, default=1, choices=[1, 2])
    args = ap.parse_args()

    mat = json.load(open(args.material))
    assert mat["day"] == args.day, f"material file is for {mat['day']}, not {args.day}"
    write = write_local if args.writer == "local" else write_claude

    parts = []
    for name, budget, prompt in segments_for(mat, args.speakers):
        print(f"[episode] {args.day}/{args.writer}: writing {name} (~{budget}w)...",
              file=sys.stderr)
        parts.append(write(prompt, budget))
    script = "\n\n".join(parts)
    if args.speakers == 2:
        # a tag that slipped mid-line would be spoken aloud; force it to line start
        script = re.sub(r"(?<=\S)[ \t]+(Speaker\s*[12]\s*:)", r"\n\1", script)

    title, blurb = DAY_META[args.day]
    label = args.writer + (", 2 voices" if args.speakers == 2 else "")
    suffix = "_2v" if args.speakers == 2 else ""
    stamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M")
    open(args.script_out, "w").write(script)
    json.dump({"title": f"{title} — {mat['date']} [{label}]",
               "description": f"({label} writer) {blurb}",
               "mp3": f"{stamp}_{args.day}_{args.writer}{suffix}.mp3",
               "speakers": args.speakers},
              open(args.script_out + ".meta.json", "w"))
    print(f"[episode] {args.day}/{args.writer}: {len(script.split())} words "
          f"-> {args.script_out}")


if __name__ == "__main__":
    main()
