'use strict';

const {
  app,
  BrowserWindow,
  dialog,
  ipcMain,
  Menu,
  nativeTheme,
  screen,
  session,
  shell,
} = require('electron');
const { inspectMp4 } = require('./mp4-validator.cjs');
const { remuxMp4 } = require('./mp4-remux.cjs');
const {
  WINDOW_BOUNDS_VERSION,
  finiteBounds,
  migrateLegacyWindowBounds,
  videoContentBounds,
} = require('./window-sizing.cjs');
const fs = require('node:fs');
const fsp = require('node:fs/promises');
const path = require('node:path');
const { performance } = require('node:perf_hooks');

const APP_NAME = 'Shattercut Electron';
const VIDEO_EXTENSIONS = new Set(['.mp4', '.mkv', '.mov', '.avi', '.webm', '.m4v', '.ts', '.mts']);
const startedAt = performance.now();
const windows = new Set();
const windowStates = new Map();
const exportsInFlight = new Map();
const exportSessions = new Map();
const losslessJobs = new Map();
const releaseWaiters = new Map();
const exactContentBounds = new WeakMap();
let nextExportId = 1;
let nextExportSessionId = 1;
let preferences = {
  recent: [],
  playheads: {},
  volume: 0.8,
  muted: false,
  windowBounds: null,
  windowContentBounds: null,
  windowBoundsVersion: WINDOW_BOUNDS_VERSION,
  zoom: {},
};
let preferencesPath = '';
let packageBytes = 0;

try {
  packageBytes = Number(require(path.join(__dirname, '..', 'package-metadata.json')).bytes) || 0;
} catch {
  // Development runs do not have package metadata yet.
}

nativeTheme.themeSource = 'dark';
Menu.setApplicationMenu(null);
app.setName(APP_NAME);
app.commandLine.appendSwitch('disable-background-networking');
app.commandLine.appendSwitch('disable-component-update');
app.commandLine.appendSwitch('disable-default-apps');
app.commandLine.appendSwitch('disable-domain-reliability');
app.commandLine.appendSwitch('disable-sync');
app.commandLine.appendSwitch('no-pings');
app.commandLine.appendSwitch('disable-features', 'WebGPU');
if (process.platform === 'win32') app.setAppUserModelId('Bantamware.ShattercutElectron');

function isVideoPath(candidate) {
  return typeof candidate === 'string' && VIDEO_EXTENSIONS.has(path.extname(candidate).toLowerCase());
}

async function existingVideo(candidate) {
  if (!isVideoPath(candidate)) return '';
  try {
    const resolved = path.resolve(candidate);
    return (await fsp.stat(resolved)).isFile() ? resolved : '';
  } catch {
    return '';
  }
}

async function videoInfo(candidate) {
  const filePath = await existingVideo(candidate);
  if (!filePath) return null;
  const stat = await fsp.stat(filePath);
  return {
    path: filePath,
    identity: `${process.platform === 'win32' ? filePath.toLowerCase() : filePath}|${stat.size}|${Math.trunc(stat.mtimeMs)}`,
    bytes: stat.size,
    modified: Math.trunc(stat.mtimeMs),
  };
}

async function loadPreferences() {
  preferencesPath = path.join(app.getPath('userData'), 'state.json');
  try {
    const parsed = JSON.parse(await fsp.readFile(preferencesPath, 'utf8'));
    if (Array.isArray(parsed.recent)) preferences.recent = parsed.recent.slice(0, 10);
    if (parsed.playheads && typeof parsed.playheads === 'object') preferences.playheads = parsed.playheads;
    if (Number.isFinite(parsed.volume)) preferences.volume = Math.max(0, Math.min(1, parsed.volume));
    preferences.muted = Boolean(parsed.muted);
    if (finiteBounds(parsed.windowBounds)) {
      preferences.windowBounds = parsed.windowBounds;
      preferences.windowBoundsVersion = Number.isInteger(parsed.windowBoundsVersion) ? parsed.windowBoundsVersion : 0;
    }
    if (finiteBounds(parsed.windowContentBounds)) preferences.windowContentBounds = parsed.windowContentBounds;
    if (parsed.zoom && typeof parsed.zoom === 'object') preferences.zoom = parsed.zoom;
  } catch {
    // A missing or malformed optional state file must never delay launch.
  }
  const available = [];
  for (const item of preferences.recent) if (await existingVideo(item)) available.push(item);
  preferences.recent = available.slice(0, 10);
}

let saveQueued = Promise.resolve();
function savePreferences() {
  const snapshot = JSON.stringify(preferences);
  saveQueued = saveQueued.then(async () => {
    await fsp.mkdir(path.dirname(preferencesPath), { recursive: true });
    const temporary = `${preferencesPath}.tmp`;
    await fsp.writeFile(temporary, snapshot);
    await fsp.rename(temporary, preferencesPath);
  }).catch(() => {});
}

