// media-slot.jsx — React drop-zone for video files. Replaces the previous
// <media-slot> custom element, which Chrome refused to upgrade via
// createElement (some quirk in our environment). Plain React sidesteps the
// custom-element registration path entirely.
//
// API:
//   <MediaSlot
//     id="weather-video"          // stable id for the preview pipeline
//     placeholder="Drop a video"  // empty-state caption
//     fit="cover"                  // object-fit on the inner <video>
//     onChange={({src, file, duration}) => …}
//   />
//
// Behaviour: drag-and-drop video file, OR click to browse. Once filled, the
// video auto-plays muted in a loop (Reel-preview style). Hover the slot to
// reveal a small × button to clear it. The element's blob URL is held only
// in component state — videos are not persisted across reloads (too big to
// dump into a sidecar JSON).
//
// Externally addressable: each <MediaSlot> stamps a `data-media-slot-id`
// attribute on its outer div so an export pipeline can `querySelector` it
// and grab the inner <video> via the same data hook on the video element.

const ACCEPTED_VIDEO = ['video/mp4', 'video/quicktime', 'video/webm', 'video/x-m4v'];

function MediaSlot({
  id,
  placeholder = 'Drop a video',
  fit = 'cover',
  onChange,
  style,
}) {
  const [fileURL, setFileURL] = React.useState(null);
  const [dragOver, setDragOver] = React.useState(false);
  const inputRef = React.useRef(null);
  const videoRef = React.useRef(null);
  const prevURL = React.useRef(null);

  // Revoke the prior blob URL whenever we get a new one (or unmount).
  React.useEffect(() => {
    return () => {
      if (prevURL.current) {
        try { URL.revokeObjectURL(prevURL.current); } catch (e) {}
      }
    };
  }, []);

  const acceptFile = (file) => {
    if (!file || !file.type.startsWith('video/')) return;
    if (prevURL.current) {
      try { URL.revokeObjectURL(prevURL.current); } catch (e) {}
    }
    const url = URL.createObjectURL(file);
    prevURL.current = url;
    setFileURL(url);
    // Defer onChange until metadata arrives (so duration is real).
    // Handled in the <video onLoadedMetadata>.
    if (onChange) onChange({ src: url, file, duration: 0 });
  };

  const onLoadedMeta = () => {
    if (!videoRef.current) return;
    if (onChange) {
      onChange({
        src: fileURL,
        file: null,
        duration: videoRef.current.duration,
      });
    }
  };

  const onDrop = (e) => {
    e.preventDefault();
    setDragOver(false);
    const f = e.dataTransfer.files && e.dataTransfer.files[0];
    if (f) acceptFile(f);
  };

  const onClear = (e) => {
    e.stopPropagation();
    if (prevURL.current) {
      try { URL.revokeObjectURL(prevURL.current); } catch (e) {}
      prevURL.current = null;
    }
    setFileURL(null);
    if (onChange) onChange({ src: null, file: null, duration: 0 });
  };

  // Shared outer wrapper — the slot fills its parent.
  const outerStyle = {
    position: 'absolute',
    inset: 0,
    width: '100%',
    height: '100%',
    overflow: 'hidden',
    background: 'linear-gradient(135deg, #02102B 0%, #0F6D96 60%, #001172 100%)',
    ...style,
  };

  if (fileURL) {
    return (
      <div data-media-slot-id={id} style={outerStyle}>
        <video
          ref={videoRef}
          data-media-slot-id={id}
          src={fileURL}
          autoPlay
          loop
          muted
          playsInline
          preload="auto"
          onLoadedMetadata={onLoadedMeta}
          style={{
            position: 'absolute',
            inset: 0,
            width: '100%',
            height: '100%',
            objectFit: fit,
            display: 'block',
            // Allow clicks (they're harmless on a muted autoplay video) but
            // pointer events stay on for parent overlays' drop handlers.
            pointerEvents: 'none',
          }}
        />
        <button
          type="button"
          onClick={onClear}
          title="Remove video"
          style={{
            position: 'absolute', top: 6, right: 6,
            width: 22, height: 22,
            background: 'rgba(2, 16, 43, 0.7)',
            color: '#61D4F2',
            border: 'none', cursor: 'pointer', padding: 0,
            font: '600 12px/1 "Bai Jamjuree", sans-serif',
            zIndex: 2,
          }}
        >×</button>
      </div>
    );
  }

  // Empty / drop-zone state.
  return (
    <div
      data-media-slot-id={id}
      style={outerStyle}
      onClick={() => inputRef.current && inputRef.current.click()}
      onDragEnter={(e) => { e.preventDefault(); setDragOver(true); }}
      onDragOver={(e) => { e.preventDefault(); }}
      onDragLeave={() => setDragOver(false)}
      onDrop={onDrop}
    >
      <input
        ref={inputRef}
        type="file"
        accept={ACCEPTED_VIDEO.join(',')}
        style={{ display: 'none' }}
        onChange={(e) => {
          const f = e.target.files && e.target.files[0];
          if (f) acceptFile(f);
          e.target.value = '';
        }}
      />
      <div style={{
        position: 'absolute', inset: 0,
        display: 'flex', flexDirection: 'column',
        alignItems: 'center', justifyContent: 'center', gap: 14,
        color: dragOver ? '#61D4F2' : 'rgba(97, 212, 242, 0.55)',
        fontFamily: '"Bai Jamjuree", sans-serif',
        fontSize: 12, letterSpacing: '0.18em', textTransform: 'uppercase', fontWeight: 600,
        border: `1px dashed ${dragOver ? '#61D4F2' : 'rgba(97, 212, 242, 0.3)'}`,
        background: dragOver ? 'rgba(97, 212, 242, 0.08)' : 'transparent',
        boxSizing: 'border-box',
        padding: 16,
        cursor: 'pointer',
        transition: 'color .15s, border-color .15s, background .15s',
      }}>
        <svg viewBox="0 0 32 32" width="36" height="36" fill="none" style={{ opacity: 0.7 }} aria-hidden="true">
          <rect x="3" y="7" width="20" height="18" stroke="currentColor" strokeWidth="1.4"/>
          <path d="M23 12 L29 8 V24 L23 20 Z" stroke="currentColor" strokeWidth="1.4" fill="none"/>
        </svg>
        <div>{placeholder}</div>
        <div style={{ fontSize: 10, opacity: 0.7, letterSpacing: '0.12em' }}>MP4 · MOV · WEBM</div>
      </div>
    </div>
  );
}

