import { useEffect, useRef, useState } from "react";
import { api, fileUrl, pollJob } from "../api";
import { IcDownload, IcPlus, IcTrash } from "./Icons";

// Font NHÚNG kèm app (backend/app/assets/fonts, nạp qua /fonts) — mã nguồn mở,
// hỗ trợ đầy đủ tiếng Việt/en/fil. Trình duyệt (css/canvas) và libass (ass) dùng
// CHUNG file font nên bản xem trước khớp video render trên MỌI máy chủ, không cần
// font Microsoft. `ass` = ĐÚNG family name nội bộ của .ttf (khớp Fontname libass).
// Các file là face nét đậm sẵn, khai báo weight thường -> KHÔNG làm-đậm-giả.
// k = HỆ SỐ CO CỠ CHỮ CSS↔libass (đo thực nghiệm). libass vẽ `Fontsize=F` ra cỡ
// pixel NHỎ hơn CSS `font-size:F` (CSS đặt EM = F px). Đo render_frame(libass) vs
// canvas trình duyệt cùng ngưỡng (chuỗi H/Hg/Hàng) -> tỉ lệ ink_libass/ink_css NHẤT
// QUÁN <1% -> là hệ số co cố định mỗi font. Nhân k vào cỡ chữ CSS preview để KHỚP
// libass. (KHÔNG dùng mscale=hheaH/winH lý thuyết: libass build này vẽ nhỏ hơn nữa.)
const FONTS = [
  { label: "Be Vietnam Pro", css: "'Be Vietnam Pro', sans-serif", ass: "Be Vietnam Pro", k: 0.655 },
  { label: "Roboto", css: "'Roboto', sans-serif", ass: "Roboto", k: 0.829 },
  { label: "Open Sans", css: "'Open Sans', sans-serif", ass: "Open Sans", k: 0.692 },
  { label: "Montserrat", css: "'Montserrat', sans-serif", ass: "Montserrat", k: 0.641 },
  { label: "Arimo (giống Arial)", css: "'Arimo', sans-serif", ass: "Arimo", k: 0.694 },
  { label: "Tinos (serif, giống Times)", css: "'Tinos', serif", ass: "Tinos", k: 0.905 },
  { label: "Oswald (hẹp, cho tiêu đề)", css: "'Oswald', sans-serif", ass: "Oswald", k: 0.587 },
  { label: "Nunito (bo tròn)", css: "'Nunito', sans-serif", ass: "Nunito", k: 0.724 },
  { label: "Lato", css: "'Lato', sans-serif", ass: "Lato", k: 0.714 },
  { label: "Merriweather (serif)", css: "'Merriweather', serif", ass: "Merriweather", k: 0.577 },
  { label: "Playfair Display (serif sang)", css: "'Playfair Display', serif", ass: "Playfair Display", k: 0.705 },
  { label: "Kanit (đậm, cho phụ đề)", css: "'Kanit', sans-serif", ass: "Kanit", k: 0.630 },
  { label: "Dancing Script (viết tay)", css: "'Dancing Script', cursive", ass: "Dancing Script", k: 0.710 },
];
const BGS = [
  ["Viền đen mỏng", "outlineThin"], ["Viền đen đậm", "outlineThick"],
  ["Viền trắng", "outlineWhite"], ["Bóng đổ", "shadow"], ["Phát sáng", "glow"],
  ["Hộp đen mờ", "boxDark"], ["Hộp đen đậm", "boxSolid"], ["Hộp bo tròn", "boxRound"],
  ["Thanh nền cả dòng", "bar"], ["Hộp màu highlight", "boxColor"], ["Không nền", "none"],
];
const MODES = [
  ["whole", "Hiện cả câu"], ["progressive", "Hiện dần"], ["highlight", "Highlight từng chữ"],
];
const DEF = { font: "Be Vietnam Pro", fontSize: 54, color: "#ffffff", highlight: "#ffd400", bg: "outlineThin", x: 50, y: 86, w: 82 };
const cssOf = (ass) => (FONTS.find((f) => f.ass === ass) || FONTS[0]).css;
const kOf = (ass) => (FONTS.find((f) => f.ass === ass) || FONTS[0]).k;
const trunc = (t, n) => { t = (t || "").trim(); return t.length > n ? t.slice(0, n).trim() + "…" : t; };
const wordsFor = (text, start, end) => {
  const words = (text || "").split(/\s+/).filter(Boolean);
  const dur = Math.max(0.2, end - start);
  const wt = words.map((w) => w.length + 1); const tot = wt.reduce((a, b) => a + b, 0) || 1;
  let t = start; return words.map((w, i) => { const wd = dur * wt[i] / tot; const o = { w, start: t, end: t + wd }; t += wd; return o; });
};