function setExactContentBounds(window, bounds) {
  exactContentBounds.set(window, { bounds: { ...bounds }, until: Date.now() + 1000 });
  window.setContentBounds(bounds, false);
  preferences.windowBounds = window.getBounds();
  preferences.windowContentBounds = { ...bounds };
  preferences.windowBoundsVersion = WINDOW_BOUNDS_VERSION;
  savePreferences();
}

async function addRecent(filePath) {
  const valid = await existingVideo(filePath);
  if (!valid) return;
  preferences.recent = [valid, ...preferences.recent.filter((item) => item.toLowerCase() !== valid.toLowerCase())].slice(0, 10);
  app.addRecentDocument(valid);
  savePreferences();
  installMenu();
}

function sendCommandTo(target, command, payload) {
  if (target && !target.isDestroyed()) target.webContents.send('app:command', command, payload);
}

function sendCommand(command, payload) {
  sendCommandTo(BrowserWindow.getFocusedWindow() || [...windows][0], command, payload);
}

function stateFor(target = BrowserWindow.getFocusedWindow()) {
  return target ? (windowStates.get(target.webContents.id) || {}) : {};
}

function windowForContentsId(id) {
  return [...windows].find((window) => !window.isDestroyed() && window.webContents.id === id);
}

function recordLastExport(owner, filePath) {
  const state = windowStates.get(owner) || {};
  windowStates.set(owner, { ...state, lastExport: filePath });
  const window = windowForContentsId(owner);
  if (window && !window.isDestroyed()) {
    window.setProgressBar(-1);
    if (!window.isFocused()) window.flashFrame(true);
  }
  installMenu();
}

async function requestSourceRelease(owner) {
  const window = windowForContentsId(owner);
  if (!window || window.isDestroyed()) return;
  await new Promise((resolve) => {
    const timer = setTimeout(() => {
      releaseWaiters.delete(owner);
      resolve();
    }, 5000);
    releaseWaiters.set(owner, {
      resolve: () => {
        clearTimeout(timer);
        releaseWaiters.delete(owner);
        resolve();
      },
    });
    window.webContents.send('file:release-source');
  });
}

async function atomicReplace(temporary, target) {
  try {
    await fsp.rename(temporary, target);
    return;
  } catch (error) {
    if (!['EEXIST', 'EPERM', 'EACCES'].includes(error.code)) throw error;
  }
  const backup = `${target}.shattercut-backup-${process.pid}-${Date.now()}`;
  await fsp.rename(target, backup);
  try {
    await fsp.rename(temporary, target);
  } catch (error) {
    await fsp.rename(backup, target).catch(() => {});
    throw error;
  }
  await fsp.rm(backup, { force: true }).catch(() => {});
}

async function hasTemporarySpace(source) {
  try {
    const [stat, volume] = await Promise.all([fsp.stat(source), fsp.statfs(path.dirname(source))]);
    const available = Number(volume.bavail) * Number(volume.bsize);
    return { ok: available >= stat.size + 64 * 1024 * 1024, required: stat.size, available };
  } catch {
    return { ok: true, required: 0, available: 0 };
  }
}

function commandItem(target, label, command, options = {}) {
  return {
    label,
    enabled: options.enabled ?? true,
    accelerator: options.accelerator,
    type: options.type,
    checked: options.checked,
    click: () => sendCommandTo(target, command, options.payload),
  };
}

