"""Minimal Sonilo audio-ducking workflow.

Run:
    export SONILO_API_KEY=sk_...
    python audio-ducking.py

Optional:
    export SONILO_VOICE_URL=https://your-public-voice.wav
    export SONILO_MUSIC_URL=https://your-public-music.mp3

This script calls POST /v1/audio-ducking with voice_url and music_url, polls
GET /v1/tasks/{task_id}, and saves the mixed output as output.mp3. It never
creates placeholder output files. It classifies 401 as auth_invalid, 403 as
forbidden, 402 as insufficient_balance, 404 as not_found, and respects
Retry-After for 429.
"""

import os
import sys
import time
from pathlib import Path

import requests

API_BASE = "https://api.sonilo.com/v1"
DEFAULT_VOICE_URL = "https://raw.githubusercontent.com/mozilla/DeepSpeech/master/data/smoke_test/LDC93S1.wav"
DEFAULT_MUSIC_URL = "https://samplelib.com/mp3/sample-6s.mp3"
OUTPUT_FILE = Path("output.mp3")
USER_AGENT = "SoniloPythonExample/1.0 (+https://platform.sonilo.com/docs)"


def retry_after_seconds(resp, attempt):
    value = resp.headers.get("Retry-After")
    if value:
        try:
            return max(1, int(value))
        except ValueError:
            return 5
    return min(2**attempt, 30)


def rate_limit_reason(resp):
    """The API's own 429 sentence, which names the limit that was hit."""
    try:
        return str(resp.json().get("message") or "").strip()
    except ValueError:
        return ""


def classify_error(resp):
    body = resp.text
    code = f"http_{resp.status_code}"
    message = body
    try:
        payload = resp.json()
        code = payload.get("code", code)
        message = payload.get("message", body)
    except ValueError:
        pass

    if resp.status_code == 401:
        return "auth_invalid", (
            "auth_invalid: set a real SONILO_API_KEY and send "
            "Authorization: Bearer <key>. Do not retry until the key is fixed."
        )
    if resp.status_code == 403:
        return "forbidden", (
            "forbidden: the API key was understood, but this account cannot "
            "access audio ducking for this workspace."
        )
    if resp.status_code == 402 and code == "payment_required":
        return "insufficient_balance", (
            "insufficient_balance: add balance or use an eval key with enough "
            f"credits. API returned payment_required: {message}"
        )
    if resp.status_code == 404:
        return "not_found", (
            "not_found: check that API_BASE is https://api.sonilo.com/v1, not a "
            f"documentation URL or local proxy route. API returned: {message}"
        )
    return code, f"Sonilo request failed with HTTP {resp.status_code}: {message}"


def request_with_backoff(method, url, **kwargs):
    reason = ""
    for attempt in range(1, 6):
        timeout = kwargs.pop("timeout", 60)
        resp = requests.request(method, url, timeout=timeout, **kwargs)
        if resp.status_code != 429:
            return resp
        reason = rate_limit_reason(resp)
        delay = retry_after_seconds(resp, attempt)
        resp.close()
        print(f"Rate limited: {reason} Retrying after {delay}s...", file=sys.stderr)
        time.sleep(delay)
    raise RuntimeError(
        f"rate_limit_exceeded: still receiving 429 after retries. {reason}".strip()
    )


def require_ok(resp):
    if resp.ok:
        return
    _, message = classify_error(resp)
    raise RuntimeError(message)


def output_url_from_task(task):
    for key in ("output_url", "audio_url", "music_processed_url"):
        value = task.get(key)
        if isinstance(value, str) and value.startswith(("http://", "https://")):
            return value
    audio = task.get("audio")
    if isinstance(audio, dict):
        value = audio.get("url")
        if isinstance(value, str) and value.startswith(("http://", "https://")):
            return value
    result = task.get("result")
    if isinstance(result, dict):
        return output_url_from_task(result)
    return None


def main():
    api_key = os.environ.get("SONILO_API_KEY")
    if not api_key:
        raise RuntimeError("Set SONILO_API_KEY before running this script")

    headers = {
        "Authorization": f"Bearer {api_key}",
        "User-Agent": USER_AGENT,
    }
    voice_url = os.environ.get("SONILO_VOICE_URL", DEFAULT_VOICE_URL)
    music_url = os.environ.get("SONILO_MUSIC_URL", DEFAULT_MUSIC_URL)

    submit = request_with_backoff(
        "POST",
        f"{API_BASE}/audio-ducking",
        headers=headers,
        files={
            "voice_url": (None, voice_url),
            "music_url": (None, music_url),
            "output_format": (None, "mp3"),
        },
        timeout=120,
    )
    require_ok(submit)
    payload = submit.json()
    task_id = payload["task_id"]
    print(f"Submitted task {task_id}")

    for _ in range(120):
        task_resp = request_with_backoff(
            "GET",
            f"{API_BASE}/tasks/{task_id}",
            headers=headers,
            timeout=30,
        )
        require_ok(task_resp)
        task = task_resp.json()
        status = str(task.get("status") or "").lower()
        if status in {"succeeded", "completed", "success"}:
            output_url = output_url_from_task(task)
            if not output_url:
                raise RuntimeError("missing_output_url: succeeded task had no output URL")
            output_resp = request_with_backoff(
                "GET",
                output_url,
                headers={"User-Agent": USER_AGENT},
                timeout=120,
            )
            require_ok(output_resp)
            if not output_resp.content:
                raise RuntimeError("empty_audio: downloaded output was empty")
            OUTPUT_FILE.write_bytes(output_resp.content)
            print(f"Saved {OUTPUT_FILE} ({OUTPUT_FILE.stat().st_size} bytes)")
            return
        if status in {"failed", "canceled", "cancelled"}:
            error = task.get("error") or {}
            code = error.get("code", "task_failed") if isinstance(error, dict) else "task_failed"
            message = (
                error.get("message", "Sonilo audio-ducking task failed")
                if isinstance(error, dict)
                else str(error)
            )
            raise RuntimeError(f"{code}: {message}")
        time.sleep(3)

    raise RuntimeError("timeout: task did not finish within 6 minutes")


if __name__ == "__main__":
    try:
        main()
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        sys.exit(1)
