"""When each word really starts, measured on the waveform (Whisper puts the start up to 0.3 s early).

The transcription of the cut marks word starts early, most of all after a joint or a pause: "éééé" came 0.22 s
early and "Eu" at 0.00 when the voice starts at 0.58 (exemplos, 29/09/2026). A caption can live with that; an
animation, a sound or a cut that must land ON the word cannot. This helper takes the cut (or any video) and its
transcript and, for every word, looks at the waveform around the Whisper start (0.10 s before to 0.40 s after,
never into the previous word):

  pausa   the voice comes back after a quiet run: the start is the first sustained sound after it (sharp,
          ~0.01 s). The usual case after a joint of the cut or a breath.
  vale    no pause (the words run together): the start is the rise after the deepest dip (10 dB or more) near
          the Whisper start (about 0.03 s).
  whisper nothing to measure (a dip too shallow): the Whisper time is kept.

Same measure as corte_por_onda.py (mono RMS in 10 ms windows; pause threshold from the floor of the file).

Output: a table of the words that moved 0.05 s or more (or the ones asked for with --palavra / --de --ate) and
<edit>/palavras_onda.json: [{"i", "text", "whisper", "start", "end", "dif", "como"}], times in seconds of the video.

Use: to fire an animation, a sound or a cut on a word, put the `start` of this file in the project as a time in
seconds (`"em": 3.42` instead of `"em": "éééé"`); the word name uses the Whisper time. Rerun after every new cut.

Usage:
    uv run --directory '<SKILL>' python helpers/palavras_na_onda.py '<EDIT>' [--palavra éééé] [--de 2 --ate 6] [--todas]
    uv run --directory '<SKILL>' python helpers/palavras_na_onda.py --video X.mp4 --transcricao X.json [...]
"""

from __future__ import annotations

import argparse
import re
import sys
import unicodedata
from pathlib import Path

import numpy as np

sys.path.insert(0, str(Path(__file__).resolve().parent))
import corte_por_onda as onda  # noqa: E402
from common import fmt_time, load_json, save_json  # noqa: E402

ANTES_S = 0.10          # Whisper can also be a little late
DEPOIS_S = 0.40         # ... and is up to 0.3 s early
QUIETO_MIN_S = 0.03     # a quiet run this long before the word is a pause
SOM_MIN_S = 0.04        # sound this long after it is the word (not a click)
VALE_DB = 10.0          # a dip this deep between two words marks the start of the second
MOSTRAR_S = 0.05        # the table shows words that moved at least this much


def _norm(texto: str) -> str:
    t = unicodedata.normalize("NFKD", (texto or "").lower())
    return re.sub(r"[^\w]+", "", "".join(c for c in t if not unicodedata.combining(c)))


def medir(db: np.ndarray, th: float, whisper: float, depois_de: float, ate: float) -> tuple[float, str]:
    """(start, como) of one word, searched between `depois_de` (the previous word has started) and `ate` (where the
    next word starts: its onset must not be taken for this one)."""
    hop = onda.HOP_S
    lo = max(depois_de, whisper - ANTES_S, 0.0)
    hi = max(lo + hop, ate)
    i0, i1 = int(round(lo / hop)), min(len(db), int(round(hi / hop)))
    if i1 - i0 < 2:
        return round(whisper, 3), "whisper"
    seg = db[i0:i1]
    quiet_need, som_need = int(round(QUIETO_MIN_S / hop)), int(round(SOM_MIN_S / hop))
    # 1. pause -> sound: first sustained sound after a quiet run; the one nearest after the Whisper start wins
    cands = []
    run = 0
    for k in range(len(seg)):
        if seg[k] < th:
            run += 1
            continue
        if run >= quiet_need and k + som_need <= len(seg) and bool((seg[k:k + som_need] >= th).all()):
            cands.append(k)
        run = 0
    if cands:
        w = int(round(whisper / hop)) - i0
        depois = [k for k in cands if k >= w - int(round(0.05 / hop))]
        k = depois[0] if depois else cands[-1]
        return round((i0 + k) * hop, 3), "pausa"
    # 2. no pause: the rise after the deepest dip near the Whisper start
    j0 = max(0, int(round((whisper - 0.08) / hop)) - i0)
    j1 = min(len(seg), int(round((whisper + 0.20) / hop)) - i0)
    if j1 - j0 >= 3:
        k = j0 + int(np.argmin(seg[j0:j1]))
        fundo = float(seg[k])
        adiante = seg[k:min(len(seg), k + int(round(0.15 / hop)))]
        if len(adiante) and float(adiante.max()) - fundo >= VALE_DB:
            meio = fundo + (float(adiante.max()) - fundo) / 2
            sobe = int(np.argmax(adiante >= meio))
            return round((i0 + k + sobe) * hop, 3), "vale"
    return round(whisper, 3), "whisper"


