"""Optional custom sound effect made in the buyer's OWN ElevenLabs account (Sound Effects, paid per use).

For a sound the built-in CC0 kit does not have (its 24 names are listed in references/phase2.md). Same rules as
gen_music.py: the buyer's own key (ELEVENLABS_API_KEY in the environment or in `.env` in the skill root),
never printed or saved; without it, exit 2 with the message that tells the agent to ask for it. The file goes
to <edit>/assets/sfx_<name>.mp3 with an entry in <edit>/assets/credits.json, and is used as a file path in
`sfx.events` (`{"at": 5.07, "name": "assets/sfx_<name>.mp3"}`) or in `inserts[].sfx`. The file starts on the
event frame; use `anchor_s` when its strong point is later in the file.

Usage:
    uv run python helpers/gen_sfx.py "<sound in English>" --edit-dir DIR [--seconds 1.5]
        [--out NAME] [--prompt-influence 0.5] [--loop] [--variations N] [--dry-run]
"""

from __future__ import annotations

import argparse
import math
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import elevenlabs_api as el  # noqa: E402

MODEL = "eleven_text_to_sound_v2"
MIN_SECONDS = 0.5                        # API: duration_seconds from 0.5 (up to 30); effects here stay short
MAX_SECONDS = 5.0
DEFAULT_SECONDS = 1.5
DEFAULT_INFLUENCE = 0.5                  # API default 0.3; a short effect should follow the text closely
MAX_VARIATIONS = 3
PREFIX = "sfx_"
SOURCE = "ElevenLabs Sound Effects (conta do usuário)"
EXAMPLE = "soft paper page flip, close and clean, no music"
TIMEOUT = 120


def pick_seconds(value: float) -> tuple[float, list[str]]:
    if not math.isfinite(value) or value <= 0:
        raise el.ElevenError("--seconds precisa ser um número maior que zero (ex.: 1.5)", 2)
    notes = []
    if value < MIN_SECONDS:
        notes.append(f"efeito sonoro tem no mínimo {MIN_SECONDS:g} s: ajustei {value:g} s para {MIN_SECONDS:g} s")
        value = MIN_SECONDS
    elif value > MAX_SECONDS:
        notes.append(f"efeito sonoro aqui vai até {MAX_SECONDS:g} s: ajustei {value:g} s para {MAX_SECONDS:g} s "
                     "(para som mais longo, use gen_music.py)")
        value = MAX_SECONDS
    return round(value, 2), notes


