import { EditorModel } from './editor-core.mjs';
import { chooseMp4MimeType, clipExportName, exportCanvasSize, mp4ExportName } from './export-format.mjs';

const launchStarted = performance.now();
const query = new URLSearchParams(location.search);
const mockMode = query.has('fixture');
const bridge = window.shattercut || Object.freeze({
  getState: async () => ({ recent: [], playheads: {}, volume: 0.8, muted: false }),
  updateState: async () => {},
  acceptPath: async (candidate) => candidate,
  pathForFile: () => '',
  openDialog: async () => {},
  confirmDiscard: async () => true,
  toggleFullscreen: async () => !document.body.classList.contains('fullscreen'),
  fitVideo: async () => false,
  updateMenu: () => {},
  setProgress: () => {},
  showTimelineMenu: async () => false,
  setTitle: () => {},
  markReady: () => {},
  markMediaReady: () => {},
  markFirstFrame: () => {},
  copySource: async () => ({ ok: false, canceled: true }),
  beginExport: async () => ({ canceled: true }),
  beginClipExport: async () => ({ canceled: true }),
  endExportSession: async () => ({ ok: false }),
  writeExportChunk: async () => false,
  endExport: async () => false,
  cancelExport: async () => false,
  losslessExport: async () => ({ ok: false, canceled: true }),
  cancelLosslessExport: () => {},
  sourceReleased: () => {},
  onReleaseSource: () => {},
  onLosslessProgress: () => {},
  onCommand: () => {},
  onOpen: () => {},
  onStats: () => {},
});

const $ = (selector) => document.querySelector(selector);
const emptyState = $('#empty-state');
const editor = $('#editor');
const footprint = $('#footprint');
const video = $('#video');
const mockVideo = $('#mock-video');
const loading = $('#loading');
const timeline = $('#timeline');
const timelinePlayhead = $('#timeline-playhead');
const timelineHoverLine = $('#timeline-hover-line');
const timelineHover = $('#timeline-hover');
const clipRename = $('#clip-rename');
const playButton = $('#play');
const removeButton = $('#remove');
const muteButton = $('#mute');
const volume = $('#volume');
const zoomButton = $('#zoom');
const clipInfo = $('#clip-info');
const mediaInfo = $('#media-info');
const status = $('#status');
const exportProgress = $('#export-progress');
const exportProgressFill = $('#export-progress span');

let sourcePath = '';
let sourceIdentity = '';
let sourceName = '';
let model = null;
let savedState = { playheads: {}, volume: 0.8, muted: false };
let mockPosition = 0;
let lastGeometry = [];
let waveform = new Float32Array(2048);
let analyser = null;
let analyserData = null;
let audioContext = null;
let animationPending = false;
let playbackFrameHandle = 0;
let playbackFrameUsesVideoCallback = false;
let lastFooterPaint = 0;
let lastWaveformPaint = 0;
let waveformDirty = false;
let pointerMode = null;
let previewTrim = null;
let armedTrim = null;
let exposure = 0;
let zoom2x = false;
let fullscreen = false;
let exporting = false;
let cancelExport = false;
let dirty = false;
let outputMode = 'joined';
let outputSize = 'lossless';
let renaming = false;
let fullscreenHideTimer = 0;
let stateSaveTimer = 0;
let lastStateSave = 0;
let mediaOpenStarted = 0;
let activePrepared = null;
let exportStartedAt = 0;
let exportTotalDuration = 0;
let sourceHandlesReleased = false;
let pendingEditorRestore = null;

function formatTime(seconds) {
  const safe = Math.max(0, Number.isFinite(seconds) ? seconds : 0);
  const hours = Math.floor(safe / 3600);
  const minutes = Math.floor((safe % 3600) / 60);
  const wholeSeconds = Math.floor(safe % 60);
  const millis = Math.floor((safe - Math.floor(safe)) * 1000);
  const prefix = hours ? `${String(hours).padStart(2, '0')}:` : '';
  return `${prefix}${String(minutes).padStart(2, '0')}:${String(wholeSeconds).padStart(2, '0')}.${String(millis).padStart(3, '0')}`;
}

function formatBytes(bytes) {
  if (!Number.isFinite(bytes) || bytes <= 0) return '…';
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
  if (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / 1024 / 1024)} MB`;
  return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
}

function localFileUrl(filePath) {
  const normalized = filePath.replace(/\\/g, '/');
  if (/^[a-zA-Z]:\//.test(normalized)) {
    const rest = normalized.slice(3).split('/').map(encodeURIComponent).join('/');
    return `file:///${normalized.slice(0, 2)}/${rest}`;
  }
  return `file://${normalized.split('/').map(encodeURIComponent).join('/')}`;
}

function currentRawTime() {
  return mockMode ? mockPosition : (Number.isFinite(video.currentTime) ? video.currentTime : 0);
}

function setRawTime(raw) {
  const next = Math.max(0, Math.min(model?.duration || 0, raw));
  if (mockMode) mockPosition = next;
  else if (video.src) video.currentTime = next;
  updateVideoEffects();
  scheduleDraw();
}

function persistPlayback(force = false) {
  if (!sourcePath || mockMode) return;
  const save = () => {
    clearTimeout(stateSaveTimer);
    stateSaveTimer = 0;
    lastStateSave = performance.now();
    bridge.updateState({ identity: sourceIdentity, playhead: video.currentTime, volume: video.volume, muted: video.muted, zoom2x });
  };
  const elapsed = performance.now() - lastStateSave;
  if (force || elapsed >= 2000) save();
  else if (!stateSaveTimer) stateSaveTimer = setTimeout(save, 2000 - elapsed);
}

async function releaseSourceHandles() {
  cancelPlaybackFrame();
  video.pause();
  await activePrepared?.cleanup();
  activePrepared = null;
  if (audioContext) await audioContext.close().catch(() => {});
  audioContext = null;
  analyser = null;
  analyserData = null;
  video.removeAttribute('src');
  video.load();
  sourceHandlesReleased = true;
}

function progressText(ratio, verb = 'Exporting') {
  const safe = Math.max(0, Math.min(1, ratio));
  const elapsed = Math.max(0, (performance.now() - exportStartedAt) / 1000);
  const eta = safe > .01 ? Math.max(0, elapsed / safe - elapsed) : 0;
  const processed = exportTotalDuration * safe;
  return `${verb} ${Math.round(safe * 100)}% · ${formatTime(processed)} / ${formatTime(exportTotalDuration)}${safe > .01 && safe < 1 ? ` · ETA ${formatTime(eta)}` : ''}`;
}

function updateFootprint(stats) {
  const ram = formatBytes((stats.memoryKb || 0) * 1024);
  const cpu = Math.max(0, stats.cpu || 0).toFixed(stats.cpu >= 10 ? 0 : 1);
  footprint.textContent = `RAM ${ram}   ·   CPU ${cpu}%   ·   DISK ${formatBytes(stats.diskBytes)}   ·   ${stats.dependencies} DEP   ·   ${stats.network} NET`;
}