function menuTemplate() {
  const target = BrowserWindow.getFocusedWindow() || [...windows][0];
  const state = stateFor(target);
  const loaded = Boolean(state.loaded);
  const exporting = Boolean(state.exporting);
  const canEdit = loaded && !exporting;
  const outputMode = state.outputMode || 'joined';
  const outputSize = state.outputSize || 'lossless';
  const recent = preferences.recent.map((item, index) => ({
    label: `${index < 9 ? `&${index + 1} ` : ''}${item}`,
    click: () => openVideo(item, target),
  }));
  if (recent.length) recent.push({ type: 'separator' }, {
    label: '&Clear list',
    click: () => {
      preferences.recent = [];
      app.clearRecentDocuments();
      savePreferences();
      installMenu();
    },
  });
  const fileMenu = [
    { label: '&Open video…', accelerator: 'CmdOrCtrl+O', click: () => openDialog(target) },
    {
      label: 'Open &recent',
      submenu: recent.length ? recent : [{ label: 'No recent videos', enabled: false }],
    },
    { type: 'separator' },
    commandItem(target, '&Save (replace original)', 'save-in-place', { accelerator: 'CmdOrCtrl+S', enabled: canEdit }),
    commandItem(target, exporting ? 'Cancel &export' : 'Export &As…', exporting ? 'cancel-export' : 'export', {
      accelerator: 'CmdOrCtrl+Shift+S',
      enabled: loaded,
    }),
    {
      label: 'Export &settings',
      enabled: !exporting,
      submenu: [
        commandItem(target, 'One &joined file', 'set-output-mode', { type: 'radio', checked: outputMode === 'joined', payload: 'joined' }),
        commandItem(target, 'Separate &clips', 'set-output-mode', { type: 'radio', checked: outputMode === 'clips', payload: 'clips' }),
        { type: 'separator' },
        commandItem(target, '&Lossless — cuts snap to keyframes', 'set-output-size', { type: 'radio', checked: outputSize === 'lossless', payload: 'lossless' }),
        commandItem(target, 'Source si&ze re-encode (frame-accurate)', 'set-output-size', { type: 'radio', checked: outputSize === 'source', payload: 'source' }),
        commandItem(target, '1920 × 1080 re-encode', 'set-output-size', { type: 'radio', checked: outputSize === '1080', payload: '1080' }),
        commandItem(target, '1280 × 720 re-encode', 'set-output-size', { type: 'radio', checked: outputSize === '720', payload: '720' }),
        commandItem(target, '854 × 480 re-encode', 'set-output-size', { type: 'radio', checked: outputSize === '480', payload: '480' }),
      ],
    },
    commandItem(target, '&Optimise for editing (rebuild index)', 'optimise', { enabled: canEdit }),
    {
      label: 'Show &last export in folder',
      enabled: Boolean(state.lastExport) && !exporting,
      click: () => revealLastExport(target),
    },
    { type: 'separator' },
    process.platform === 'darwin' ? { role: 'close' } : { label: 'E&xit', role: 'quit' },
  ];
  const template = [
    { label: '&File', submenu: fileMenu },
    {
      label: '&Edit',
      submenu: [
        commandItem(target, state.undoAction ? `&Undo ${state.undoAction}` : '&Undo', 'undo', { accelerator: 'CmdOrCtrl+Z', enabled: canEdit && Boolean(state.undoAction) }),
        commandItem(target, state.redoAction ? `&Redo ${state.redoAction}` : '&Redo', 'redo', { accelerator: process.platform === 'darwin' ? 'CmdOrCtrl+Shift+Z' : 'CmdOrCtrl+Y', enabled: canEdit && Boolean(state.redoAction) }),
        { type: 'separator' },
        commandItem(target, '&Split at playhead', 'split', { accelerator: 'S', enabled: canEdit }),
        commandItem(target, '&Remove selected clip', 'remove', { accelerator: 'Delete', enabled: canEdit && (state.segmentCount || 0) > 1 }),
        commandItem(target, 'Re&name clip', 'rename', { accelerator: 'F2', enabled: canEdit }),
        commandItem(target, 'Toggle &fade in', 'fade-in', { type: 'checkbox', checked: Boolean(state.fadeIn), enabled: canEdit }),
        commandItem(target, 'Toggle fade &out', 'fade-out', { type: 'checkbox', checked: Boolean(state.fadeOut), enabled: canEdit }),
      ],
    },
    {
      label: '&View',
      submenu: [
        commandItem(target, '&Fullscreen', 'fullscreen', { accelerator: 'F11', type: 'checkbox', checked: Boolean(state.fullscreen), enabled: loaded }),
        { type: 'separator' },
        commandItem(target, '&Auto-fix exposure', 'exposure-auto', { enabled: canEdit }),
        commandItem(target, 'Exposure &brighter', 'exposure-up', { accelerator: ']', enabled: canEdit }),
        commandItem(target, 'Exposure &darker', 'exposure-down', { accelerator: '[', enabled: canEdit }),
        commandItem(target, '&Reset exposure', 'exposure-reset', { enabled: canEdit && Math.abs(state.exposure || 0) > .001 }),
        { type: 'separator' },
        commandItem(target, '&Mute', 'mute', { accelerator: 'M', type: 'checkbox', checked: Boolean(state.muted), enabled: loaded }),
        commandItem(target, '&Actual size / 2×', 'zoom', { accelerator: 'Z', enabled: loaded }),
      ],
    },
    {
      label: '&Help',
      submenu: [
        {
          label: '&About Shattercut Electron',
          click: () => dialog.showMessageBox({
            type: 'info',
            title: APP_NAME,
            message: `${APP_NAME} ${app.getVersion()}`,
            detail: 'One pinned dependency. No framework. No network. An honest Electron proof of life.',
          }),
        },
      ],
    },
  ];
  if (process.platform === 'darwin') template.unshift({ label: APP_NAME, submenu: [{ role: 'about' }, { type: 'separator' }, { role: 'quit' }] });
  return template;
}

function installMenu() {
  Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate()));
}

async function confirmDiscard(target) {
  const state = stateFor(target);
  if (state.exporting) {
    await dialog.showMessageBox(target, { type: 'warning', message: 'An export is still running.', detail: 'Cancel it before opening another video.' });
    return false;
  }
  if (!state.dirty) return true;
  const answer = await dialog.showMessageBox(target, {
    type: 'warning',
    buttons: ['Keep editing', 'Discard edits'],
    defaultId: 0,
    cancelId: 0,
    message: 'Your edits have not been exported.',
    detail: 'Open another video and discard them?',
  });
  return answer.response === 1;
}