def output_names(edit: Path, text: str, out: str | None, count: int) -> list[Path]:
    stem = el.output_stem(out) if out else el.slugify(text)
    if not stem.lower().startswith(PREFIX):
        stem = PREFIX + stem
    names = [f"{stem}.mp3"] if count == 1 else [f"{stem}_{i}.mp3" for i in range(1, count + 1)]
    return el.free_paths(edit / "assets", names)


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description="Gera um efeito sonoro com IA na conta ElevenLabs da própria pessoa "
                                             "(opcional, pago), para um som que o kit pronto não tem.")
    ap.add_argument("text", help="descrição do som, de preferência em inglês")
    ap.add_argument("--edit-dir", required=True, help="pasta edit da sessão")
    ap.add_argument("--seconds", type=float, default=DEFAULT_SECONDS,
                    help=f"duração em segundos, de {MIN_SECONDS:g} a {MAX_SECONDS:g} (padrão {DEFAULT_SECONDS:g})")
    ap.add_argument("--out", help=f"nome do arquivo em assets/ (padrão: {PREFIX}<texto>.mp3; o prefixo {PREFIX} "
                                  "entra sozinho)")
    ap.add_argument("--prompt-influence", type=float, default=DEFAULT_INFLUENCE,
                    help=f"de 0 a 1: quanto o som segue o texto (padrão {DEFAULT_INFLUENCE:g})")
    ap.add_argument("--loop", action="store_true", help="som que emenda sem corte quando repete")
    ap.add_argument("--variations", type=int, default=1,
                    help=f"quantas versões gerar, uma requisição cada (padrão 1, no máximo {MAX_VARIATIONS})")
    ap.add_argument("--dry-run", action="store_true",
                    help="só mostra o pedido e o custo estimado, sem gerar e sem internet")
    args = ap.parse_args(argv)

    key = None
    saved: list[str] = []

    def say(*parts: str, flush: bool = False) -> None:
        print(el.redact(" ".join(parts), key), flush=flush)

    try:
        text = (args.text or "").strip()
        if not text:
            raise el.ElevenError("a descrição do som está vazia", 2)
        el.check_prompt(text, EXAMPLE)
        if not 1 <= args.variations <= MAX_VARIATIONS:
            raise el.ElevenError(f"--variations vai de 1 a {MAX_VARIATIONS} (cada uma é uma cobrança)", 2)
        influence = args.prompt_influence
        if not math.isfinite(influence) or not 0 <= influence <= 1:
            raise el.ElevenError("--prompt-influence vai de 0 a 1 (ex.: 0.5)", 2)
        edit = el.edit_folder(args.edit_dir)
        seconds, notes = pick_seconds(args.seconds)
        key = el.api_key()
        el.check_no_key(text, key)
        if not key and not args.dry_run:
            raise el.ElevenError(el.missing_key_message(
                "Gerar efeito sonoro", "os 24 efeitos prontos do kit (whoosh, cash, notification, error, grave, "
                "teclado e os outros da lista em references/phase2.md) já vêm com a skill"), 2)
        paths = output_names(edit, text, args.out, args.variations)
        payload = {"text": text, "duration_seconds": seconds, "prompt_influence": round(influence, 3),
                   "loop": bool(args.loop), "model_id": MODEL}
        for note in notes:
            say(f"aviso: {note}")
        if el.looks_portuguese(text):
            say("aviso: a descrição parece estar em português. A ElevenLabs entende melhor em inglês "
                f"(ex.: '{EXAMPLE}').")

        each = el.credits_estimate("sfx", seconds)
        if args.dry_run:
            say(f"efeito: {seconds:g} s{', em loop' if args.loop else ''}, influência do texto {influence:g}")
            say("arquivos: " + ", ".join(f"assets/{p.name}" for p in paths))
            say(f"custo estimado: cerca de {each} créditos por versão, {each * len(paths)} no total "
                f"(a ElevenLabs cobra cerca de {el.SFX_CREDITS_PER_SECOND} créditos por segundo de efeito; "
                "o valor certo aparece no painel dela)")
            say(el.PAID_PLAN_LINE)
            if not key:
                say(f"chave: ausente. Antes de gerar, peça a {el.KEY_NAME} "
                    "(veja a mensagem que sai sem --dry-run).")
            say("pedido (não enviado):")
            say(el.dry_run_request(el.SFX_URL, payload, key))
            return 0

        for i, path in enumerate(paths, 1):
            say(f"gerando efeito {i}/{len(paths)} ({seconds:g} s) na conta ElevenLabs da pessoa...", flush=True)
            data = el.post_audio(el.SFX_URL, payload, key or "", "Sound Effects", TIMEOUT)
            rel = f"assets/{path.name}"
            el.store(edit, path, data, el.new_credit(rel, SOURCE, text, seconds, loop=bool(args.loop),
                                                     prompt_influence=round(influence, 3), model=MODEL))
            saved.append(rel)
            say(f"ok: {rel}  {seconds:g} s  {len(data) // 1024} KB", flush=True)
        say(f"créditos anotados em assets/credits.json ({el.COMMERCIAL_NOTE})")
        say(f'em sfx.events: {{"at": 5.07, "name": "{saved[0]}"}} (o começo do arquivo cai no "at"; '
            'use "anchor_s" se o ponto forte vier depois)')
        return 0
    except Exception as e:  # noqa: BLE001  (never a traceback: its text could carry the key)
        if not isinstance(e, el.ElevenError):
            e = el.unexpected(e)
        if saved:
            e = el.ElevenError(f"{e} Já ficaram salvos: {', '.join(saved)}.", e.code)
        el.print_error(e, key)
        return e.code


if __name__ == "__main__":
    el.utf8_console()
    sys.exit(main())
