import { useCallback, useEffect, useRef, useState } from "react";
import { Sparkles, Wand2, Copy, Trash2, Plus, Minus, SlidersHorizontal, Gauge } from "lucide-react";
import { Player, type PlayerRef } from "@remotion/player";
import { EditorComposition } from "../remotion/EditorComposition";
import { useStore, selectStats, type MotionItem } from "../store";
import { useShallow } from "zustand/react/shallow";
import { msToFrame, frameToMs, FPS } from "../lib/time";
import { pipBox } from "../lib/pip";
import { COMP_W, COMP_H, OV_ANCHORS, clampOverlayScale } from "../lib/overlay";
import { isOverlayTemplate, isImpactTemplate, MG_ACCENTS } from "../remotion/mg/catalog.mjs";

// overlays AND full-screen Impact scenes can be moved + corner-resized in the preview
const isMovable = (id?: string | null): boolean => !!id && (isOverlayTemplate(id) || isImpactTemplate(id));
// the pivot/anchor a scene scales about: Impact scenes are centred; overlays use their corner `pos`
const anchorPos = (item?: { template?: string; props?: Record<string, unknown> } | null): string =>
  item && item.template && isImpactTemplate(item.template) ? "center" : String((item?.props && item.props.pos) ?? "bc");

export function Preview() {
  const edl = useStore((s) => s.edl);
  const wordsById = useStore((s) => s.wordsById);
  const motion = useStore((s) => s.motion);
  const videos = useStore((s) => s.videos);
  const tracks = useStore((s) => s.tracks);
  const captionsOn = useStore((s) => s.captionsOn);
  const captionStyle = useStore((s) => s.captionStyle);
  const captionColors = useStore((s) => s.captionColors);
  const captionAnim = useStore((s) => s.captionAnim);
  const lang = useStore((s) => s.lang);
  const audioSrc = useStore((s) => s.audioSrc);
  const music = useStore((s) => s.music);
  const musicVolume = useStore((s) => s.musicVolume);
  const musicFadeInMs = useStore((s) => s.musicFadeInMs);
  const musicFadeOutMs = useStore((s) => s.musicFadeOutMs);
  const sfxStyle = useStore((s) => s.sfxStyle);
  const projectName = useStore((s) => s.projectName);
  const setPlayhead = useStore((s) => s.setPlayhead);
  const setSeekFn = useStore((s) => s.setSeekFn);
  const setToggleFn = useStore((s) => s.setToggleFn);
  const setPlaying = useStore((s) => s.setPlaying);
  const playing = useStore((s) => s.playing);
  const playbackQuality = useStore((s) => s.playbackQuality);
  const setPlaybackQuality = useStore((s) => s.setPlaybackQuality);
  const stats = useStore(useShallow(selectStats));
  const ref = useRef<PlayerRef | null>(null);
  const stageRef = useRef<HTMLDivElement | null>(null);
  const [ready, setReady] = useState(false);
  const durationInFrames = Math.max(1, msToFrame(stats.durationMs));

  // Adaptive playback (pro-NLE "reduced-resolution / draft" preview): while PLAYING, drop the heavy
  // per-frame effects ("preview" quality → no backdrop-blur etc.) and, on "smooth", also render the
  // preview at ~60% resolution then upscale. Paused frames + every export stay full fidelity.
  const fast = playing && playbackQuality !== "full";
  const playQuality: "preview" | "final" = fast ? "preview" : "final";
  const playScale = playing && playbackQuality === "smooth" ? 0.6 : 1;

  // callback ref so the effect re-runs once the Player actually mounts (it isn't
  // in the tree on the empty-state render, so a plain ref would stay null forever)
  const setPlayerRef = useCallback((r: PlayerRef | null) => { ref.current = r; setReady(!!r); }, []);

  useEffect(() => {
    const p = ref.current;
    if (!p) return;
    const onFrame = (e: any) => setPlayhead(frameToMs(e.detail.frame));
    const onPlay = () => setPlaying(true);
    const onPause = () => setPlaying(false);
    p.addEventListener("frameupdate", onFrame);
    p.addEventListener("play", onPlay);
    p.addEventListener("pause", onPause);
    setSeekFn((ms: number) => { try { p.seekTo(msToFrame(ms)); } catch {} });
    setToggleFn(() => { try { p.toggle(); } catch {} });
    return () => {
      p.removeEventListener("frameupdate", onFrame);
      p.removeEventListener("play", onPlay);
      p.removeEventListener("pause", onPause);
      setSeekFn(null); setToggleFn(null); setPlaying(false);
    };
  }, [ready, setPlayhead, setSeekFn, setToggleFn, setPlaying]);

  const empty = edl.length === 0 && motion.length === 0 && videos.length === 0;

  return (
    <section data-tour="ed-preview" className="flex min-w-0 flex-1 flex-col bg-[#0e0e12]">
      <div id="program-monitor" className="flex min-h-0 flex-1 items-center justify-center bg-[#0e0e12] p-3">
        {empty ? (
          <div className="grid aspect-video w-full max-w-3xl place-items-center rounded-md border border-dashed border-white/10">
            <div className="flex max-w-sm flex-col items-center gap-3 px-6 text-center">
              <div className="grid h-12 w-12 place-items-center rounded-full bg-primary/15"><Wand2 size={22} className="text-primary" /></div>
              <div>
                <div className="text-sm font-semibold text-white/90">Nothing to preview yet</div>
                <div className="mt-1 text-[12.5px] leading-relaxed text-white/50">Let the AI agent build {projectName ? <span className="text-white/75">“{projectName}”</span> : "your video"} from the script — it’ll outline the sections and add the scenes.</div>
              </div>
              <button onClick={() => useStore.getState().requestAgent("Build an explainer video from my script — propose a section outline with treatments, then start building: set a design style and add the scenes in order.")}
                className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-[13px] font-semibold text-white hover:brightness-105">
                <Sparkles size={15} /> Build it with AI
              </button>
            </div>
          </div>
        ) : (
          <div ref={stageRef} className="relative h-full w-full">
            {/* reduced-resolution playback: render the Player into a smaller box while playing, then CSS-scale
                it up to fill — fewer pixels to rasterise/composite (incl. the video draw). Identity when 1. */}
            <div data-mg-playerwrap="" style={playScale < 1
              ? { position: "absolute", top: 0, left: 0, width: `${playScale * 100}%`, height: `${playScale * 100}%`, transform: `scale(${1 / playScale})`, transformOrigin: "top left", willChange: "transform" }
              : { position: "absolute", inset: 0 }}>
              <Player
                ref={setPlayerRef}
                component={EditorComposition}
                inputProps={{ edl, wordsById, motion, videos, tracks, audioSrc, music, musicVolume, musicFadeInMs, musicFadeOutMs, sfxStyle, captionsOn, captionStyle, captionColors, captionAnim, lang, quality: playQuality }}
                durationInFrames={durationInFrames}
                compositionWidth={1920}
                compositionHeight={1080}
                fps={FPS}
                style={{ width: "100%", height: "100%" }}
                controls
                // stacking many scenes that start together (each may fire an SFX) exceeds Remotion's default
                // pool of 5 shared audio tags → the Player throws "Tried to mount N Html5Audio tags". Raise the
                // pool so a busy timeline of overlays doesn't crash the preview. Playback is user-initiated, so
                // the autoplay workaround these tags provide isn't needed beyond covering simultaneous cues.
                numberOfSharedAudioTags={24}
                acknowledgeRemotionLicense
              />
            </div>
            <SceneSelectionLayer stageRef={stageRef} />
            <PipTransformOverlay stageRef={stageRef} />
            <CropOverlay stageRef={stageRef} />
          </div>
        )}
      </div>
      <div className="flex shrink-0 items-center justify-between gap-3 border-t border-white/5 bg-[#0e0e12] px-3 py-1 text-[11px] text-white/40">
        <div className="flex items-center gap-3">
          <span>1920×1080</span><span>·</span><span>{Math.round(stats.durationMs / 1000)}s</span><span>·</span>
          <span>{stats.kept} words</span><span>·</span><span>{stats.motionCount} scene{stats.motionCount === 1 ? "" : "s"}</span>
        </div>
        {/* Playback quality (pro-NLE style) — trade preview fidelity for smoother playback on big videos /
            many graphics. Exports are ALWAYS full quality regardless of this. Persisted per device. */}
        <div className="flex items-center gap-1" title="Playback quality — lower it for smoother playback on big videos or many overlays. Paused frames and every export stay full quality.">
          <Gauge size={12} className="mr-0.5 text-white/35" />
          {([["full", "Full"], ["balanced", "Balanced"], ["smooth", "Smooth"]] as const).map(([q, label]) => (
            <button key={q} onClick={() => setPlaybackQuality(q)}
              className={"rounded px-1.5 py-0.5 text-[10.5px] font-medium transition " + (playbackQuality === q ? "bg-white/15 text-white/90" : "text-white/40 hover:bg-white/5 hover:text-white/70")}>
              {label}
            </button>
          ))}
        </div>
      </div>
    </section>
  );
}

