#!/usr/bin/env python3
"""before_after.py — the competitor's ad beside OUR answer to it, on one page.

For every ad in a teardown: the THEIR column plays the competitor's clip (as fetched by
adlib_fetch.py) with the verbatim hook, the summary, the structure timeline, the angle
pills, offer and CTA; the OURS column plays the clip we rendered from the teardown's
`ugc_script` (any route — the free Veo lane, the cloak, a paid take) with the script, the
route it came from and what we kept / changed. An ad we have not answered yet says so in
plain words; nothing is invented to fill the column.

Our clips are COPIED into `build/ad-intel/after/<id>.mp4` (sha sidecar; the source may live
in another project) so the page and its media stay together under one project. The page
is self-contained apart from those two relative media paths (frame grabs inline).

Usage:
  before_after.py --project <projects/slug> [--teardown <brand>-teardown.json]
                  [--after <libraryId>=<our.mp4>]... [--route "veo-useapi free (0 credits)"]
                  [--note <libraryId>="what we changed"]... [--out build/ad-intel/<brand>-before-after.html]
Exit 0 page written · 2 bad inputs (no teardown, an --after id not in the teardown, a missing file).
"""
from __future__ import annotations

import argparse
import glob
import hashlib
import html
import json
import os
import shutil
import subprocess
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from ad_intel import _e, frame_grab, now_iso, probe_seconds  # noqa: E402


def say(msg: str) -> None:
    print(f"before_after: {msg}", file=sys.stderr)


def die(msg: str, code: int) -> None:
    say(msg)
    raise SystemExit(code)


