"""Every check of a rendered cut at once: speech, breaths, edges/levels and the junction sheet.

Run one after the other these took ~32 s per round on a 2:20 cut (transcription 11 s, verify
8 s, junction sheet 13 s). They do not depend on each other, so they run side by side and the
round costs about as much as the slowest one:

    A. transcribe.py cut.mp4 (the speech of the cut, printed trecho by trecho next to its quote)
    B. respiros.py cut.mp4 (every silence of 0.25 s or more still in the cut: the André rule is none)
    C. verify_cut.py --sem-aprovacao (edges in speech, clicks, repeats, levels)
    D. contact_sheet.py --junctions

Output: each check's own report, in that order, and then the speech of the cut: for every trecho of
edl_manual.json (consecutive ranges with the same beat and quote), what was written in `quote` and what the
cut says, in short phrases with the time each starts in cut.mp4. A trecho whose words differ from its quote is
marked "≠ confira" with the words missing and the words in excess. The same speech goes to
<edit>/fala_do_corte.md.

A breath found by B is not an error of the tools: split the range there in edl_manual.json or rerun
corte_por_onda.py with a higher --limiar (references/corte.md). A silence inside a range kept whole on purpose
("dry": false) is listed apart and does not count.

Usage:
    uv run --directory '<SKILL>' python helpers/conferir_corte.py '<EDIT>' [--prompt 'Hubla, Kiwify'] [--min-silence 0.7]
"""

from __future__ import annotations

import argparse
import difflib
import json
import re
import subprocess
import sys
import time
import unicodedata
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

HELPERS = Path(__file__).resolve().parent
SKILL = HELPERS.parent
sys.path.insert(0, str(HELPERS))
from common import fmt_time, load_json  # noqa: E402
from pack_transcripts import cut_phrases, group_into_phrases, render_cut_markdown, split_long  # noqa: E402


def run(cmd: list[str]) -> tuple[int, str, float]:
    t0 = time.monotonic()
    proc = subprocess.run([sys.executable, *cmd], capture_output=True, text=True, cwd=HELPERS.parent)
    return proc.returncode, (proc.stdout + proc.stderr).strip(), time.monotonic() - t0


def cmd(helper: str, *args: str) -> str:
    """A command the agent can paste from any folder (the skill's own Python, by --directory)."""
    return f"uv run --directory '{SKILL}' python helpers/{helper} " + " ".join(args)


# ------------------------------------------------------------------ speech of the cut, trecho by trecho

def _norm_words(text: str) -> list[str]:
    t = unicodedata.normalize("NFKD", (text or "").lower())
    t = "".join(c for c in t if not unicodedata.combining(c))
    return re.findall(r"[a-z0-9]+", t)


def trechos(timeline: dict, edl: dict) -> list[dict]:
    """Consecutive segments with the same beat and quote are one trecho of edl_manual.json (corte_por_onda
    splits a range at its breaths and copies beat and quote to every piece)."""
    dry = {r.get("id"): not r.get("dry", True) for r in (edl or {}).get("ranges", [])}
    out: list[dict] = []
    for s in timeline.get("segments", []):
        key = (s.get("beat") or "", s.get("quote") or "")
        a0 = float(s.get("audio_out_start", s["out_start"]))
        if out and out[-1]["key"] == key and not dry.get(s.get("id")) and not out[-1]["dry"]:
            out[-1]["fim"] = float(s["out_end"])
            out[-1]["ids"].append(s.get("id"))
            continue
        out.append({"key": key, "beat": key[0], "quote": key[1], "inicio": float(s["out_start"]), "audio": a0,
                    "fim": float(s["out_end"]), "ids": [s.get("id")], "dry": bool(dry.get(s.get("id")))})
    return out


def palavras_por_trecho(words: list[dict], grupos: list[dict]) -> list[list[dict]]:
    """A word goes to the trecho whose audio has started when the word starts (the J-cut lead starts the audio of
    a trecho a few frames before its picture)."""
    starts = [g["audio"] for g in grupos]
    out: list[list[dict]] = [[] for _ in grupos]
    for w in words:
        k = 0
        for i, a in enumerate(starts):
            if float(w["start"]) >= a - 0.02:
                k = i
        out[k].append(w)
    return out