// Make available to templates.jsx and templates-light.jsx (separate Babel
// scripts) via window.
window.MediaSlot = MediaSlot;

// ─────────────────────────────────────────────────────────────────────────
// VideoControls — sidebar UI for scrubbing/playing the loaded video.
// Imperative: it walks the preview node for a <video data-media-slot-id> on
// every poll tick and binds to it. Sidesteps refs through deep templates.
// ─────────────────────────────────────────────────────────────────────────
function VideoControls({ getPreviewNode }) {
  const [video, setVideo] = React.useState(null);
  const [time, setTime] = React.useState(0);
  const [duration, setDuration] = React.useState(0);
  const [playing, setPlaying] = React.useState(true);

  // Poll for a video element — it may not be there yet on first render, and
  // it changes whenever the slot reloads.
  React.useEffect(() => {
    let active = true;
    const find = () => {
      const node = getPreviewNode && getPreviewNode();
      const v = node && node.querySelector('video[data-media-slot-id]');
      if (!active) return;
      if (v !== video) setVideo(v || null);
    };
    find();
    const t = setInterval(find, 400);
    return () => { active = false; clearInterval(t); };
  }, [getPreviewNode, video]);

  // Bind playback state to local state.
  React.useEffect(() => {
    if (!video) return;
    const onTime = () => setTime(video.currentTime || 0);
    const onDur  = () => setDuration(video.duration || 0);
    const onPlay = () => setPlaying(true);
    const onPause = () => setPlaying(false);
    video.addEventListener('timeupdate', onTime);
    video.addEventListener('durationchange', onDur);
    video.addEventListener('loadedmetadata', onDur);
    video.addEventListener('play', onPlay);
    video.addEventListener('pause', onPause);
    // Seed
    onTime(); onDur(); setPlaying(!video.paused);
    return () => {
      video.removeEventListener('timeupdate', onTime);
      video.removeEventListener('durationchange', onDur);
      video.removeEventListener('loadedmetadata', onDur);
      video.removeEventListener('play', onPlay);
      video.removeEventListener('pause', onPause);
    };
  }, [video]);

  if (!video) {
    return (
      <div style={{
        fontFamily: 'var(--font-brand)', fontSize: 11, fontWeight: 500,
        letterSpacing: '0.10em', color: 'rgba(138,153,184,0.7)',
        textTransform: 'none', padding: '4px 0',
      }}>Drop a video onto the preview to enable playback controls.</div>
    );
  }

  const fmt = (s) => {
    if (!isFinite(s) || s < 0) s = 0;
    const m = Math.floor(s / 60); const ss = (s - m * 60).toFixed(1).padStart(4, '0');
    return `${m}:${ss}`;
  };

  return (
    <div>
      <input
        type="range"
        min={0}
        max={duration || 0}
        step={0.05}
        value={Math.min(time, duration || 0)}
        onChange={(e) => {
          const t = parseFloat(e.target.value);
          video.pause();
          try { video.currentTime = t; } catch (err) {}
        }}
        style={{
          width: '100%',
          accentColor: 'var(--blue-light, #61D4F2)',
          margin: '4px 0 6px',
        }}
      />
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        gap: 8,
        fontFamily: 'var(--font-brand)', fontSize: 11, fontWeight: 600,
        letterSpacing: '0.12em', textTransform: 'uppercase',
        color: 'var(--fg-muted, #8A99B8)',
      }}>
        <button
          type="button"
          onClick={() => playing ? video.pause() : video.play().catch(() => {})}
          style={{
            background: 'transparent', border: '1px solid currentColor',
            color: 'inherit', padding: '4px 10px', cursor: 'pointer',
            fontFamily: 'inherit', fontSize: 10, fontWeight: 700,
            letterSpacing: '0.14em', textTransform: 'uppercase',
          }}
        >{playing ? '❚❚ Pause' : '▶ Play'}</button>
        <span style={{ fontVariantNumeric: 'tabular-nums', textTransform: 'none', letterSpacing: '0.06em' }}>
          {fmt(time)} / {fmt(duration)}
        </span>
      </div>
    </div>
  );
}