function selectedSegment() {
  return model?.segments?.[model.selected] || null;
}

function hasActiveEdits() {
  return Boolean(model?.isEdited()) || Math.abs(exposure) > .001;
}

function updateMenuState() {
  const segment = selectedSegment();
  bridge.updateMenu({
    loaded: Boolean(model),
    exporting,
    dirty,
    undoAction: model?.undoAction() || '',
    redoAction: model?.redoAction() || '',
    segmentCount: model?.segments.length || 0,
    fadeIn: Boolean(segment?.fadeIn),
    fadeOut: Boolean(segment?.fadeOut),
    exposure,
    muted: video.muted,
    fullscreen,
    outputMode,
    outputSize,
  });
}

function setDirty(value) {
  dirty = Boolean(value);
  updateMenuState();
}

function showEditor() {
  emptyState.hidden = true;
  editor.hidden = false;
  removeButton.disabled = !model || model.segments.length <= 1;
  updateTransport();
  scheduleDraw();
  updateMenuState();
}

function segmentLabel(segment, index) {
  return segment.name || `CLIP ${index + 1}`;
}

function timelineSegments() {
  return model?.segments || [];
}

function drawTimeline() {
  animationPending = false;
  if (!model || editor.hidden) return;
  const rect = timeline.getBoundingClientRect();
  const scale = window.devicePixelRatio || 1;
  const width = Math.max(1, Math.round(rect.width));
  const height = Math.max(1, Math.round(rect.height));
  if (timeline.width !== Math.round(width * scale) || timeline.height !== Math.round(height * scale)) {
    timeline.width = Math.round(width * scale);
    timeline.height = Math.round(height * scale);
  }
  const context = timeline.getContext('2d', { alpha: false });
  context.setTransform(scale, 0, 0, scale, 0, 0);
  context.fillStyle = '#12110d';
  context.fillRect(0, 0, width, height);

  const segments = timelineSegments();
  const kept = segments.reduce((total, segment) => total + segment.end - segment.start, 0) || 1;
  let cursor = 0;
  lastGeometry = [];
  for (let index = 0; index < segments.length; index += 1) {
    const segment = segments[index];
    const length = segment.end - segment.start;
    const left = cursor / kept * width;
    const right = (cursor + length) / kept * width;
    cursor += length;
    lastGeometry.push({ left, right, segment, index });
    context.fillStyle = index === model.selected ? '#3a1e1a' : '#26241e';
    context.fillRect(left, 0, Math.max(1, right - left), height);

    const middle = height / 2;
    const maxHalf = height / 2 - 7;
    context.fillStyle = index === model.selected ? '#968c78' : '#504c42';
    const step = 2;
    for (let x = Math.ceil(left) + 1; x < right - 1; x += step) {
      const raw = segment.start + (x - left) / Math.max(1, right - left) * length;
      const bucket = Math.max(0, Math.min(waveform.length - 1, Math.floor(raw / model.duration * waveform.length)));
      const half = Math.max(1, waveform[bucket] * maxHalf);
      context.fillRect(x, middle - half, step, half * 2);
    }

    context.strokeStyle = index === model.selected ? '#e25b45' : '#4a463c';
    context.lineWidth = index === model.selected ? 2 : 1;
    context.strokeRect(left + .5, .5, Math.max(0, right - left - 1), height - 1);
    if (index === model.selected) {
      context.fillStyle = '#c6beaf';
      context.fillRect(left + 1, 1, 3, height - 2);
      context.fillRect(right - 4, 1, 3, height - 2);
    }

    context.font = segment.name ? '15px "Segoe UI"' : '12px Consolas, monospace';
    context.fillStyle = '#e0dcd0';
    context.textBaseline = 'middle';
    context.fillText(segmentLabel(segment, index), left + 8, 17, Math.max(0, right - left - 12));
    context.font = '12px Consolas, monospace';
    context.fillStyle = '#9a948a';
    context.fillText(formatTime(length), left + 8, height - 15, Math.max(0, right - left - 12));

    const drawFade = (ending) => {
      const fade = ending ? segment.fadeOut : segment.fadeIn;
      if (!fade) return;
      const edge = ending ? right : left;
      const ramp = ending ? right - fade / kept * width : left + fade / kept * width;
      context.fillStyle = '#100f0b';
      context.beginPath();
      context.moveTo(edge, 1);
      context.lineTo(ramp, 1);
      context.lineTo(edge, height - 1);
      context.closePath();
      context.fill();
      context.strokeStyle = '#e0dcd0';
      context.lineWidth = 1;
      context.beginPath();
      context.moveTo(edge, height - 1);
      context.lineTo(ramp, 1);
      context.stroke();
    };
    drawFade(false);
    drawFade(true);
  }

  if (previewTrim) {
    const geometry = lastGeometry.find((item) => item.index === previewTrim.index);
    if (geometry) {
      const original = geometry.segment;
      const changedStart = Math.abs(previewTrim.value.start - original.start) > .001;
      const raw = changedStart ? previewTrim.value.start : previewTrim.value.end;
      const ratio = (raw - original.start) / Math.max(.001, original.end - original.start);
      const edge = geometry.left + Math.max(0, Math.min(1, ratio)) * (geometry.right - geometry.left);
      const discardLeft = changedStart ? geometry.left : edge;
      const discardRight = changedStart ? edge : geometry.right;
      context.save();
      context.beginPath();
      context.rect(discardLeft, 0, Math.max(0, discardRight - discardLeft), height);
      context.clip();
      context.fillStyle = 'rgba(8, 7, 5, .58)';
      context.fillRect(discardLeft, 0, discardRight - discardLeft, height);
      context.strokeStyle = 'rgba(226, 91, 69, .62)';
      context.lineWidth = 1;
      for (let x = discardLeft - height; x < discardRight + height; x += 8) {
        context.beginPath();
        context.moveTo(x, height);
        context.lineTo(x + height, 0);
        context.stroke();
      }
      context.restore();
      context.fillStyle = '#f5d7c7';
      context.fillRect(edge - 1, 0, 2, height);
    }
  }

  updatePlayhead();
  updateFooter();
}

function scheduleDraw() {
  if (!animationPending) {
    animationPending = true;
    requestAnimationFrame(drawTimeline);
  }
}

function updatePlayhead() {
  if (!model || editor.hidden) return;
  const kept = model.keptDuration() || 1;
  const width = timeline.getBoundingClientRect().width;
  const edited = model.editedFromRaw(currentRawTime());
  const x = Math.max(0, Math.min(Math.max(0, width - 1), edited / kept * width));
  timelinePlayhead.style.transform = `translate3d(${x.toFixed(2)}px, 0, 0)`;
}

