"""Renders one animated template (helpers/motion) to check it: transparent .mov, PNG frames, an .mp4 over a
background and a contact sheet. The compose uses the same renderer (SPEC Contract 12).

Usage:
    uv run python helpers/motion_render.py --list [--json]
    uv run python helpers/motion_render.py --describe contador
    uv run python helpers/motion_render.py --template contador --params '{"value": 1904152.87, "prefix": "R$"}' \\
        --out DIR [--formato mov,sheet,mp4,png,clip] [--duracao 3] [--fps 30] [--tamanho 1080x1920] \\
        [--cor '#FFC400'] [--regiao centro | --regiao 140,640,800,640] [--fundo frame.png] [--quadros 8]
    --params-file F  reads the params from a JSON file (easier with R$ and quotes).

The last stdout line is a JSON object with the files, the frames and the sound cues.
Exit 2 for things to fix (unknown template, bad params), 1 for ffmpeg or disk trouble.
"""

from __future__ import annotations

import argparse
import json
import sys
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from motion import engine as E  # noqa: E402
from motion import registry as Rg  # noqa: E402
from motion import render as R  # noqa: E402
from motion.engine import MotionError  # noqa: E402

FORMATS = ("mov", "sheet", "mp4", "png", "clip")


def _utf8() -> None:
    for stream in (sys.stdout, sys.stderr):
        try:
            stream.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]
        except (AttributeError, ValueError):
            pass


def parse_size(text: str) -> tuple[int, int]:
    try:
        w, h = (int(v) for v in str(text).lower().split("x"))
    except ValueError:
        raise MotionError(f"--tamanho inválido: {text!r} (ex.: 1080x1920)") from None
    if w < 16 or h < 16 or w > 4096 or h > 4096:
        raise MotionError(f"--tamanho fora do limite: {text!r}")
    return w, h


def parse_region(text: str | None):
    if text is None:
        return None
    t = text.strip()
    if "," in t:
        parts = t.replace("[", "").replace("]", "").split(",")
        try:
            return [float(v) for v in parts]
        except ValueError:
            raise MotionError(f"--regiao inválida: {text!r} (use um nome ou x,y,largura,altura)") from None
    return t


def load_params(args) -> dict:
    if args.params and args.params_file:
        raise MotionError("use --params ou --params-file, não os dois")
    raw = None
    if args.params_file:
        p = Path(args.params_file)
        if not p.exists():
            raise MotionError(f"arquivo de params não encontrado: {p}")
        raw = p.read_text(encoding="utf-8")
    elif args.params:
        raw = args.params
    if raw is None:
        return {}
    try:
        data = json.loads(raw)
    except ValueError as e:
        raise MotionError(f"params não é um JSON válido ({e}). Dica: use --params-file com o JSON num arquivo") from None
    if not isinstance(data, dict):
        raise MotionError("params precisa ser um objeto JSON, ex.: {\"value\": 37}")
    return data


def cmd_list(as_json: bool) -> int:
    found, errs = Rg.discover()
    if as_json:
        print(json.dumps({"templates": [t.describe() for t in found.values()], "errors": errs}, ensure_ascii=False))
        return 0
    if not found:
        print("nenhum modelo de animação encontrado")
    for t in sorted(found.values(), key=lambda x: x.id):
        print(f"{t.id:18s} {t.label} · {t.default_duration:g} s · {t.description}")
    for e in errs:
        print(f"aviso: {e}", file=sys.stderr)
    return 0


def _limits(spec: dict) -> list[str]:
    extra = []
    if "choices" in spec:
        extra.append("opções: " + ", ".join(map(str, spec["choices"])))
    if "min" in spec or "max" in spec:
        lo = E.format_number(spec["min"], E.auto_decimals(spec["min"])) if "min" in spec else "-"
        hi = E.format_number(spec["max"], E.auto_decimals(spec["max"])) if "max" in spec else "-"
        extra.append(f"de {lo} a {hi}")
    if "max_len" in spec:
        extra.append(f"até {spec['max_len']} letras")
    if "min_items" in spec or "max_items" in spec:
        extra.append(f"de {spec.get('min_items', 0)} a {spec.get('max_items', '?')} itens")
    if spec.get("nullable"):
        extra.append("pode ser null")
    return extra


def cmd_describe(tid: str) -> int:
    t = Rg.get(tid)
    print(f"{t.id}: {t.label}")
    print(t.description)
    if t.use_when:
        print(f"Quando usar: {t.use_when}")
    if t.notes:
        print(f"Atenção: {t.notes}")
    region = t.default_region if isinstance(t.default_region, str) else list(t.default_region)
    print(f"Duração padrão: {t.default_duration:g} s · região padrão: {region} · saída: {t.exit}")
    print("Parâmetros:")
    for name, spec in t.all_params().items():
        extra = _limits(spec)
        default = json.dumps(spec["default"], ensure_ascii=False) if "default" in spec else "obrigatório"
        print(f"  {name} ({spec['type']}, padrão {default}){' · ' + '; '.join(extra) if extra else ''}: "
              f"{spec.get('label_pt', '')}")
        if spec.get("type") == "list" and isinstance(spec.get("item"), dict):
            item = spec["item"]
            if item.get("type") == "object":
                for fname, fspec in (item.get("fields") or {}).items():
                    fextra = _limits(fspec)
                    fdefault = (json.dumps(fspec["default"], ensure_ascii=False) if "default" in fspec
                                else "obrigatório")
                    print(f"    cada item.{fname} ({fspec['type']}, padrão {fdefault})"
                          f"{' · ' + '; '.join(fextra) if fextra else ''}: {fspec.get('label_pt', '')}")
            else:
                iextra = _limits(item)
                if iextra:
                    print(f"    cada item ({item['type']}): {'; '.join(iextra)}")
    print("Sons: " + (", ".join(f"{c['name']} em {c.get('at', 0):g} s" for c in t.sfx) or "nenhum"))
    if t.example:
        print("Exemplo de params: " + json.dumps(t.example, ensure_ascii=False))
    return 0