window.VideoControls = VideoControls;

// ─────────────────────────────────────────────────────────────────────────
// exportFinalVideo — bake the design overlay onto the playing video, record
// the canvas via MediaRecorder, return a Blob.
//
// Captures the design at production resolution by temporarily hiding the
// video element + close button, snapshotting via html-to-image, restoring,
// then plays the video real-time while compositing each frame onto a
// target-resolution canvas. captureStream(fps) on the canvas feeds
// MediaRecorder, which gives us a WebM/MP4 blob.
//
// Args:
//   previewNode   The DOM node that contains the design + video.
//   targetW/H     Production resolution (e.g. 1080×1080).
//   maxDuration   Cap (seconds). Defaults to 90.
//   onProgress    Optional (frac, label) callback for UI feedback.
// Returns a Blob (the final video).
// ─────────────────────────────────────────────────────────────────────────
async function exportFinalVideo({ previewNode, targetW, targetH, maxDuration = 90, fps = 30, onProgress }) {
  const video = previewNode.querySelector('video[data-media-slot-id]');
  if (!video) throw new Error('No video loaded. Drop a video on the preview first.');
  if (!video.duration || !isFinite(video.duration)) {
    throw new Error('Video metadata not ready yet — wait a moment and try again.');
  }

  // 1. Capture the overlay (everything except the video pixels) at full res.
  if (onProgress) onProgress(0, 'Capturing overlay');
  const slotWrapper = video.closest('[data-media-slot-id]');
  const closeBtn = slotWrapper && slotWrapper.querySelector('button');
  const prevVideoVis = video.style.visibility;
  const prevBtnVis = closeBtn && closeBtn.style.visibility;
  // Hide the video pixels & the close button. We use visibility (preserves
  // layout) so the design doesn't reflow.
  video.style.visibility = 'hidden';
  if (closeBtn) closeBtn.style.visibility = 'hidden';
  // Also hide anything marked [data-no-export] (resize handles, dev grips,
  // etc.) so they don't bake into the video.
  const hiddenForExport = [...previewNode.querySelectorAll('[data-no-export]')];
  const prevExportVis = hiddenForExport.map((el) => el.style.visibility);
  hiddenForExport.forEach((el) => { el.style.visibility = 'hidden'; });
  // Also hide the gradient background of the slot so the overlay is fully
  // transparent in the video area. The wrapper's inline style is auth'd by
  // MediaSlot; we override here and restore after.
  const prevBg = slotWrapper && slotWrapper.style.background;
  if (slotWrapper) slotWrapper.style.background = 'transparent';

  let overlayURL;
  try {
    const scale = targetW / previewNode.offsetWidth;
    overlayURL = await htmlToImage.toPng(previewNode, {
      width: targetW,
      height: targetH,
      pixelRatio: 1,
      style: {
        transform: `scale(${scale})`,
        transformOrigin: 'top left',
        width: previewNode.offsetWidth + 'px',
        height: previewNode.offsetHeight + 'px',
      },
      cacheBust: true,
    });
  } finally {
    video.style.visibility = prevVideoVis;
    if (closeBtn) closeBtn.style.visibility = prevBtnVis;
    hiddenForExport.forEach((el, i) => { el.style.visibility = prevExportVis[i]; });
    if (slotWrapper) slotWrapper.style.background = prevBg;
  }

  // 2. Load overlay image.
  const overlayImg = new Image();
  await new Promise((res, rej) => {
    overlayImg.onload = res;
    overlayImg.onerror = () => rej(new Error('Overlay image load failed'));
    overlayImg.src = overlayURL;
  });

  // 3. Set up canvas + MediaRecorder.
  const canvas = document.createElement('canvas');
  canvas.width = targetW;
  canvas.height = targetH;
  const ctx = canvas.getContext('2d');
  // Paint a frame immediately so captureStream picks up dimensions.
  ctx.fillStyle = '#02102B';
  ctx.fillRect(0, 0, targetW, targetH);

  const stream = canvas.captureStream(fps);
  // Pick the best mime type the browser supports. Prefer mp4 (broader social
  // compatibility) but fall back to webm.
  const candidates = [
    'video/mp4; codecs=avc1.42E01E',
    'video/mp4',
    'video/webm; codecs=vp9',
    'video/webm; codecs=vp8',
    'video/webm',
  ];
  const mimeType = candidates.find((m) => {
    try { return MediaRecorder.isTypeSupported(m); } catch (e) { return false; }
  }) || 'video/webm';
  const ext = mimeType.startsWith('video/mp4') ? 'mp4' : 'webm';

  const chunks = [];
  const recorder = new MediaRecorder(stream, { mimeType, videoBitsPerSecond: 8_000_000 });
  recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };

  // Save + override video state.
  const origLoop   = video.loop;
  const origMuted  = video.muted;
  const origPaused = video.paused;
  const origTime   = video.currentTime;
  video.loop = false;
  video.muted = true;
  try { video.currentTime = 0; } catch (e) {}

  const recDuration = Math.min(maxDuration, video.duration);

  // object-fit: cover math, recomputed once per frame in case videoWidth
  // changes (some sources only know after the first frame plays).
  const computeCover = () => {
    const vw = video.videoWidth || targetW;
    const vh = video.videoHeight || targetH;
    const ta = targetW / targetH;
    const va = vw / vh;
    let dw, dh, dx, dy;
    if (va > ta) {
      dh = targetH; dw = va * dh; dx = (targetW - dw) / 2; dy = 0;
    } else {
      dw = targetW; dh = dw / va; dx = 0; dy = (targetH - dh) / 2;
    }
    return { dx, dy, dw, dh };
  };

  return new Promise((resolve, reject) => {
    let stopped = false;
    let startedAt = 0;

    const cleanup = () => {
      video.loop = origLoop;
      video.muted = origMuted;
      try { video.currentTime = origTime; } catch (e) {}
      if (!origPaused) video.play().catch(() => {});
    };

    recorder.onstop = () => {
      cleanup();
      const blob = new Blob(chunks, { type: mimeType });
      blob.extension = ext;
      resolve(blob);
    };
    recorder.onerror = (e) => { cleanup(); reject(e.error || new Error('Recorder error')); };

    const renderFrame = () => {
      if (stopped) return;
      const elapsed = (performance.now() - startedAt) / 1000;
      if (elapsed >= recDuration || video.ended) {
        stopped = true;
        try { recorder.stop(); } catch (e) {}
        return;
      }
      // Black background, then video (object-fit cover), then overlay.
      ctx.fillStyle = '#02102B';
      ctx.fillRect(0, 0, targetW, targetH);
      const { dx, dy, dw, dh } = computeCover();
      try { ctx.drawImage(video, dx, dy, dw, dh); } catch (e) {}
      ctx.drawImage(overlayImg, 0, 0, targetW, targetH);
      if (onProgress) onProgress(Math.min(1, elapsed / recDuration), `Recording ${elapsed.toFixed(1)}s / ${recDuration.toFixed(1)}s`);
      requestAnimationFrame(renderFrame);
    };

    if (onProgress) onProgress(0, 'Starting playback');
    recorder.start();
    video.play().then(() => {
      startedAt = performance.now();
      requestAnimationFrame(renderFrame);
    }).catch((e) => {
      cleanup();
      reject(e);
    });
  });
}

window.exportFinalVideo = exportFinalVideo;
