#!/usr/bin/env python3
"""Preview a CSV batch; --execute calls the direct ElevenLabs SFX v3 API."""

import argparse
import csv
import json
import math
import os
import re
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("csv_file", type=Path)
    parser.add_argument("--out", type=Path, default=Path("sfx-output"))
    parser.add_argument("--max-requests", type=int, default=10)
    parser.add_argument("--max-seconds", type=float, default=60)
    parser.add_argument("--execute", action="store_true")
    args = parser.parse_args()
    if args.max_requests < 1 or not math.isfinite(args.max_seconds) or args.max_seconds <= 0:
        raise SystemExit("Request and duration caps must be positive and finite.")

    jobs = []
    seen = set()
    try:
        with args.csv_file.open(encoding="utf-8-sig", newline="") as source:
            reader = csv.DictReader(source)
            required = {"id", "prompt", "duration_seconds"}
            if not required.issubset(reader.fieldnames or []):
                raise SystemExit("CSV needs id, prompt and duration_seconds columns.")
            for line, row in enumerate(reader, start=2):
                name = (row.get("id") or "").strip()
                prompt = (row.get("prompt") or "").strip()
                if None in row or not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}", name):
                    raise SystemExit(f"Invalid CSV fields or filename ID at line {line}.")
                # Case-insensitive uniqueness also protects typical macOS/Windows filesystems.
                if name.casefold() in seen or not prompt:
                    raise SystemExit(f"Duplicate ID or empty prompt at line {line}.")
                try:
                    duration = float(row["duration_seconds"])
                except (ValueError, TypeError):
                    raise SystemExit(f"Invalid duration at line {line}.") from None
                if not math.isfinite(duration) or not 0.5 <= duration <= 30:
                    raise SystemExit(f"Duration must be 0.5 to 30 seconds at line {line}.")
                seen.add(name.casefold())
                jobs.append((name, {
                    "text": prompt,
                    "model_id": "eleven_text_to_sound_v3",
                    "duration_seconds": duration,
                    "prompt_influence": 0.3,
                }))
    except (OSError, UnicodeError, csv.Error):
        raise SystemExit("Could not read the CSV; check its path and UTF-8 format.") from None

    total_seconds = sum(body["duration_seconds"] for _, body in jobs)
    if not jobs or len(jobs) > args.max_requests or total_seconds > args.max_seconds:
        raise SystemExit("Empty batch or request/duration cap exceeded; no requests sent.")
    # Validate the entire plan before the first request. Do not overwrite an old result or receipt.
    for name, _ in jobs:
        for suffix in (".mp3", ".submitted.json"):
            path = args.out / (name + suffix)
            if path.exists() or path.is_symlink():
                raise SystemExit(f"Existing result/receipt for {name}; review before resubmitting.")

    print(f"Plan: {len(jobs)} requests, {total_seconds:g} specified seconds, sequential execution.")
    for name, body in jobs:
        print(f"  {name}.mp3: {body['duration_seconds']:g}s")
    if not args.execute:
        print("Preview only: no API calls or output files. Add --execute to submit paid requests.")
        return

    key = os.environ.get("ELEVENLABS_API_KEY", "").strip()
    if not key:
        raise SystemExit("Set ELEVENLABS_API_KEY in your server environment before execution.")
    if not key.isascii() or any(ord(char) < 33 or ord(char) > 126 for char in key):
        raise SystemExit("API key must contain printable ASCII with no whitespace.")
    args.out.mkdir(parents=True, exist_ok=True)
    for name, body in jobs:
        receipt = args.out / f"{name}.submitted.json"
        output = args.out / f"{name}.mp3"
        # Write a marker first: a timeout can leave billing uncertain. Never auto-retry the POST.
        try:
            with receipt.open("x", encoding="utf-8") as target:
                json.dump({"status": "submitted", "request": body}, target, ensure_ascii=False, indent=2)
            request = Request(
                "https://api.elevenlabs.io/v1/sound-generation?output_format=mp3_44100_128",
                data=json.dumps(body).encode("utf-8"),
                headers={"xi-api-key": key, "Content-Type": "application/json"},
                method="POST",
            )
            with urlopen(request, timeout=120) as response:
                if not response.headers.get_content_type().startswith("audio/"):
                    raise SystemExit(f"{name}: unexpected content type; stopped without retrying.")
                audio = response.read()
                if not audio:
                    raise SystemExit(f"{name}: empty audio; stopped without retrying.")
            with output.open("xb") as target:
                target.write(audio)
            receipt.write_text(json.dumps({
                "status": "complete", "request": body, "output": output.name,
            }, ensure_ascii=False, indent=2), encoding="utf-8")
        except HTTPError as error:
            raise SystemExit(f"{name}: HTTP {error.code}; stopped. Review the receipt and provider history before retrying.") from None
        except (URLError, TimeoutError, OSError):
            raise SystemExit(f"{name}: request or file operation did not complete. Check provider history; no automatic retry.") from None
        print(f"Saved {output.name}")


if __name__ == "__main__":
    main()