function revealLastExport(target) {
  const filePath = stateFor(target).lastExport;
  if (filePath) shell.showItemInFolder(filePath);
}

async function openDialog(target = BrowserWindow.getFocusedWindow()) {
  const result = await dialog.showOpenDialog(target || undefined, {
    properties: ['openFile'],
    filters: [{ name: 'Video files', extensions: [...VIDEO_EXTENSIONS].map((extension) => extension.slice(1)) }],
  });
  if (!result.canceled && result.filePaths[0]) await openVideo(result.filePaths[0], target);
}

async function openVideo(candidate, target = BrowserWindow.getFocusedWindow()) {
  const info = await videoInfo(candidate);
  if (!info) return false;
  if (target && !target.isDestroyed()) {
    if (!await confirmDiscard(target)) return false;
    target.webContents.send('file:open', info);
    if (target.isMinimized()) target.restore();
    target.show();
    target.focus();
  } else {
    createWindow(info.path);
  }
  await addRecent(info.path);
  return true;
}

function createWindow(initialPath = '') {
  const remembered = preferences.windowBounds;
  const rememberedContent = preferences.windowContentBounds;
  const window = new BrowserWindow({
    width: rememberedContent?.width || remembered?.width || 1320,
    height: rememberedContent?.height || remembered?.height || 820,
    ...(remembered ? { x: remembered.x, y: remembered.y } : {}),
    useContentSize: Boolean(rememberedContent),
    minWidth: 680,
    minHeight: 520,
    show: true,
    backgroundColor: '#161510',
    title: 'Shattercut',
    icon: path.join(__dirname, '..', 'assets', 'app-icon.png'),
    autoHideMenuBar: false,
    webPreferences: {
      preload: path.join(__dirname, 'preload.cjs'),
      contextIsolation: true,
      sandbox: true,
      nodeIntegration: false,
      spellcheck: false,
      devTools: !app.isPackaged,
      disableBlinkFeatures: 'WebGPU',
    },
  });
  if (remembered && preferences.windowBoundsVersion < WINDOW_BOUNDS_VERSION) {
    const outer = window.getBounds();
    const content = window.getContentBounds();
    const display = screen.getDisplayMatching(outer);
    const migrated = migrateLegacyWindowBounds({
      bounds: remembered,
      scaleFactor: display.scaleFactor,
      frameSize: { width: outer.width - content.width, height: outer.height - content.height },
      workArea: display.workArea,
    });
    if (migrated) window.setBounds(migrated);
    preferences.windowBounds = migrated;
    preferences.windowBoundsVersion = WINDOW_BOUNDS_VERSION;
    savePreferences();
  }
  windows.add(window);
  const contentsId = window.webContents.id;
  windowStates.set(contentsId, {});
  let closeApproved = false;
  let closePromptOpen = false;
  let boundsSaveTimer = null;
  const rememberBounds = () => {
    if (window.isDestroyed() || window.isMaximized() || window.isFullScreen()) return;
    clearTimeout(boundsSaveTimer);
    const exact = exactContentBounds.get(window);
    if (exact && Date.now() <= exact.until) return;
    boundsSaveTimer = setTimeout(() => {
      preferences.windowBounds = window.getBounds();
      preferences.windowContentBounds = window.getContentBounds();
      preferences.windowBoundsVersion = WINDOW_BOUNDS_VERSION;
      savePreferences();
    }, 250);
  };
  window.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
  window.webContents.on('will-navigate', (event, url) => {
    if (!url.startsWith('file:')) event.preventDefault();
  });
  window.webContents.once('did-finish-load', () => {
    // Persist and restore the editing surface instead of round-tripping the
    // decorated frame. Chromium and Win32 disagree by a few DIPs about those
    // frame bounds at fractional display scales.
    if (rememberedContent) setExactContentBounds(window, rememberedContent);
    if (initialPath) openVideo(initialPath, window);
  });
  window.on('focus', installMenu);
  window.on('move', rememberBounds);
  window.on('resize', rememberBounds);
  window.on('close', async (event) => {
    if (closeApproved) return;
    const state = stateFor(window);
    if (!state.exporting && !state.dirty) return;
    event.preventDefault();
    if (closePromptOpen) return;
    closePromptOpen = true;
    const exporting = Boolean(state.exporting);
    const answer = await dialog.showMessageBox(window, {
      type: 'warning',
      buttons: [exporting ? 'Keep exporting' : 'Keep editing', exporting ? 'Cancel export and close' : 'Discard edits and close'],
      defaultId: 0,
      cancelId: 0,
      message: exporting ? 'An export is still running.' : 'Your edits have not been exported.',
      detail: exporting ? 'Cancel the export and close Shattercut?' : 'Close without exporting?',
    });
    closePromptOpen = false;
    if (answer.response !== 1) return;
    closeApproved = true;
    losslessJobs.get(contentsId)?.abort();
    for (const [id, job] of exportsInFlight) if (job.owner === contentsId) await closeExport(id, true);
    window.destroy();
  });
  window.on('closed', () => {
    clearTimeout(boundsSaveTimer);
    losslessJobs.get(contentsId)?.abort();
    releaseWaiters.get(contentsId)?.resolve();
    windows.delete(window);
    windowStates.delete(contentsId);
    installMenu();
  });
  window.loadFile(path.join(__dirname, 'index.html'));
  return window;
}

