#!/usr/bin/env python3
"""build_release_joins — audition the record as a LISTENER hears it, before release.

The boundary lab answers "where should this cut be" against the master. This
answers the different question PLN asked last: *"i wanna confirm all tracks
start/ends/transitions"* — and it cannot be done on the master, for one concrete
reason:

    Desire is cut from the release. In the master, Vague de CRIME ends at
    4166.3 and Desire runs until 4547.57. A ±45 s master window around that
    edge plays six minutes of a track that does not ship. The join a listener
    actually hears — CRIME's tail straight into REVOLUTION's head — exists
    ONLY in the rendered output.

So this builds its clips from the split FLACs, not the master: for every
consecutive pair it concatenates the tail of one and the head of the next, which
is exactly the seam. That has a second benefit worth as much as the first — it
audits the files that ship, so a split error (wrong offset, truncated encode,
a STREAMINFO lie) shows up here rather than after upload.

Two edge entries bracket the record: the head of track 1 (does it open right —
the 1.45 s silence trim) and the tail of the last track (does it end, or stop).

Emits the same JSON shape as `build_bounds_set.py`, so the SAME UI renders it:

    python3 build_release_joins.py judge_specs/opal26.json \\
        --tracks /home/pln/Work/Sound/Prod/Opal26_master/tracks_v4_streaming
    -> ui/public/bounds-<gig>-release.json    (open with ?set=<gig>-release)
"""
from __future__ import annotations

import argparse
import json
import subprocess
import sys
import tempfile
from pathlib import Path

HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
from build_judge_set import clean_title, ffprobe_dur, peaks  # noqa: E402

TAIL = 20.0     # seconds of the outgoing track
HEAD = 20.0     # seconds of the incoming track
PEAKS_N = 900


def cut(src: Path, out: Path, ss: float, dur: float) -> bool:
    r = subprocess.run(
        ["ffmpeg", "-v", "error", "-y", "-ss", f"{ss:.4f}", "-t", f"{dur:.4f}",
         "-i", str(src), "-ac", "2", "-c:a", "flac", "-compression_level", "3",
         str(out)], capture_output=True)
    return r.returncode == 0


def concat(parts: list[Path], out: Path) -> bool:
    """Join the pieces with ffmpeg's concat DEMUXER, not the filter.

    The demuxer is sample-exact for identical formats; re-filtering would
    resample and could hide the very click we are looking for.
    """
    with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
        for p in parts:
            f.write(f"file '{p}'\n")
        lst = f.name
    r = subprocess.run(
        ["ffmpeg", "-v", "error", "-y", "-f", "concat", "-safe", "0", "-i", lst,
         "-c:a", "flac", "-compression_level", "3", str(out)], capture_output=True)
    Path(lst).unlink(missing_ok=True)
    return r.returncode == 0


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("spec")
    ap.add_argument("--tracks", required=True)
    ap.add_argument("--tail", type=float, default=TAIL)
    ap.add_argument("--head", type=float, default=HEAD)
    ap.add_argument("--out")
    a = ap.parse_args()

    spec = json.loads(Path(a.spec).read_text())
    tdir = Path(a.tracks)
    files = sorted(p for p in tdir.glob("*.flac"))
    if not files:
        print(f"no tracks in {tdir}")
        return 1
    durs = [ffprobe_dur(f) for f in files]
    joins_dir = tdir.parent / "joins_v4"
    joins_dir.mkdir(parents=True, exist_ok=True)

    def title_of(p: Path) -> str:
        # "07 - Piment Bresilien.flac" -> "Piment Bresilien"
        return clean_title(p.stem.split(" - ", 1)[-1])

    rows = []

    # --- opening: does the record START right (the 1.45 s trim)
    op = joins_dir / "00-open.flac"
    if not cut(files[0], op, 0.0, min(a.head, durs[0])):
        print("! failed to render the opening")
    rows.append({
        "track": 0, "title": f"OPEN · {title_of(files[0])}", "fromTitle": "silence",
        "nominal": 0.0, "clipStart": 0.0, "clipEnd": ffprobe_dur(op),
        "url": f"/audio/joins/{op.name}", "peaks": [round(x, 4) for x in peaks(op, PEAKS_N)],
        "marks": [{"key": "join", "label": "record starts", "t": 0.0}],
        "settled": False, "noCandidate": False,
        "note": "First sound of the album. The head was trimmed by 1.45 s of digital silence.",
    })

    # --- every seam, as the listener hears it
    for i in range(len(files) - 1):
        outp = joins_dir / f"{i + 1:02d}-join.flac"
        tail_len = min(a.tail, durs[i])
        head_len = min(a.head, durs[i + 1])
        with tempfile.TemporaryDirectory() as td:
            pa, pb = Path(td) / "a.flac", Path(td) / "b.flac"
            okA = cut(files[i], pa, max(0.0, durs[i] - tail_len), tail_len)
            okB = cut(files[i + 1], pb, 0.0, head_len)
            if not (okA and okB and concat([pa, pb], outp)):
                print(f"! failed join {i + 1}")
                continue
        rows.append({
            "track": i + 1,
            "title": f"{title_of(files[i])} → {title_of(files[i + 1])}",
            "fromTitle": title_of(files[i]),
            "nominal": tail_len,                 # the seam, in clip seconds
            "clipStart": 0.0, "clipEnd": ffprobe_dur(outp),
            "url": f"/audio/joins/{outp.name}",
            "peaks": [round(x, 4) for x in peaks(outp, PEAKS_N)],
            "marks": [{"key": "join", "label": "seam", "t": tail_len}],
            "settled": False, "noCandidate": False,
            "note": f"end of #{i + 1} into start of #{i + 2}, as it ships "
                    f"({tail_len:.0f}s + {head_len:.0f}s)",
        })

    # --- closing: does the record END, or merely stop
    cl = joins_dir / "99-close.flac"
    tail_len = min(a.tail + 10, durs[-1])
    if cut(files[-1], cl, max(0.0, durs[-1] - tail_len), tail_len):
        rows.append({
            "track": 99, "title": f"CLOSE · {title_of(files[-1])}",
            "fromTitle": title_of(files[-1]), "nominal": tail_len,
            "clipStart": 0.0, "clipEnd": ffprobe_dur(cl),
            "url": f"/audio/joins/{cl.name}",
            "peaks": [round(x, 4) for x in peaks(cl, PEAKS_N)],
            "marks": [{"key": "join", "label": "record ends", "t": tail_len}],
            "settled": False, "noCandidate": False,
            "note": "Last sound of the album.",
        })

    doc = {"gig": f"{spec['gig']}-release", "title": f"{spec.get('title','')} — release check",
           "generated": True, "pad": a.tail, "boundaries": rows}
    dest = Path(a.out or HERE.parent / "ui" / "public" / f"bounds-{spec['gig']}-release.json")
    dest.write_text(json.dumps(doc, indent=1))
    print(f"✓ {dest}  ({len(rows)} clips: open + {len(files)-1} seams + close)")
    print(f"  clips: {joins_dir}")
    return 0


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