"""How much background noise each source has, and whether to offer the noise removal (audio.denoise).

For every video: the noise floor in its pauses (RMS of edit/regions/<stem>.json silences trimmed 60 ms,
measured now when the regions file is missing or belongs to another file), the speech level (RMS in the
speech regions) and their difference. With almost no pause (under 0.1 s) the noise is estimated on the
quietest 50 ms windows (+3 dB, they catch the dips of the noise). The channels are folded like render_cut.

  difference >= 30 dB  "limpo, não precisa"
  20 to 30 dB          "ruído leve"   (offer Remover ruído de fundo: Leve)
  under 20 dB          "ruído forte"  (offer Forte)

Prints one PT-BR line per video plus a recommendation, and writes edit/verify/noise_check.json.

It also measures, per video, saturation and room echo of the voice (helpers/voz_check.py): one "Voz" line per
video and the key "voz" in the JSON. Saturated voice does not come back whole; echo is treated lightly and judged
at the same perceived loudness (references/revisao.md, Voz). `--sem-voz` skips it.

Usage:
    uv run python helpers/noise_check.py <videos_dir | video ...> [--edit-dir DIR] [--json]
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from common import now_iso, require_binary, save_json  # noqa: E402
import denoise  # noqa: E402
import voz_check  # noqa: E402

VIDEO_EXT = {".mov", ".mp4", ".m4v", ".mkv", ".webm"}
ORDER = {"limpo": 0, "leve": 1, "forte": 2}


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


def collect(paths: list[Path]) -> list[Path]:
    out: list[Path] = []
    for p in paths:
        if p.is_dir():
            out += sorted(f for f in p.iterdir() if f.is_file() and f.suffix.lower() in VIDEO_EXT
                          and not f.name.startswith("."))
        elif p.is_file():
            out.append(p)
        else:
            raise FileNotFoundError(f"não encontrado: {p}")
    return list(dict.fromkeys(out))


def line_for(r: dict) -> str:
    name = r.get("stem", "?")
    if r.get("error"):
        return f"  {name}: {r['error']}"
    extra = " (quase sem pausa: fundo estimado nas partes mais quietas)" if r.get("noise_from") != "pausas" else ""
    return (f"  {name} · fundo {r['noise_db']:.1f} dB · voz {r['speech_db']:.1f} dB · diferença {r['snr_db']:.0f} dB · "
            f"{r['advice']}{extra}")


def recommendation(results: list[dict]) -> dict:
    measured = [r for r in results if r.get("level")]
    if not measured:
        return {"suggest": None, "text": "Não deu para medir o ruído (sem áudio ou sem fala)."}
    worst = max(measured, key=lambda r: (ORDER[r["level"]], -r["snr_db"]))
    if worst["level"] == "limpo":
        return {"suggest": "off", "worst": worst["stem"],
                "text": "Recomendação: nenhum vídeo precisa de limpeza de ruído (deixe Desligado)."}
    label = "Leve" if worst["level"] == "leve" else "Forte"
    return {"suggest": worst["suggest"], "worst": worst["stem"],
            "text": (f"Recomendação: ofereça Remover ruído de fundo {label} (o pior é {worst['stem']}, "
                     f"diferença de {worst['snr_db']:.0f} dB entre voz e fundo).")}


def main() -> int:
    ap = argparse.ArgumentParser(description="Mede o ruído de fundo de cada vídeo e diz se vale limpar a voz.")
    ap.add_argument("paths", nargs="+", type=Path, help="pasta dos vídeos ou arquivos de vídeo")
    ap.add_argument("--edit-dir", type=Path, default=None,
                    help="pasta edit (padrão: <pasta dos vídeos>/edit); usa regions/<nome>.json de lá")
    ap.add_argument("--json", action="store_true", help="imprime o JSON em vez das linhas")
    ap.add_argument("--sem-voz", action="store_true", help="não mede saturação nem eco da voz")
    args = ap.parse_args()
    utf8_console()
    require_binary("ffmpeg")
    require_binary("ffprobe")
    paths = [p.expanduser().absolute() for p in args.paths]
    try:
        videos = collect(paths)
    except FileNotFoundError as e:
        print(f"erro: {e}", file=sys.stderr)
        return 2
    if not videos:
        print("erro: nenhum vídeo encontrado (.mov, .mp4, .m4v, .mkv, .webm)", file=sys.stderr)
        return 2
    first = paths[0] if paths[0].is_dir() else videos[0].parent
    edit_dir = (args.edit_dir.expanduser().absolute() if args.edit_dir else first / "edit")
    results = [denoise.measure_source(v, edit_dir if edit_dir.is_dir() else None) for v in videos]
    if not args.sem_voz:
        for v, r in zip(videos, results):
            try:
                r["voz"] = voz_check.measure_voice(v)
            except Exception as exc:  # noqa: BLE001
                r["voz"] = {"error": f"não consegui medir a voz ({exc.__class__.__name__})"}
    rec = recommendation(results)
    doc = {"created_at": now_iso(), "edit_dir": str(edit_dir), "videos": results, "recommendation": rec,
           "thresholds_db": {"limpo": 30, "leve": 20}}
    if edit_dir.is_dir():
        try:
            save_json(edit_dir / "verify" / "noise_check.json", doc)
        except OSError:
            pass
    if args.json:
        print(json.dumps(doc, ensure_ascii=False, indent=2))
        return 0
    print("Ruído de fundo (fundo = volume nas pausas, voz = volume da fala):")
    for r in results:
        print(line_for(r))
    print(rec["text"])
    if not args.sem_voz:
        print("Voz (saturação e eco de sala, medidos na onda; confirme ouvindo os tempos citados):")
        for r in results:
            print(voz_check.line_for(r.get("stem", "?"), r.get("voz") or {"error": "sem medida"}))
    return 0


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