const { app, BrowserWindow, session, shell, desktopCapturer, net, Notification: ElectronNotification } = require('electron');
const path = require('path');
const fs = require('fs');

const CONFIG_PATH = path.join(app.getPath('userData'), 'harmony-config.json');

function loadConfig() {
  try {
    return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
  } catch {
    return null;
  }
}

function saveConfig(config) {
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
}

let mainWindow;
let loginInProgress = false;
let _injectTitlebarCode = '';
let _offlinePagePath = '';

function createWindow() {
  const isMac = process.platform === 'darwin';
  mainWindow = new BrowserWindow({
    width: 1280,
    height: 800,
    minWidth: 940,
    minHeight: 560,
    title: 'Harmony',
    backgroundColor: '#09090b',
    frame: false,
    titleBarStyle: isMac ? 'hiddenInset' : 'hidden',
    trafficLightPosition: isMac ? { x: 12, y: 8 } : undefined,
    autoHideMenuBar: true,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: false
    }
  });

  mainWindow.removeMenu();

  // F12 or Ctrl+Shift+I to open DevTools
  mainWindow.webContents.on('before-input-event', (_event, input) => {
    if (input.key === 'F12' || (input.control && input.shift && input.key.toLowerCase() === 'i')) {
      mainWindow.webContents.toggleDevTools();
    }
  });

  // ── Permissions: always allow mic, camera, notifications ──
  session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
    const allowed = [
      'media', 'mediaKeySystem', 'geolocation',
      'notifications', 'fullscreen', 'pointerLock',
      'display-capture', 'idle-detection'
    ];
    callback(allowed.includes(permission));
  });

  session.defaultSession.setPermissionCheckHandler((webContents, permission) => {
    return true;
  });

  // ── Screen sharing ──
  // The renderer shows a custom source picker (thumbnails of all screens
  // and windows) then tells us which source to use via set-screenshare-source.
  // audio:'loopback' provides system audio on Windows/macOS.  On Linux it's
  // handled separately via ffmpeg + PipeWire.
  let _pendingDisplaySourceId = null;

  ipcMain.handle('set-screenshare-source', (_event, id) => {
    _pendingDisplaySourceId = id || null;
  });

  if (process.platform === 'linux') {
    // Linux (Wayland): let the OS handle source selection via
    // xdg-desktop-portal / PipeWire screen capture dialog.
    // Audio comes from venmic, not loopback.
    // The fallback handler is called if the system picker is unavailable
    // (e.g. X11 without portal) — pick the first screen as fallback.
    session.defaultSession.setDisplayMediaRequestHandler(
      async (_request, callback) => {
        try {
          const sources = await desktopCapturer.getSources({ types: ['screen', 'window'] });
          if (sources.length > 0) callback({ video: sources[0] });
          else callback({});
        } catch { callback({}); }
      },
      { useSystemPicker: true }
    );
  } else {
    // macOS/Windows: custom source picker in the renderer.
    session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => {
      try {
        const sources = await desktopCapturer.getSources({ types: ['screen', 'window'] });
        if (sources.length === 0) { callback({}); return; }
        let chosen = null;
        if (_pendingDisplaySourceId) {
          chosen = sources.find(s => s.id === _pendingDisplaySourceId) || null;
          _pendingDisplaySourceId = null;
        }
        if (!chosen) chosen = sources[0];
        // Audio captured separately: audiotee (macOS), loopback_capture.exe (Windows).
        callback({ video: chosen });
      } catch {
        callback({});
      }
    });
  }

  // ── Disable CORS / web-security annoyances for the Harmony domain ──
  session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
    callback({ requestHeaders: details.requestHeaders });
  });
  session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
    const headers = { ...details.responseHeaders };
    delete headers['x-frame-options'];
    delete headers['X-Frame-Options'];
    delete headers['content-security-policy'];
    delete headers['Content-Security-Policy'];
    callback({ responseHeaders: headers });
  });

  // ── External link handling: open non-Harmony URLs in system browser ──
  mainWindow.webContents.setWindowOpenHandler(({ url }) => {
    const config = loadConfig();
    const serverBase = config ? config.server.replace(/\/+$/, '') : '';
    if (serverBase && url.startsWith(serverBase)) {
      return { action: 'allow' };
    }
    shell.openExternal(url);
    return { action: 'deny' };
  });

  // ── Inject custom titlebar into every page ──
  const TITLEBAR_CSS = fs.readFileSync(path.join(__dirname, 'titlebar', 'titlebar.css'), 'utf-8');
  let TITLEBAR_HTML = fs.readFileSync(path.join(__dirname, 'titlebar', 'titlebar.html'), 'utf-8');
  const TITLEBAR_JS = fs.readFileSync(path.join(__dirname, 'titlebar', 'titlebar.js'), 'utf-8');

  // Embed the frameless logo as a data URI so it works on both file:// and http:// pages
  const logoPath = path.join(__dirname, 'assets', 'frameless_logo.png');
  const logoFallback = path.join(__dirname, '..', 'static', 'frameless_logo.png');
  const logoFile = fs.existsSync(logoPath) ? logoPath : (fs.existsSync(logoFallback) ? logoFallback : null);
  const logoB64 = logoFile ? fs.readFileSync(logoFile).toString('base64') : '';
  if (logoB64) {
    TITLEBAR_HTML = TITLEBAR_HTML.replace(
      'src="/static/frameless_logo.png"',
      'src="data:image/png;base64,' + logoB64 + '"'
    );
  }

  const INJECT_TITLEBAR = `
(function() {
  if (document.getElementById('harmony-titlebar')) return;
  var isMac = ${process.platform === 'darwin'};

  var style = document.createElement('style');
  style.textContent = ${JSON.stringify(TITLEBAR_CSS)};
  if (isMac) {
    style.textContent += ${JSON.stringify(`
      /* macOS: native traffic lights handle close/min/max — hide custom buttons,
         center the title, and add left padding for the traffic light area. */
      #harmony-titlebar-controls { display: none !important; }
      #harmony-titlebar-brand {
        position: absolute;
        left: 50%;
        transform: translateX(-50%);
        padding-left: 0;
      }
      #harmony-titlebar {
        padding-left: 78px; /* space for native traffic lights */
      }
    `)};
  }
  document.head.appendChild(style);

  var bar = document.createElement('div');
  bar.innerHTML = ${JSON.stringify(TITLEBAR_HTML)};
  document.body.prepend(bar.firstElementChild);

  document.body.style.paddingTop = '32px';
  document.documentElement.style.setProperty('--harmony-titlebar-h', '32px');
  document.body.style.overflow = 'hidden';
  document.documentElement.style.overflow = 'hidden';

  if (!isMac) {
    var s = document.createElement('script');
    s.textContent = ${JSON.stringify(TITLEBAR_JS)};
    document.body.appendChild(s);
  }
})();
`;

  _injectTitlebarCode = INJECT_TITLEBAR;

  // ── Reconnect overlay: injected into server pages when socket disconnects ──
  // Injected into server pages — after 15s of socket disconnect,
  // tells main process to go to the offline/reconnect page.
  const INJECT_DISCONNECT_WATCHER = `
(function() {
  if (window._harmonyDisconnectWatcher) return;
  window._harmonyDisconnectWatcher = true;
  var timer = null;
  function onDisconnect() {
    if (timer) return;
    timer = setTimeout(function() {
      timer = null;
      if (window._harmonySocket && window._harmonySocket.connected) return;
      /* Navigate to offline page via IPC */
      if (window.harmony && window.harmony.goOffline) {
        window.harmony.goOffline();
      }
    }, 15000);
  }
  function onConnect() {
    if (timer) { clearTimeout(timer); timer = null; }
  }
  var poll = setInterval(function() {
    if (!window._harmonySocket) return;
    clearInterval(poll);
    window._harmonySocket.on('disconnect', onDisconnect);
    window._harmonySocket.on('connect', onConnect);
  }, 500);
})();
`;

  // ── Offline / splash page HTML (local, no server needed) ──
  const OFFLINE_PAGE = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Harmony</title>