def diferenca(quote: str, ouvido: str) -> tuple[list[str], list[str]]:
    """(words of the quote missing in the cut, words of the cut not in the quote), accents and case ignored."""
    a, b = _norm_words(quote), _norm_words(ouvido)
    falta, sobra = [], []
    for op, i1, i2, j1, j2 in difflib.SequenceMatcher(a=a, b=b, autojunk=False).get_opcodes():
        if op in ("delete", "replace"):
            falta += a[i1:i2]
        if op in ("insert", "replace"):
            sobra += b[j1:j2]
    return falta, sobra


def fala_do_corte(edit: Path, video: Path) -> list[str]:
    data = load_json(edit / "transcripts" / f"{video.stem}.json")
    if not isinstance(data, dict):
        return ["(sem a transcrição do corte: veja o erro do transcribe acima)"]
    words = sorted(data.get("words", []), key=lambda w: w["start"])
    try:
        (edit / "fala_do_corte.md").write_text(render_cut_markdown(data, cut_phrases(data)), encoding="utf-8")
    except OSError:
        pass
    timeline = load_json(edit / f"{video.stem}_timeline.json") or {}
    grupos = trechos(timeline, load_json(edit / "edl.json") or {}) if timeline.get("segments") else []
    if not grupos or not any(g["quote"] for g in grupos):
        linhas = ["(sem o quote dos trechos: só a fala, com o tempo de começo de cada frase)"]
        return linhas + [f"  {fmt_time(p['start'])}  {p['text']}" for p in cut_phrases(data)]
    linhas, n_dif = [], 0
    for g, ws in zip(grupos, palavras_por_trecho(words, grupos)):
        ouvido = " ".join(w["text"] for w in ws)
        marca = ""
        if g["dry"]:
            marca = "  (dry: false, sem comparar)"
        elif g["quote"]:
            falta, sobra = diferenca(g["quote"], ouvido)
            if falta or sobra:
                n_dif += 1
                partes = ([f'falta "{" ".join(falta)}"'] if falta else []) + \
                         ([f'a mais "{" ".join(sobra)}"'] if sobra else [])
                marca = "  ≠ confira: " + ", ".join(partes)
        titulo = f"{g['beat']} · " if g["beat"] else ""
        linhas.append(f"{fmt_time(g['inicio'])}  {titulo}escrito: \"{g['quote']}\"{marca}")
        corte = [f["words"] for f in split_long(group_into_phrases(ws, 0.3))] if ws else []
        if not corte:
            linhas.append("          (nada falado)")
        for c in corte:
            linhas.append(f"  {fmt_time(float(c[0]['start']))}  {' '.join(w['text'] for w in c)}")
    resumo = (f"{n_dif} trecho(s) com palavras diferentes do quote: ouça esses pontos (borda mordendo palavra, "
              f"resto de tomada, ou só o quote escrito de outro jeito)." if n_dif else
              "A fala bate com o quote de todos os trechos (acento e pontuação à parte).")
    return linhas + ["", resumo]


# ------------------------------------------------------------------ breaths, minus the ranges kept whole