function aggregateStats() {
  let memoryKb = 0;
  let cpu = 0;
  for (const metric of app.getAppMetrics()) {
    memoryKb += metric.memory?.workingSetSize || 0;
    cpu += metric.cpu?.percentCPUUsage || 0;
  }
  return { memoryKb, cpu, diskBytes: packageBytes, dependencies: 1, network: 0, processes: app.getAppMetrics().length };
}

function startStats() {
  const publish = () => {
    const stats = aggregateStats();
    for (const window of windows) if (!window.isDestroyed()) window.webContents.send('app:stats', stats);
  };
  setInterval(publish, 2000).unref();
  publish();
}

async function closeExport(id, removeOutput) {
  const job = exportsInFlight.get(id);
  if (!job) return { ok: false, error: 'The export is no longer active.' };
  exportsInFlight.delete(id);
  if (removeOutput) {
    if (!job.stream.closed) {
      await new Promise((resolve) => {
        job.stream.once('close', resolve);
        job.stream.destroy();
      });
    }
    await fsp.rm(job.output, { force: true }).catch(() => {});
    return { ok: true, canceled: true };
  }
  try {
    await new Promise((resolve, reject) => {
      job.stream.once('error', reject);
      job.stream.end(resolve);
    });
    const mp4 = await inspectMp4(job.output, { requireAudio: job.requireAudio });
    let completedPath = job.output;
    if (job.replaceTarget) {
      await requestSourceRelease(job.owner);
      await atomicReplace(job.output, job.replaceTarget);
      completedPath = job.replaceTarget;
    }
    recordLastExport(job.owner, completedPath);
    return { ok: true, path: completedPath, mp4, replaced: Boolean(job.replaceTarget) };
  } catch (error) {
    await fsp.rm(job.output, { force: true }).catch(() => {});
    return { ok: false, error: error.message };
  }
}