const MIN_PX = 90;
type Corner = "tl" | "tr" | "bl" | "br";
type SelRect = { left: number; top: number; w: number; h: number };

// Measure the TIGHT on-screen box of a scene's ACTUAL visible content (not its full-frame wrapper) by
// unioning the rects of its first non-full-frame descendants — so the selection box hugs the graphic
// itself wherever it has animated/been dragged to. Returns viewport-coord edges, clamped to the frame.
function measureContentRect(root: Element): { left: number; top: number; right: number; bottom: number } | null {
  const full = root.getBoundingClientRect();
  if (full.width < 4 || full.height < 4) return null;
  let l = Infinity, t = Infinity, r = -Infinity, b = -Infinity, found = false;
  const visit = (el: Element, depth: number) => {
    for (const child of Array.from(el.children)) {
      const cr = child.getBoundingClientRect();
      if (cr.width < 3 || cr.height < 3) continue;
      const cs = window.getComputedStyle(child);
      if (cs.visibility === "hidden" || cs.display === "none" || cs.opacity === "0") continue;
      // near-full-frame nodes are wrappers/masks — recurse THROUGH them but don't count them as content
      if (cr.width >= full.width * 0.9 && cr.height >= full.height * 0.9) { if (depth < 6) visit(child, depth + 1); continue; }
      if (cr.left < l) l = cr.left; if (cr.top < t) t = cr.top;
      if (cr.right > r) r = cr.right; if (cr.bottom > b) b = cr.bottom; found = true;
    }
  };
  visit(root, 0);
  if (!found) return null;
  return { left: Math.max(full.left, l), top: Math.max(full.top, t), right: Math.min(full.right, r), bottom: Math.min(full.bottom, b) };
}