function playbackFrame(now) {
  playbackFrameHandle = 0;
  playbackFrameUsesVideoCallback = false;
  if (!model || video.paused || video.ended) return;

  if (applyPlaybackBoundaries()) scheduleDraw();
  updateVideoEffects();
  updatePlayhead();
  if (now - lastFooterPaint >= 100) {
    lastFooterPaint = now;
    updateFooter();
  }
  waveformDirty = sampleWaveform() || waveformDirty;
  if (waveformDirty && now - lastWaveformPaint >= 125) {
    waveformDirty = false;
    lastWaveformPaint = now;
    scheduleDraw();
  }
  schedulePlaybackFrame();
}

function schedulePlaybackFrame() {
  if (mockMode || !model || video.paused || playbackFrameHandle) return;
  playbackFrameUsesVideoCallback = typeof video.requestVideoFrameCallback === 'function';
  playbackFrameHandle = playbackFrameUsesVideoCallback
    ? video.requestVideoFrameCallback(playbackFrame)
    : requestAnimationFrame(playbackFrame);
}

function cancelPlaybackFrame() {
  if (!playbackFrameHandle) return;
  if (playbackFrameUsesVideoCallback && typeof video.cancelVideoFrameCallback === 'function') {
    video.cancelVideoFrameCallback(playbackFrameHandle);
  } else {
    cancelAnimationFrame(playbackFrameHandle);
  }
  playbackFrameHandle = 0;
  playbackFrameUsesVideoCallback = false;
}

function updateFooter() {
  if (!model) return;
  clipInfo.textContent = `Clip ${model.selected + 1}/${model.segments.length}  ·  ${formatTime(model.editedFromRaw(currentRawTime()))} / ${formatTime(model.keptDuration())}`;
  removeButton.disabled = model.segments.length <= 1 || exporting;
}

function updateTransport() {
  const playing = !mockMode && !video.paused && !video.ended;
  playButton.textContent = playing ? 'Ⅱ' : '▷';
  playButton.setAttribute('aria-label', playing ? 'Pause' : 'Play');
  muteButton.textContent = video.muted || video.volume === 0 ? '◁×' : '◁))';
  zoomButton.textContent = zoom2x ? '2×' : '1×';
  video.style.width = zoom2x && video.videoWidth ? `${video.videoWidth * 2}px` : '';
  video.style.height = zoom2x && video.videoHeight ? `${video.videoHeight * 2}px` : '';
  updateVideoEffects();
  updateMenuState();
}

function fadeLevelAt(raw = currentRawTime()) {
  if (!model) return 1;
  const index = model.segmentAtRaw(raw);
  const segment = model.segments[index];
  if (!segment) return 1;
  const local = Math.max(0, raw - segment.start);
  const remaining = Math.max(0, segment.end - raw);
  const fadeIn = segment.fadeIn ? Math.min(1, local / segment.fadeIn) : 1;
  const fadeOut = segment.fadeOut ? Math.min(1, remaining / segment.fadeOut) : 1;
  return Math.max(0, Math.min(fadeIn, fadeOut));
}

function updateVideoEffects() {
  video.style.filter = `brightness(${2 ** exposure})`;
  video.style.opacity = String(fadeLevelAt());
}

async function ensureAudioAnalysis() {
  if (mockMode || analyser || !video.src) return;
  try {
    audioContext = new AudioContext({ latencyHint: 'playback' });
    const source = audioContext.createMediaElementSource(video);
    analyser = audioContext.createAnalyser();
    analyser.fftSize = 256;
    analyserData = new Uint8Array(analyser.fftSize);
    source.connect(analyser);
    analyser.connect(audioContext.destination);
    await audioContext.resume();
  } catch {
    analyser = null;
  }
}

function sampleWaveform() {
  if (!analyser || !model || video.paused) return false;
  analyser.getByteTimeDomainData(analyserData);
  let peak = 0;
  for (const sample of analyserData) peak = Math.max(peak, Math.abs(sample - 128) / 128);
  const bucket = Math.max(0, Math.min(waveform.length - 1, Math.floor(currentRawTime() / model.duration * waveform.length)));
  if (peak <= waveform[bucket]) return false;
  waveform[bucket] = peak;
  return true;
}

function applyPlaybackBoundaries() {
  if (!model || video.paused) return false;
  const previousSelection = model.selected;
  const index = model.segmentAtRaw(video.currentTime);
  if (index >= 0) {
    model.selected = index;
    const segment = model.segments[index];
    if (video.currentTime >= segment.end - .015) {
      const next = model.segments[index + 1];
      if (next) video.currentTime = next.start;
      else video.pause();
    }
  } else {
    const next = model.segments.find((segment) => segment.start > video.currentTime);
    if (next) video.currentTime = next.start;
    else video.pause();
  }
  return model.selected !== previousSelection;
}

async function togglePlay() {
  if (!model || exporting) return;
  if (mockMode) { status.textContent = 'Paused'; return; }
  if (video.paused) {
    const index = model.segmentAtRaw(video.currentTime);
    if (index < 0) video.currentTime = model.segments[0].start;
    await ensureAudioAnalysis();
    await video.play().catch(() => {});
  } else video.pause();
  updateTransport();
}

function edit(action) {
  if (!model || exporting) return;
  model.setPlayhead(currentRawTime());
  const changed = action();
  if (changed) {
    status.textContent = 'Edited';
    setDirty(hasActiveEdits());
    scheduleDraw();
  }
}

function split() { edit(() => model.split(currentRawTime())); }
function remove() {
  const selected = model?.selected || 0;
  edit(() => model.remove(selected));
  if (model) setRawTime(model.segments[model.selected].start);
}
function undo() {
  if (!model || exporting) return;
  model.setPlayhead(currentRawTime());
  if (model.undo()) {
    setRawTime(model.playhead);
    setDirty(hasActiveEdits());
    status.textContent = 'Edited';
  }
}
function redo() {
  if (!model || exporting) return;
  model.setPlayhead(currentRawTime());
  if (model.redo()) {
    setRawTime(model.playhead);
    setDirty(hasActiveEdits());
    status.textContent = 'Edited';
  }
}
function toggleFade(edge) { edit(() => model.toggleFade(model.selected, edge)); }

function beginRename(index = model?.selected || 0) {
  if (!model || exporting || fullscreen || !model.select(index)) return;
  const geometry = lastGeometry.find((item) => item.index === index);
  if (!geometry) return;
  renaming = true;
  clipRename.value = model.segments[index].name;
  clipRename.style.left = `${Math.max(28, geometry.left + 36)}px`;
  clipRename.style.width = `${Math.max(80, Math.min(360, geometry.right - geometry.left - 16))}px`;
  clipRename.hidden = false;
  clipRename.focus();
  clipRename.select();
}

function finishRename(apply, navigation = 0) {
  if (!renaming || !model) return;
  const index = model.selected;
  const value = clipRename.value;
  renaming = false;
  clipRename.hidden = true;
  if (apply) edit(() => model.rename(index, value));
  if (navigation) {
    model.select((index + navigation + model.segments.length) % model.segments.length);
    scheduleDraw();
    requestAnimationFrame(() => beginRename(model.selected));
  } else timeline.focus();
}