def respiros_do_corte(out: str, edit: Path, video: Path) -> tuple[list[str], int]:
    """Report of respiros.py --json with the silences inside a "dry": false range listed apart."""
    try:
        rep = json.loads(out.strip().splitlines()[-1])
    except (json.JSONDecodeError, IndexError):
        return [out], -1
    edl = load_json(edit / "edl.json") or {}
    dry_ids = {r.get("id") for r in edl.get("ranges", []) if not r.get("dry", True)}
    segs = [s for s in (load_json(edit / f"{video.stem}_timeline.json") or {}).get("segments", [])
            if s.get("id") in dry_ids]
    conta, fica = [], []
    for r in rep.get("respiros", []):
        dentro = next((s for s in segs if r["de"] >= float(s["out_start"]) - 0.05
                       and r["ate"] <= float(s["out_end"]) + 0.05), None)
        (fica if dentro else conta).append((r, dentro))
    th, mn = rep.get("limiar_db"), rep.get("min_s", 0.25)
    linhas = []
    if not conta:
        linhas.append(f"respiros: nenhum silêncio de {mn:.2f} s ou mais (abaixo de {th:.0f} dB)")
    else:
        linhas.append(f"respiros: {len(conta)} silêncio(s) de {mn:.2f} s ou mais (abaixo de {th:.0f} dB):")
        for r, _ in conta:
            linhas.append(f"  {fmt_time(r['de'])} a {fmt_time(r['ate'])}  ({r['dur']:.2f} s)")
        linhas.append("  cada um é respiro que sobrou: parta o trecho ali no edl_manual.json ou rode o corte pela onda "
                      "com --limiar uns 3 dB mais alto que o da saída dele (references/corte.md)")
    for r, s in fica:
        linhas.append(f"  fica: {fmt_time(r['de'])} a {fmt_time(r['ate'])} ({r['dur']:.2f} s) dentro do trecho "
                      f"{s.get('id')} com \"dry\": false (\"{(s.get('quote') or '')[:40]}\")")
    return linhas, len(conta)


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
    ap.add_argument("edit_dir", type=Path)
    ap.add_argument("--video", default="cut.mp4", help="nome do corte dentro da pasta (padrão cut.mp4)")
    ap.add_argument("--prompt", default=None, help="nomes que aparecem na fala, como no transcribe.py")
    ap.add_argument("--min-silence", type=float, default=None, help="repassado ao verify_cut.py")
    args = ap.parse_args()

    edit = args.edit_dir.resolve()
    video, edl = edit / args.video, edit / "edl.json"
    for p in (video, edl):
        if not p.exists():
            print(f"erro: não achei {p}", file=sys.stderr)
            return 2

    h = lambda name: str(HELPERS / name)  # noqa: E731
    tr = [h("transcribe.py"), str(video), "--edit-dir", str(edit)]
    if args.prompt:
        tr += ["--prompt", args.prompt]
    vf = [h("verify_cut.py"), str(edl), str(video), "--sem-aprovacao"]
    if args.min_silence is not None:
        vf += ["--min-silence", str(args.min_silence)]

    t0 = time.monotonic()
    with ThreadPoolExecutor(max_workers=4) as pool:
        jobs = {"Transcrição do corte (transcribe)": pool.submit(run, tr),
                "Respiros que sobraram (respiros)": pool.submit(run, [h("respiros.py"), str(video), "--json"]),
                "Bordas, volume e silêncio (verify_cut)": pool.submit(run, vf),
                "Folha das junções (contact_sheet)": pool.submit(run, [h("contact_sheet.py"), str(video), "--junctions"])}
        failed, breaths = False, 0
        for title, fut in jobs.items():
            code, out, dt = fut.result()
            if title.startswith("Respiros") and code in (0, 1):
                linhas, breaths = respiros_do_corte(out, edit, video)
                failed |= breaths < 0
                breaths = max(0, breaths)
                out = "\n".join(linhas)
                code = 0
            else:
                failed |= code != 0
            print(f"=== {title} · {dt:.0f} s{'' if code == 0 else f' · ERRO (saída {code})'}\n{out}\n")
    print("=== Fala do corte, trecho a trecho (escrito = quote do edl; embaixo, o que o corte diz, com o tempo)")
    print("\n".join(fala_do_corte(edit, video)) + "\n")
    passos = ["leia a fala do corte acima contra o quote de cada trecho"]
    if breaths:
        passos.append("tire os respiros que sobraram (" + cmd("corte_por_onda.py", f"'{edit / 'edl_manual.json'}'",
                                                            "-o", f"'{edl}'", "--limiar <3 dB acima>") + ")")
    passos.append("depois meça o rosto e mostre o corte no estúdio (references/corte.md, passo 7)")
    print(f"Tudo conferido em {time.monotonic() - t0:.0f} s. Agora: " + "; ".join(passos) + ".")
    return 1 if failed or breaths else 0


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