// DIRECT-MANIPULATION layer over the Player. Click a graphic IN the video to select it (no trip to the
// timeline); the selection box is MEASURED so it hugs the graphic itself. For overlays: drag the interior
// to move (ox/oy, with magenta centre-snap guides) and the corner handles to resize (scale, pivoting
// about the overlay anchor so it grows in place). A floating quick toolbar pins above the selection for
// in-context colour/size/duplicate/delete + a jump to the full inspector. Clicking empty video or Esc
// deselects. ox/oy/scale are applied identically in SceneContent so the gizmo == the exported result.
function SceneSelectionLayer({ stageRef }: { stageRef: React.RefObject<HTMLDivElement | null> }) {
  const selId = useStore((s) => s.selectedIds[0] ?? s.inspectId ?? null);
  const m = useStore((s) => (selId ? s.motion.find((x) => x.id === selId) : undefined));
  const playheadMs = useStore((s) => s.playheadMs);
  const playing = useStore((s) => s.playing);
  const updateMotion = useStore((s) => s.updateMotion);
  const selectMotion = useStore((s) => s.selectMotion);
  const clearSelection = useStore((s) => s.clearSelection);
  const pushHistory = useStore((s) => s.pushHistory);
  const [box, setBox] = useState({ w: 0, h: 0 });
  const [sel, setSel] = useState<SelRect | null>(null);
  const [guide, setGuide] = useState({ v: false, h: false });
  const [sizing, setSizing] = useState(false);
  const lastKey = useRef("");

  // stage size
  useEffect(() => {
    const el = stageRef.current; if (!el) return;
    const measure = () => setBox({ w: el.clientWidth, h: el.clientHeight });
    measure();
    const ro = new ResizeObserver(measure); ro.observe(el);
    return () => ro.disconnect();
  }, [stageRef]);

  // continuously re-measure the SELECTED scene's content box so the gizmo tracks it as it animates / is
  // dragged. Only re-renders when the box actually moves (key diff) → no per-frame churn while static.
  useEffect(() => {
    // hide the gizmo while playing (matches pro NLEs) — and skip the per-frame measurement entirely
    if (!selId || playing) { setSel(null); lastKey.current = ""; return; }
    let raf = 0, alive = true;
    const tick = () => {
      if (!alive) return;
      const stage = stageRef.current;
      const node = stage?.querySelector(`[data-mg-scene-id="${CSS.escape(selId)}"]`);
      let next: SelRect | null = null;
      if (stage && node) {
        const sb = stage.getBoundingClientRect();
        const r = measureContentRect(node);
        if (r) next = { left: r.left - sb.left, top: r.top - sb.top, w: r.right - r.left, h: r.bottom - r.top };
      }
      const key = next ? `${next.left | 0}:${next.top | 0}:${next.w | 0}:${next.h | 0}` : "none";
      if (key !== lastKey.current) { lastKey.current = key; setSel(next); }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => { alive = false; cancelAnimationFrame(raf); };
  }, [selId, stageRef, box.w, box.h, playing]);

  // Esc deselects
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && selId) clearSelection(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [selId, clearSelection]);

  // letterboxed 16:9 content rect inside the stage box (same mapping as the PiP overlay)
  const cw = Math.min(box.w, (box.h * COMP_W) / COMP_H);
  const ch = (cw * COMP_H) / COMP_W;
  const cl = (box.w - cw) / 2, ct = (box.h - ch) / 2;
  const disp = cw / COMP_W;       // display scale (screen px per composition px)
  const CONTROLS = 58, SNAP = 18;

  const isOv = isMovable(m?.template);
  const sc = m?.scale && m.scale > 0 ? m.scale : 1;
  // anchor (composition px) → its live position on screen, the pivot the resize scales about
  const A = OV_ANCHORS[anchorPos(m)] || OV_ANCHORS.bc;
  const ax = A.x * COMP_W, ay = A.y * COMP_H;
  const pvx = cl + disp * (ax + (m?.ox || 0)), pvy = ct + disp * (ay + (m?.oy || 0));

  // hit-test every visible scene under the cursor; the SMALLEST box wins (an overlay beats a full-frame bg)
  const pick = (cx: number, cy: number): string | null => {
    const stage = stageRef.current; if (!stage) return null;
    let hitId: string | null = null, hitArea = Infinity;
    stage.querySelectorAll("[data-mg-scene-id]").forEach((node) => {
      const id = node.getAttribute("data-mg-scene-id"); if (!id) return;
      const r = measureContentRect(node); if (!r) return;
      if (cx >= r.left && cx <= r.right && cy >= r.top && cy <= r.bottom) {
        const area = (r.right - r.left) * (r.bottom - r.top);
        if (area <= hitArea) { hitArea = area; hitId = id; }
      }
    });
    return hitId;
  };

  // click the video: select the graphic under the cursor (empty → deselect); for an overlay, the same
  // gesture can grab-and-move it. Resize is on the corner handles. Centre-snap guides show while moving.
  const onSurfaceDown = (e: React.PointerEvent) => {
    const id = pick(e.clientX, e.clientY);
    if (!id) { clearSelection(); return; }
    if (id !== selId) selectMotion(id);
    const item = useStore.getState().motion.find((x) => x.id === id);
    if (!item || !item.template || !isMovable(item.template)) return; // overlays + Impact scenes move on drag
    e.preventDefault();
    const a2 = OV_ANCHORS[anchorPos(item)] || OV_ANCHORS.bc;
    const cxOff = COMP_W / 2 - a2.x * COMP_W, cyOff = COMP_H / 2 - a2.y * COMP_H;
    const startX = e.clientX, startY = e.clientY, o = { ox: item.ox || 0, oy: item.oy || 0 };
    let moved = false;
    const onMove = (ev: PointerEvent) => {
      if (!moved) { if (Math.abs(ev.clientX - startX) < 3 && Math.abs(ev.clientY - startY) < 3) return; moved = true; pushHistory(); }
      let nx = o.ox + (ev.clientX - startX) / disp;
      let ny = o.oy + (ev.clientY - startY) / disp;
      const nearV = Math.abs(nx - cxOff) < SNAP, nearH = Math.abs(ny - cyOff) < SNAP;
      if (nearV) nx = cxOff; if (nearH) ny = cyOff;
      setGuide({ v: nearV, h: nearH });
      nx = Math.max(-COMP_W, Math.min(COMP_W, nx)); ny = Math.max(-COMP_H, Math.min(COMP_H, ny));
      updateMotion(id, { ox: Math.round(nx), oy: Math.round(ny) });
    };
    const onUp = () => { setGuide({ v: false, h: false }); window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); };
    window.addEventListener("pointermove", onMove); window.addEventListener("pointerup", onUp);
  };

  // corner-resize → uniform scale about the overlay anchor (matches overlayOrigin in SceneContent), so the
  // graphic grows exactly in place. Scale = ratio of pointer→anchor distance now vs. at gesture start.
  const resize = (e: React.PointerEvent) => {
    if (!m) return;
    e.preventDefault(); e.stopPropagation();
    const rect = stageRef.current?.getBoundingClientRect(); if (!rect) return;
    const px = rect.left + pvx, py = rect.top + pvy;
    const d0 = Math.max(24, Math.hypot(e.clientX - px, e.clientY - py));
    const startScale = sc, id = m.id;
    let moved = false; setSizing(true);
    const onMove = (ev: PointerEvent) => {
      if (!moved) { if (Math.abs(ev.clientX - e.clientX) < 2 && Math.abs(ev.clientY - e.clientY) < 2) return; moved = true; pushHistory(); }
      const d = Math.hypot(ev.clientX - px, ev.clientY - py);
      updateMotion(id, { scale: Math.round(clampOverlayScale(startScale * (d / d0)) * 100) / 100 });
    };
    const onUp = () => { setSizing(false); window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); };
    window.addEventListener("pointermove", onMove); window.addEventListener("pointerup", onUp);
  };

  if (box.w < 2) return null;
  const onScreen = !!m && playheadMs >= m.startMs && playheadMs < m.startMs + m.durationMs;
  const vX = cl + disp * (COMP_W / 2), hY = ct + disp * (COMP_H / 2);
  const HS = 14;
  const handle = (c: Corner, left: number, top: number, cur: string) => (
    <div key={c} onPointerDown={resize} title="Drag to resize"
      style={{ position: "absolute", left: left - HS / 2, top: top - HS / 2, width: HS, height: HS, borderRadius: 4, background: "#fff", border: "2px solid #E7723B", boxShadow: "0 1px 4px rgba(0,0,0,0.55)", cursor: cur, pointerEvents: "auto", touchAction: "none" }} />
  );

  return (
    <div className="absolute inset-0" style={{ pointerEvents: "none" }}>
      {/* click-capture surface over the video (minus the transport strip) — select / deselect / move */}
      <div onPointerDown={onSurfaceDown}
        style={{ position: "absolute", left: cl, top: ct, width: cw, height: Math.max(0, ch - CONTROLS), cursor: "default", pointerEvents: "auto", touchAction: "none" }} />

      {/* MEASURED selection box hugging the graphic + (overlay) corner resize handles */}
      {onScreen && sel && (
        <>
          <div style={{ position: "absolute", left: sel.left - 5, top: sel.top - 5, width: sel.w + 10, height: sel.h + 10, border: `1.6px ${isOv ? "solid" : "dashed"} #E7723B`, borderRadius: 9, boxShadow: "0 0 0 1px rgba(0,0,0,0.4), 0 0 18px rgba(231,114,59,0.28)", pointerEvents: "none" }} />
          {isOv && (
            <>
              {handle("tl", sel.left - 5, sel.top - 5, "nwse-resize")}
              {handle("tr", sel.left + sel.w + 5, sel.top - 5, "nesw-resize")}
              {handle("bl", sel.left - 5, sel.top + sel.h + 5, "nesw-resize")}
              {handle("br", sel.left + sel.w + 5, sel.top + sel.h + 5, "nwse-resize")}
            </>
          )}
        </>
      )}

      {/* in-context quick toolbar pinned to the selection */}
      {onScreen && sel && m && <SceneQuickBar item={m} sel={sel} stage={box} isOverlay={isOv} sizing={sizing} sizePct={Math.round(sc * 100)} />}

      {/* centre-snap guides while moving */}
      {guide.v && <div style={{ position: "absolute", left: vX - 0.5, top: ct, width: 1, height: ch, background: "#ff3ca0", boxShadow: "0 0 7px rgba(255,60,160,0.85)", pointerEvents: "none" }} />}
      {guide.h && <div style={{ position: "absolute", left: cl, top: hY - 0.5, width: cw, height: 1, background: "#ff3ca0", boxShadow: "0 0 7px rgba(255,60,160,0.85)", pointerEvents: "none" }} />}
    </div>
  );
}