function seekBy(delta) {
  if (!model || exporting) return;
  if (armedTrim) {
    const segment = model.segments[armedTrim.index];
    if (!segment) { armedTrim = null; return; }
    const current = armedTrim.edge === 'start' ? segment.start : segment.end;
    const next = current + delta;
    edit(() => armedTrim.edge === 'start'
      ? model.trimStartTo(armedTrim.index, next)
      : model.trimEndTo(armedTrim.index, next));
    const updated = model.segments[armedTrim.index];
    setRawTime(armedTrim.edge === 'start' ? updated.start : updated.end);
    status.textContent = `Trim ${armedTrim.edge} armed · ${formatTime(currentRawTime())} · Esc done`;
    return;
  }
  if (!mockMode) video.pause();
  setRawTime(currentRawTime() + delta);
  status.textContent = 'Paused';
  updateTransport();
}

function toggleMute() {
  video.muted = !video.muted;
  savedState.muted = video.muted;
  bridge.updateState({ muted: video.muted, volume: video.volume });
  updateTransport();
}

function changeVolume(delta) {
  video.volume = Math.max(0, Math.min(1, video.volume + delta));
  video.muted = false;
  savedState.volume = video.volume;
  savedState.muted = false;
  volume.value = String(video.volume);
  bridge.updateState({ volume: video.volume, muted: false });
  updateTransport();
}

async function toggleZoom() {
  if (!model) return;
  zoom2x = !zoom2x;
  bridge.updateState({ identity: sourceIdentity, zoom2x });
  await bridge.fitVideo({ width: video.videoWidth, height: video.videoHeight, zoom2x });
  updateTransport();
}

async function toggleFullscreen() {
  fullscreen = await bridge.toggleFullscreen();
  document.body.classList.toggle('fullscreen', fullscreen);
  document.body.classList.remove('transport-hidden');
  updateMenuState();
}

function changeExposure(delta) {
  exposure = Math.max(-2, Math.min(2, exposure + delta));
  setDirty(true);
  updateTransport();
}