ipcMain.handle('dialog:open', (event) => openDialog(BrowserWindow.fromWebContents(event.sender)));
ipcMain.handle('dialog:confirm-discard', (event) => confirmDiscard(BrowserWindow.fromWebContents(event.sender)));
ipcMain.on('file:released', (event) => releaseWaiters.get(event.sender.id)?.resolve());
ipcMain.handle('file:accept', async (_event, candidate) => videoInfo(candidate));
ipcMain.handle('state:get', () => preferences);
ipcMain.handle('state:update', (_event, update) => {
  if (Number.isFinite(update?.volume)) preferences.volume = Math.max(0, Math.min(1, update.volume));
  if (typeof update?.muted === 'boolean') preferences.muted = update.muted;
  const identity = typeof update?.identity === 'string' ? update.identity : update?.path;
  if (typeof identity === 'string' && Number.isFinite(update?.playhead)) {
    preferences.playheads[identity] = Math.max(0, update.playhead);
    const keys = Object.keys(preferences.playheads);
    for (const key of keys.slice(0, Math.max(0, keys.length - 50))) delete preferences.playheads[key];
  }
  if (typeof identity === 'string' && typeof update?.zoom2x === 'boolean') {
    preferences.zoom[identity] = update.zoom2x;
  }
  savePreferences();
});
ipcMain.on('menu:state', (event, next) => {
  const window = BrowserWindow.fromWebContents(event.sender);
  if (!window || window.isDestroyed() || !next || typeof next !== 'object') return;
  const previous = windowStates.get(event.sender.id) || {};
  windowStates.set(event.sender.id, {
    ...previous,
    loaded: Boolean(next.loaded),
    exporting: Boolean(next.exporting),
    dirty: Boolean(next.dirty),
    undoAction: typeof next.undoAction === 'string' ? next.undoAction.slice(0, 40) : '',
    redoAction: typeof next.redoAction === 'string' ? next.redoAction.slice(0, 40) : '',
    segmentCount: Math.max(0, Number(next.segmentCount) || 0),
    fadeIn: Boolean(next.fadeIn),
    fadeOut: Boolean(next.fadeOut),
    exposure: Number(next.exposure) || 0,
    muted: Boolean(next.muted),
    fullscreen: Boolean(next.fullscreen),
    outputMode: next.outputMode === 'clips' ? 'clips' : 'joined',
    outputSize: ['lossless', 'source', '1080', '720', '480'].includes(next.outputSize) ? next.outputSize : 'lossless',
  });
  if (BrowserWindow.getFocusedWindow() === window) installMenu();
});
ipcMain.handle('window:fullscreen', (event) => {
  const window = BrowserWindow.fromWebContents(event.sender);
  window.setFullScreen(!window.isFullScreen());
  return window.isFullScreen();
});
ipcMain.handle('window:fit-video', (event, request = {}) => {
  const window = BrowserWindow.fromWebContents(event.sender);
  if (!window || window.isDestroyed() || window.isMaximized() || window.isFullScreen()) return false;
  const outer = window.getBounds();
  const content = window.getContentBounds();
  const display = screen.getDisplayMatching(outer);
  const target = videoContentBounds({
    sourceWidth: request.width,
    sourceHeight: request.height,
    zoom2x: request.zoom2x,
    scaleFactor: display.scaleFactor,
    frameSize: { width: outer.width - content.width, height: outer.height - content.height },
    currentBounds: outer,
    currentContentBounds: content,
    workArea: display.workArea,
  });
  setExactContentBounds(window, target);
  return true;
});
ipcMain.on('window:progress', (event, value) => {
  const window = BrowserWindow.fromWebContents(event.sender);
  if (!window || window.isDestroyed()) return;
  if (value === 'done') {
    window.setProgressBar(-1);
    if (!window.isFocused()) window.flashFrame(true);
  } else if (Number.isFinite(value)) window.setProgressBar(Math.max(0, Math.min(1, value)));
  else window.setProgressBar(-1);
});
ipcMain.handle('timeline:menu', (event, payload = {}) => {
  const window = BrowserWindow.fromWebContents(event.sender);
  if (!window || window.isDestroyed()) return false;
  const index = Math.max(0, Number(payload.index) || 0);
  const raw = Math.max(0, Number(payload.raw) || 0);
  const invoke = (command) => sendCommandTo(window, command, { index, raw });
  const menu = Menu.buildFromTemplate([
    { label: '&Split here', enabled: Boolean(payload.canSplit), click: () => invoke('context-split') },
    { type: 'separator' },
    { label: 'Trim clip star&t to here', click: () => invoke('context-trim-start') },
    { label: 'Trim clip &end to here', click: () => invoke('context-trim-end') },
    { type: 'separator' },
    { label: 'Fade &in (1 s)', type: 'checkbox', checked: Boolean(payload.fadeIn), click: () => invoke('context-fade-in') },
    { label: 'Fade &out (1 s)', type: 'checkbox', checked: Boolean(payload.fadeOut), click: () => invoke('context-fade-out') },
    { type: 'separator' },
    { label: 'Re&name clip', accelerator: 'F2', click: () => invoke('rename') },
    { label: '&Remove clip', accelerator: 'Delete', enabled: Number(payload.segmentCount) > 1, click: () => invoke('remove') },
    { type: 'separator' },
    { label: payload.undoAction ? `&Undo ${payload.undoAction}` : '&Undo', enabled: Boolean(payload.undoAction), click: () => invoke('undo') },
    { label: payload.redoAction ? `&Redo ${payload.redoAction}` : '&Redo', enabled: Boolean(payload.redoAction), click: () => invoke('redo') },
  ]);
  menu.popup({ window });
  return true;
});
ipcMain.on('window:title', (event, title) => {
  const window = BrowserWindow.fromWebContents(event.sender);
  window.setTitle(title ? `${title} - Shattercut` : 'Shattercut');
});
function emitPerformanceMarker(line) {
  try { fs.writeSync(1, `${line}\n`); } catch {}
  if (process.env.SHATTERCUT_PERF_LOG) {
    try { fs.appendFileSync(process.env.SHATTERCUT_PERF_LOG, `${line}\n`); } catch {}
  }
}

ipcMain.on('perf:ready', (_event, rendererMs) => {
  emitPerformanceMarker(`SHATTERCUT_READY main=${(performance.now() - startedAt).toFixed(2)} renderer=${Number(rendererMs).toFixed(2)}`);
});
ipcMain.on('perf:media-ready', (_event, metrics) => {
  emitPerformanceMarker(`SHATTERCUT_MEDIA_READY total=${(performance.now() - startedAt).toFixed(2)} open=${Number(metrics?.openMs).toFixed(2)} duration=${Number(metrics?.duration).toFixed(3)} width=${Number(metrics?.width)} height=${Number(metrics?.height)}`);
});
ipcMain.on('perf:first-frame', (_event, metrics) => {
  emitPerformanceMarker(`SHATTERCUT_FIRST_FRAME total=${(performance.now() - startedAt).toFixed(2)} open=${Number(metrics?.openMs).toFixed(2)}`);
});
ipcMain.handle('file:copy', async (event, source) => {
  const valid = await existingVideo(source);
  if (!valid) return { ok: false, error: 'Source file is unavailable.' };
  const extension = path.extname(valid);
  const result = await dialog.showSaveDialog(BrowserWindow.fromWebContents(event.sender), {
    defaultPath: path.join(path.dirname(valid), `${path.basename(valid, extension)}-copy${extension}`),
  });
  if (result.canceled || !result.filePath) return { ok: false, canceled: true };
  try {
    await fsp.copyFile(valid, result.filePath);
    recordLastExport(event.sender.id, result.filePath);
    return { ok: true, path: result.filePath };
  }
  catch (error) { return { ok: false, error: error.message }; }
});