def main(argv: list[str] | None = None) -> int:
    _utf8()
    ap = argparse.ArgumentParser(description="Renderiza um modelo de animação (contador, lista, preço...) para conferir.")
    ap.add_argument("--list", action="store_true", help="lista os modelos disponíveis")
    ap.add_argument("--json", action="store_true", help="com --list: saída em JSON")
    ap.add_argument("--describe", metavar="ID", help="mostra os parâmetros de um modelo")
    ap.add_argument("--template", "-t", help="id do modelo (veja --list)")
    ap.add_argument("--params", help="parâmetros em JSON, ex.: '{\"value\": 37}'")
    ap.add_argument("--params-file", help="arquivo .json com os parâmetros")
    ap.add_argument("--out", "-o", help="pasta de saída")
    ap.add_argument("--formato", default="mov,sheet",
                    help=f"o que gerar, separado por vírgula: {', '.join(FORMATS)} (padrão mov,sheet)")
    ap.add_argument("--duracao", type=float, help="segundos na tela (padrão: o do modelo)")
    ap.add_argument("--fps", type=int, default=30)
    ap.add_argument("--tamanho", default="1080x1920", help="tamanho da tela, ex.: 1080x1920 ou 540x960")
    ap.add_argument("--cor", default=E.DEFAULT_ACCENT, help="cor de destaque do vídeo (#RRGGBB)")
    ap.add_argument("--regiao", help="nome (topo, centro, baixo, faixa_topo, tela) ou x,y,largura,altura em 1080x1920")
    ap.add_argument("--fundo", help="imagem de fundo para a folha e o .mp4 (ex.: um quadro do corte)")
    ap.add_argument("--quadros", type=int, default=8, help="quadros na folha (padrão 8)")
    ap.add_argument("--nome", help="nome base dos arquivos (padrão: o id do modelo)")
    args = ap.parse_args(argv)

    try:
        if args.list:
            return cmd_list(args.json)
        if args.describe:
            return cmd_describe(args.describe)
        if not args.template:
            raise MotionError("diga o modelo com --template (veja --list)")
        if not args.out:
            raise MotionError("diga a pasta de saída com --out")
        formats = [f.strip() for f in args.formato.split(",") if f.strip()]
        bad = [f for f in formats if f not in FORMATS]
        if bad or not formats:
            raise MotionError(f"--formato inválido: {', '.join(bad) or '(vazio)'} (use {', '.join(FORMATS)})")
        E.rgba(args.cor)
        if args.quadros < 1 or args.quadros > 48:
            raise MotionError("--quadros precisa ficar entre 1 e 48")
        if args.nome is not None and (not args.nome.strip() or any(c in args.nome for c in '/\\:*?"<>|')):
            raise MotionError("--nome precisa ser um nome de arquivo simples, sem barras")
        params = load_params(args)
        t0 = time.time()
        item = R.plan_item(args.template, params, fps=args.fps, canvas=parse_size(args.tamanho),
                           region=parse_region(args.regiao), duration=args.duracao, accent=args.cor)
        out = Path(args.out).expanduser().resolve()
        out.mkdir(parents=True, exist_ok=True)
        base = args.nome or item.template.id
        result: dict = {"template": item.template.id, "frames": item.frames, "fps": item.fps,
                        "duration": round(item.frames / item.fps, 4), "canvas": list(item.canvas),
                        "region": [round(v, 2) for v in item.region], "params": item.params,
                        "sfx": R.sfx_cues(item, check_names=True), "files": {}}
        print(f"{item.template.label}: {item.frames} quadros ({E.format_number(item.frames / item.fps, 2)} s) "
              f"em {item.canvas[0]}x{item.canvas[1]}")
        if "mov" in formats:
            result["files"]["mov"] = str(R.write_mov(item, out / f"{base}.mov"))
        if "clip" in formats:
            clip = R.render_clip(item, out, cache=False)
            result["files"]["clip"] = clip["file"]
            result["clip"] = {k: clip[k] for k in ("x", "y", "w", "h", "ink")}
        if "png" in formats:
            pngs = R.write_pngs(item, out / f"{base}_quadros")
            result["files"]["png"] = str(out / f"{base}_quadros")
            result["png_count"] = len(pngs)
        if "mp4" in formats:
            result["files"]["mp4"] = str(R.write_mp4(item, out / f"{base}.mp4", args.fundo))
        if "sheet" in formats:
            result["files"]["sheet"] = str(R.contact_sheet(item, out / f"{base}_folha.png", args.quadros, args.fundo))
        result["seconds"] = round(time.time() - t0, 2)
        for kind, path in result["files"].items():
            print(f"ok    {kind}: {path}")
        sounds = ", ".join(f"{c['name']} no quadro {c['frame']}" for c in result["sfx"]) or "nenhum"
        print(f"sons sugeridos: {sounds}")
        for w in item.warnings:
            print(f"aviso: {w}")
        print(f"pronto em {result['seconds']:.1f} s")
        print(json.dumps(result, ensure_ascii=False))
        return 0
    except MotionError as e:
        print(f"erro: {e}", file=sys.stderr)
        return e.code
    except KeyboardInterrupt:
        print("interrompido", file=sys.stderr)
        return 130
    except OSError as e:
        print(f"erro: {e}", file=sys.stderr)
        return 1


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