const QB_BTN: React.CSSProperties = { display: "grid", placeItems: "center", width: 28, height: 28, borderRadius: 7, background: "transparent", border: "none", color: "inherit", cursor: "pointer" };
const QB_ACCENTS = (MG_ACCENTS as string[]).slice(0, 6);

// Floating in-context toolbar pinned just above (or below) the selected graphic — the quick edits without
// a trip to the side panel: accent swatches + custom colour, size −/＋ (overlays), duplicate, delete, and
// a jump to the full inspector. A little arrow points at the graphic. Clamped to stay inside the stage.
function SceneQuickBar({ item, sel, stage, isOverlay, sizing, sizePct }: { item: MotionItem; sel: SelRect; stage: { w: number; h: number }; isOverlay: boolean; sizing: boolean; sizePct: number }) {
  const updateMotion = useStore((s) => s.updateMotion);
  const duplicateMotion = useStore((s) => s.duplicateMotion);
  const removeMotion = useStore((s) => s.removeMotion);
  const selectMotion = useStore((s) => s.selectMotion);
  const pushHistory = useStore((s) => s.pushHistory);
  const commit = (patch: Partial<MotionItem>) => { pushHistory(); updateMotion(item.id, patch); };

  const BARW = isOverlay ? 384 : 250, BARH = 46;
  const cxOnBar = sel.left + sel.w / 2;
  let left = Math.max(8, Math.min(stage.w - BARW - 8, cxOnBar - BARW / 2));
  let top = sel.top - BARH - 16;
  const below = top < 6;
  if (below) top = Math.min(stage.h - BARH - 8, sel.top + sel.h + 16);

  const sc = item.scale && item.scale > 0 ? item.scale : 1;
  const bump = (d: number) => commit({ scale: Math.round(clampOverlayScale(sc + d) * 100) / 100 });
  const cur = (item.accent || (MG_ACCENTS as string[])[0]).toLowerCase();
  const arrowLeft = Math.max(14, Math.min(BARW - 14, cxOnBar - left)) - 6;

  return (
    <div style={{ position: "absolute", left, top, width: BARW, height: BARH, pointerEvents: "auto", zIndex: 5 }}
      onPointerDown={(e) => e.stopPropagation()}>
      <div style={{ display: "flex", alignItems: "center", gap: 9, height: "100%", padding: "0 12px", borderRadius: 13, background: "rgba(20,20,26,0.93)", border: "1px solid rgba(255,255,255,0.14)", backdropFilter: "blur(12px)", WebkitBackdropFilter: "blur(12px)", boxShadow: "0 10px 30px rgba(0,0,0,0.6)", color: "#fff" }}>
        {/* accent swatches + custom colour */}
        <div style={{ display: "flex", alignItems: "center", gap: 5 }}>
          {QB_ACCENTS.map((c) => (
            <button key={c} onClick={() => commit({ accent: c })} title={c}
              style={{ width: 18, height: 18, borderRadius: "50%", background: c, border: cur === c.toLowerCase() ? "2px solid #fff" : "2px solid rgba(255,255,255,0.18)", boxShadow: cur === c.toLowerCase() ? "0 0 0 1.5px rgba(0,0,0,0.5)" : "none", cursor: "pointer", padding: 0 }} />
          ))}
          <label title="Custom colour" style={{ position: "relative", width: 18, height: 18, borderRadius: "50%", display: "block", cursor: "pointer", background: "conic-gradient(from 0deg,#ff0033,#ffae00,#37d67a,#2bb0ff,#7c5cff,#ff0033)", border: "2px solid rgba(255,255,255,0.5)" }}>
            <input type="color" value={/^#[0-9a-fA-F]{6}$/.test(item.accent || "") ? (item.accent as string) : "#E7723B"} onChange={(e) => commit({ accent: e.target.value })} style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer", width: "100%", height: "100%" }} />
          </label>
        </div>

        {isOverlay && (
          <>
            <span style={{ width: 1, height: 24, background: "rgba(255,255,255,0.14)" }} />
            <div style={{ display: "flex", alignItems: "center", gap: 3 }}>
              <button onClick={() => bump(-0.1)} title="Smaller" style={QB_BTN}><Minus size={15} /></button>
              <span style={{ width: 40, textAlign: "center", fontSize: 11.5, fontVariantNumeric: "tabular-nums", color: sizing ? "#E7723B" : "#fff" }}>{sizePct}%</span>
              <button onClick={() => bump(0.1)} title="Bigger" style={QB_BTN}><Plus size={15} /></button>
            </div>
          </>
        )}

        <span style={{ width: 1, height: 24, background: "rgba(255,255,255,0.14)" }} />
        <button onClick={() => duplicateMotion(item.id)} title="Duplicate" style={QB_BTN}><Copy size={15} /></button>
        <button onClick={() => selectMotion(item.id)} title="All edits (open inspector)" style={QB_BTN}><SlidersHorizontal size={15} /></button>
        <button onClick={() => removeMotion(item.id)} title="Delete" style={{ ...QB_BTN, color: "#ff6b6b" }}><Trash2 size={15} /></button>
      </div>
      {/* arrow pointing at the graphic */}
      <div style={{ position: "absolute", left: arrowLeft, [below ? "top" : "bottom"]: -5, width: 10, height: 10, background: "rgba(20,20,26,0.93)", borderRight: "1px solid rgba(255,255,255,0.14)", borderBottom: "1px solid rgba(255,255,255,0.14)", transform: below ? "rotate(225deg)" : "rotate(45deg)" } as React.CSSProperties} />
    </div>
  );
}

// Interactive transform handles for the selected floating-overlay (PiP) clip, drawn over the Player.
// Maps the composition's 1920×1080 space to the on-screen (letterboxed) video rect so drag = move
// and corner handles = resize, writing straight back to the clip's layout.
function PipTransformOverlay({ stageRef }: { stageRef: React.RefObject<HTMLDivElement | null> }) {
  const selId = useStore((s) => s.selectedVideoId);
  const v = useStore((s) => (selId ? s.videos.find((x) => x.id === selId) : undefined));
  const playheadMs = useStore((s) => s.playheadMs);
  const updateVideo = useStore((s) => s.updateVideo);
  const pushHistory = useStore((s) => s.pushHistory);
  const [box, setBox] = useState({ w: 0, h: 0 });

  useEffect(() => {
    const el = stageRef.current; if (!el) return;
    const measure = () => setBox({ w: el.clientWidth, h: el.clientHeight });
    measure();
    const ro = new ResizeObserver(measure); ro.observe(el);
    return () => ro.disconnect();
  }, [stageRef]);

  const L = v?.layout;
  if (!v || !L || box.w < 2) return null;
  const dur = Math.max(1, (v.durationMs / 1000) * FPS);
  const frameInClip = ((playheadMs - v.startMs) / 1000) * FPS;
  if (frameInClip < -0.5 || frameInClip >= dur) return null; // clip not on screen now

  // live geometry (tracks the morph) — handles always wrap the actual rendered video
  const g = pipBox(v, Math.max(0, frameInClip), dur, FPS);
  const settled = g.t > 0.85;
  if (!settled) return null; // hide the selection box entirely while the clip is morphing in/out
  const zoom = Math.max(1, v.zoom || 1);

  // letterboxed 16:9 content rect inside the stage box
  const cw = Math.min(box.w, (box.h * COMP_W) / COMP_H);
  const ch = (cw * COMP_H) / COMP_W;
  const cl = (box.w - cw) / 2, ct = (box.h - ch) / 2;
  const scale = cw / COMP_W;
  const toPx = (n: number) => n * scale;
  const circle = v.shape === "circle";

  // move / resize → write the overlay layout
  const drag = (mode: "move" | Corner) => (e: React.PointerEvent) => {
    e.preventDefault(); e.stopPropagation();
    const sx = e.clientX, sy = e.clientY, o = { ...L };
    let moved = false;
    const onMove = (ev: PointerEvent) => {
      if (!moved) { if (Math.abs(ev.clientX - sx) < 2 && Math.abs(ev.clientY - sy) < 2) return; moved = true; pushHistory(); }
      const dx = (ev.clientX - sx) / scale, dy = (ev.clientY - sy) / scale;
      let { x, y, w, h } = o;
      if (mode === "move") { x = o.x + dx; y = o.y + dy; }
      else {
        const left = mode === "tl" || mode === "bl", top = mode === "tl" || mode === "tr";
        let nw = left ? o.w - dx : o.w + dx;
        let nh = top ? o.h - dy : o.h + dy;
        if (circle) { const d = Math.max(MIN_PX, Math.max(nw, nh)); nw = d; nh = d; }
        nw = Math.max(MIN_PX, nw); nh = Math.max(MIN_PX, nh);
        w = nw; h = nh;
        if (left) x = o.x + (o.w - nw);
        if (top) y = o.y + (o.h - nh);
      }
      x = Math.max(-w + 40, Math.min(COMP_W - 40, x));
      y = Math.max(-h + 40, Math.min(COMP_H - 40, y));
      updateVideo(v.id, { layout: { x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h) } });
    };
    const onUp = () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); };
    window.addEventListener("pointermove", onMove); window.addEventListener("pointerup", onUp);
  };

  // drag the content inside the frame → free pan (frame any subject, even off-centre)
  const panDrag = (e: React.PointerEvent) => {
    e.preventDefault(); e.stopPropagation();
    const sx = e.clientX, sy = e.clientY, ox = v.pan?.x || 0, oy = v.pan?.y || 0;
    const bw = Math.max(1, toPx(g.w)), bh = Math.max(1, toPx(g.h));
    let moved = false;
    const cl2 = (n: number) => Math.max(-0.6, Math.min(0.6, n));
    const onMove = (ev: PointerEvent) => {
      if (!moved) { if (Math.abs(ev.clientX - sx) < 2 && Math.abs(ev.clientY - sy) < 2) return; moved = true; pushHistory(); }
      updateVideo(v.id, { pan: { x: cl2(ox + (ev.clientX - sx) / bw), y: cl2(oy + (ev.clientY - sy) / bh) } });
    };
    const onUp = () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); };
    window.addEventListener("pointermove", onMove); window.addEventListener("pointerup", onUp);
  };

  const handle = (c: Corner, cur: string, pos: React.CSSProperties) => (
    <div onPointerDown={drag(c)} style={{ position: "absolute", width: 16, height: 16, borderRadius: 4, background: "#fff", border: "2px solid #E7723B", boxShadow: "0 1px 4px rgba(0,0,0,0.4)", cursor: cur, pointerEvents: "auto", touchAction: "none", ...pos }} />
  );

  return (
    <div className="absolute inset-0" style={{ pointerEvents: "none" }}>
      <div
        onPointerDown={settled ? drag("move") : undefined}
        style={{ position: "absolute", left: cl + toPx(g.x), top: ct + toPx(g.y), width: toPx(g.w), height: toPx(g.h), border: "1.5px solid #E7723B", borderRadius: circle ? "50%" : g.radius > 1 ? toPx(g.radius) : 2, boxShadow: "0 0 0 1px rgba(0,0,0,0.35)", cursor: settled ? "move" : "default", pointerEvents: settled ? "auto" : "none", touchAction: "none", opacity: settled ? 1 : 0.7 }}
      >
        {settled && <>{handle("tl", "nwse-resize", { left: -8, top: -8 })}{handle("tr", "nesw-resize", { right: -8, top: -8 })}{handle("bl", "nesw-resize", { left: -8, bottom: -8 })}{handle("br", "nwse-resize", { right: -8, bottom: -8 })}</>}
        {settled && zoom > 1.01 && (
          <div onPointerDown={panDrag} title="Drag to position the subject (pan)" style={{ position: "absolute", left: "50%", top: "50%", transform: "translate(-50%,-50%)", width: 34, height: 34, borderRadius: "50%", background: "rgba(231,114,59,0.85)", border: "2px solid #fff", boxShadow: "0 2px 8px rgba(0,0,0,0.5)", cursor: "move", pointerEvents: "auto", touchAction: "none", display: "grid", placeItems: "center" }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M5 9l-3 3 3 3M9 5l3-3 3 3M15 19l-3 3-3-3M19 9l3 3-3 3M2 12h20M12 2v20" /></svg>
          </div>
        )}
      </div>
    </div>
  );
}

