// Electron entry point. Creates the single BrowserWindow, wires all IPC
// module registrars, and keeps a single running instance (second launch
// focuses the existing window instead of opening a new one).
//
// Loads the renderer from the Vite dev server in dev (port 1420, matches
// the old Tauri devUrl so nothing in the renderer code needs to change)
// and from the built dist in packaged mode.

import { app, BrowserWindow, desktopCapturer, Menu, session } from 'electron';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { register as registerAudioLoopback } from './modules/audio-loopback';
import { register as registerAutostart } from './modules/autostart';
import { register as registerFsScoped } from './modules/fs-scoped';
import { register as registerNotifications } from './modules/notifications';
import { register as registerScreenAudio } from './modules/screen-audio';
import {
  consumePendingShareSourceId,
  register as registerScreenSources,
} from './modules/screen-sources';
import { register as registerSecureStore } from './modules/secure-store';
import { register as registerShortcuts } from './modules/shortcuts';
import { register as registerSql } from './modules/sql';
import { register as registerTray } from './modules/tray';
import { register as registerUpdater } from './modules/updater';
import { register as registerWindowFullscreen } from './modules/window-fullscreen';
import { attach as attachWindowState, loadState } from './window-state';

// __dirname in an ESM main-process bundle resolves to the out/main dir
// after electron-vite builds. Use a safe helper that works in both CJS
// (electron-vite main target defaults to CJS) and ESM contexts.
const __filenameSafe =
  typeof __filename === 'string' ? __filename : fileURLToPath(import.meta.url);
const __dirnameSafe = path.dirname(__filenameSafe);

const DEV_URL = 'http://localhost:1420';
const WINDOW_STATE_FILE = 'window-state.json';

// Pin userData FIRST — before any other Electron call that might cache a
// productName-derived path. The 0.17.0 release saw users get logged out
// after upgrading from 0.16.x: the most likely culprit was an internal
// path resolution kicking off the moment `setName('Netralax')` ran, so
// 0.17.1 swaps the order so the explicit override wins regardless of
// what setName triggers internally. The literal 'ChatApp' here is the
// pre-rename product folder — installed users' SQLite, secrets, sounds,
// IndexedDB all live there and we never want to leave them stranded by
// a future rebrand.
app.setPath('userData', path.join(app.getPath('appData'), 'ChatApp'));

// App branding. productName in package.json drives the packaged exe name
// (Netralax.exe) and electron-builder installer title. setName + the
// AppUserModelId cover the live process: window title fallback, Windows
// taskbar grouping, notification source attribution.
app.setName('Netralax');
if (process.platform === 'win32') {
  app.setAppUserModelId('cloud.netralax.desktop');
}

// Run dev side-by-side with the installed packaged build by isolating the
// renderer profile / secret-store / SQLite / IndexedDB / localStorage in
// a separate userData dir. Without this both share `%APPDATA%\ChatApp`,
// the single-instance lock fires, and `pnpm dev` exits immediately while
// the installed prod app holds the lock. Must run BEFORE the lock check
// below + before any other module reads `app.getPath('userData')`.
if (!app.isPackaged) {
  app.setPath('userData', app.getPath('userData') + '-Dev');
}

// Startup diagnostics — the 0.17.0 logout regression was hard to debug
// because we had no record of the actual resolved paths. With this log
// any future user can paste their main-process output and we can tell
// at a glance whether userData ended up where we intended.
console.log(
  '[main] resolved paths',
  JSON.stringify({
    appName: app.getName(),
    appData: app.getPath('appData'),
    userData: app.getPath('userData'),
    isPackaged: app.isPackaged,
    platform: process.platform,
  }),
);

let mainWindow: BrowserWindow | null = null;

function resolvePreloadPath(): string {
  // electron-vite names the preload bundle after the entry filename, so
  // electron/preload.ts → out/preload/preload.mjs. ESM preload works in
  // modern Electron when sandbox: false (which we have — preload needs
  // Node APIs for libsodium init etc.). The previous `index.cjs` lookup
  // was a stale leftover that silently never loaded — every IPC call
  // (secret store, autostart, native notifications, screen sources,
  // pending-share-source) returned undefined and any .x access threw.
  return path.join(__dirnameSafe, '..', 'preload', 'preload.mjs');
}

function resolveRendererIndex(): string {
  return path.join(__dirnameSafe, '..', 'renderer', 'index.html');
}

function resolveIconPath(): string {
  const packaged = path.join(process.resourcesPath || '', 'icon.ico');
  const dev = path.join(app.getAppPath(), 'resources', 'icon.ico');
  return app.isPackaged ? packaged : dev;
}