export default function SubtitleEditor({ session, videoUrl, videoRel, srtRel, username, onClose }) {
  const [segs, setSegs] = useState([]);          // {start,end,text,words,locked,style}
  const [sel, setSel] = useState(0);
  const [mode, setMode] = useState("whole");
  const [playing, setPlaying] = useState(false);
  const [t, setT] = useState(0);
  const [dur, setDur] = useState(0);
  const [thumbs, setThumbs] = useState([]);
  const [busy, setBusy] = useState(false);
  const [result, setResult] = useState(null);
  const [editing, setEditing] = useState(false);
  const [, force] = useState(0);
  // Xem trước KHỚP 100%: khi TẠM DỪNG, server render đúng 1 khung bằng CHÍNH
  // ffmpeg/libass của bản burn -> ảnh giống HỆT video xuất (không dùng bộ vẽ thứ 2
  // như libass-wasm vốn khác phiên bản libass server nên lệch).
  const [exact, setExact] = useState(true);
  const [fit, setFit] = useState({ w: 0, h: 0 });   // cỡ hiển thị video = nội dung THẬT (bỏ letterbox)
  const [previewImg, setPreviewImg] = useState(null); // ảnh khung chuẩn hiện đè video
  const [previewLoading, setPreviewLoading] = useState(false);
  const [previewErr, setPreviewErr] = useState(false); // server render khung lỗi
  const [dragging, setDragging] = useState(false);   // đang kéo/giãn ô -> hiện CSS nhanh
  const [frame, setFrame] = useState(null);          // ảnh xem chuẩn PHÓNG TO (modal)
  const [frameBusy, setFrameBusy] = useState(false);
  const videoRef = useRef();
  const wrapRef = useRef();
  const colRef = useRef();          // cột trái -> đo bề rộng khả dụng để fit video
  const frameTimer = useRef(null);  // debounce auto render khung chuẩn
  const frameSeq = useRef(0);       // chống race: chỉ nhận kết quả mới nhất
  const nat = useRef({ w: 1080, h: 1920 });
  const act = useRef(null);
  // Huỷ vòng poll job burn phụ đề khi đóng editor / rời trang.
  const pollAbort = useRef(new AbortController());
  useEffect(() => () => pollAbort.current.abort(), []);

  // Tự sinh phụ đề từ lời dịch (SRT). User bấm nút này khi muốn.
  const autoSubtitle = async () => {
    try {
      const r = await api.subtitlePlan(session, srtRel);
      const loaded = (r.segments || []).map((s) => ({
        ...s, locked: true, words: s.words || wordsFor(s.text, s.start, s.end),
        style: JSON.parse(JSON.stringify(DEF)),
      }));
      setSegs((cur) => [...cur.filter((x) => !x.locked), ...loaded]);
    } catch (e) { alert("Lỗi tạo phụ đề: " + e.message); }
  };

  // chụp ~8 thumbnail từ video (video same-origin nên canvas không bị taint)
  useEffect(() => {
    const v = document.createElement("video");
    v.src = videoUrl; v.muted = true; v.crossOrigin = "anonymous";
    const seek = (time) => new Promise((res) => { const h = () => { v.removeEventListener("seeked", h); res(); }; v.addEventListener("seeked", h); v.currentTime = time; });
    v.addEventListener("loadedmetadata", async () => {
      try {
        const N = 8, cv = document.createElement("canvas"); cv.width = 64; cv.height = 114;
        const ctx = cv.getContext("2d"); const out = [];
        for (let i = 0; i < N; i++) { await seek(v.duration * (i + 0.5) / N); ctx.drawImage(v, 0, 0, cv.width, cv.height); out.push(cv.toDataURL("image/jpeg", 0.5)); }
        setThumbs(out);
      } catch { /* bỏ qua nếu chụp lỗi */ }
    });
    // eslint-disable-next-line
  }, [videoUrl]);

  // đồng bộ thời gian video -> state (rAF cho mượt)
  useEffect(() => {
    const v = videoRef.current; if (!v) return; let raf;
    const loop = () => { setT(v.currentTime); raf = requestAnimationFrame(loop); };
    const onMeta = () => { setDur(v.duration); nat.current = { w: v.videoWidth || 1080, h: v.videoHeight || 1920 }; };
    const onPlay = () => { setPlaying(true); cancelAnimationFrame(raf); loop(); };
    const onPause = () => { setPlaying(false); cancelAnimationFrame(raf); setT(v.currentTime); };
    v.addEventListener("loadedmetadata", onMeta); v.addEventListener("play", onPlay); v.addEventListener("pause", onPause); v.addEventListener("seeked", () => setT(v.currentTime));
    return () => { cancelAnimationFrame(raf); v.removeEventListener("loadedmetadata", onMeta); v.removeEventListener("play", onPlay); v.removeEventListener("pause", onPause); };
    // eslint-disable-next-line
  }, []);

  const patch = (i, p) => setSegs((cur) => cur.map((s, j) => (j === i ? { ...s, ...p } : s)));
  const patchStyle = (p) => setSegs((cur) => cur.map((s, j) => (j === sel ? { ...s, style: { ...s.style, ...p } } : s)));
  const cur = () => segs[sel];

  // đoạn đang hiển thị theo thời gian (nhiều đoạn có thể cùng lúc)
  const activeIdx = segs.map((s, i) => i).filter((i) => t >= segs[i].start && t <= segs[i].end);
  useEffect(() => { if (!editing && activeIdx.length && !activeIdx.includes(sel)) setSel(activeIdx[activeIdx.length - 1]); });

  const total = dur || 1;
  const scale = () => { const v = videoRef.current; return v && v.videoHeight ? v.clientHeight / v.videoHeight : 1; };

  // ---- lanes cho timeline ----
  const lanes = (() => {
    const order = segs.map((s, i) => i).sort((a, b) => segs[a].start - segs[b].start);
    const ends = []; const lane = {};
    order.forEach((i) => { const s = segs[i]; let L = ends.findIndex((e) => s.start >= e - 0.001); if (L < 0) { L = ends.length; ends.push(s.end); } else ends[L] = s.end; lane[i] = L; });
    return { lane, n: Math.max(1, ends.length) };
  })();

  const seekTo = (time) => { const v = videoRef.current; if (v) { v.currentTime = Math.max(0, Math.min(total, time)); } setPlaying(false); if (v) v.pause(); };

  // ---- kéo/giãn ô text trên video ----
  const boxDown = (i) => (e) => {
    if (editing || e.target.dataset.h) return;
    setSel(i); const v = videoRef.current; if (v) v.pause();
    act.current = { kind: "move", i, sx: e.clientX, sy: e.clientY, moved: false };
    e.currentTarget.setPointerCapture(e.pointerId);
  };
  // dir = "w" (tay trái/phải -> đổi BỀ RỘNG vùng) | "h" (tay trên/dưới -> đổi CỠ CHỮ).
  const handleDown = (i, dir) => (e) => {
    if (editing) return; e.stopPropagation(); setSel(i); const v = videoRef.current; if (v) v.pause();
    // Số dòng cố định trong lúc kéo -> ánh xạ cỡ chữ ổn định, không nhảy.
    const nLines = measureBreaks(segs[i]).length + 1;
    act.current = { kind: "resize", dir, i, sx: e.clientX, sy: e.clientY, nLines, moved: false };
    e.currentTarget.parentElement.setPointerCapture(e.pointerId);
  };
  const boxMove = (e) => {
    const a = act.current; if (!a) return;
    if (!a.moved && Math.hypot(e.clientX - a.sx, (e.clientY || 0) - (a.sy || 0)) < 3) return;
    if (!a.moved) setDragging(true);   // bắt đầu kéo thật -> hiện CSS nhanh cho mượt
    a.moved = true; const r = wrapRef.current.getBoundingClientRect(); const s = segs[a.i].style;
    if (a.kind === "move") { const hw = s.w / 2; patch(a.i, { style: { ...s, x: Math.max(hw + 1, Math.min(99 - hw, (e.clientX - r.left) / r.width * 100)), y: Math.max(6, Math.min(94, (e.clientY - r.top) / r.height * 100)) } }); }
    else if (a.dir === "h") {
      // Kéo cạnh TRÊN/DƯỚI -> đổi cỡ chữ (giãn to/nhỏ). Khoảng cách từ TÂM ô tới con
      // trỏ (px hiển thị) ~ nửa chiều cao khối chữ = nLines*fontSize*scale/2 -> con
      // trỏ luôn bám cạnh: fontSize = 2*dist / (nLines*scale).
      const cyPx = r.top + (s.y / 100) * r.height;
      const dist = Math.abs(e.clientY - cyPx);
      const sc = scale() || 1;
      const font = Math.round((2 * dist) / (Math.max(1, a.nLines) * sc));
      patch(a.i, { style: { ...s, fontSize: Math.max(24, Math.min(120, font)) } });
    } else {
      // Kéo cạnh TRÁI/PHẢI -> đổi bề rộng vùng (đối xứng quanh tâm x).
      const dx = Math.abs((e.clientX - r.left) / r.width * 100 - s.x);
      patch(a.i, { style: { ...s, w: Math.max(24, Math.min(96, dx * 2)) } });
    }
  };
  const boxUp = (i) => (e) => {
    const a = act.current; if (a && !a.moved && !segs[i].locked) startEdit(i);
    act.current = null; setDragging(false);   // thả -> render lại khung chuẩn
  };

  const startEdit = (i) => { setSel(i); setEditing(true); const v = videoRef.current; if (v) v.pause(); setTimeout(() => { const el = document.getElementById("subedit-" + i); if (el) { el.focus(); const rg = document.createRange(); rg.selectNodeContents(el); const se = window.getSelection(); se.removeAllRanges(); se.addRange(rg); } }, 0); };
  const endEdit = (i) => (e) => { setEditing(false); const txt = (e.currentTarget.textContent || "").trim() || "Text"; setSegs((cur) => cur.map((s, j) => (j === i ? { ...s, text: txt, words: wordsFor(txt, s.start, s.end) } : s))); };

  const addText = () => {
    const start = Math.max(0, Math.min(t, total - 1)); const end = Math.min(total, start + 2.5);
    const style = JSON.parse(JSON.stringify(DEF)); style.y = 50;
    const seg = { start, end, text: "Text mới", locked: false, style, words: wordsFor("Text mới", start, end) };
    setSegs((cur) => { const n = [...cur, seg]; setSel(n.length - 1); return n; });
    seekTo((start + end) / 2); setTimeout(() => startEdit(segs.length), 30);
  };
  const delText = () => { const s = cur(); if (!s || s.locked) return; setSegs((c) => c.filter((_, j) => j !== sel)); setSel(0); };
  const applyAll = () => { const s = cur().style; setSegs((c) => c.map((sg) => ({ ...sg, style: { ...JSON.parse(JSON.stringify(s)), x: sg.style.x, y: sg.style.y } }))); };
  // Ngược với applyAll: chỉ chép VỊ TRÍ + bề rộng vùng của đoạn đang chọn sang các
  // đoạn còn lại, giữ nguyên font/màu/nền riêng của từng đoạn.
  const applyPosAll = () => {
    const s = cur(); if (!s) return;
    const { x, y, w } = s.style;
    setSegs((c) => c.map((sg, j) => (j === sel ? sg : { ...sg, style: { ...sg.style, x, y, w } })));
  };

  // Nút hành động tức thì (về giữa‑đáy / áp style / áp vị trí) chạy xong không đổi
  // gì trên nút -> user không rõ đã bấm chưa. doFlash loé NHẸ nền cam ~0.6s để báo
  // "đã nhấn". flash = key nút vừa bấm.
  const [flash, setFlash] = useState("");
  const flashTimer = useRef(null);
  useEffect(() => () => clearTimeout(flashTimer.current), []);
  const doFlash = (key, fn) => {
    if (fn) fn();
    setFlash(key);
    clearTimeout(flashTimer.current);
    flashTimer.current = setTimeout(() => setFlash(""), 600);
  };
  const flashStyle = (key) => (flash === key
    ? { background: "var(--orange-soft)", borderColor: "var(--orange)", color: "var(--orange-700)" }
    : undefined);

  // Đo chữ thật (canvas, đúng font/cỡ ở độ phân giải GỐC của video) -> trả về
  // index các từ BẮT ĐẦU dòng mới, để bản burn xuống dòng GIỐNG HỆT preview.
  const measureBreaks = (seg) => {
    const v = videoRef.current; const natW = (v && v.videoWidth) || 1080;
    const st = seg.style; const boxPx = Math.max(80, (st.w / 100) * natW * 0.97);
    const words = (seg.text || "").split(/\s+/).filter(Boolean);
    const cv = measureBreaks._cv || (measureBreaks._cv = document.createElement("canvas"));
    const ctx = cv.getContext("2d");
    // Font nhúng đã là nét đậm sẵn + libass render Bold=0 -> ĐO weight thường
    // (không "bold") để độ rộng đo khớp đúng glyph libass dùng. Nhân k để đo ở
    // ĐÚNG cỡ libass vẽ ra -> điểm ngắt dòng khớp cách libass lấp đầy vùng.
    ctx.font = `${st.fontSize * kOf(st.font)}px ${cssOf(st.font)}`;
    const sp = ctx.measureText(" ").width;
    const breaks = []; let lineW = 0;
    words.forEach((w, i) => {
      const ww = ctx.measureText(w).width;
      if (i > 0 && lineW + sp + ww > boxPx) { breaks.push(i); lineW = ww; }
      else { lineW = i === 0 ? ww : lineW + sp + ww; }
    });
    return breaks;
  };

  // Payload đoạn phụ đề (kèm breaks đo thật) — DÙNG CHUNG cho burn, ASS libass và
  // ảnh xem trước -> mọi nơi ăn cùng dữ liệu.
  const segPayload = () => segs.map((s) => ({
    start: s.start, end: s.end, text: s.text, style: s.style, breaks: measureBreaks(s),
  }));

  const burn = async () => {
    if (!segs.length) return alert("Chưa có đoạn text nào. Bấm 'Tự tạo phụ đề' hoặc 'Thêm text'.");
    setBusy(true); setResult(null);
    try {
      // Đợi font nhúng nạp xong -> measureBreaks đo bằng ĐÚNG font (không fallback).
      try { await document.fonts.ready; } catch { /* trình duyệt cũ: bỏ qua */ }
      const { job_id } = await api.subtitleBurn({
        session, video: videoRel, mode, username: username || "",
        segments: segPayload(),
      });
      const done = await pollJob(job_id, () => {}, 1000, pollAbort.current.signal);
      const out = done.result?.output || (done.items || []).find((i) => i.status === "done")?.result;
      // _v = mốc render -> cache-bust <video> khi burn LẠI cùng tên file (không thì
      // trình duyệt giữ khung đen/bytes cũ; tải về vẫn đúng vì là request mới).
      if (out) setResult({ ...out, _v: Date.now() }); else alert("Lỗi tạo video phụ đề (xem log).");
    } catch (e) { alert("Lỗi: " + e.message); } finally { setBusy(false); }
  };

  // "Xem trước chuẩn": server render 1 khung tại thời điểm hiện tại = CHÍNH bản
  // burn (cùng ASS+libass+font) -> ảnh khớp 100% để đối chiếu tuyệt đối.
  const showFrame = async () => {
    if (!segs.length) return alert("Chưa có đoạn text nào.");
    setFrameBusy(true);
    try {
      try { await document.fonts.ready; } catch { /* bỏ qua */ }
      const r = await api.subtitlePreviewFrame(session, videoRel, segPayload(), mode, t);
      setFrame(fileUrl(r.image_url) + "?v=" + r.v);
    } catch (e) { alert("Lỗi xem trước: " + e.message); }
    finally { setFrameBusy(false); }
  };

  // Cỡ hiển thị video = nội dung THẬT (bỏ letterbox ngang) -> overlay (libass & CSS)
  // ánh xạ đúng toạ độ video nên khớp bản burn. Trước đây overlay tính theo khung
  // thẻ <video> (kèm viền đen) -> lệch ngang & sai bề rộng hộp.
  useEffect(() => {
    const compute = () => {
      const nw = nat.current.w || 1080, nh = nat.current.h || 1920;
      const avail = (colRef.current && colRef.current.clientWidth) || 300;
      const h = Math.min(460, avail * nh / nw);
      setFit({ w: Math.round(h * nw / nh), h: Math.round(h) });
    };
    compute();
    window.addEventListener("resize", compute);
    return () => window.removeEventListener("resize", compute);
  }, [dur]);

  // idle    = lúc NÊN render ảnh chuẩn (bật exact + tạm dừng + không gõ/kéo).
  // showExact = ảnh chuẩn ĐANG hiện trên màn (đã render xong). CHỈ khi đó mới ẩn
  //   lớp CSS. Còn lại (đang phát / đang chờ ảnh / lỗi / gõ / kéo) -> LUÔN hiện CSS
  //   làm nền => KHÔNG bao giờ bị trống chữ. Ảnh chuẩn về thì đè lên CSS.
  const idle = exact && !playing && !editing && !dragging;
  const showExact = idle && !!previewImg;
  useEffect(() => {
    if (!idle) return;
    const v = videoRef.current;
    if (!v || !v.videoWidth || !segs.length) { setPreviewImg(null); return; }
    clearTimeout(frameTimer.current);
    const seq = ++frameSeq.current;
    setPreviewImg(null);                 // xoá ảnh cũ -> hiện khung video thật trong lúc chờ
    setPreviewLoading(true); setPreviewErr(false);
    frameTimer.current = setTimeout(async () => {
      try { await document.fonts.ready; } catch { /* */ }
      try {
        const r = await api.subtitlePreviewFrame(session, videoRel, segPayload(), mode, t);
        if (seq === frameSeq.current) setPreviewImg(fileUrl(r.image_url) + "?v=" + r.v);
      } catch (e) { if (seq === frameSeq.current) setPreviewErr(true); }
      finally { if (seq === frameSeq.current) setPreviewLoading(false); }
    }, 200);
    return () => clearTimeout(frameTimer.current);
    // eslint-disable-next-line
  }, [idle, segs, mode, t, dur]);

  // ---- render 1 ô text ----
  const boxStyle = (s, on) => {
    const st = s.style;
    const sc = scale();
    const fpx = st.fontSize * sc;         // = libass Fontsize ở display px (mốc hình học hộp/viền, KHÔNG co K)
    const px = fpx * kOf(st.font);         // cỡ chữ CSS đã co K -> nét chữ KHỚP libass
    // lineHeight = 1/K: khoảng cách dòng CSS = fpx = advance libass (đo thực nghiệm
    // libass giãn dòng đúng 1.0*fsize). px = fpx*K nên lineHeight*px = fpx -> khớp.
    const base = { position: "absolute", left: st.x + "%", top: st.y + "%", transform: "translate(-50%,-50%)", width: st.w + "%", textAlign: "center", lineHeight: 1 / kOf(st.font), fontWeight: 400, fontFamily: cssOf(st.font), fontSize: px + "px", color: st.color, boxSizing: "border-box", padding: "0 1.5%", cursor: editing ? "text" : "move", outline: on ? "1.5px dashed rgba(255,255,255,.85)" : "none", zIndex: on ? 5 : 2, wordWrap: "break-word" };
    const b = st.bg; const ol = (p, c) => `${-p}px ${-p}px 0 ${c},${p}px ${-p}px 0 ${c},${-p}px ${p}px 0 ${c},${p}px ${p}px 0 ${c},0 ${p * 1.3}px 3px ${c}`;
    // Viền: libass Outline (ScaledBorderAndShadow, PlayResY=videoH) = px ở video, quy
    // display = value*sc. outlineThin=2, outlineThick=5, outlineWhite=3 (khớp _style_line).
    if (b === "outlineThin") base.textShadow = ol(Math.max(0.6, 2 * sc), "#000");
    else if (b === "outlineThick") base.textShadow = ol(Math.max(0.8, 5 * sc), "#000");
    else if (b === "outlineWhite") base.textShadow = ol(Math.max(0.6, 3 * sc), "#fff");
    else if (b === "shadow") base.textShadow = "0 3px 7px rgba(0,0,0,.95)";
    else if (b === "glow") base.textShadow = `0 0 9px ${st.highlight},0 0 4px ${st.highlight}`;
    // Hộp nền: đệm DỌC quy theo fpx (KHÔNG co K) -> KHỚP _bg_event backend
    // (pad_y=0.14/0.23*fsize). Đệm NGANG = 1.5% bề rộng hộp (CSS % = theo width) ->
    // nội dung = 97% vùng, đúng bằng measureBreaks (*0.97) -> ngắt dòng live khớp burn.
    // Bo góc quy theo fpx (radius={..}*fsize trong _bg_event).
    else if (b === "boxDark") { base.background = "rgba(0,0,0,.5)"; base.padding = `${0.14 * fpx}px 1.5%`; base.borderRadius = 0.20 * fpx + "px"; }
    else if (b === "boxSolid") { base.background = "#000"; base.padding = `${0.14 * fpx}px 1.5%`; base.borderRadius = 0.13 * fpx + "px"; }
    else if (b === "boxRound") { base.background = "rgba(0,0,0,.62)"; base.padding = `${0.23 * fpx}px 1.5%`; base.borderRadius = 0.73 * fpx + "px"; }
    else if (b === "bar") { base.background = "rgba(0,0,0,.5)"; base.padding = `${0.23 * fpx}px 1.5%`; }
    else if (b === "boxColor") { base.background = st.highlight; base.padding = `${0.14 * fpx}px 1.5%`; base.borderRadius = 0.20 * fpx + "px"; }
    // Ảnh chuẩn đang đè -> ẩn chữ/nền/viền CSS (chỉ giữ vùng bắt sự kiện + khung
    // chọn). Lúc khác luôn hiện CSS làm nền -> không bao giờ trống chữ.
    if (showExact && !(editing && on)) {
      base.color = "transparent"; base.textShadow = "none";
      base.background = "none"; base.padding = 0;
    }
    return base;
  };
  const boxContent = (s) => {
    if (editing && segs[sel] === s) return null;   // ô đang gõ: contentEditable tự giữ text
    if (showExact) return null;                     // ảnh chuẩn đang đè -> CSS không vẽ (tránh 2 lớp)
    if (mode === "whole") return s.text;
    if (mode === "progressive") return s.words.filter((w) => t >= w.start).map((w) => w.w).join(" ") || " ";
    return s.words.map((w, i) => <span key={i} style={{ color: t >= w.start ? s.style.highlight : s.style.color }}>{w.w}{i < s.words.length - 1 ? " " : ""}</span>);
  };

  const fmt = (x) => { x = Math.max(0, x || 0); const m = Math.floor(x / 60), ss = Math.floor(x % 60); return `${String(m).padStart(2, "0")}:${String(ss).padStart(2, "0")}`; };

  return (
    <div className="card" style={{ borderColor: "var(--orange)" }}>
      <div className="flex between center">
        <div className="panel-title" style={{ margin: 0 }}>Chèn phụ đề (hard sub)</div>
        <button className="btn sm ghost" onClick={onClose}>Đóng</button>
      </div>

      <div className="grid-2" style={{ gridTemplateColumns: "300px 1fr", gap: 16, marginTop: 10 }}>
        {/* preview + timeline */}
        <div ref={colRef}>
          <div ref={wrapRef} style={{ position: "relative", width: fit.w ? fit.w + "px" : "100%", height: fit.h ? fit.h + "px" : "auto", margin: "0 auto", background: "#000", borderRadius: 12, overflow: "hidden" }}>
            <video ref={videoRef} src={videoUrl} onClick={() => { const v = videoRef.current; v.paused ? v.play() : v.pause(); }}
                   style={{ display: "block", width: "100%", height: "100%", objectFit: "contain" }} />
            {/* Ảnh khung CHUẨN (server ffmpeg = đúng bản burn) đè lên video khi rảnh. */}
            {idle && previewImg && (
              <img src={previewImg} alt="" draggable={false}
                   onError={() => { setPreviewImg(null); setPreviewErr(true); }}
                   style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "contain", zIndex: 1, pointerEvents: "none" }} />
            )}
            {idle && previewLoading && (
              <div style={{ position: "absolute", right: 6, top: 6, zIndex: 6, background: "rgba(0,0,0,.55)", color: "#fff", fontSize: 11, padding: "2px 8px", borderRadius: 10, pointerEvents: "none" }}>đang dựng khung chuẩn…</div>
            )}
            {idle && previewErr && !previewLoading && (
              <div style={{ position: "absolute", right: 6, top: 6, zIndex: 6, background: "rgba(200,40,40,.9)", color: "#fff", fontSize: 11, padding: "2px 8px", borderRadius: 10, pointerEvents: "none" }}>Lỗi dựng khung chuẩn (server)</div>
            )}
            {segs.map((s, i) => (t >= s.start && t <= s.end) ? (
              <div key={i} style={boxStyle(s, i === sel)} onPointerDown={boxDown(i)} onPointerMove={boxMove} onPointerUp={boxUp(i)}>
                <div id={"subedit-" + i} contentEditable={editing && i === sel} suppressContentEditableWarning
                     onBlur={endEdit(i)} style={{ outline: "none" }}>
                  {boxContent(s)}
                </div>
                {i === sel && !editing && (() => {
                  const hst = { position: "absolute", background: "var(--orange)", border: "1px solid #fff", borderRadius: 2, zIndex: 6 };
                  return (<>
                    <span data-h="1" onPointerDown={handleDown(i, "w")} title="Kéo đổi bề rộng vùng"
                          style={{ ...hst, left: -4, top: "50%", transform: "translateY(-50%)", width: 6, height: 13, cursor: "ew-resize" }} />
                    <span data-h="1" onPointerDown={handleDown(i, "w")} title="Kéo đổi bề rộng vùng"
                          style={{ ...hst, right: -4, top: "50%", transform: "translateY(-50%)", width: 6, height: 13, cursor: "ew-resize" }} />
                    <span data-h="1" onPointerDown={handleDown(i, "h")} title="Kéo giãn to/nhỏ cỡ chữ"
                          style={{ ...hst, top: -4, left: "50%", transform: "translateX(-50%)", width: 13, height: 6, cursor: "ns-resize" }} />
                    <span data-h="1" onPointerDown={handleDown(i, "h")} title="Kéo giãn to/nhỏ cỡ chữ"
                          style={{ ...hst, bottom: -4, left: "50%", transform: "translateX(-50%)", width: 13, height: 6, cursor: "ns-resize" }} />
                  </>);
                })()}
              </div>
            ) : null)}
          </div>

          <div className="flex center gap8 mt8">
            <button className="btn sm" onClick={() => { const v = videoRef.current; v.paused ? v.play() : v.pause(); }}>{playing ? "❚❚" : "►"}</button>
            <span className="small muted">{fmt(t)} / {fmt(total)}</span>
            <button className="btn sm" style={{ marginLeft: "auto" }} onClick={addText}><IcPlus size={13} /> Thêm text</button>
          </div>
          <div className="flex center gap8 mt8" style={{ flexWrap: "wrap" }}>
            <label className="small" style={{ display: "inline-flex", alignItems: "center", gap: 6, cursor: "pointer" }}
                   title="Bật: khi DỪNG, hiện đúng khung video sẽ xuất (server render). Tắt: chỉ xem nhanh bằng CSS (gần đúng).">
              <input type="checkbox" checked={exact} onChange={(e) => setExact(e.target.checked)} />
              Xem trước khớp 100% (dừng để thấy)
            </label>
            <button className="btn sm ghost" onClick={showFrame} disabled={frameBusy || !segs.length}
                    title="Mở ảnh khung chuẩn phóng to (đúng video sẽ xuất)">
              {frameBusy ? "Đang render…" : "🔍 Phóng to ảnh chuẩn"}
            </button>
          </div>
          {exact && (
            <p className="small muted" style={{ margin: "6px 0 0" }}>
              {playing ? "▶ Đang phát: xem nhanh (gần đúng). Bấm DỪNG (⏸) để xem phụ đề CHUẨN."
                : showExact ? "✅ Phụ đề đang hiện = ĐÚNG y video sẽ xuất."
                  : previewErr ? "⚠ Chưa dựng được khung chuẩn (đang hiện tạm bản gần đúng)."
                    : "⏳ Đang dựng khung chuẩn… (đang hiện tạm bản gần đúng)"}
            </p>
          )}

          {/* timeline */}
          <div style={{ position: "relative", marginTop: 10 }} >
            <div style={{ display: "flex", height: 30, borderRadius: 6, overflow: "hidden", border: "0.5px solid var(--line)", cursor: "pointer" }}
                 onPointerDown={(e) => { const r = e.currentTarget.getBoundingClientRect(); seekTo((e.clientX - r.left) / r.width * total); }}>
              {(thumbs.length ? thumbs : Array.from({ length: 8 })).map((th, i) => (
                <div key={i} style={{ flex: 1, background: th ? `center/cover url(${th})` : "#334", borderRight: i < 7 ? "1px solid rgba(255,255,255,.15)" : "none" }} />
              ))}
            </div>
            <div style={{ position: "relative", height: lanes.n * 22 + (lanes.n - 1) * 3, marginTop: 4 }}>
              {segs.map((s, i) => (
                <div key={i} onClick={() => { seekTo((s.start + s.end) / 2); setSel(i); }}
                     title={s.text}
                     style={{ position: "absolute", left: (s.start / total * 100) + "%", width: Math.max(6, (s.end - s.start) / total * 100) + "%", top: lanes.lane[i] * 25, height: 22, borderRadius: 5, fontSize: 10, lineHeight: "20px", padding: "0 6px", boxSizing: "border-box", overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis", cursor: "pointer", border: i === sel ? "1.5px solid var(--orange)" : "1px solid var(--line)", borderStyle: s.locked ? "solid" : "dashed", background: i === sel ? "var(--orange-soft)" : "var(--white)", color: i === sel ? "var(--orange-700)" : "var(--muted)" }}>
                  {s.locked ? "🔒 " : ""}{trunc(s.text, 16)}
                </div>
              ))}
            </div>
            <div style={{ position: "absolute", top: 0, width: 2, height: 34 + lanes.n * 22 + (lanes.n - 1) * 3, background: "#ff3b30", left: `calc(${t / total * 100}% - 1px)`, pointerEvents: "none" }} />
          </div>
        </div>

        {/* controls */}
        <div className="stack" style={{ gap: 12 }}>
          {segs.filter((s) => s.locked).length === 0 && (
            <button className="btn primary" onClick={autoSubtitle}>Tự tạo phụ đề từ lời dịch</button>
          )}
          <div className="flex gap8 center">
            <button className="btn sm ghost" onClick={delText} style={{ display: cur() && !cur().locked ? "inline-flex" : "none" }}><IcTrash size={13} /> Xóa đoạn</button>
            <span className="small muted">{cur() && cur().locked ? "🔒 text lồng tiếng — không xóa được" : ""}</span>
          </div>

          <div>
            <span className="field-label">Kiểu hiển thị (chung cả video)</span>
            <div className="seg-group">{MODES.map(([id, lb]) => <button key={id} className={mode === id ? "active" : ""} onClick={() => setMode(id)}>{lb}</button>)}</div>
          </div>

          {cur() && (
            <>
              <div className="row wrap" style={{ gap: 10 }}>
                <div style={{ flex: 1 }}><span className="field-label">Phông</span>
                  <select value={cur().style.font} onChange={(e) => patchStyle({ font: e.target.value })} style={{ width: "100%" }}>
                    {FONTS.map((f) => <option key={f.ass} value={f.ass}>{f.label}</option>)}</select></div>
                <div style={{ flex: 1 }}><span className="field-label">Kiểu nền</span>
                  <select value={cur().style.bg} onChange={(e) => patchStyle({ bg: e.target.value })} style={{ width: "100%" }}>
                    {BGS.map(([lb, id]) => <option key={id} value={id}>{lb}</option>)}</select></div>
              </div>
              <div className="row wrap" style={{ gap: 10 }}>
                <label className="field-label" style={{ flex: 1 }}>Màu chữ
                  <input type="color" value={cur().style.color} onChange={(e) => patchStyle({ color: e.target.value })}
                         style={{ width: "100%", height: 34, marginTop: 4, border: "1px solid var(--line)", borderRadius: 6, padding: 2, cursor: "pointer", background: "var(--white)" }} /></label>
                <label className="field-label" style={{ flex: 1 }}>Màu highlight
                  <input type="color" value={cur().style.highlight} onChange={(e) => patchStyle({ highlight: e.target.value })}
                         style={{ width: "100%", height: 34, marginTop: 4, border: "1px solid var(--line)", borderRadius: 6, padding: 2, cursor: "pointer", background: "var(--white)" }} /></label>
              </div>
              <div><span className="field-label">Cỡ chữ: {cur().style.fontSize}</span>
                <input className="slider" type="range" min="24" max="120" value={cur().style.fontSize} onChange={(e) => patchStyle({ fontSize: +e.target.value })} /></div>
              <div className="flex gap8" style={{ flexWrap: "wrap" }}>
                <button className="btn sm" style={flashStyle("center")} onClick={() => doFlash("center", () => patchStyle({ x: 50, y: 86 }))}>Về giữa‑đáy</button>
                <button className="btn sm" style={flashStyle("all")} onClick={() => doFlash("all", applyAll)}>Áp style cho mọi đoạn</button>
                <button className="btn sm" style={flashStyle("pos")} onClick={() => doFlash("pos", applyPosAll)} disabled={segs.length < 2}
                        title="Chép vị trí (và bề rộng vùng) của đoạn đang chọn sang tất cả đoạn khác">
                  Áp vị trí cho mọi đoạn
                </button>
              </div>
            </>
          )}

          <button className="btn primary block mt8" onClick={burn} disabled={busy || !segs.length}>
            {busy ? "Đang tạo video có phụ đề…" : "Tạo video có phụ đề"}
          </button>
          <p className="small muted">Đoạn 🔒 là phụ đề từ lời dịch (không xóa). Bấm vào chữ trên video để sửa; kéo để đặt vị trí; kéo tay <b>trái/phải</b> để đổi bề rộng vùng, kéo tay <b>trên/dưới</b> để giãn to/nhỏ cỡ chữ. Đặt xong 1 đoạn, bấm “Áp vị trí cho mọi đoạn” để các đoạn kia về cùng chỗ.</p>
        </div>
      </div>

      {result && (
        <div className="mt16" style={{ borderTop: "1px solid var(--line)", paddingTop: 14 }}>
          <div className="panel-title" style={{ marginBottom: 10 }}>Video đã chèn phụ đề</div>
          <div className="flex" style={{ gap: 14, flexWrap: "wrap", alignItems: "flex-start" }}>
            <video key={result._v} src={fileUrl(result.video_url) + (result._v ? "?v=" + result._v : "")} controls style={{ maxWidth: 260, maxHeight: 420, borderRadius: 12, background: "#000" }} />
            <a className="btn primary" href={fileUrl(result.video_url)} download><IcDownload size={15} /> Tải video có phụ đề</a>
          </div>
        </div>
      )}

      {frame && (
        <div onClick={() => setFrame(null)}
             style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.72)", zIndex: 60, display: "flex", alignItems: "center", justifyContent: "center", padding: 20 }}>
          <div onClick={(e) => e.stopPropagation()} style={{ background: "var(--white)", borderRadius: 12, padding: 12, maxWidth: "92vw", maxHeight: "92vh", overflow: "auto" }}>
            <div className="flex between center" style={{ marginBottom: 8, gap: 12 }}>
              <div className="panel-title" style={{ margin: 0 }}>Xem trước chuẩn — đúng như video sẽ xuất</div>
              <button className="btn sm ghost" onClick={() => setFrame(null)}>Đóng</button>
            </div>
            <img src={frame} alt="preview" style={{ display: "block", maxWidth: "100%", maxHeight: "78vh", borderRadius: 8, background: "#000" }} />
          </div>
        </div>
      )}
    </div>
  );
}