// Crop gizmo: when a full-frame clip is in crop mode (store.croppingId), draw a draggable
// crop rectangle over the letterboxed video. The box is LOCKED to the frame aspect (16:9)
// so whatever is kept fills the output with no bars; corner handles resize, the interior
// drags, and the area outside is dimmed. Writes v.crop (normalized 0..1 of the frame).
function CropOverlay({ stageRef }: { stageRef: React.RefObject<HTMLDivElement | null> }) {
  const cropId = useStore((s) => s.croppingId);
  const v = useStore((s) => (cropId ? s.videos.find((x) => x.id === cropId) : undefined));
  const locked = useStore((s) => s.cropLock);
  const updateVideo = useStore((s) => s.updateVideo);
  const pushHistory = useStore((s) => s.pushHistory);
  const [box, setBox] = useState({ w: 0, h: 0 });

  useEffect(() => {
    const el = stageRef.current; if (!el) return;
    const measure = () => setBox({ w: el.clientWidth, h: el.clientHeight });
    measure();
    const ro = new ResizeObserver(measure); ro.observe(el);
    return () => ro.disconnect();
  }, [stageRef]);

  if (!v || v.layout || box.w < 2) return null; // crop is for full-frame clips
  const c = v.crop && v.crop.w > 0 ? v.crop : { x: 0, y: 0, w: 1, h: 1 };

  // letterboxed 16:9 content rect inside the stage (same mapping as the PiP overlay)
  const cw = Math.min(box.w, (box.h * COMP_W) / COMP_H);
  const ch = (cw * COMP_H) / COMP_W;
  const cl = (box.w - cw) / 2, ct = (box.h - ch) / 2;
  // crop rect → on-screen px
  const rx = cl + c.x * cw, ry = ct + c.y * ch, rw = c.w * cw, rh = c.h * ch;
  const MINW = 0.12; // min crop width (fraction of frame)

  // A frame-aspect (16:9) region of a 16:9 frame has EQUAL width/height FRACTIONS,
  // so locking the aspect = keeping w==h in frame-fraction space.
  const drag = (mode: "move" | Corner) => (e: React.PointerEvent) => {
    e.preventDefault(); e.stopPropagation();
    const sx = e.clientX, sy = e.clientY, o = { ...c };
    let moved = false;
    const onMove = (ev: PointerEvent) => {
      if (!moved) { if (Math.abs(ev.clientX - sx) < 2 && Math.abs(ev.clientY - sy) < 2) return; moved = true; pushHistory(); }
      const dx = (ev.clientX - sx) / cw, dy = (ev.clientY - sy) / ch; // frame fractions
      let { x, y, w, h } = o;
      if (mode === "move") {
        x = Math.max(0, Math.min(1 - o.w, o.x + dx));
        y = Math.max(0, Math.min(1 - o.h, o.y + dy));
      } else {
        const left = mode === "tl" || mode === "bl", top = mode === "tl" || mode === "tr";
        const fixedX = left ? o.x + o.w : o.x; // the vertical edge that stays put
        const fixedY = top ? o.y + o.h : o.y;  // the horizontal edge that stays put
        let nw = Math.max(MINW, Math.min(1, left ? o.w - dx : o.w + dx));
        let nh = Math.max(MINW, Math.min(1, top ? o.h - dy : o.h + dy));
        // LOCKED: a 16:9 region of a 16:9 frame ⇒ equal w/h FRACTIONS (drive both together)
        if (locked) { const m = Math.max(nw, nh); nw = m; nh = m; }
        // clamp so the box stays inside the frame from the fixed corner
        nw = left ? Math.min(nw, fixedX) : Math.min(nw, 1 - o.x);
        nh = top ? Math.min(nh, fixedY) : Math.min(nh, 1 - o.y);
        if (locked) { const m = Math.min(nw, nh); nw = m; nh = m; }
        w = nw; h = nh;
        x = left ? fixedX - nw : o.x;
        y = top ? fixedY - nh : o.y;
      }
      updateVideo(v.id, { crop: { x: +x.toFixed(4), y: +y.toFixed(4), w: +w.toFixed(4), h: +h.toFixed(4) } });
    };
    const onUp = () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); };
    window.addEventListener("pointermove", onMove); window.addEventListener("pointerup", onUp);
  };

  const handle = (cn: Corner, cur: string, pos: React.CSSProperties) => (
    <div onPointerDown={drag(cn)} style={{ position: "absolute", width: 16, height: 16, borderRadius: 4, background: "#fff", border: "2px solid #36C2B8", boxShadow: "0 1px 4px rgba(0,0,0,0.5)", cursor: cur, pointerEvents: "auto", touchAction: "none", ...pos }} />
  );

  const dim = (s: React.CSSProperties) => <div style={{ position: "absolute", background: "rgba(8,10,14,0.62)", pointerEvents: "none", ...s }} />;
  return (
    <div className="absolute inset-0" style={{ pointerEvents: "none" }}>
      {/* dim the discarded area (four panels around the kept region) */}
      {dim({ left: cl, top: ct, width: cw, height: Math.max(0, ry - ct) })}
      {dim({ left: cl, top: ry + rh, width: cw, height: Math.max(0, ct + ch - (ry + rh)) })}
      {dim({ left: cl, top: ry, width: Math.max(0, rx - cl), height: rh })}
      {dim({ left: rx + rw, top: ry, width: Math.max(0, cl + cw - (rx + rw)), height: rh })}
      <div onPointerDown={drag("move")} style={{ position: "absolute", left: rx, top: ry, width: rw, height: rh, border: "1.5px solid #36C2B8", boxShadow: "0 0 0 1px rgba(0,0,0,0.4)", cursor: "move", pointerEvents: "auto", touchAction: "none" }}>
        <div style={{ position: "absolute", inset: 0, pointerEvents: "none", backgroundImage: "linear-gradient(rgba(255,255,255,0.18) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.18) 1px,transparent 1px)", backgroundSize: "33.33% 33.33%" }} />
        {handle("tl", "nwse-resize", { left: -8, top: -8 })}
        {handle("tr", "nesw-resize", { right: -8, top: -8 })}
        {handle("bl", "nesw-resize", { left: -8, bottom: -8 })}
        {handle("br", "nwse-resize", { right: -8, bottom: -8 })}
      </div>
    </div>
  );
}