async function createWindow(): Promise<BrowserWindow> {
  const state = await loadState(WINDOW_STATE_FILE);

  const win = new BrowserWindow({
    title: app.isPackaged ? 'Netralax' : 'Netralax (Dev)',
    width: state.width,
    height: state.height,
    ...(state.x !== undefined ? { x: state.x } : {}),
    ...(state.y !== undefined ? { y: state.y } : {}),
    minWidth: 800,
    minHeight: 600,
    resizable: true,
    icon: resolveIconPath(),
    show: false,
    // Discord-style: no menu bar at all (no File/Edit/View/Window/Help).
    // Belt: autoHideMenuBar hides it visually; suspenders: the global
    // Menu.setApplicationMenu(null) below removes it entirely so Alt
    // can't reveal it either.
    autoHideMenuBar: true,
    webPreferences: {
      contextIsolation: true,
      nodeIntegration: false,
      // sandbox: false — preload needs Node APIs (libsodium init etc.)
      sandbox: false,
      preload: resolvePreloadPath(),
      // Strip DevTools from release builds. Disables openDevTools(),
      // F12, Ctrl+Shift+I, and the right-click Inspect entry for end
      // users. Packaged = release; unpackaged = `electron-vite dev`,
      // where we still want the inspector for local debugging.
      devTools: !app.isPackaged,
    },
  });

  if (state.maximized) win.maximize();

  win.once('ready-to-show', () => win.show());

  attachWindowState(win, WINDOW_STATE_FILE);

  if (!app.isPackaged) {
    await win.loadURL(DEV_URL);
  } else {
    await win.loadFile(resolveRendererIndex());
  }

  win.on('closed', () => {
    if (mainWindow === win) mainWindow = null;
  });

  return win;
}

function configureDisplayMediaHandler(): void {
  // Our custom picker runs in the renderer; when LiveKit (or anything
  // else) calls navigator.mediaDevices.getDisplayMedia with a specific
  // chromeMediaSourceId constraint, Chromium defers to the handler we
  // install here.
  //
  // Two paths:
  //  1. Discord-style picker case — the renderer set a pending source id
  //     via SCREEN_SET_PENDING_SOURCE before calling getDisplayMedia. We
  //     resolve that id back to a desktopCapturer Source and pass it as
  //     the video grant. This is what makes the user's tile choice
  //     actually take effect.
  //  2. Fallback — no pending id (e.g. a stray getDisplayMedia from
  //     another code path). Grant whatever the renderer's constraint
  //     selected by passing the frame back, matching the legacy v0.11.x
  //     behavior so we don't break anything off the picker code path.
  //
  // Audio: 'loopback' captures all system audio. We tried
  // 'loopbackWithMute' (which excludes this app's own audio output from
  // the capture) but it ALSO mutes the renderer's audio playback
  // locally — so the user couldn't hear remote peers during a share
  // and the stream had no audio either when the only thing playing was
  // the muted call audio. There is no clean Electron equivalent of
  // Tauri's EXCLUDE_TARGET_PROCESS_TREE that affects only the capture
  // and not local playback. Trade-off chosen here: keep local audio
  // working ('loopback'), let the user opt into the JS-level auto-duck
  // (`screenShareSettings.duckRemoteAudioWhileSharing`) when they
  // actually need the echo prevention. Default: off.
  // https://www.electronjs.org/docs/latest/api/session#sessetdisplaymediarequesthandlerhandler-opts
  session.defaultSession.setDisplayMediaRequestHandler(
    (request, callback) => {
      const pendingId = consumePendingShareSourceId();
      if (pendingId) {
        void desktopCapturer
          .getSources({ types: ['screen', 'window'] })
          .then((sources) => {
            const source = sources.find((s) => s.id === pendingId);
            if (source) {
              callback({ video: source, audio: 'loopback' });
            } else {
              // Source vanished between picker confirm and grant (window
              // closed, monitor unplugged). Fall back to the frame grant
              // so getDisplayMedia doesn't hang the renderer.
              callback({
                video: request.frame as unknown as Electron.WebFrameMain,
                audio: 'loopback',
              });
            }
          })
          .catch(() => {
            callback({
              video: request.frame as unknown as Electron.WebFrameMain,
              audio: 'loopback',
            });
          });
        return;
      }
      callback({
        video: request.frame as unknown as Electron.WebFrameMain,
        audio: 'loopback',
      });
    },
    { useSystemPicker: false },
  );
}

// Single-instance lock — second launch focuses existing window instead
// of spawning a duplicate process.
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
  app.quit();
} else {
  app.on('second-instance', () => {
    if (!mainWindow) return;
    if (mainWindow.isMinimized()) mainWindow.restore();
    mainWindow.show();
    mainWindow.focus();
  });

  void app.whenReady().then(async () => {
    // Strip the default application menu globally — we don't ship any
    // File/Edit/View/Window/Help entries (Discord-style chrome). Must run
    // BEFORE the first BrowserWindow is created; setting it later causes
    // the menu to flash for a frame on Windows.
    Menu.setApplicationMenu(null);
    configureDisplayMediaHandler();
    mainWindow = await createWindow();

    // Stateless registrars first.
    registerScreenSources();
    registerScreenAudio();
    registerNotifications();
    registerSecureStore();
    registerFsScoped();
    registerSql();
    registerAutostart();

    // Window-dependent registrars — only call once mainWindow exists so
    // event emitters have somewhere to send.
    registerShortcuts(mainWindow);
    registerTray(mainWindow);
    registerUpdater(mainWindow);
    registerWindowFullscreen(mainWindow);
    registerAudioLoopback(mainWindow);
  });

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

  app.on('activate', () => {
    if (!mainWindow) {
      void createWindow().then((w) => {
        mainWindow = w;
      });
    }
  });
}