function safeClipName(value) {
  const clean = String(value || 'clip').replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').replace(/[. ]+$/g, '').trim();
  return `${clean || 'clip'}.mp4`;
}

async function createExportJob(output, options, owner) {
  await fsp.rm(output, { force: true }).catch(() => {});
  const stream = fs.createWriteStream(output, { flags: 'wx' });
  try { await new Promise((resolve, reject) => { stream.once('open', resolve); stream.once('error', reject); }); }
  catch (error) { return { error: error.message }; }
  const id = nextExportId++;
  exportsInFlight.set(id, {
    stream,
    output,
    requireAudio: options?.requireAudio === true,
    owner,
    replaceTarget: options?.replaceTarget || '',
  });
  return { id, path: output };
}

ipcMain.handle('export:begin', async (event, suggestedName, options = {}) => {
  const window = BrowserWindow.fromWebContents(event.sender);
  const source = await existingVideo(options?.sourcePath || '');
  if (options?.mode === 'clips') {
    const result = await dialog.showOpenDialog(window, {
      title: 'Choose clip output folder',
      defaultPath: source ? path.dirname(source) : undefined,
      properties: ['openDirectory', 'createDirectory'],
    });
    if (result.canceled || !result.filePaths[0]) return { canceled: true };
    const id = nextExportSessionId++;
    exportSessions.set(id, { owner: event.sender.id, directory: result.filePaths[0], outputs: [] });
    return { id, path: result.filePaths[0], clips: true };
  }
  if (options?.inPlace) {
    if (!source) return { error: 'The source file is unavailable.' };
    const answer = await dialog.showMessageBox(window, {
      type: 'warning',
      buttons: ['Cancel', 'Replace original'],
      defaultId: 0,
      cancelId: 0,
      message: `Replace "${path.basename(source)}" with your edit?`,
      detail: 'The new MP4 is written beside the original and validated before it replaces the source.',
    });
    if (answer.response !== 1) return { canceled: true };
    return createExportJob(`${source}.shattercut-tmp.mp4`, { ...options, replaceTarget: source }, event.sender.id);
  }
  const result = await dialog.showSaveDialog(window, {
    defaultPath: source ? path.join(path.dirname(source), suggestedName) : suggestedName,
    filters: [{ name: 'MP4 video', extensions: ['mp4'] }],
  });
  if (result.canceled || !result.filePath) return { canceled: true };
  return createExportJob(result.filePath, options, event.sender.id);
});
ipcMain.handle('export:begin-clip', async (event, sessionId, fileName, options = {}) => {
  const session = exportSessions.get(sessionId);
  if (!session || session.owner !== event.sender.id) return { error: 'The clip export session is no longer active.' };
  const output = path.join(session.directory, safeClipName(fileName));
  const result = await createExportJob(output, options, event.sender.id);
  if (result.id) session.outputs.push(output);
  return result;
});
ipcMain.handle('export:end-session', (event, sessionId, canceled = false) => {
  const session = exportSessions.get(sessionId);
  if (!session || session.owner !== event.sender.id) return { ok: false, error: 'The clip export session is no longer active.' };
  exportSessions.delete(sessionId);
  if (!canceled && session.outputs.length) recordLastExport(event.sender.id, session.outputs[0]);
  return { ok: true, path: session.directory, outputs: session.outputs };
});
ipcMain.handle('export:chunk', async (_event, id, data) => {
  const job = exportsInFlight.get(id);
  if (!job || (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data))) return false;
  const chunk = data instanceof ArrayBuffer
    ? Buffer.from(data)
    : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
  await new Promise((resolve, reject) => {
    job.stream.write(chunk, (error) => error ? reject(error) : resolve());
  });
  return true;
});
ipcMain.handle('export:end', (_event, id) => closeExport(id, false));
ipcMain.handle('export:cancel', (_event, id) => closeExport(id, true));

function samePath(left, right) {
  const normalize = (value) => process.platform === 'win32' ? path.resolve(value).toLowerCase() : path.resolve(value);
  return normalize(left) === normalize(right);
}