async function autoExposure() {
  if (!model || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
  const canvas = document.createElement('canvas');
  canvas.width = 160;
  canvas.height = Math.max(1, Math.round(160 * video.videoHeight / Math.max(1, video.videoWidth)));
  const context = canvas.getContext('2d', { alpha: false, willReadFrequently: true });
  context.drawImage(video, 0, 0, canvas.width, canvas.height);
  const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
  const luminance = [];
  for (let offset = 0; offset < pixels.length; offset += 16) {
    luminance.push(.2126 * pixels[offset] + .7152 * pixels[offset + 1] + .0722 * pixels[offset + 2]);
  }
  luminance.sort((left, right) => left - right);
  const reference = luminance[Math.floor(luminance.length * .72)] || 1;
  exposure = Math.max(-1, Math.min(1, Math.log2(150 / reference)));
  setDirty(true);
  status.textContent = `Exposure ${exposure >= 0 ? '+' : ''}${exposure.toFixed(2)}`;
  updateTransport();
}

async function toggleAutoExposure() {
  if (Math.abs(exposure) > .001) {
    exposure = 0;
    setDirty(hasActiveEdits());
    updateTransport();
  } else await autoExposure();
}

function timelinePoint(event) {
  const rect = timeline.getBoundingClientRect();
  return {
    x: Math.max(0, Math.min(rect.width, event.clientX - rect.left)),
    y: Math.max(0, Math.min(rect.height, event.clientY - rect.top)),
    width: rect.width,
    height: rect.height,
  };
}

function geometryAt(x) {
  return lastGeometry.find((item) => x >= item.left && x <= item.right) || null;
}

function updateTimelineHover(point) {
  if (!model || pointerMode || renaming) return;
  const mapped = model.rawFromEdited(point.x / Math.max(1, point.width) * model.keptDuration());
  timelineHoverLine.hidden = false;
  timelineHover.hidden = false;
  timelineHoverLine.style.transform = `translate3d(${point.x.toFixed(2)}px, 0, 0)`;
  timelineHover.style.left = `${Math.max(76, Math.min(point.width - 20, point.x + 28))}px`;
  timelineHover.textContent = formatTime(model.editedFromRaw(mapped.raw));
  const nearest = lastGeometry.flatMap((item) => [Math.abs(point.x - item.left), Math.abs(point.x - item.right)]);
  timeline.style.cursor = nearest.some((distance) => distance <= 7) ? 'ew-resize' : 'pointer';
}

timeline.addEventListener('pointerdown', (event) => {
  if (!model || exporting) return;
  if (renaming) finishRename(true);
  timeline.setPointerCapture(event.pointerId);
  const point = timelinePoint(event);
  const itemAtPoint = geometryAt(point.x);
  if (itemAtPoint && itemAtPoint.index === model.selected && point.y <= 34 && point.x >= itemAtPoint.left + 5 && point.x <= itemAtPoint.right - 5) {
    timeline.releasePointerCapture(event.pointerId);
    beginRename(itemAtPoint.index);
    return;
  }
  const handle = lastGeometry.flatMap((item) => [
    { distance: Math.abs(point.x - item.left), item, edge: 'start' },
    { distance: Math.abs(point.x - item.right), item, edge: 'end' },
  ]).sort((a, b) => a.distance - b.distance)[0];
  if (handle && handle.distance <= 7) {
    if (!mockMode) video.pause();
    model.select(handle.item.index);
    armedTrim = { index: handle.item.index, edge: handle.edge };
    pointerMode = { type: 'trim', pointerId: event.pointerId, index: handle.item.index, edge: handle.edge, original: { ...handle.item.segment }, playhead: currentRawTime(), moved: false };
    timeline.style.cursor = 'ew-resize';
    status.textContent = `Trim ${handle.edge} armed · ← → 5 s · , . frame · Esc done`;
    updateMenuState();
    return;
  }
  armedTrim = null;
  const mapped = model.rawFromEdited(point.x / point.width * model.keptDuration());
  model.select(mapped.index);
  pointerMode = { type: 'scrub', pointerId: event.pointerId, wasPlaying: !mockMode && !video.paused };
  if (!mockMode) video.pause();
  setRawTime(mapped.raw);
});

timeline.addEventListener('pointermove', (event) => {
  const point = timelinePoint(event);
  if (!pointerMode) { updateTimelineHover(point); return; }
  if (pointerMode.pointerId !== event.pointerId || !model) return;
  timelineHover.hidden = true;
  timelineHoverLine.hidden = true;
  if (pointerMode.type === 'scrub') {
    const mapped = model.rawFromEdited(point.x / point.width * model.keptDuration());
    model.select(mapped.index);
    setRawTime(mapped.raw);
  } else {
    pointerMode.moved = true;
    const original = pointerMode.original;
    const geometry = lastGeometry.find((item) => item.index === pointerMode.index);
    const raw = geometry
      ? original.start + (point.x - geometry.left) / Math.max(1, geometry.right - geometry.left) * (original.end - original.start)
      : original.start;
    const value = pointerMode.edge === 'start'
      ? { start: Math.max(0, Math.min(raw, original.end - .04)), end: original.end }
      : { start: original.start, end: Math.min(model.duration, Math.max(raw, original.start + .04)) };
    previewTrim = { index: pointerMode.index, value };
    setRawTime(pointerMode.edge === 'start' ? value.start : value.end);
  }
});

timeline.addEventListener('pointerup', async (event) => {
  if (!pointerMode || pointerMode.pointerId !== event.pointerId || !model) return;
  const completedMode = pointerMode;
  if (pointerMode.type === 'trim' && previewTrim) {
    model.setPlayhead(completedMode.playhead);
    const changed = model.trim(previewTrim.index, previewTrim.value.start, previewTrim.value.end);
    previewTrim = null;
    if (changed) setDirty(hasActiveEdits());
    status.textContent = `Trim ${armedTrim?.edge || ''} armed · ← → 5 s · , . frame · Esc done`;
  }
  const resume = pointerMode.type === 'scrub' && pointerMode.wasPlaying;
  pointerMode = null;
  timeline.style.cursor = '';
  timeline.releasePointerCapture(event.pointerId);
  scheduleDraw();
  updateMenuState();
  if (resume && !mockMode) await video.play().catch(() => {});
});

timeline.addEventListener('pointercancel', () => {
  pointerMode = null;
  previewTrim = null;
  timeline.style.cursor = '';
  scheduleDraw();
});

timeline.addEventListener('pointerleave', () => {
  if (pointerMode) return;
  timelineHover.hidden = true;
  timelineHoverLine.hidden = true;
  timeline.style.cursor = 'pointer';
});

timeline.addEventListener('contextmenu', (event) => {
  event.preventDefault();
  if (!model || exporting) return;
  const point = timelinePoint(event);
  const mapped = model.rawFromEdited(point.x / Math.max(1, point.width) * model.keptDuration());
  model.select(mapped.index);
  const segment = model.segments[mapped.index];
  scheduleDraw();
  bridge.showTimelineMenu({
    index: mapped.index,
    raw: mapped.raw,
    canSplit: mapped.raw > segment.start + .001 && mapped.raw < segment.end - .001,
    fadeIn: segment.fadeIn > 0,
    fadeOut: segment.fadeOut > 0,
    segmentCount: model.segments.length,
    undoAction: model.undoAction(),
    redoAction: model.redoAction(),
  });
});

async function openPath(candidate) {
  const accepted = candidate && typeof candidate === 'object' && candidate.path
    ? candidate
    : await bridge.acceptPath(candidate);
  if (!accepted?.path) { status.textContent = 'That file cannot be opened.'; return; }
  if (sourcePath && dirty && typeof candidate === 'string' && !await bridge.confirmDiscard()) return;
  if (renaming) finishRename(false);
  persistPlayback(true);
  sourcePath = accepted.path;
  sourceHandlesReleased = false;
  sourceIdentity = accepted.identity;
  mediaOpenStarted = performance.now();
  sourceName = sourcePath.replace(/\\/g, '/').split('/').pop();
  waveform = new Float32Array(2048);
  waveformDirty = false;
  model = null;
  previewTrim = null;
  armedTrim = null;
  exposure = 0;
  dirty = false;
  loading.hidden = false;
  emptyState.hidden = true;
  editor.hidden = false;
  video.hidden = false;
  mockVideo.hidden = true;
  video.src = localFileUrl(sourcePath);
  video.load();
  bridge.setTitle(sourceName);
  updateMenuState();
}

async function initializeVideo() {
  const restore = pendingEditorRestore;
  pendingEditorRestore = null;
  model = restore?.model || new EditorModel(video.duration);
  const restored = savedState.playheads?.[sourceIdentity];
  zoom2x = Boolean(savedState.zoom?.[sourceIdentity]);
  video.currentTime = restore
    ? Math.min(restore.playhead, video.duration)
    : Number.isFinite(restored) ? Math.min(restored, video.duration) : 0;
  if (restore) {
    exposure = restore.exposure;
    dirty = restore.dirty;
  }
  video.volume = savedState.volume ?? 0.8;
  video.muted = savedState.muted ?? false;
  volume.value = String(video.volume);
  mediaInfo.textContent = `${video.videoWidth}×${video.videoHeight}`;
  performance.mark('shattercut-media-ready', { detail: { openMs: performance.now() - mediaOpenStarted } });
  bridge.markMediaReady({
    openMs: performance.now() - mediaOpenStarted,
    duration: video.duration,
    width: video.videoWidth,
    height: video.videoHeight,
  });
  loading.hidden = true;
  showEditor();
  await bridge.fitVideo({ width: video.videoWidth, height: video.videoHeight, zoom2x });
  await video.play().catch(() => {});
  if (!video.paused && video.requestVideoFrameCallback) {
    video.requestVideoFrameCallback(() => {
      const openMs = performance.now() - mediaOpenStarted;
      performance.mark('shattercut-first-frame', { detail: { openMs } });
      bridge.markFirstFrame({ openMs });
    });
  }
  status.textContent = restore?.status || (video.paused ? 'Paused' : 'Playing');
  updateTransport();
}

async function prepareMp4Encoder() {
  const encoderVideo = document.createElement('video');
  const canvas = document.createElement('canvas');
  encoderVideo.muted = true;
  encoderVideo.playsInline = true;
  encoderVideo.preload = 'auto';
  encoderVideo.src = video.src;
  await new Promise((resolve, reject) => {
    encoderVideo.onloadedmetadata = resolve;
    encoderVideo.onerror = () => reject(new Error('The export decoder could not open this video.'));
    encoderVideo.load();
  });

  const size = exportCanvasSize(encoderVideo.videoWidth, encoderVideo.videoHeight, outputSize === 'lossless' ? 'source' : outputSize);
  canvas.width = size.width;
  canvas.height = size.height;
  const context = canvas.getContext('2d', { alpha: false });
  const sourceStream = encoderVideo.captureStream();
  const canvasStream = canvas.captureStream(30);
  const requireAudio = sourceStream.getAudioTracks().length > 0;
  let audioContext = null;
  let audioGain = null;
  let audioDestination = null;
  if (requireAudio) {
    audioContext = new AudioContext({ sampleRate: 48_000, latencyHint: 'playback' });
    audioGain = audioContext.createGain();
    audioDestination = audioContext.createMediaStreamDestination();
    audioContext.createMediaElementSource(encoderVideo).connect(audioGain).connect(audioDestination);
    await audioContext.resume();
  }
  const outputStream = new MediaStream([
    canvasStream.getVideoTracks()[0],
    ...(audioDestination?.stream.getAudioTracks() || []),
  ]);
  const mimeType = chooseMp4MimeType(MediaRecorder.isTypeSupported.bind(MediaRecorder), { audio: requireAudio });

  function paint(segment) {
    const local = Math.max(0, encoderVideo.currentTime - segment.start);
    const remaining = Math.max(0, segment.end - encoderVideo.currentTime);
    const fadeIn = segment.fadeIn ? Math.min(1, local / segment.fadeIn) : 1;
    const fadeOut = segment.fadeOut ? Math.min(1, remaining / segment.fadeOut) : 1;
    const fade = Math.max(0, Math.min(fadeIn, fadeOut));
    context.globalAlpha = 1;
    context.filter = 'none';
    context.fillStyle = '#000';
    context.fillRect(0, 0, canvas.width, canvas.height);
    const scale = Math.min(canvas.width / encoderVideo.videoWidth, canvas.height / encoderVideo.videoHeight);
    const width = encoderVideo.videoWidth * scale;
    const height = encoderVideo.videoHeight * scale;
    context.globalAlpha = fade;
    context.filter = `brightness(${2 ** exposure})`;
    context.drawImage(encoderVideo, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height);
    context.globalAlpha = 1;
    context.filter = 'none';
    if (audioGain) audioGain.gain.value = fade;
  }

  let cleaned = false;
  async function cleanup() {
    if (cleaned) return;
    cleaned = true;
    encoderVideo.pause();
    encoderVideo.removeAttribute('src');
    encoderVideo.load();
    for (const track of outputStream.getTracks()) track.stop();
    for (const track of sourceStream.getTracks()) track.stop();
    await audioContext?.close();
  }

  return { encoderVideo, outputStream, requireAudio, mimeType, paint, cleanup };
}

async function encodePlan(prepared, plan, destination, reportProgress) {
  let recorder;
  let writes = Promise.resolve();
  let bytesWritten = 0;
  try {
    const seek = async (seconds, segment) => {
      prepared.encoderVideo.pause();
      if (Math.abs(prepared.encoderVideo.currentTime - seconds) > .01) {
        await new Promise((resolve, reject) => {
          prepared.encoderVideo.addEventListener('seeked', resolve, { once: true });
          prepared.encoderVideo.addEventListener('error', reject, { once: true });
          prepared.encoderVideo.currentTime = seconds;
        });
      }
      prepared.paint(segment);
    };
    await seek(plan[0].start, plan[0]);
    recorder = new MediaRecorder(prepared.outputStream, {
      mimeType: prepared.mimeType,
      videoBitsPerSecond: 12_000_000,
      audioBitsPerSecond: 192_000,
    });
    recorder.ondataavailable = (event) => {
      if (!event.data.size) return;
      bytesWritten += event.data.size;
      writes = writes.then(async () => bridge.writeExportChunk(destination.id, await event.data.arrayBuffer()));
    };
    const stopped = new Promise((resolve, reject) => {
      recorder.onstop = resolve;
      recorder.onerror = () => reject(recorder.error || new Error('MediaRecorder failed.'));
    });
    recorder.start(1000);
    let completed = 0;
    for (let index = 0; index < plan.length && !cancelExport; index += 1) {
      const segment = plan[index];
      if (index) {
        recorder.pause();
        await seek(segment.start, segment);
        recorder.resume();
      }
      await prepared.encoderVideo.play();
      await new Promise((resolve) => {
        let finished = false;
        const deadline = setTimeout(finish, Math.max(2000, (segment.end - segment.start + 2) * 1000));
        const reachedEnd = () => prepared.encoderVideo.ended || prepared.encoderVideo.currentTime >= segment.end - .015;
        function finish() {
          if (finished) return;
          finished = true;
          clearTimeout(deadline);
          prepared.encoderVideo.removeEventListener('ended', finish);
          prepared.encoderVideo.removeEventListener('timeupdate', checkProgress);
          resolve();
        }
        function checkProgress() { if (cancelExport || reachedEnd()) finish(); }
        prepared.encoderVideo.addEventListener('ended', finish, { once: true });
        prepared.encoderVideo.addEventListener('timeupdate', checkProgress);
        const paint = () => {
          if (finished) return;
          prepared.paint(segment);
          const local = Math.min(segment.end - segment.start, Math.max(0, prepared.encoderVideo.currentTime - segment.start));
          reportProgress(completed + local);
          if (cancelExport || reachedEnd()) finish();
          else if (prepared.encoderVideo.requestVideoFrameCallback) prepared.encoderVideo.requestVideoFrameCallback(paint);
          else requestAnimationFrame(paint);
        };
        paint();
      });
      prepared.encoderVideo.pause();
      completed += segment.end - segment.start;
      reportProgress(completed);
    }
    recorder.requestData();
    await new Promise((resolve) => setTimeout(resolve, 100));
    recorder.stop();
    await stopped;
    await writes;
    if (!bytesWritten) throw new Error('Chromium produced no MP4 media data. Please retry the export.');
    if (cancelExport) {
      await bridge.cancelExport(destination.id);
      return { ok: false, canceled: true };
    }
    const result = await bridge.endExport(destination.id);
    if (!result?.ok) throw new Error(result?.error || 'The MP4 output did not pass validation.');
    return result;
  } catch (error) {
    if (recorder?.state === 'recording' || recorder?.state === 'paused') recorder.stop();
    await bridge.cancelExport(destination.id);
    throw error;
  }
}

async function reencodeExport({ inPlace = false } = {}) {
  if (!model || exporting || !sourcePath) return { ok: false };
  const originalSource = sourcePath;
  const editorBeforeReplace = { model, exposure, dirty, playhead: currentRawTime(), status: 'Save failed — the original and your edits were preserved' };
  exporting = true;
  cancelExport = false;
  exportStartedAt = performance.now();
  exportTotalDuration = Math.max(.001, model.keptDuration());
  exportProgress.hidden = false;
  removeButton.disabled = true;
  status.textContent = inPlace ? 'Preparing safe in-place save…' : 'Preparing MP4 export…';
  updateMenuState();
  let prepared;
  let session;
  let succeeded = false;
  try {
    prepared = await prepareMp4Encoder();
    activePrepared = prepared;
    const plan = model.exportPlan();
    const total = exportTotalDuration;
    const showProgress = (seconds) => {
      const ratio = Math.max(0, Math.min(1, seconds / total));
      exportProgressFill.style.width = `${ratio * 100}%`;
      bridge.setProgress(ratio);
      status.textContent = cancelExport ? 'Cancelling…' : progressText(ratio);
    };

    if (outputMode === 'clips' && !inPlace) {
      session = await bridge.beginExport('', { mode: 'clips', sourcePath });
      if (!session?.id) return { ok: false, canceled: true };
      let completed = 0;
      for (let index = 0; index < plan.length && !cancelExport; index += 1) {
        const segment = plan[index];
        const destination = await bridge.beginClipExport(session.id, clipExportName(index, segment.name), { requireAudio: prepared.requireAudio });
        if (!destination?.id) throw new Error(destination?.error || 'Could not create the clip output.');
        const result = await encodePlan(prepared, [segment], destination, (local) => showProgress(completed + local));
        if (!result.ok) break;
        completed += segment.end - segment.start;
      }
      const result = await bridge.endExportSession(session.id, cancelExport);
      session = null;
      succeeded = !cancelExport && result?.ok;
    } else {
      const destination = await bridge.beginExport(mp4ExportName(sourceName), {
        sourcePath,
        requireAudio: prepared.requireAudio,
        inPlace,
      });
      if (!destination?.id) return { ok: false, canceled: true };
      const result = await encodePlan(prepared, plan, destination, showProgress);
      succeeded = Boolean(result?.ok);
    }

    if (succeeded) {
      setDirty(false);
      exportProgressFill.style.width = '100%';
      bridge.setProgress('done');
      status.textContent = inPlace ? 'Saved — the original was replaced' : 'Export complete';
    } else if (cancelExport) status.textContent = 'Export cancelled';
    return { ok: succeeded, canceled: cancelExport };
  } catch (error) {
    if (session?.id) await bridge.endExportSession(session.id, true);
    bridge.setProgress(null);
    status.textContent = `Export failed: ${error.message}`;
    return { ok: false, error: error.message };
  } finally {
    await prepared?.cleanup();
    if (activePrepared === prepared) activePrepared = null;
    exporting = false;
    cancelExport = false;
    updateMenuState();
    updateFooter();
    setTimeout(() => { exportProgress.hidden = true; exportProgressFill.style.width = '0'; }, 1200);
    if (inPlace && sourceHandlesReleased) {
      if (!succeeded) pendingEditorRestore = editorBeforeReplace;
      await openPath(await bridge.acceptPath(originalSource));
    }
  }
}

function effectsRequireEncoding() {
  return Math.abs(exposure) > .001 || model?.segments.some((segment) => segment.fadeIn > .001 || segment.fadeOut > .001);
}

function supportsLosslessContainer() {
  return /\.(?:mp4|m4v|mov)$/i.test(sourcePath);
}

async function losslessExport({ inPlace = false, optimise = false } = {}) {
  if (!model || exporting || !sourcePath) return { ok: false };
  const originalSource = sourcePath;
  const editorBeforeReplace = { model, exposure, dirty, playhead: currentRawTime(), status: 'Save failed — the original and your edits were preserved' };
  exporting = true;
  cancelExport = false;
  exportStartedAt = performance.now();
  exportTotalDuration = optimise ? model.duration : Math.max(.001, model.keptDuration());
  exportProgress.hidden = false;
  removeButton.disabled = true;
  status.textContent = optimise ? 'Preparing lossless index rebuild…' : 'Preparing lossless export…';
  updateMenuState();
  let succeeded = false;
  try {
    const plan = (optimise ? [{ start: 0, end: model.duration, name: '' }] : model.exportPlan()).map((segment, index) => ({
      start: segment.start,
      end: segment.end,
      fileName: clipExportName(index, segment.name),
    }));
    const result = await bridge.losslessExport({
      sourcePath: originalSource,
      suggestedName: mp4ExportName(sourceName),
      segments: plan,
      mode: optimise || inPlace ? 'joined' : outputMode,
      inPlace,
      optimise,
    });
    succeeded = Boolean(result?.ok);
    if (succeeded) {
      setDirty(false);
      exportProgressFill.style.width = '100%';
      bridge.setProgress('done');
      status.textContent = optimise ? 'Optimised — lossless flat index rebuilt' : inPlace ? 'Saved — the original was replaced' : 'Lossless export complete';
    } else {
      bridge.setProgress(null);
      status.textContent = result?.canceled ? 'Export cancelled' : `Export failed: ${result?.error || 'Unknown remux error'}`;
    }
    return result;
  } finally {
    exporting = false;
    cancelExport = false;
    updateMenuState();
    updateFooter();
    setTimeout(() => { exportProgress.hidden = true; exportProgressFill.style.width = '0'; }, 1200);
    if ((inPlace || optimise) && sourceHandlesReleased) {
      if (!succeeded) pendingEditorRestore = editorBeforeReplace;
      await openPath(await bridge.acceptPath(originalSource));
    }
  }
}

async function exportAs() {
  if (!model || !sourcePath) return;
  if (exporting) { cancelExport = true; bridge.cancelLosslessExport(); return; }
  if (!hasActiveEdits() && outputMode === 'joined' && (outputSize === 'lossless' || outputSize === 'source')) {
    status.textContent = 'Copying source…';
    const result = await bridge.copySource(sourcePath);
    if (result.ok) { setDirty(false); status.textContent = 'Copy complete'; }
    else status.textContent = result.canceled ? 'Export cancelled' : `Export failed: ${result.error}`;
    return;
  }
  if (outputSize === 'lossless' && !effectsRequireEncoding() && supportsLosslessContainer()) await losslessExport();
  else await reencodeExport();
}

async function saveInPlace() {
  if (!model || !sourcePath || exporting) return;
  if (!hasActiveEdits()) { status.textContent = 'Nothing to save — this video is unedited'; return; }
  if (outputSize === 'lossless' && !effectsRequireEncoding() && supportsLosslessContainer()) await losslessExport({ inPlace: true });
  else await reencodeExport({ inPlace: true });
}

async function optimiseForEditing() {
  if (!model || exporting) return;
  if (!supportsLosslessContainer()) {
    status.textContent = 'Optimise for editing supports MP4, M4V, and MOV sources';
    return;
  }
  await losslessExport({ inPlace: true, optimise: true });
}

const saveOrExport = exportAs;

function command(name, payload) {
  const actions = {
    open: () => bridge.openDialog(),
    save: saveOrExport,
    'save-in-place': saveInPlace,
    export: exportAs,
    'cancel-export': () => { cancelExport = true; bridge.cancelLosslessExport(); status.textContent = 'Cancelling…'; },
    undo,
    redo,
    split,
    remove,
    rename: () => {
      if (Number.isInteger(payload?.index)) model?.select(payload.index);
      beginRename();
    },
    'fade-in': () => toggleFade('in'),
    'fade-out': () => toggleFade('out'),
    mute: toggleMute,
    zoom: toggleZoom,
    fullscreen: toggleFullscreen,
    'exposure-auto': autoExposure,
    'exposure-up': () => changeExposure(.15),
    'exposure-down': () => changeExposure(-.15),
    'exposure-reset': () => { exposure = 0; setDirty(hasActiveEdits()); updateTransport(); },
    'set-output-mode': () => { outputMode = payload === 'clips' ? 'clips' : 'joined'; updateMenuState(); },
    'set-output-size': () => { outputSize = ['lossless', 'source', '1080', '720', '480'].includes(payload) ? payload : 'lossless'; updateMenuState(); },
    'context-split': () => edit(() => model.split(payload.raw)),
    'context-trim-start': () => edit(() => model.trimStartTo(payload.index, payload.raw)),
    'context-trim-end': () => edit(() => model.trimEndTo(payload.index, payload.raw)),
    'context-fade-in': () => { model.select(payload.index); toggleFade('in'); },
    'context-fade-out': () => { model.select(payload.index); toggleFade('out'); },
    optimise: optimiseForEditing,
  };
  actions[name]?.();
}

$('#play').addEventListener('click', togglePlay);
$('#back').addEventListener('click', () => seekBy(-5));
$('#forward').addEventListener('click', () => seekBy(5));
$('#step-back').addEventListener('click', () => seekBy(-1 / 30));
$('#step-forward').addEventListener('click', () => seekBy(1 / 30));
$('#split').addEventListener('click', split);
$('#remove').addEventListener('click', remove);
$('#mute').addEventListener('click', toggleMute);
$('#zoom').addEventListener('click', toggleZoom);
$('#exposure-down').addEventListener('click', () => changeExposure(-.15));
$('#exposure-auto').addEventListener('click', toggleAutoExposure);
$('#exposure-up').addEventListener('click', () => changeExposure(.15));
clipRename.addEventListener('keydown', (event) => {
  if (event.key === 'Enter') { event.preventDefault(); finishRename(true); }
  else if (event.key === 'Escape') { event.preventDefault(); finishRename(false); }
  else if (event.key === 'Tab') { event.preventDefault(); finishRename(true, event.shiftKey ? -1 : 1); }
});
clipRename.addEventListener('blur', () => { if (renaming) finishRename(true); });
volume.addEventListener('input', () => {
  video.volume = Number(volume.value);
  video.muted = false;
  bridge.updateState({ volume: video.volume, muted: false });
  updateTransport();
});
video.addEventListener('click', togglePlay);
video.addEventListener('dblclick', toggleFullscreen);
video.addEventListener('loadedmetadata', initializeVideo);
video.addEventListener('play', () => { status.textContent = 'Playing'; updateTransport(); schedulePlaybackFrame(); });
video.addEventListener('pause', () => {
  cancelPlaybackFrame();
  if (waveformDirty) { waveformDirty = false; scheduleDraw(); }
  if (!exporting) status.textContent = 'Paused';
  persistPlayback(true);
  updateTransport();
  updatePlayhead();
  updateFooter();
});
video.addEventListener('timeupdate', () => {
  if (applyPlaybackBoundaries()) scheduleDraw();
  updateVideoEffects();
  persistPlayback();
  updatePlayhead();
});
video.addEventListener('error', () => { loading.hidden = true; status.textContent = 'This video could not be decoded by Chromium.'; });

document.addEventListener('keydown', (event) => {
  if (event.target instanceof HTMLInputElement) return;
  const control = event.ctrlKey || event.metaKey;
  if (control && event.key.toLowerCase() === 'o') { event.preventDefault(); command('open'); return; }
  if (control && event.key.toLowerCase() === 's') { event.preventDefault(); event.shiftKey ? exportAs() : saveInPlace(); return; }
  if (control && event.key.toLowerCase() === 'z') { event.preventDefault(); event.shiftKey ? redo() : undo(); return; }
  if (control && event.key.toLowerCase() === 'y') { event.preventDefault(); redo(); return; }
  const key = event.key.toLowerCase();
  const actions = {
    ' ': togglePlay,
    s: split,
    delete: remove,
    arrowleft: () => seekBy(-5),
    arrowright: () => seekBy(5),
    ',': () => seekBy(-1 / 30),
    '.': () => seekBy(1 / 30),
    home: () => setRawTime(model?.segments[0].start || 0),
    end: () => setRawTime(model?.segments.at(-1).end || 0),
    escape: () => {
      if (renaming) finishRename(false);
      else if (pointerMode) {
        pointerMode = null;
        previewTrim = null;
        timeline.style.cursor = 'pointer';
        scheduleDraw();
      } else if (armedTrim) {
        armedTrim = null;
        status.textContent = 'Paused';
      } else if (fullscreen) toggleFullscreen();
    },
    f2: beginRename,
    m: toggleMute,
    z: toggleZoom,
    f: toggleFullscreen,
    f11: toggleFullscreen,
    '[': () => changeExposure(-.15),
    ']': () => changeExposure(.15),
    arrowup: () => changeVolume(.05),
    arrowdown: () => changeVolume(-.05),
  };
  if (event.shiftKey && key === 'f10') {
    event.preventDefault();
    const segment = selectedSegment();
    if (segment) bridge.showTimelineMenu({
      index: model.selected,
      raw: Math.max(segment.start, Math.min(segment.end, currentRawTime())),
      canSplit: currentRawTime() > segment.start + .001 && currentRawTime() < segment.end - .001,
      fadeIn: segment.fadeIn > 0,
      fadeOut: segment.fadeOut > 0,
      segmentCount: model.segments.length,
      undoAction: model.undoAction(),
      redoAction: model.redoAction(),
    });
    return;
  }
  if (actions[key]) { event.preventDefault(); actions[key](); }
});

let dragDepth = 0;
document.addEventListener('dragenter', (event) => { event.preventDefault(); dragDepth += 1; document.body.classList.add('dragging'); });
document.addEventListener('dragleave', () => { dragDepth -= 1; if (dragDepth <= 0) document.body.classList.remove('dragging'); });
document.addEventListener('dragover', (event) => event.preventDefault());
document.addEventListener('drop', async (event) => {
  event.preventDefault();
  dragDepth = 0;
  document.body.classList.remove('dragging');
  const file = event.dataTransfer.files[0];
  if (file) await openPath(bridge.pathForFile(file));
});

document.addEventListener('mousemove', () => {
  if (!document.body.classList.contains('fullscreen')) return;
  document.body.classList.remove('transport-hidden');
  clearTimeout(fullscreenHideTimer);
  fullscreenHideTimer = setTimeout(() => document.body.classList.add('transport-hidden'), 1800);
});

bridge.onReleaseSource(async () => {
  try { await releaseSourceHandles(); }
  finally { bridge.sourceReleased(); }
});
bridge.onLosslessProgress((ratio) => {
  if (!exporting) return;
  const safe = Math.max(0, Math.min(1, Number(ratio) || 0));
  exportProgressFill.style.width = `${safe * 100}%`;
  bridge.setProgress(safe);
  status.textContent = cancelExport ? 'Cancelling…' : progressText(safe, 'Lossless copy');
});
bridge.onCommand(command);
bridge.onOpen(openPath);
bridge.onStats(updateFootprint);
new ResizeObserver(scheduleDraw).observe(timeline);

savedState = await bridge.getState();
video.volume = savedState.volume ?? 0.8;
video.muted = savedState.muted ?? false;
volume.value = String(video.volume);
updateTransport();

if (mockMode) {
  sourceName = 'fixture_frag.mp4';
  model = new EditorModel(5);
  mockVideo.hidden = false;
  video.hidden = true;
  mediaInfo.textContent = '640×480  ·  177 KB';
  status.textContent = 'Playing';
  updateFootprint({ memoryKb: 262144, cpu: 0.3, diskBytes: 214748364, dependencies: 1, network: 0 });
  showEditor();
}

requestAnimationFrame(() => {
  bridge.markReady(performance.now() - launchStarted);
});
