#!/usr/bin/env python3
"""Agora course batch transcription via Supadata + Tailscale Funnel.

Walks the local audio staging mirror, submits each file's public funnel URL to
Supadata (mode=generate), polls jobs, writes plain-text transcripts to a local
mirror. Resumable via state.json. Stops hard on 429 (quota shared with n8n).
"""
import json, os, subprocess, sys, time, urllib.parse, urllib.request

BASE = os.path.expanduser("~/agora-funnel-staging")
OUT = os.path.expanduser("~/agora-transcripts")
FUNNEL = "https://christians-mac-mini.echidna-morpho.ts.net"
STATE_F = os.path.join(BASE, "state.json")
CONCURRENCY = 3
POLL_SEC = 15

KEY = subprocess.run(["security", "find-generic-password", "-s", "supadata-api-key", "-w"],
                     capture_output=True, text=True).stdout.strip()
if not KEY:
    sys.exit("no supadata key in keychain")

def api(url, timeout=240, retries=3):
    req = urllib.request.Request(url, headers={"x-api-key": KEY})
    for attempt in range(retries):
        try:
            with urllib.request.urlopen(req, timeout=timeout) as r:
                return r.status, json.loads(r.read().decode())
        except urllib.error.HTTPError as e:
            body = e.read().decode()[:300]
            return e.code, {"error": body}
        except (TimeoutError, OSError) as e:
            if attempt == retries - 1:
                return 0, {"error": f"network: {e}"}
            time.sleep(10)

def load_state():
    if os.path.exists(STATE_F):
        return json.load(open(STATE_F))
    return {}

def save_state(s):
    json.dump(s, open(STATE_F, "w"), indent=1)

def audio_files():
    for root, _, files in os.walk(BASE):
        for f in sorted(files):
            if f.lower().endswith((".m4a", ".mp3")):
                yield os.path.relpath(os.path.join(root, f), BASE)

def txt_path(rel):
    p = os.path.join(OUT, rel)
    return os.path.splitext(p)[0] + ".txt"

def assemble(content):
    if isinstance(content, str):
        return content
    return "\n".join(c.get("text", "") for c in content)

def main():
    state = load_state()
    todo = [r for r in audio_files() if not os.path.exists(txt_path(r))
            and state.get(r, {}).get("status") != "failed-permanent"]
    print(f"{len(todo)} files to transcribe")
    active = {}  # rel -> jobId
    quota_hit = False

    while (todo or active) and not quota_hit:
        # submit up to CONCURRENCY
        while todo and len(active) < CONCURRENCY:
            rel = todo.pop(0)
            st = state.get(rel, {})
            if st.get("jobId") and st.get("status") not in ("completed", "failed"):
                active[rel] = st["jobId"]
                continue
            url = FUNNEL + "/" + urllib.parse.quote(rel)
            code, resp = api(f"https://api.supadata.ai/v1/transcript?url={urllib.parse.quote(url, safe='')}&text=true&mode=generate")
            if code == 429:
                print("429 QUOTA — stopping all submissions"); quota_hit = True
                todo.insert(0, rel); break
            if code in (200, 202) and "jobId" in resp:
                active[rel] = resp["jobId"]
                state[rel] = {"jobId": resp["jobId"], "status": "active"}
                print(f"submitted: {rel} -> {resp['jobId']}")
            elif code in (200, 202) and "content" in resp:  # small file, sync response
                _write(rel, resp); state[rel] = {"status": "completed"}
                print(f"sync-done: {rel}")
            else:
                print(f"submit-error {code}: {rel} {str(resp)[:150]}")
                state[rel] = {"status": "failed", "error": str(resp)[:300]}
            save_state(state)

        if not active:
            break
        time.sleep(POLL_SEC)
        for rel, jid in list(active.items()):
            code, resp = api(f"https://api.supadata.ai/v1/transcript/{jid}")
            if code == 429:
                print("429 on poll — pausing 60s"); time.sleep(60); continue
            status = resp.get("status")
            if status == "completed" or (code == 200 and "content" in resp and not status):
                _write(rel, resp)
                state[rel] = {"jobId": jid, "status": "completed"}
                del active[rel]
                print(f"completed: {rel}")
            elif status == "failed":
                state[rel] = {"jobId": jid, "status": "failed", "error": str(resp.get("error"))[:300]}
                del active[rel]
                print(f"FAILED: {rel} {resp.get('error')}")
            save_state(state)

    done = sum(1 for r in audio_files() if os.path.exists(txt_path(r)))
    print(f"RUN END. transcripts on disk: {done}. quota_hit={quota_hit}")

def _write(rel, resp):
    tp = txt_path(rel)
    os.makedirs(os.path.dirname(tp), exist_ok=True)
    with open(tp, "w") as f:
        f.write(assemble(resp.get("content", "")))

if __name__ == "__main__":
    main()