ipcMain.on('lossless:cancel', (event) => losslessJobs.get(event.sender.id)?.abort());
ipcMain.handle('lossless:export', async (event, options = {}) => {
  const owner = event.sender.id;
  const window = BrowserWindow.fromWebContents(event.sender);
  const source = await existingVideo(options.sourcePath || '');
  if (!source) return { ok: false, error: 'The source file is unavailable.' };
  const requested = Array.isArray(options.segments) ? options.segments : [];
  if (!requested.length) return { ok: false, error: 'There are no retained clips to export.' };
  try {
    const shape = await inspectMp4(source);
    const audio = shape.tracks.find((track) => track.handler === 'soun');
    if (audio && !audio.codecs.includes('mp4a')) throw new Error('The source audio is not AAC.');
  } catch (error) {
    return { ok: false, error: `Lossless export requires an H.264/AAC MP4-family source. ${error.message}` };
  }
  const controller = new AbortController();
  losslessJobs.set(owner, controller);
  let temporary = '';
  try {
    const progress = (ratio) => {
      if (!window?.isDestroyed()) window.webContents.send('lossless:progress', Math.max(0, Math.min(1, ratio)));
    };
    if (options.mode === 'clips' && !options.inPlace) {
      const chosen = await dialog.showOpenDialog(window, {
        title: 'Choose clip output folder',
        defaultPath: path.dirname(source),
        properties: ['openDirectory', 'createDirectory'],
      });
      if (chosen.canceled || !chosen.filePaths[0]) return { ok: false, canceled: true };
      const directory = chosen.filePaths[0];
      const outputs = [];
      const weights = requested.map((segment) => Math.max(0.001, Number(segment.end) - Number(segment.start)));
      const total = weights.reduce((sum, value) => sum + value, 0);
      let completed = 0;
      for (let index = 0; index < requested.length; index += 1) {
        const segment = requested[index];
        const output = path.join(directory, safeClipName(segment.fileName || `clip-${String(index + 1).padStart(2, '0')}`));
        const result = await remuxMp4({
          input: source,
          output,
          segments: [segment],
          signal: controller.signal,
          onProgress: (ratio) => progress((completed + ratio * weights[index]) / total),
        });
        await inspectMp4(output, { requireAudio: result.tracks.some((track) => track.type === 'audio') });
        outputs.push(output);
        completed += weights[index];
      }
      if (outputs.length) recordLastExport(owner, outputs[0]);
      return { ok: true, path: directory, outputs };
    }

    let inPlace = Boolean(options.inPlace || options.optimise);
    let output;
    if (!inPlace) {
      const chosen = await dialog.showSaveDialog(window, {
        defaultPath: path.join(path.dirname(source), String(options.suggestedName || 'video-cut.mp4')),
        filters: [{ name: 'MP4 video', extensions: ['mp4'] }],
      });
      if (chosen.canceled || !chosen.filePath) return { ok: false, canceled: true };
      output = chosen.filePath;
      inPlace = samePath(output, source);
    }
    if (inPlace) {
      const room = await hasTemporarySpace(source);
      if (!room.ok) {
        return { ok: false, error: `There is not enough free space to write a safe temporary copy (${Math.ceil(room.required / 1024 / 1024)} MB required).` };
      }
      const answer = await dialog.showMessageBox(window, options.optimise ? {
        type: 'info',
        buttons: ['Cancel', 'Optimise'],
        defaultId: 1,
        cancelId: 0,
        message: `Rewrite "${path.basename(source)}" so it seeks quickly?`,
        detail: 'No frame is re-encoded. Video and audio samples are copied exactly, a flat index is rebuilt, and current timeline edits are not applied.',
      } : {
        type: 'warning',
        buttons: ['Cancel', 'Replace original'],
        defaultId: 0,
        cancelId: 0,
        message: `Replace "${path.basename(source)}" with your edit?`,
        detail: 'The new MP4 is written beside the original and validated before it replaces the source.',
      });
      if (answer.response !== 1) return { ok: false, canceled: true };
      temporary = `${source}.shattercut-tmp.mp4`;
      output = temporary;
    }
    const result = await remuxMp4({
      input: source,
      output,
      segments: requested,
      signal: controller.signal,
      onProgress: progress,
    });
    const mp4 = await inspectMp4(output, { requireAudio: result.tracks.some((track) => track.type === 'audio') });
    let completedPath = output;
    if (inPlace) {
      await requestSourceRelease(owner);
      await atomicReplace(output, source);
      temporary = '';
      completedPath = source;
    }
    recordLastExport(owner, completedPath);
    return { ok: true, path: completedPath, mp4, remux: result, replaced: inPlace };
  } catch (error) {
    if (temporary) await fsp.rm(temporary, { force: true }).catch(() => {});
    return { ok: false, canceled: controller.signal.aborted, error: controller.signal.aborted ? 'Export cancelled' : error.message };
  } finally {
    if (losslessJobs.get(owner) === controller) losslessJobs.delete(owner);
  }
});

app.on('open-file', (event, filePath) => {
  event.preventDefault();
  if (app.isReady()) createWindow(filePath);
  else app.once('ready', () => createWindow(filePath));
});

app.whenReady().then(async () => {
  await loadPreferences();
  session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
  installMenu();
  const initial = await existingVideo(process.argv.find((argument, index) => index > 0 && isVideoPath(argument)) || '');
  createWindow(initial);
  startStats();
  app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

app.on('will-quit', () => {
  for (const controller of losslessJobs.values()) controller.abort();
  for (const waiter of releaseWaiters.values()) waiter.resolve();
  for (const id of [...exportsInFlight.keys()]) closeExport(id, true);
});
