"""Turn every episode of a podcast feed into speaker-labelled notes with Patchwell.

    PATCHWELL_KEY=pw_live_... python podcast_to_notes.py https://example.com/feed.xml notes/ [md|docx|srt|vtt|txt|csv]

Safe to run on a schedule (cron, Task Scheduler, GitHub Actions): each episode's id is sent as the Idempotency-Key,
so a retry or a second run never pays twice, and episodes whose notes already exist are skipped. Standard library
only. https://patchwell.io/automations
"""
import hashlib
import json
import os
import sys
import time
import urllib.error
import urllib.request
import xml.etree.ElementTree as ET
from pathlib import Path

API = os.environ.get("PATCHWELL_API", "https://patchwell.io")
KEY = os.environ.get("PATCHWELL_KEY", "")


def call(method, path, body=None, headers=None, raw=False):
    data = None if body is None else json.dumps(body).encode()
    req = urllib.request.Request(API + path, data=data, method=method, headers={
        "Authorization": f"Bearer {KEY}", "Content-Type": "application/json", "User-Agent": "patchwell-podcast-notes/1",
        **(headers or {})})
    for attempt in range(5):                     # retries are safe: the Idempotency-Key makes them free
        try:
            with urllib.request.urlopen(req, timeout=180) as r:
                out = r.read()
                return out if raw else json.loads(out)
        except urllib.error.HTTPError as e:
            if e.code in (409, 429, 500, 502, 503, 504) and attempt < 4:
                time.sleep(int(e.headers.get("Retry-After", 5 * (attempt + 1))))
                continue
            raise SystemExit(f"{method} {path} failed: HTTP {e.code} {e.read()[:300].decode('utf-8', 'replace')}")
        except (urllib.error.URLError, TimeoutError):
            if attempt < 4:
                time.sleep(5 * (attempt + 1))
                continue
            raise


def episodes(feed_url):
    with urllib.request.urlopen(urllib.request.Request(feed_url, headers={"User-Agent": "patchwell-podcast-notes/1"}),
                                timeout=60) as r:
        root = ET.fromstring(r.read())
    for item in root.iter("item"):
        enc = item.find("enclosure")
        if enc is None or not enc.get("url"):
            continue
        guid = (item.findtext("guid") or enc.get("url")).strip()
        yield {"id": guid, "title": (item.findtext("title") or guid).strip(), "url": enc.get("url")}


def transcribe(ep):
    key = "podcast-" + hashlib.sha256(ep["id"].encode()).hexdigest()[:32]
    r = call("POST", "/v1/transcribe", {"url": ep["url"]}, {"Idempotency-Key": key})
    while r.get("status") == "running":           # long episodes: check back every 30 seconds
        time.sleep(30)
        r = call("GET", f"/v1/jobs/{r['job_id']}")
    if r.get("error") or r.get("status") == "failed":
        raise SystemExit(f"{ep['title']}: {r.get('error')}")
    return r


def main():
    if len(sys.argv) < 3 or not KEY:
        raise SystemExit(__doc__)
    feed, out_dir = sys.argv[1], Path(sys.argv[2])
    fmt = sys.argv[3] if len(sys.argv) > 3 else "md"
    out_dir.mkdir(parents=True, exist_ok=True)
    for ep in episodes(feed):
        safe = "".join(c if c.isalnum() or c in " -_" else "_" for c in ep["title"])[:80].strip() or "episode"
        target = out_dir / f"{safe}.{fmt}"
        if target.exists():
            print(f"skip   {ep['title']} (notes already there)")
            continue
        r = transcribe(ep)
        target.write_bytes(call("POST", f"/v1/export?format={fmt}", r, raw=True))
        who = ", ".join(f"{s['label']} {s.get('talk_s', 0):.0f}s" for s in r.get("speakers") or [])
        print(f"done   {ep['title']} -> {target}  ({who}; charged ${r.get('billing', {}).get('price', 0)})")


if __name__ == "__main__":
    main()