<style>
body{margin:0;background:#09090b;color:#e4e4e7;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;gap:1.25rem}
@keyframes spin{0%{transform:rotate(0) scale(1);opacity:1}25%{transform:rotate(8deg) scale(1.05);opacity:.8}
50%{transform:rotate(0) scale(.95);opacity:.6}75%{transform:rotate(-8deg) scale(1.05);opacity:.8}100%{transform:rotate(0) scale(1);opacity:1}}
#logo{animation:spin 2.5s ease-in-out infinite}
button{padding:.5rem 1.25rem;border-radius:.5rem;border:1px solid #3f3f46;background:#27272a;color:#d4d4d8;font-size:.8rem;font-weight:500;cursor:pointer;transition:background .15s}
button:hover{background:#3f3f46;color:#fafafa}
#timer{font-size:.7rem;color:#52525b;margin-top:.25rem}
</style></head><body>
<img id="logo" src="data:image/png;base64,${logoB64}" alt="" width="64" height="64"/>
<div style="text-align:center">
<p id="title" style="font-size:1.1rem;font-weight:600">Connecting...</p>
<p id="sub" style="font-size:.8rem;color:#71717a;margin-top:.35rem">Reaching the Harmony server</p>
<p id="timer" style="display:none"></p>
</div>
<div id="btns" style="display:none">
<button id="change">Change Server</button>
</div>
<script>
var title=document.getElementById("title"),
    sub=document.getElementById("sub"),
    timer=document.getElementById("timer"),
    btns=document.getElementById("btns"),
    countdown=0,interval=null;

function showOffline(){
  title.textContent="Server Offline";
  sub.textContent="Could not connect to the Harmony server";
  btns.style.display="flex";
  startCountdown();
}
function startCountdown(){
  countdown=5;
  timer.style.display="block";
  timer.textContent="Retrying in "+countdown+"s...";
  if(interval)clearInterval(interval);
  interval=setInterval(function(){
    countdown--;
    if(countdown<=0){
      clearInterval(interval);interval=null;
      timer.textContent="Retrying...";
      doRetry();
    }else{
      timer.textContent="Retrying in "+countdown+"s...";
    }
  },1000);
}
function doRetry(){
  /* No flicker — keep showing "Server Offline", just update the timer */
  timer.textContent="Retrying...";
  if(window.harmony&&window.harmony.tryConnect){
    window.harmony.tryConnect().then(function(ok){
      if(!ok)startCountdown();
    }).catch(function(){startCountdown();});
  }else{startCountdown();}
}
window._harmonyShowOffline=showOffline;
document.getElementById("change").addEventListener("click",function(){
  if(interval)clearInterval(interval);interval=null;
  if(window.harmony&&window.harmony.loadPage)window.harmony.loadPage("login.html");
});
</script></body></html>`;

  const offlinePath = path.join(app.getPath('userData'), 'harmony-offline.html');
  _offlinePagePath = offlinePath;
  fs.writeFileSync(offlinePath, OFFLINE_PAGE);

  // ── Inject titlebar + disconnect watcher into loaded server pages ──
  let isOnSplash = false;
  mainWindow.webContents.on('dom-ready', () => {
    mainWindow.webContents.executeJavaScript(INJECT_TITLEBAR).catch(() => {});
    if (!isOnSplash) {
      mainWindow.webContents.executeJavaScript(INJECT_DISCONNECT_WATCHER).catch(() => {});
    }
  });

  // Send maximize state changes to renderer
  mainWindow.on('maximize', () => {
    mainWindow.webContents.send('window-maximized', true);
  });
  mainWindow.on('unmaximize', () => {
    mainWindow.webContents.send('window-maximized', false);
  });

  // ── Auth page detection helper ──
  function isServerAuthUrl(url) {
    const config = loadConfig();
    const serverBase = config ? config.server.replace(/\/+$/, '') : '';
    if (!serverBase) return false;
    const loginUrl = serverBase + '/login';
    const registerUrl = serverBase + '/register';
    return url === loginUrl || url.startsWith(loginUrl + '?') ||
           url === registerUrl || url.startsWith(registerUrl + '?');
  }

  // ── Intercept navigations to the server's login/register pages ──
  mainWindow.webContents.on('will-navigate', (event, url) => {
    if (loginInProgress) return;
    if (isServerAuthUrl(url)) {
      event.preventDefault();
      mainWindow.loadFile(path.join(__dirname, 'pages', 'login.html'));
      return;
    }
    const config = loadConfig();
    const serverBase = config ? config.server.replace(/\/+$/, '') : '';
    if (url.startsWith('file://') || (serverBase && url.startsWith(serverBase))) {
      return;
    }
    event.preventDefault();
    shell.openExternal(url);
  });

  mainWindow.webContents.on('did-navigate', (event, url) => {
    if (isServerAuthUrl(url)) {
      mainWindow.loadFile(path.join(__dirname, 'pages', 'login.html'));
      return;
    }
  });

  // ── Server reachability check — HTTP level (catches 502/503/etc) ──
  function checkServerReachable(serverUrl, callback) {
    let finished = false;
    const done = (ok) => { if (!finished) { finished = true; callback(ok); } };
    // Hard timeout — if nothing happens in 5s, server is down
    const hardTimer = setTimeout(() => done(false), 5000);
    net.fetch(serverUrl.replace(/\/+$/, '') + '/login', { method: 'HEAD' })
      .then((resp) => {
        clearTimeout(hardTimer);
        // 502, 503, 504 = backend is down behind a proxy
        done(resp.status < 500);
      })
      .catch(() => {
        clearTimeout(hardTimer);
        done(false);
      });
  }

  // ── Decide which page to show ──
  const config = loadConfig();
  if (!config || !config.server) {
    mainWindow.loadFile(path.join(__dirname, 'pages', 'setup.html'));
  } else {
    // Show splash instantly ("Connecting..."), check server in background.
    // If the server is up, loadURL happens from checkServerReachable.
    // If down, splash flips to offline with auto-retry countdown.
    isOnSplash = true;
    mainWindow.loadFile(offlinePath);

    checkServerReachable(config.server, (reachable) => {
      if (reachable) {
        isOnSplash = false;
        mainWindow.loadURL(config.server);
      } else {
        // Server is down — flip splash to offline + start auto-retry loop
        mainWindow.webContents.executeJavaScript(
          'if(window._harmonyShowOffline)window._harmonyShowOffline();'
        ).catch(() => {});
      }
    });
  }
}

// ── IPC handlers ──
const { ipcMain } = require('electron');

ipcMain.handle('save-server', (event, server) => {
  const url = server.replace(/\/+$/, '') + '/';
  saveConfig({ server: url });
  return url;
});

ipcMain.handle('get-config', () => {
  return loadConfig();
});

ipcMain.handle('try-connect', async () => {
  const config = loadConfig();
  if (!config || !config.server) return false;
  const url = config.server.replace(/\/+$/, '') + '/login';
  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 5000);
    const resp = await net.fetch(url, { method: 'HEAD', signal: controller.signal });
    clearTimeout(timer);
    if (resp.status >= 500) return false;
    // Server is up — navigate the window
    mainWindow.loadURL(config.server);
    return true;
  } catch {
    return false;
  }
});

// ── Native notifications (Electron main process) ──
const _activeNotifications = new Map(); // tag → Notification instance

ipcMain.handle('show-notification', (event, { tag, title, body }) => {
  try {
    // Close existing notification with same tag
    if (tag && _activeNotifications.has(tag)) {
      try { _activeNotifications.get(tag).close(); } catch (_e) {}
      _activeNotifications.delete(tag);
    }

    // Use the local logo as the notification icon
    const iconPath = path.join(__dirname, 'assets', 'frameless_logo.png');
    const opts = {
      title: title || 'Harmony',
      body: body || '',
    };
    if (fs.existsSync(iconPath)) opts.icon = iconPath;

    const n = new ElectronNotification(opts);
    let clicked = false;
    n.on('click', () => {
      if (clicked) return;
      clicked = true;
      if (mainWindow) {
        if (mainWindow.isMinimized()) mainWindow.restore();
        mainWindow.show();
        mainWindow.focus();
      }
      if (mainWindow && tag) {
        mainWindow.webContents.executeJavaScript(
          'if(window._harmonyOnNotificationClick)window._harmonyOnNotificationClick(' + JSON.stringify(tag) + ');'
        ).catch(() => {});
      }
    });
    n.on('close', () => {
      _activeNotifications.delete(tag || '');
    });
    n.show();
    if (tag) _activeNotifications.set(tag, n);
    return true;
  } catch (err) {
    console.error('[Notification] Failed to show:', err.message);
    return false;
  }
});

ipcMain.handle('close-notification', (event, tag) => {
  if (tag && _activeNotifications.has(tag)) {
    try { _activeNotifications.get(tag).close(); } catch (_e) {}
    _activeNotifications.delete(tag);
  }
});

ipcMain.handle('go-offline', () => {
  if (!_offlinePagePath) return;
  mainWindow.loadFile(_offlinePagePath);
  mainWindow.webContents.once('dom-ready', () => {
    if (_injectTitlebarCode) {
      mainWindow.webContents.executeJavaScript(_injectTitlebarCode).catch(() => {});
    }
    mainWindow.webContents.executeJavaScript(
      'if(window._harmonyShowOffline)window._harmonyShowOffline();'
    ).catch(() => {});
  });
});

ipcMain.handle('navigate-to-server', () => {
  const config = loadConfig();
  if (config && config.server) {
    mainWindow.loadURL(config.server);
  }
});

ipcMain.handle('load-page', (event, page) => {
  mainWindow.loadFile(path.join(__dirname, 'pages', page));
});

// Helper: POST JSON to server using Electron's net module (shares session cookies).
async function serverPost(endpoint, body) {
  const config = loadConfig();
  if (!config || !config.server) throw new Error('No server configured');
  const serverBase = config.server.replace(/\/+$/, '');
  const url = serverBase + endpoint;

  const resp = await net.fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Accept': 'application/json',
    },
    body: Object.entries(body).map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&'),
  });

  const text = await resp.text();
  try {
    return JSON.parse(text);
  } catch {
    throw new Error('Server returned invalid response');
  }
}

ipcMain.handle('do-login', async (event, { identifier, password }) => {
  const result = await serverPost('/login', { identifier, password });
  if (!result.ok) throw new Error(result.error || 'Login failed');
  if (result.action === '2fa') {
    return { action: '2fa', channel_token: result.channel_token, purpose: result.purpose || 'login' };
  }
  const config = loadConfig();
  if (config && config.server) {
    await mainWindow.loadURL(config.server);
  }
  return { action: 'redirect' };
});

ipcMain.handle('do-register', async (event, { username, email, password, confirm }) => {
  const result = await serverPost('/register', { username, email, password, confirm });
  if (!result.ok) throw new Error(result.error || 'Registration failed');
  if (result.action === '2fa') {
    return { action: '2fa', channel_token: result.channel_token, purpose: result.purpose || 'register' };
  }
  const config = loadConfig();
  if (config && config.server) {
    await mainWindow.loadURL(config.server);
  }
  return { action: 'redirect' };
});

ipcMain.handle('do-verify-2fa', async (event, { code, purpose }) => {
  const result = await serverPost('/api/auth/verify-2fa', { code, purpose });
  if (!result.ok) throw new Error(result.error || 'Verification failed');
  const config = loadConfig();
  if (config && config.server) {
    await mainWindow.loadURL(config.server);
  }
  return { action: 'redirect' };
});

// ── Window control IPC ──
ipcMain.handle('window-minimize', () => {
  if (mainWindow) mainWindow.minimize();
});
ipcMain.handle('window-maximize', () => {
  if (mainWindow) {
    if (mainWindow.isMaximized()) mainWindow.unmaximize();
    else mainWindow.maximize();
  }
});
ipcMain.handle('window-close', () => {
  if (mainWindow) mainWindow.close();
});
ipcMain.handle('window-is-maximized', () => {
  return mainWindow ? mainWindow.isMaximized() : false;
});

// ── Desktop capturer: expose screen/window sources for custom picker ──
ipcMain.handle('get-desktop-sources', async (_event, opts) => {
  try {
    const types = (opts && opts.types) || ['screen', 'window'];
    const thumbW = (opts && opts.thumbnailWidth) || 320;
    const thumbH = (opts && opts.thumbnailHeight) || 180;
    const sources = await desktopCapturer.getSources({
      types,
      thumbnailSize: { width: thumbW, height: thumbH },
    });
    return sources.map((s) => ({
      id: s.id,
      name: s.name,
      display_id: s.display_id,
      thumbnail: s.thumbnail ? s.thumbnail.toDataURL() : null,
    }));
  } catch {
    return [];
  }
});

// ── Linux system-audio capture via venmic ──
// Electron's audio:'loopback' doesn't work on Linux and manual PipeWire
// rerouting (pactl move / pw-link) gets undone by WirePlumber.
// venmic is the same native PipeWire addon Discord uses — it creates a
// virtual microphone that captures all system audio EXCEPT our own app,
// with proper session-manager integration so the routing sticks.
let _venmic = null;
let _venmicLinked = false;

function _getVenmic() {
  if (_venmic) return _venmic;
  if (process.platform !== 'linux') return null;
  try {
    const { PatchBay } = require('@vencord/venmic');
    if (!PatchBay.hasPipeWire()) return null;
    _venmic = new PatchBay();
    return _venmic;
  } catch { return null; }
}

ipcMain.handle('venmic-list', async () => {
  const pb = _getVenmic();
  if (!pb) return [];
  try { return pb.list([]); } catch { return []; }
});

function _cleanVenmicLinks() {
  // venmic's exclude filter only blocks DIRECT app→venmic links, but audio
  // processing chains (EasyEffects, PulseEffects) create indirect paths that
  // bypass the filter.  Disconnect those system/effect nodes from venmic so
  // only direct app outputs remain.
  try {
    const { execSync: ex } = require('child_process');
    const bad = [
      'ee_soe_output_level:output_FL', 'ee_soe_output_level:output_FR',
      'easyeffects_sink:monitor_FL', 'easyeffects_sink:monitor_FR',
      'alsa_output.pci-0000_78_00.6.analog-stereo:monitor_FL',
      'alsa_output.pci-0000_78_00.6.analog-stereo:monitor_FR',
    ];
    // Also disconnect any monitor source dynamically
    try {
      const defaultSink = ex('pactl get-default-sink', { encoding: 'utf-8', timeout: 2000 }).trim();
      if (defaultSink) {
        bad.push(defaultSink + ':monitor_FL', defaultSink + ':monitor_FR');
      }
    } catch {}
    const dst = ['vencord-screen-share:input_FL', 'vencord-screen-share:input_FR'];
    for (let attempt = 0; attempt < 3; attempt++) {
      for (const s of bad) {
        for (const d of dst) {
          try { ex(`pw-link -d "${s}" "${d}"`, { timeout: 1000 }); } catch {}
        }
      }
    }
  } catch {}
}

// ── macOS system-audio capture via audiotee ──
// audiotee uses Core Audio Taps (macOS 14.2+) with excludeProcesses to
// capture system audio minus Harmony's own output — same concept as venmic.
let _audiotee = null;
// Windows: loopback_capture.exe uses WASAPI PROCESS_LOOPBACK_MODE_EXCLUDE
let _winLoopbackProc = null;

ipcMain.handle('start-system-audio-capture', async (_event, opts) => {
  const _captureOpts = opts || {};
  // ── Linux: venmic ──
  if (process.platform === 'linux') {
    const pb = _getVenmic();
    if (!pb) return { ok: false, error: 'venmic_unavailable' };
    if (_venmicLinked) return { ok: true, already: true };
    try {
      pb.link({
        exclude: [{ 'application.process.binary': 'harmony' }],
        ignore_devices: true,
      });
      _venmicLinked = true;
      setTimeout(_cleanVenmicLinks, 200);
      return { ok: true, method: 'venmic' };
    } catch (e) {
      return { ok: false, error: String(e.message || e) };
    }
  }
  // ── macOS: audiotee ──
  if (process.platform === 'darwin') {
    if (_audiotee) return { ok: true, already: true, method: 'audiotee' };
    try {
      const { AudioTee } = require('audiotee');
      const { execSync: _ex } = require('child_process');
      // In packaged Electron, the binary is in app.asar.unpacked
      const auditeeBin = path.join(
        __dirname.replace('app.asar', 'app.asar.unpacked'),
        'node_modules', 'audiotee', 'bin', 'audiotee'
      );
      // Exclude main PID + the audio utility subprocess (audio.mojom.AudioService).
      // Core Audio associates output with the process that actually plays audio,
      // which is the audio helper, not the main Electron process.
      const excludePids = [process.pid];
      try {
        // ps -ww prevents line truncation on macOS
        const ps = require('child_process').execSync('ps -eo pid,args -ww', { encoding: 'utf-8', timeout: 2000 });
        for (const line of ps.split('\n')) {
          // Match the audio utility helper (case-insensitive)
          if (/harmony/i.test(line) && /audio\.mojom\.AudioService/i.test(line)) {
            const pid = parseInt(line.trim(), 10);
            if (pid > 0 && pid !== process.pid) excludePids.push(pid);
          }
        }
      } catch {}
      console.log('[audiotee] excluding PIDs:', excludePids);
      _audiotee = new AudioTee({
        excludeProcesses: excludePids,
        binaryPath: auditeeBin,
        chunkDurationMs: 40,
      });
      let chunkCount = 0;
      let _atLeftover = Buffer.alloc(0);
      _audiotee.on('data', (chunk) => {
        if (!mainWindow || mainWindow.isDestroyed() || !chunk || !chunk.data) return;
        chunkCount++;
        // Byte-align to float32 boundaries (4 bytes)
        const raw = chunk.data;
        const buf = _atLeftover.length > 0 ? Buffer.concat([_atLeftover, raw]) : raw;
        const aligned = buf.length - (buf.length % 4);
        if (aligned > 0) {
          mainWindow.webContents.send('system-audio-data',
            buf.buffer.slice(buf.byteOffset, buf.byteOffset + aligned));
        }
        _atLeftover = aligned < buf.length ? buf.subarray(aligned) : Buffer.alloc(0);
      });
      _audiotee.on('error', (err) => {
        console.error('[audiotee] error:', err);
      });
      _audiotee.on('log', (level, msg) => {
        console.log('[audiotee]', level, msg);
      });
      await _audiotee.start();
      console.log('[audiotee] started, pid excluded:', process.pid);
      // Log after 2s to confirm data is flowing
      setTimeout(() => console.log('[audiotee] chunks received so far:', chunkCount), 2000);
      return { ok: true, method: 'audiotee' };
    } catch (e) {
      console.error('[audiotee] failed to start:', e);
      _audiotee = null;
      return { ok: false, error: String(e.message || e) };
    }
  }
  // ── Windows: loopback_capture.exe with PROCESS_LOOPBACK_MODE_EXCLUDE ──
  if (process.platform === 'win32') {
    if (_winLoopbackProc) return { ok: true, already: true, method: 'win-loopback' };
    try {
      const { spawn: _spawn } = require('child_process');
      const binPath = path.join(
        __dirname.replace('app.asar', 'app.asar.unpacked'),
        'bin', 'loopback_capture.exe'
      );
      // Exclude our main PID — the Windows API excludes the entire process tree
      // (all child processes), so we only need the top-level PID.
      let _winCaptureInfo = { rate: 48000, channels: 2 };
      const targetRate = _captureOpts.sampleRate ? String(_captureOpts.sampleRate) : '0';
      _winLoopbackProc = _spawn(binPath, [String(process.pid), targetRate], {
        windowsHide: true,
      });
      // stdout delivers chunks of arbitrary size that may not align to
      // float32 sample boundaries (4 bytes).  Buffer leftover bytes.
      let _winLeftover = Buffer.alloc(0);
      _winLoopbackProc.stdout.on('data', (chunk) => {
        if (!mainWindow || mainWindow.isDestroyed()) return;
        const buf = _winLeftover.length > 0 ? Buffer.concat([_winLeftover, chunk]) : chunk;
        const aligned = buf.length - (buf.length % 4);
        if (aligned > 0) {
          mainWindow.webContents.send('system-audio-data',
            buf.buffer.slice(buf.byteOffset, buf.byteOffset + aligned));
        }
        _winLeftover = aligned < buf.length ? buf.subarray(aligned) : Buffer.alloc(0);
      });
      // Parse format info from stderr (e.g. "format: ch=2 rate=48000 bits=32")
      _winLoopbackProc.stderr.on('data', (d) => {
        const line = d.toString().trim();
        console.error('[win-loopback]', line);
        const m = line.match(/ch=(\d+)\s+rate=(\d+)/);
        if (m) {
          _winCaptureInfo.channels = parseInt(m[1], 10);
          _winCaptureInfo.rate = parseInt(m[2], 10);
          console.log('[win-loopback] detected format:', _winCaptureInfo);
          // Tell renderer the actual capture sample rate
          if (mainWindow && !mainWindow.isDestroyed()) {
            mainWindow.webContents.send('system-audio-format', _winCaptureInfo);
          }
        }
      });
      _winLoopbackProc.on('close', () => { _winLoopbackProc = null; });
      _winLoopbackProc.on('error', (e) => {
        console.error('[win-loopback] spawn error:', e);
        _winLoopbackProc = null;
      });
      console.log('[win-loopback] started, excluding PID tree:', process.pid);
      return { ok: true, method: 'win-loopback' };
    } catch (e) {
      console.error('[win-loopback] failed:', e);
      _winLoopbackProc = null;
      return { ok: false, error: String(e.message || e) };
    }
  }
  return { ok: false, error: 'unsupported_platform' };
});

ipcMain.handle('stop-system-audio-capture', async () => {
  // Linux: venmic
  if (_venmicLinked) {
    try { const pb = _getVenmic(); if (pb) pb.unlink(); } catch {}
    _venmicLinked = false;
  }
  // macOS: audiotee
  if (_audiotee) {
    try { await _audiotee.stop(); } catch {}
    _audiotee = null;
  }
  // Windows: loopback_capture.exe
  if (_winLoopbackProc) {
    try { _winLoopbackProc.kill(); } catch {}
    _winLoopbackProc = null;
  }
  return { ok: true };
});

// ── App lifecycle ──
app.setName('Harmony');
if (process.platform === 'linux') {
  app.setDesktopName('Harmony');
}
// Platform-specific Chromium feature flags
if (process.platform === 'linux') {
  app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer,PulseaudioLoopbackForScreenShare,VaapiVideoDecoder');
  app.commandLine.appendSwitch('disable-features', 'UseChromeOSDirectVideoDecoder');
} else if (process.platform === 'darwin') {
  // ScreenCaptureKit captures system audio on macOS 13+.
  // Both flags are required:
  //   MacLoopbackAudioForScreenShare — enables the loopback audio feature
  //   MacSckSystemAudioLoopbackOverride — forces ScreenCaptureKit path
  // Without BOTH, audio:'loopback' silently returns no audio track.
  // Self-exclusion of Harmony's audio is NOT supported by Electron.
  app.commandLine.appendSwitch('enable-features', 'MacLoopbackAudioForScreenShare,MacSckSystemAudioLoopbackOverride');
} else {
  // Windows: audio:'loopback' uses WASAPI natively, no extra flags needed
  app.commandLine.appendSwitch('enable-features', '');
}
app.commandLine.appendSwitch('enable-gpu-rasterization');
app.commandLine.appendSwitch('enable-zero-copy');
app.commandLine.appendSwitch('ignore-certificate-errors');
app.commandLine.appendSwitch('disable-http-cache', 'false');
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
app.commandLine.appendSwitch('disable-background-timer-throttling');
app.commandLine.appendSwitch('disable-renderer-backgrounding');
app.commandLine.appendSwitch('disable-backgrounding-occluded-windows');

// ── Auto-updater ──
// Reads a plain-text version.txt from the repo:
//   VERSION=(1.0.0)
//   LINUX=(https://...)
//   MACOS=(https://...)
//   WINDOWS=(https://...)
const UPDATE_VERSION_URL = 'https://gitssy.nakildias.com/Nakildias/HarmonyApp/raw/master/version.txt';
let _updateAssetUrl = null;
let _updateVersion = null;
let _updateDownloading = false;

function _compareVersions(a, b) {
  const pa = a.split('.').map(Number);
  const pb = b.split('.').map(Number);
  for (let i = 0; i < 3; i++) {
    const va = pa[i] || 0, vb = pb[i] || 0;
    if (va > vb) return 1;
    if (va < vb) return -1;
  }
  return 0;
}

function _parseVersionTxt(text) {
  const out = {};
  for (const line of text.split('\n')) {
    const m = line.match(/^\s*(\w+)\s*=\s*\((.+?)\)\s*$/);
    if (m) out[m[1].toUpperCase()] = m[2].trim();
  }
  return out;
}

async function checkForUpdates() {
  if (!mainWindow || mainWindow.isDestroyed()) return;
  const currentVersion = app.getVersion();
  try {
    const resp = await net.fetch(UPDATE_VERSION_URL);
    if (!resp.ok) return;
    const text = await resp.text();
    const info = _parseVersionTxt(text);
    const remoteVersion = (info.VERSION || '').replace(/^v/, '');
    if (!remoteVersion || _compareVersions(remoteVersion, currentVersion) <= 0) return;

    let url = null;
    if (process.platform === 'linux') url = info.LINUX;
    else if (process.platform === 'darwin') url = info.MACOS;
    else if (process.platform === 'win32') url = info.WINDOWS;
    if (!url) return;

    _updateAssetUrl = url;
    _updateVersion = remoteVersion;

    mainWindow.webContents.send('update-available', {
      currentVersion,
      newVersion: remoteVersion,
    });
    mainWindow.webContents.executeJavaScript(INJECT_UPDATE_UI).catch(() => {});
  } catch (e) {
    console.error('[updater] check failed:', e.message || e);
  }
}

async function downloadAndApplyUpdate() {
  if (_updateDownloading || !_updateAssetUrl || !_updateVersion) return;
  _updateDownloading = true;
  const os = require('os');
  const { execFile, spawn: _sp } = require('child_process');
  try {
    const resp = await net.fetch(_updateAssetUrl, { redirect: 'follow' });
    if (!resp.ok) throw new Error('Download failed: ' + resp.status);
    const total = parseInt(resp.headers.get('content-length') || '0', 10);
    const reader = resp.body.getReader();
    const chunks = [];
    let received = 0;
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(Buffer.from(value));
      received += value.length;
      if (total > 0 && mainWindow && !mainWindow.isDestroyed()) {
        mainWindow.webContents.send('update-progress', Math.round((received / total) * 100));
      }
    }
    const data = Buffer.concat(chunks);

    // ── Apply per platform ──
    if (process.platform === 'linux') {
      const appImagePath = process.env.APPIMAGE;
      if (!appImagePath) throw new Error('Not running as AppImage');
      // Replace the current AppImage in-place so the desktop entry
      // and any shortcuts keep working.
      const tmpPath = appImagePath + '.tmp';
      fs.writeFileSync(tmpPath, data);
      fs.chmodSync(tmpPath, 0o755);
      fs.renameSync(tmpPath, appImagePath);
      // Relaunch the same path (now the new version)
      const child = _sp(appImagePath, [], { detached: true, stdio: 'ignore' });
      child.unref();
      app.quit();
    } else if (process.platform === 'darwin') {
      const tmpZip = path.join(os.tmpdir(), 'Harmony-update.zip');
      const tmpExtract = path.join(os.tmpdir(), 'Harmony-update-extract');
      fs.writeFileSync(tmpZip, data);
      // Remove old extract dir if exists
      fs.rmSync(tmpExtract, { recursive: true, force: true });
      fs.mkdirSync(tmpExtract, { recursive: true });
      // Extract with ditto (preserves macOS attributes)
      require('child_process').execFileSync('ditto', ['-xk', tmpZip, tmpExtract]);
      // Find the .app in extracted contents
      const appDir = path.resolve(process.execPath, '..', '..', '..');
      const appName = path.basename(appDir);
      const newApp = path.join(tmpExtract, appName);
      if (fs.existsSync(newApp)) {
        const oldBackup = appDir + '.old';
        // Use shell rm -rf for cleanup — fs.rmSync chokes on asar files
        const _execRm = (p) => { try { require('child_process').execFileSync('rm', ['-rf', p]); } catch {} };
        _execRm(oldBackup);
        fs.renameSync(appDir, oldBackup);
        fs.renameSync(newApp, appDir);
        try { require('child_process').execFileSync('xattr', ['-cr', appDir]); } catch {}
        const child = _sp('open', ['-n', appDir], { detached: true, stdio: 'ignore' });
        child.unref();
        // Clean up in background — don't block or error
        _execRm(oldBackup);
      }
      try { require('child_process').execFileSync('rm', ['-rf', tmpZip]); } catch {}
      try { require('child_process').execFileSync('rm', ['-rf', tmpExtract]); } catch {}
      app.quit();
    } else if (process.platform === 'win32') {
      const tmpExe = path.join(os.tmpdir(), `Harmony-Setup-${_updateVersion}.exe`);
      fs.writeFileSync(tmpExe, data);
      const child = _sp(tmpExe, ['/S'], { detached: true, stdio: 'ignore', windowsHide: true });
      child.unref();
      app.quit();
    }
  } catch (e) {
    console.error('[updater] download/apply failed:', e.message || e);
    _updateDownloading = false;
    if (mainWindow && !mainWindow.isDestroyed()) {
      mainWindow.webContents.send('update-error', e.message || String(e));
    }
  }
}

ipcMain.handle('apply-update', () => { downloadAndApplyUpdate(); });
ipcMain.handle('dismiss-update', () => { _updateAssetUrl = null; _updateVersion = null; });

const INJECT_UPDATE_UI = `
(function() {
  if (document.getElementById('harmony-update-overlay')) return;
  var h = window.harmony;
  if (!h) return;
  var ov = document.createElement('div');
  ov.id = 'harmony-update-overlay';
  ov.style.cssText = 'position:fixed;inset:0;z-index:999999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.7);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;';
  var box = document.createElement('div');
  box.style.cssText = 'background:#1e1f22;border:1px solid #333;border-radius:12px;padding:24px;max-width:400px;width:90%;color:#e0e0e0;text-align:center;';
  var title = document.createElement('h3');
  title.textContent = 'Update Available';
  title.style.cssText = 'margin:0 0 8px;font-size:16px;color:white;';
  var info = document.createElement('p');
  info.style.cssText = 'margin:0 0 16px;font-size:13px;color:#a0a0a0;';
  var bar = document.createElement('div');
  bar.id = 'harmony-update-bar';
  bar.style.cssText = 'display:none;height:6px;border-radius:3px;background:#333;margin:0 0 16px;overflow:hidden;';
  var fill = document.createElement('div');
  fill.id = 'harmony-update-fill';
  fill.style.cssText = 'height:100%;width:0%;background:#5865f2;border-radius:3px;transition:width 0.2s;';
  bar.appendChild(fill);
  var btns = document.createElement('div');
  btns.style.cssText = 'display:flex;gap:8px;justify-content:center;';
  var btnUpdate = document.createElement('button');
  btnUpdate.textContent = 'Update Now';
  btnUpdate.style.cssText = 'padding:8px 20px;border:none;border-radius:8px;background:#5865f2;color:white;font-size:13px;font-weight:600;cursor:pointer;';
  var btnLater = document.createElement('button');
  btnLater.textContent = 'Later';
  btnLater.style.cssText = 'padding:8px 20px;border:1px solid #555;border-radius:8px;background:transparent;color:#a0a0a0;font-size:13px;cursor:pointer;';
  btns.appendChild(btnUpdate);
  btns.appendChild(btnLater);
  box.appendChild(title);
  box.appendChild(info);
  box.appendChild(bar);
  box.appendChild(btns);
  ov.appendChild(box);
  document.body.appendChild(ov);

  h.onUpdateAvailable(function(d) {
    info.textContent = 'v' + d.currentVersion + ' → v' + d.newVersion;
  });
  h.onUpdateProgress(function(pct) {
    bar.style.display = 'block';
    fill.style.width = pct + '%';
    btnUpdate.disabled = true;
    btnUpdate.textContent = 'Downloading ' + pct + '%';
  });
  h.onUpdateError(function(msg) {
    info.textContent = 'Update failed: ' + msg;
    btnUpdate.disabled = false;
    btnUpdate.textContent = 'Retry';
    bar.style.display = 'none';
  });
  btnUpdate.addEventListener('click', function() {
    btnUpdate.disabled = true;
    btnUpdate.textContent = 'Starting...';
    h.applyUpdate();
  });
  btnLater.addEventListener('click', function() {
    ov.remove();
    h.dismissUpdate();
  });
})();
`;

app.whenReady().then(() => {
  createWindow();
  setTimeout(checkForUpdates, 5000);
});

app.on('window-all-closed', () => {
  // Clean up audio capture before quitting
  if (_venmicLinked) {
    try { const pb = _getVenmic(); if (pb) pb.unlink(); } catch {}
    _venmicLinked = false;
  }
  if (_audiotee) {
    try { _audiotee.stop(); } catch {}
    _audiotee = null;
  }
  if (_winLoopbackProc) {
    try { _winLoopbackProc.kill(); } catch {}
    _winLoopbackProc = null;
  }
  app.quit();
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