def medir_palavras(db: np.ndarray, th: float, words: list[dict]) -> list[dict]:
    out = []
    ws = sorted(words, key=lambda w: float(w["start"]))
    for i, w in enumerate(ws):
        s, e = float(w["start"]), float(w.get("end", w["start"]))
        depois_de = out[-1]["start"] + 0.05 if out else 0.0
        # up to 0.4 s after the Whisper start (more when Whisper stretched the word over the silence before it,
        # as the first word of a cut), never into the next word
        ate = max(s + DEPOIS_S, e - 0.02)
        if i + 1 < len(ws):
            ate = min(ate, float(ws[i + 1]["start"]))
        ate = max(ate, s + 0.05)
        start, como = medir(db, th, s, depois_de, ate)
        out.append({"i": i, "text": w.get("text", ""), "whisper": round(s, 3), "start": start,
                    "end": round(max(e, start), 3), "dif": round(start - s, 3), "como": como})
    return out


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description="Começo de cada palavra medido na onda (o Whisper adianta até 0,3 s).")
    ap.add_argument("edit", nargs="?", type=Path, help="pasta edit do corte (usa cut.mp4 e transcripts/cut.json)")
    ap.add_argument("--video", type=Path, help="outro vídeo (com --transcricao)")
    ap.add_argument("--transcricao", type=Path, help="transcrição do vídeo (transcripts/<nome>.json)")
    ap.add_argument("--palavra", action="append", default=[], help="só esta palavra (pode repetir; sem acento conta)")
    ap.add_argument("--de", type=float, help="só palavras a partir deste segundo")
    ap.add_argument("--ate", type=float, help="só palavras até este segundo")
    ap.add_argument("--todas", action="store_true", help="mostra todas as palavras, não só as que mudaram")
    ap.add_argument("--limiar", type=float, help="dB abaixo do qual é pausa (padrão: medido, como no corte pela onda)")
    ap.add_argument("-o", "--saida", type=Path, help="JSON de saída (padrão: <edit>/palavras_onda.json)")
    a = ap.parse_args(argv)

    if a.edit:
        edit = a.edit.resolve()
        video = a.video or edit / "cut.mp4"
        tr = a.transcricao or edit / "transcripts" / f"{Path(video).stem}.json"
        saida = a.saida or edit / "palavras_onda.json"
    elif a.video and a.transcricao:
        video, tr = a.video, a.transcricao
        saida = a.saida or Path(tr).with_name(Path(tr).stem + "_onda.json")
    else:
        ap.error("passe a pasta edit, ou --video e --transcricao")
    video, tr = Path(video), Path(tr)
    for p in (video, tr):
        if not p.is_file():
            print(f"ERRO: não achei {p}" + (" (rode o conferir_corte.py antes: ele transcreve o corte)"
                                            if p == tr else ""), file=sys.stderr)
            return 2
    data = load_json(tr)
    words = data.get("words", []) if isinstance(data, dict) else []
    if not words:
        print(f"ERRO: {tr.name} não tem palavras", file=sys.stderr)
        return 2
    db = onda.envelope(video, None)
    th = a.limiar if a.limiar is not None else onda.threshold(db)
    res = medir_palavras(db, th, words)
    save_json(saida, {"video": str(video), "transcricao": str(tr), "limiar_db": th, "palavras": res})

    alvo = {_norm(x) for x in a.palavra}
    def quer(r: dict) -> bool:
        if alvo and _norm(r["text"]) not in alvo:
            return False
        if a.de is not None and r["start"] < a.de - 0.3:
            return False
        if a.ate is not None and r["start"] > a.ate:
            return False
        return a.todas or bool(alvo) or a.de is not None or a.ate is not None or abs(r["dif"]) >= MOSTRAR_S
    linhas = [r for r in res if quer(r)]
    n_mov = sum(1 for r in res if abs(r["dif"]) >= MOSTRAR_S)
    print(f"{video.name}: {len(res)} palavras, {n_mov} com o começo 0,05 s ou mais longe do Whisper "
          f"(pausa abaixo de {th:.0f} dB) · {saida}")
    if linhas:
        print(f"  {'onda':>8s}  {'whisper':>8s}  {'dif':>6s}  {'como':7s}  palavra")
    for r in linhas:
        print(f"  {fmt_time(r['start']):>8s}  {fmt_time(r['whisper']):>8s}  {r['dif']:+.2f}  {r['como']:7s}  {r['text']}")
    if alvo and not linhas:
        print("  nenhuma palavra com esse texto (confira a grafia na fala do corte)")
    print("  para animação, som ou corte na palavra: use o tempo da coluna onda (\"em\": segundos) no projeto.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