def sha256_file(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as fh:
        for chunk in iter(lambda: fh.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()


def newest_teardown(project: str) -> str | None:
    hits = sorted(glob.glob(os.path.join(project, "artifacts", "ad-intel", "*-teardown.json")),
                  key=os.path.getmtime, reverse=True)
    return hits[0] if hits else None


def parse_pairs(items: list[str] | None, what: str) -> dict[str, str]:
    out: dict[str, str] = {}
    for item in items or []:
        if "=" not in item:
            die(f"--{what} wants <libraryId>=<value>, got {item!r}", 2)
        k, v = item.split("=", 1)
        out[k.strip()] = v.strip()
    return out


def copy_after(project: str, ad_id: str, src: str) -> str:
    """Our clip under the project, copied once per content (sha sidecar)."""
    if not os.path.isfile(src):
        die(f"--after {ad_id}: {src} is not a file", 2)
    dest_dir = os.path.join(project, "build", "ad-intel", "after")
    os.makedirs(dest_dir, exist_ok=True)
    dest = os.path.join(dest_dir, f"{ad_id}.mp4")
    sha = sha256_file(src)
    side = dest + ".sha256"
    if os.path.isfile(dest) and os.path.isfile(side) and open(side).read().strip() == sha:
        return dest
    shutil.copyfile(src, dest)
    with open(side, "w") as fh:
        fh.write(sha + "\n")
    return dest


def load_ad(project: str, ad_id: str) -> dict | None:
    path = os.path.join(project, "artifacts", "ad-intel", f"{ad_id}.json")
    if not os.path.isfile(path):
        return None
    with open(path, encoding="utf-8") as fh:
        return json.load(fh)


def rel(from_dir: str, path: str) -> str:
    return os.path.relpath(path, from_dir).replace(os.sep, "/")


def kept_changed(analysis: dict, note: str | None) -> tuple[list[str], list[str]]:
    """What our version keeps from theirs and what it changes — READ from the teardown,
    never guessed: the hook formula, the structure, the CTA posture, the visual pattern."""
    kept: list[str] = []
    changed: list[str] = []
    hook = (analysis.get("hook") or "").strip()
    if hook:
        kept.append(f"the hook shape — theirs opens “{hook[:90]}{'…' if len(hook) > 90 else ''}”")
    segs = [s.get("segment") for s in analysis.get("structure") or [] if isinstance(s, dict)]
    if segs:
        kept.append("the structure — " + " → ".join(dict.fromkeys(s for s in segs if s)))
    if analysis.get("visual_pattern"):
        kept.append(f"the visual pattern — {analysis['visual_pattern']}")
    changed.append("the product and the proof: every claim is ours, no line is copied")
    if analysis.get("offer") and analysis["offer"].lower() not in ("none", "none stated", ""):
        changed.append(f"the offer — theirs is “{analysis['offer']}”; ours is what the script says")
    if analysis.get("cta"):
        changed.append(f"the CTA — theirs: “{analysis['cta']}”")
    if note:
        changed.append(note)
    return kept, changed


def render(project: str, out_html: str, teardown: dict, rows: list[dict], route: str | None) -> str:
    page_dir = os.path.dirname(os.path.abspath(out_html))
    css = """
    body{font:15px/1.5 -apple-system,Segoe UI,Helvetica,Arial,sans-serif;margin:0;background:#f4f5f7;color:#1c2229}
    main{max-width:1240px;margin:0 auto;padding:28px 20px 80px}
    h1{font-size:1.7rem;margin:0 0 4px} h2{font-size:1.15rem;margin:36px 0 10px;border-top:1px solid #d9dde3;padding-top:14px}
    .lede{color:#5a6570;margin:0 0 18px}
    .top{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin:14px 0 6px}
    .fact{background:#fff;border:1px solid #d9dde3;padding:10px 14px} .fact b{display:block;font-size:1.25rem}
    .fact small{color:#5a6570}
    .pair{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:12px 0 28px}
    @media(max-width:820px){.pair{grid-template-columns:1fr}}
    .col{background:#fff;border:1px solid #d9dde3;padding:14px 16px;min-width:0}
    .col h3{margin:0 0 8px;font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:#5a6570}
    .col.ours h3{color:#0e6f6b}
    video{width:100%;max-height:420px;background:#000;border-radius:4px}
    .hook{font-size:1.1rem;font-weight:600;margin:10px 0 6px}
    .meta{color:#5a6570;font-size:.9rem}
    .pill{display:inline-block;font-size:.72rem;padding:2px 8px;border-radius:999px;background:#e3f1ef;color:#0a4f4c;margin:2px 4px 2px 0}
    .pill.warn{background:#fbf0d9;color:#8a5a00}
    .pill.cat{background:#eef0f3;color:#3b4650}
    .tl{margin:8px 0;padding:0;list-style:none;font-size:.88rem} .tl li{display:flex;gap:8px;margin:2px 0}
    .tl b{font-variant-numeric:tabular-nums;color:#5a6570;min-width:86px}
    .script{white-space:pre-wrap;background:#f7f9fa;border:1px solid #e3e7eb;padding:10px 12px;border-radius:4px;font-size:.93rem}
    ul.kc{margin:6px 0;padding-left:20px;font-size:.9rem} ul.kc li{margin:3px 0}
    .none{border:1px dashed #c9d0d6;padding:24px;text-align:center;color:#5a6570;border-radius:4px}
    table.qc{border-collapse:collapse;width:100%;font-size:.82rem;margin:6px 0}table.qc th,table.qc td{text-align:left;vertical-align:top;padding:4px 6px;border-bottom:1px solid #e3e7eb}
    table.qc th{font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:#5a6570}table.qc tr.bad td{background:#fbf0d9}
    """
    brand = teardown.get("brand") or "competitor"
    ours = teardown.get("ourBrand") or "us"
    answered = sum(1 for r in rows if r.get("ourClip"))
    parts = [f"<!doctype html><meta charset='utf-8'><title>Before / after · {_e(brand)} vs {_e(ours)}</title>"
             f"<style>{css}</style><main>",
             f"<h1>{_e(brand)}'s ads, and {_e(ours)}'s answer to each</h1>",
             f"<p class='lede'>Left: the competitor's active ad as fetched from the public Meta Ad Library, "
             f"with what the teardown read from it. Right: the clip we rendered from that teardown's script"
             f"{' on ' + _e(route) if route else ''}. Their footage is never reused; only the shape is.</p>",
             "<div class='top'>",
             f"<div class='fact'><small>ads torn down</small><b>{len(rows)}</b></div>",
             f"<div class='fact'><small>answered with our clip</small><b>{answered} / {len(rows)}</b></div>",
             f"<div class='fact'><small>route for ours</small><b style='font-size:1rem'>{_e(route or 'not rendered yet')}</b></div>",
             f"<div class='fact'><small>generated</small><b style='font-size:1rem'>{_e(now_iso())}</b></div>",
             "</div>"]
    for r in rows:
        a = r["analysis"]
        src = r["source"]
        parts.append(f"<h2>{_e(src.get('pageName') or brand)} · Library ID {_e(r['id'])}"
                     f"{' · since ' + _e(src['startedOn']) if src.get('startedOn') else ''}"
                     f"{' · ' + _e(src['versions']) + ' versions' if src.get('versions') else ''}</h2>")
        parts.append("<div class='pair'>")
        # THEIRS
        parts.append("<div class='col'><h3>Before — their ad</h3>")
        if r.get("theirClip"):
            parts.append(f"<video controls preload='metadata' src='{_e(rel(page_dir, r['theirClip']))}'"
                         f"{' poster=' + chr(39) + r['theirPoster'] + chr(39) if r.get('theirPoster') else ''}></video>")
        else:
            parts.append("<div class='none'>the fetched clip is not on disk</div>")
        parts.append(f"<div class='hook'>“{_e(a.get('hook') or '—')}”</div>")
        parts.append(f"<div class='meta'>{_e(a.get('summary') or '')}</div>")
        pills = "".join(f"<span class='pill cat'>{_e(x.get('category'))}</span>{_e(x.get('angle'))}<br>"
                        for x in (a.get("angles") or []) if isinstance(x, dict))
        if pills:
            parts.append(f"<div style='margin:8px 0'>{pills}</div>")
        segs = [s for s in (a.get("structure") or []) if isinstance(s, dict)]
        if segs:
            parts.append("<ul class='tl'>" + "".join(
                f"<li><b>{float(s.get('start') or 0):.1f}–{float(s.get('end') or 0):.1f}s</b>"
                f"<span><span class='pill'>{_e(s.get('segment'))}</span>{_e(s.get('note') or '')}</span></li>"
                for s in segs) + "</ul>")
        meta = []
        if a.get("offer"):
            meta.append(f"offer: {a['offer']}")
        if a.get("cta"):
            meta.append(f"CTA: {a['cta']}")
        if src.get("landingUrl"):
            meta.append(f"lands on {src['landingUrl'].split('/')[2] if '://' in src['landingUrl'] else src['landingUrl']}")
        if r.get("theirSeconds"):
            meta.append(f"{r['theirSeconds']:.0f} s")
        parts.append(f"<div class='meta'>{_e(' · '.join(meta))}</div></div>")
        # OURS
        parts.append(f"<div class='col ours'><h3>After — {_e(ours)}'s version</h3>")
        if r.get("ourClip"):
            parts.append(f"<video controls preload='metadata' src='{_e(rel(page_dir, r['ourClip']))}'"
                         f"{' poster=' + chr(39) + r['ourPoster'] + chr(39) if r.get('ourPoster') else ''}></video>")
            parts.append(f"<div class='meta'>{_e(route or '')}{' · ' if route else ''}{r.get('ourSeconds') or 0:.0f} s"
                         f" · sha {r.get('ourSha', '')[:12]}</div>")
        else:
            parts.append("<div class='none'>not rendered yet — the script below is what to render</div>")
        parts.append(f"<div class='hook'>the script</div><div class='script'>{_e(a.get('ugc_script') or '—')}</div>")
        if r.get("qc"):
            qrows = []
            for b in r["qc"]:
                cov = b.get("coverage")
                cell = "refused" if b["refused"] else (f"{cov:.2f}" if isinstance(cov, (int, float)) else "—")
                qrows.append(f"<tr class='{'bad' if b['refused'] else ''}'><td>{_e(b['beat'])}</td><td>{_e(b['line'])}</td>"
                             f"<td>{_e(b['heard'])}</td><td>{cell}</td></tr>")
            parts.append("<div class='hook' style='font-size:.95rem'>what each beat says, and what whisper heard</div>"
                         "<table class='qc'><thead><tr><th>beat</th><th>scripted</th><th>heard</th><th>cov.</th></tr></thead><tbody>"
                         + "".join(qrows) + "</tbody></table>")
        kept, changed = kept_changed(a, r.get("note"))
        parts.append("<div class='hook' style='font-size:.95rem'>kept</div><ul class='kc'>" +
                     "".join(f"<li>{_e(k)}</li>" for k in kept) + "</ul>")
        parts.append("<div class='hook' style='font-size:.95rem'>changed</div><ul class='kc'>" +
                     "".join(f"<li>{_e(c)}</li>" for c in changed) + "</ul></div>")
        parts.append("</div>")
    parts.append("</main>")
    return "".join(parts)


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("--project", required=True)
    ap.add_argument("--teardown", help="default: the newest artifacts/ad-intel/*-teardown.json")
    ap.add_argument("--after", action="append", help="<libraryId>=<our clip .mp4>")
    ap.add_argument("--note", action="append", help="<libraryId>=<what we changed, one line>")
    ap.add_argument("--route", help="the route our clips came from, shown on the page")
    ap.add_argument("--out", help="default: build/ad-intel/<brand>-before-after.html")
    ap.add_argument("--qc", action="append", help="<libraryId>=<answer-qc.json from assemble_answer.py>: a heard-vs-scripted row per beat")
    args = ap.parse_args()

    project = os.path.abspath(args.project)
    teardown_path = args.teardown or newest_teardown(project)
    if not teardown_path or not os.path.isfile(teardown_path):
        die("no teardown found — run ad_intel.py first", 2)
    with open(teardown_path, encoding="utf-8") as fh:
        teardown = json.load(fh)
    ids = [a.get("id") for a in teardown.get("ads") or [] if isinstance(a, dict)]
    if not ids:
        die(f"{teardown_path} lists no ads", 2)
    afters = parse_pairs(args.after, "after")
    notes = parse_pairs(args.note, "note")
    qcs = parse_pairs(args.qc, "qc")
    for k, v in qcs.items():
        if not os.path.isfile(v):
            die(f"--qc {k}: {v} is not a file", 2)
    for k in list(afters) + list(notes) + list(qcs):
        if k not in ids:
            die(f"{k} is not an ad in {os.path.basename(teardown_path)} (ads: {', '.join(ids)})", 2)

    brand_slug = (teardown.get("brand") or "competitor").lower()
    brand_slug = "".join(c if c.isalnum() else "-" for c in brand_slug).strip("-") or "competitor"
    out_html = os.path.abspath(args.out or os.path.join(project, "build", "ad-intel", f"{brand_slug}-before-after.html"))
    os.makedirs(os.path.dirname(out_html), exist_ok=True)
    thumbs_dir = os.path.join(project, "build", "ad-intel", "thumbs")
    os.makedirs(thumbs_dir, exist_ok=True)

    rows: list[dict] = []
    for ad_id in ids:
        ad = load_ad(project, ad_id)
        if not ad:
            say(f"{ad_id}: no per-ad artifact under artifacts/ad-intel — shown from the teardown only")
            ad = {"analysis": next((a for a in teardown["ads"] if a.get("id") == ad_id), {}), "source": {}, "media": {}}
        analysis = ad.get("analysis") or {}
        source = ad.get("source") or {}
        media = ad.get("media") or {}
        row: dict = {"id": ad_id, "analysis": analysis, "source": source, "note": notes.get(ad_id)}
        if ad_id in qcs:
            with open(qcs[ad_id], encoding="utf-8") as fh:
                q = json.load(fh)
            row["qc"] = [{"beat": b.get("beat"), "line": b.get("line"), "heard": b.get("heard"), "coverage": b.get("coverage"),
                          "refused": bool(b.get("refused"))} for b in (q.get("beats") or []) if isinstance(b, dict)]
        their = media.get("file")
        if their:
            their_abs = their if os.path.isabs(their) else os.path.join(project, their)
            if os.path.isfile(their_abs):
                row["theirClip"] = their_abs
                row["theirSeconds"] = probe_seconds(their_abs)
                row["theirPoster"] = frame_grab(their_abs, os.path.join(thumbs_dir, f"{ad_id}.jpg"))
        if ad_id in afters:
            ours = copy_after(project, ad_id, os.path.abspath(afters[ad_id]))
            row["ourClip"] = ours
            row["ourSeconds"] = probe_seconds(ours)
            row["ourSha"] = open(ours + ".sha256").read().strip()
            row["ourPoster"] = frame_grab(ours, os.path.join(thumbs_dir, f"{ad_id}-ours.jpg"))
        rows.append(row)

    page = render(project, out_html, teardown, rows, args.route)
    tmp = out_html + ".tmp"
    with open(tmp, "w", encoding="utf-8") as fh:
        fh.write(page)
    os.replace(tmp, out_html)
    twin = {
        "schemaVersion": 1, "generatedAt": now_iso(), "brand": teardown.get("brand"), "ourBrand": teardown.get("ourBrand"),
        "route": args.route, "teardown": os.path.relpath(teardown_path, project),
        "ads": [{"id": r["id"], "hook": r["analysis"].get("hook"), "their": os.path.relpath(r["theirClip"], project) if r.get("theirClip") else None,
                 "ours": os.path.relpath(r["ourClip"], project) if r.get("ourClip") else None, "oursSha256": r.get("ourSha"),
                 "note": r.get("note"), "qc": r.get("qc")} for r in rows],
    }
    with open(out_html[:-5] + ".json", "w", encoding="utf-8") as fh:
        json.dump(twin, fh, indent=1)
    print(json.dumps({"page": out_html, "ads": len(rows), "answered": sum(1 for r in rows if r.get("ourClip"))}))
    return 0


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