import path from 'node:path';

import { app, BrowserWindow, screen } from 'electron';

import { windowChromeTitle, type AppWindowKind } from '../../shared/appBranding';
import { ipcChannels } from '../../shared/ipc/contracts';

import { safeConsoleError } from '../safeConsole';

import { getBootSplashWindow } from './bootWindow';
import { loadBrandingWindowIcon } from './brandingIcon';

export type WindowKind =
  | 'editor'
  | 'presentation'
  | 'control'
  | 'sceneDescription'
  | 'materials'
  | 'npcsEditor'
  | 'sceneEditor'
  | 'tokenPathEditor'
  | 'npcs';

/** Окна, которые реально слушают session.stateChanged (редактор синхронизируется через invoke). */
export const SESSION_STATE_WINDOW_KINDS: readonly WindowKind[] = [
  'presentation',
  'control',
  'materials',
  'npcs',
  'npcsEditor',
  'sceneEditor',
  'tokenPathEditor',
] as const;

const windows = new Map<WindowKind, BrowserWindow>();

/** Язык заголовков окон (из редактора); иначе `app.getLocale()`. */
let chromeLocaleTagOverride: string | null = null;

function resolveChromeLocaleTag(): string {
  return chromeLocaleTagOverride ?? app.getLocale();
}

let appQuitting = false;
/** Защита от каскада close(control) ↔ close(presentation). */
let closingPlaySession = false;
let pendingSceneDescriptionHtml = '';

/** Окно материалов — только колонка списка. */
const MATERIALS_WINDOW_WIDTH = 300;
const MATERIALS_WINDOW_HEIGHT = 720;

/** Редактор НПС — как основной редактор, шире инспектор. */
const NPCS_EDITOR_WINDOW_WIDTH = 1400;
const NPCS_EDITOR_WINDOW_HEIGHT = 860;

/** Пульт НПС: деталь слева + список справа. */
const NPCS_WINDOW_WIDTH = 720;
const NPCS_WINDOW_HEIGHT = 720;

/** Учитываем окна, которые уже уничтожены при каскадном закрытии (родитель → дочернее). */
function broadcastMultiWindowStateChanged(open: boolean): void {
  for (const w of BrowserWindow.getAllWindows()) {
    if (w.isDestroyed()) continue;
    if (w.webContents.isDestroyed()) continue;
    try {
      w.webContents.send(ipcChannels.windows.multiWindowStateChanged, { open });
    } catch {
      /* окно могло закрыться между проверкой и send */
    }
  }
}

export function getPresentationContentSize(): { width: number; height: number } | null {
  const pres = windows.get('presentation');
  if (!pres || pres.isDestroyed()) return null;
  const [width, height] = pres.getContentSize();
  if (width <= 0 || height <= 0) return null;
  return { width, height };
}

function broadcastPresentationContentSize(): void {
  const size = getPresentationContentSize();
  if (!size) return;
  for (const w of BrowserWindow.getAllWindows()) {
    if (w.isDestroyed() || w.webContents.isDestroyed()) continue;
    try {
      w.webContents.send(ipcChannels.windows.presentationContentSizeChanged, size);
    } catch {
      /* ignore */
    }
  }
}

function bindPresentationContentSizeTracking(win: BrowserWindow): void {
  const emit = () => broadcastPresentationContentSize();
  win.on('resize', emit);
  win.on('enter-full-screen', emit);
  win.on('leave-full-screen', emit);
  win.webContents.once('did-finish-load', emit);
}

function sendSceneDescriptionContent(win: BrowserWindow, html: string): void {
  if (win.isDestroyed() || win.webContents.isDestroyed()) return;
  try {
    win.webContents.send(ipcChannels.windows.sceneDescriptionContent, { html });
  } catch {
    /* ignore */
  }
}

/** Разрешает реальное закрытие окна редактора (выход из приложения). */
export function markAppQuitting(): void {
  appQuitting = true;
}

/** Точечная рассылка в известные окна приложения (без splash / чужих BrowserWindow). */
export function sendToAppWindows(
  channel: string,
  payload: unknown,
  kinds: readonly WindowKind[] = SESSION_STATE_WINDOW_KINDS,
): void {
  for (const kind of kinds) {
    const win = windows.get(kind);
    if (!win || win.isDestroyed() || win.webContents.isDestroyed()) continue;
    try {
      win.webContents.send(channel, payload);
    } catch {
      /* окно могло закрыться между проверкой и send */
    }
  }
}

function applyWindowChromeTitle(win: BrowserWindow, kind: WindowKind): void {
  if (win.isDestroyed()) return;
  win.setTitle(windowChromeTitle(kind as AppWindowKind, resolveChromeLocaleTag()));
}

export function syncAllWindowChromeTitles(localeTag: string): void {
  chromeLocaleTagOverride = localeTag.trim() || null;
  for (const [kind, win] of windows.entries()) {
    applyWindowChromeTitle(win, kind);
  }
}

function bindWindowChromeTitle(win: BrowserWindow, kind: WindowKind): void {
  const apply = () => applyWindowChromeTitle(win, kind);
  apply();
  win.webContents.on('page-title-updated', (event) => {
    event.preventDefault();
    apply();
  });
  win.webContents.on('did-finish-load', () => {
    apply();
  });
}

/** Закрыть окна сессии, кроме редактора и его дочерних окон. */
function closePlaySessionAuxiliaryWindows(): void {
  closeSceneDescriptionWindow();
  closeMaterialsWindow();
  closeNpcsWindow();
}

function closePresentationWindow(): void {
  const pres = windows.get('presentation');
  if (pres && !pres.isDestroyed()) {
    pres.close();
  }
}

function closeControlWindow(): void {
  const ctrl = windows.get('control');
  if (ctrl && !ctrl.isDestroyed()) {
    ctrl.close();
  }
}

function quitAppFromEditorClose(): void {
  markAppQuitting();
  app.quit();
}

function isDev() {
  return process.env.NODE_ENV === 'development' || process.env.VITE_DEV_SERVER_URL !== undefined;
}

/** Вне dev-сервера на Windows с `loadFile` + preload иногда ломается sandbox; оставляем изоляцию через preload/contextBridge. */
function shouldUseRendererSandbox(): boolean {
  if (process.env.VITE_DEV_SERVER_URL) return true;
  return process.platform !== 'win32';
}

function getRendererHtmlPath(kind: WindowKind): string {
  return path.join(app.getAppPath(), 'dist', 'renderer', `${kind}.html`);
}

function pageNameForKind(kind: WindowKind): string {
  switch (kind) {
    case 'editor':
      return 'editor.html';
    case 'presentation':
      return 'presentation.html';
    case 'control':
      return 'control.html';
    case 'sceneDescription':
      return 'sceneDescription.html';
    case 'materials':
      return 'materials.html';
    case 'npcsEditor':
      return 'npcsEditor.html';
    case 'sceneEditor':
      return 'sceneEditor.html';
    case 'tokenPathEditor':
      return 'tokenPathEditor.html';
    case 'npcs':
      return 'npcs.html';
  }
}

/**
 * В production `loadURL(file://…)` на Windows с asar иногда даёт чёрный экран;
 * `loadFile` корректно открывает HTML из asar и на Windows, и на macOS.
 */
function loadWindowPage(win: BrowserWindow, kind: WindowKind): void {
  const dev = process.env.VITE_DEV_SERVER_URL;
  if (dev) {
    void win.loadURL(new URL(pageNameForKind(kind), dev).toString());
    return;
  }
  void win.loadFile(getRendererHtmlPath(kind));
}

function getPreloadPath(): string {
  return path.join(app.getAppPath(), 'dist', 'preload', 'index.cjs');
}

/** macOS: в Dock — тот же растр, что и у окон (ICO/PNG из brandingIcon). */
export function applyDockIconIfNeeded(): void {
  if (process.platform !== 'darwin' || !app.dock) return;
  const icon = loadBrandingWindowIcon();
  if (!icon || icon.isEmpty()) return;
  try {
    app.dock.setIcon(icon);
  } catch {
    /* ignore */
  }
}

type CreateWindowOpts = {
  /** Дочернее окно (например пульт) держится над родителем (экран просмотра). */
  parent?: BrowserWindow;
  /** Не показывать окно до явного `show()` (экран загрузки / прогрев НПС). */
  deferVisibility?: boolean;
};

/**
 * Только `ready-to-show` на части систем (первый холодный старт Windows) не приходит вовремя —
 * окно остаётся с `show: false` и кажется «зависшим». Дублируем показ по `did-finish-load` и таймауту.
 */
function ensureWindowBecomesVisible(win: BrowserWindow): void {
  let shown = false;
  const showOnce = (): void => {
    if (shown) return;
    if (win.isDestroyed()) return;
    shown = true;
    win.show();
  };

  win.once('ready-to-show', showOnce);
  win.webContents.once('did-finish-load', () => {
    showOnce();
  });
  const safetyTimer = setTimeout(showOnce, 8000);
  win.once('closed', () => {
    clearTimeout(safetyTimer);
  });
}

function windowSizeForKind(kind: WindowKind): { width: number; height: number } {
  if (kind === 'editor') return { width: 1280, height: 800 };
  if (kind === 'control') return { width: 1200, height: 800 };
  if (kind === 'sceneDescription') return { width: 720, height: 640 };
  if (kind === 'materials') return { width: MATERIALS_WINDOW_WIDTH, height: MATERIALS_WINDOW_HEIGHT };
  if (kind === 'npcsEditor') return { width: NPCS_EDITOR_WINDOW_WIDTH, height: NPCS_EDITOR_WINDOW_HEIGHT };
  if (kind === 'sceneEditor') return { width: 1280, height: 800 };
  if (kind === 'tokenPathEditor') return { width: 1100, height: 760 };
  if (kind === 'npcs') return { width: NPCS_WINDOW_WIDTH, height: NPCS_WINDOW_HEIGHT };
  return { width: 1280, height: 800 };
}

function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow {
  const deferShow = opts?.deferVisibility === true;
  const icon = loadBrandingWindowIcon();
  const size = windowSizeForKind(kind);
  const win = new BrowserWindow({
    width: size.width,
    height: size.height,
    autoHideMenuBar: true,
    ...(kind === 'sceneDescription'
      ? {
          minWidth: 520,
          minHeight: 420,
        }
      : {}),
    ...(kind === 'materials'
      ? {
          width: MATERIALS_WINDOW_WIDTH,
          height: MATERIALS_WINDOW_HEIGHT,
          minWidth: 260,
          maxWidth: 360,
          minHeight: 480,
        }
      : {}),
    ...(kind === 'npcsEditor'
      ? {
          width: NPCS_EDITOR_WINDOW_WIDTH,
          height: NPCS_EDITOR_WINDOW_HEIGHT,
          minWidth: 1100,
          minHeight: 640,
        }
      : {}),
    ...(kind === 'sceneEditor'
      ? {
          width: 1280,
          height: 800,
          minWidth: 960,
          minHeight: 600,
        }
      : {}),
    ...(kind === 'tokenPathEditor'
      ? {
          width: 1100,
          height: 760,
          minWidth: 900,
          minHeight: 560,
        }
      : {}),
    ...(kind === 'npcs'
      ? {
          width: NPCS_WINDOW_WIDTH,
          height: NPCS_WINDOW_HEIGHT,
          minWidth: 560,
          maxWidth: 900,
          minHeight: 480,
        }
      : {}),
    show: false,
    backgroundColor: '#09090B',
    ...(icon ? { icon } : {}),
    ...(opts?.parent ? { parent: opts.parent } : {}),
    webPreferences: {
      contextIsolation: true,
      sandbox: shouldUseRendererSandbox(),
      nodeIntegration: false,
      devTools: isDev() || process.env.DND_OPEN_DEVTOOLS === '1',
      preload: getPreloadPath(),
      autoplayPolicy: 'no-user-gesture-required',
      // file:// + бандл Vite: без этого на Windows часто не грузятся чанки; http:// (dev server) оставляем строгим.
      webSecurity: Boolean(process.env.VITE_DEV_SERVER_URL),
    },
  });
  if (icon) {
    try {
      win.setIcon(icon);
    } catch {
      /* ignore */
    }
  }

  bindWindowChromeTitle(win, kind);
  if (
    kind === 'sceneDescription' ||
    kind === 'materials' ||
    kind === 'npcsEditor' ||
    kind === 'sceneEditor' ||
    kind === 'tokenPathEditor' ||
    kind === 'npcs'
  ) {
    win.setMenuBarVisibility(false);
  }

  win.webContents.on('preload-error', (_event, preloadPath, error) => {
    safeConsoleError(`[preload-error] ${preloadPath}:`, error);
  });
  win.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
    // -3 ERR_ABORTED: частый артефакт при navigate/maximize/закрытии — не шумим.
    if (errorCode === -3) return;
    if (!isMainFrame) return;
    safeConsoleError(`[did-fail-load] ${String(errorCode)} ${errorDescription} ${validatedURL}`);
  });
  win.webContents.on('render-process-gone', (_event, details) => {
    safeConsoleError('[render-process-gone]', details.reason, details.exitCode);
  });

  if (!deferShow) {
    ensureWindowBecomesVisible(win);
  }
  loadWindowPage(win, kind);
  if (kind === 'editor') {
    win.on('close', (e) => {
      if (appQuitting) return;
      e.preventDefault();
      quitAppFromEditorClose();
    });
  }
  if (kind === 'control') {
    win.on('close', () => {
      if (appQuitting || closingPlaySession) return;
      closingPlaySession = true;
      closePlaySessionAuxiliaryWindows();
      closePresentationWindow();
    });
  }
  if (kind === 'presentation') {
    bindPresentationContentSizeTracking(win);
    win.on('close', () => {
      if (appQuitting || closingPlaySession) return;
      closingPlaySession = true;
      closePlaySessionAuxiliaryWindows();
      closeControlWindow();
    });
  }
  win.on('closed', () => windows.delete(kind));
  if (kind === 'sceneEditor') {
    win.on('closed', () => {
      closeTokenPathEditorWindow();
    });
  }
  win.on('closed', () => {
    if (kind !== 'presentation' && kind !== 'control') return;
    const open = windows.has('presentation') || windows.has('control');
    if (!open) {
      closingPlaySession = false;
      closePlaySessionAuxiliaryWindows();
    }
    broadcastMultiWindowStateChanged(open);
  });
  windows.set(kind, win);
  return win;
}

export function createWindows() {
  if (!windows.has('editor')) {
    createWindow('editor');
  }
}

/** Редактор создаётся скрытым до окончания экрана загрузки. */
export function createEditorWindowDeferred(): BrowserWindow {
  const existing = windows.get('editor');
  if (existing) {
    return existing;
  }
  return createWindow('editor', { deferVisibility: true });
}

/** Дождаться первой отрисовки редактора (готовность к показу без чёрного экрана). */
export function waitForEditorWindowReady(win: BrowserWindow): Promise<void> {
  return new Promise<void>((resolve) => {
    let settled = false;
    const timer = setTimeout(() => {
      if (!settled) {
        settled = true;
        resolve(undefined);
      }
    }, 35000);
    const finish = (): void => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      resolve(undefined);
    };
    win.once('ready-to-show', finish);
    win.webContents.once('did-finish-load', finish);
  }).then(
    () =>
      new Promise<void>((r) => {
        setTimeout(r, 120);
      }),
  );
}

export function focusEditorWindow(): void {
  const splash = getBootSplashWindow();
  if (splash && !splash.isDestroyed()) {
    splash.focus();
    return;
  }
  const win = windows.get('editor');
  if (win) {
    if (win.isMinimized()) win.restore();
    win.show();
    win.focus();
  } else {
    createWindows();
  }
}

export function openMultiWindow() {
  let presentation = windows.get('presentation');
  if (!presentation) {
    const display = screen.getPrimaryDisplay();
    const { x, y, width, height } = display.bounds;
    presentation = createWindow('presentation');
    presentation.setBounds({ x, y, width, height });
    presentation.setMenuBarVisibility(false);
    presentation.maximize();
  }
  if (!windows.has('control')) {
    // macOS: parent-child window binding moves child with the parent (unlike Windows behavior we want).
    // Keep control window independent on darwin.
    createWindow('control', process.platform === 'darwin' ? undefined : { parent: presentation });
  }
  broadcastMultiWindowStateChanged(true);
  broadcastPresentationContentSize();
}

export function closeMultiWindow(): void {
  closingPlaySession = true;
  closePlaySessionAuxiliaryWindows();
  const pres = windows.get('presentation');
  const ctrl = windows.get('control');
  if (pres && !pres.isDestroyed()) pres.close();
  if (ctrl && !ctrl.isDestroyed()) ctrl.close();
  if (!windows.has('presentation') && !windows.has('control')) {
    closingPlaySession = false;
  }
}

export function isMultiWindowOpen(): boolean {
  return windows.has('presentation') || windows.has('control');
}

export function closeSceneDescriptionWindow(): void {
  const win = windows.get('sceneDescription');
  if (win && !win.isDestroyed()) {
    win.close();
  }
}

export function closeMaterialsWindow(): void {
  const win = windows.get('materials');
  if (win && !win.isDestroyed()) {
    win.close();
  }
}

export function closeNpcsEditorWindow(): void {
  const win = windows.get('npcsEditor');
  if (win && !win.isDestroyed()) {
    win.close();
  }
}

export function closeSceneEditorWindow(): void {
  closeTokenPathEditorWindow();
  const win = windows.get('sceneEditor');
  if (win && !win.isDestroyed()) {
    win.close();
  }
}

export function closeTokenPathEditorWindow(): void {
  const win = windows.get('tokenPathEditor');
  if (win && !win.isDestroyed()) {
    win.close();
  }
}

let pendingTokenPathEditorTarget: { kind: 'token' | 'npcToken'; placementId: string } | null = null;

export function getTokenPathEditorTarget(): { kind: 'token' | 'npcToken'; placementId: string } | null {
  return pendingTokenPathEditorTarget;
}

function broadcastTokenPathEditorTarget(): void {
  const win = windows.get('tokenPathEditor');
  if (!win || win.isDestroyed() || win.webContents.isDestroyed()) return;
  try {
    win.webContents.send(ipcChannels.windows.tokenPathEditorTargetChanged, pendingTokenPathEditorTarget);
  } catch {
    /* ignore */
  }
}

/** Одно окно пути: смена токена = фокус + targetChanged, без второго окна. */
export function openTokenPathEditorWindow(kind: 'token' | 'npcToken', placementId: string): void {
  pendingTokenPathEditorTarget = { kind, placementId };
  const existing = windows.get('tokenPathEditor');
  if (existing && !existing.isDestroyed()) {
    if (existing.isMinimized()) existing.restore();
    existing.show();
    existing.focus();
    existing.moveTop();
    broadcastTokenPathEditorTarget();
    return;
  }

  const parent = windows.get('sceneEditor') ?? windows.get('editor');
  const win = createWindow('tokenPathEditor', parent ? { parent } : undefined);
  const { width, height } = win.getBounds();
  const display = screen.getDisplayMatching(parent?.getBounds() ?? win.getBounds());
  const { x, y, width: dw, height: dh } = display.workArea;
  win.setBounds({
    x: Math.round(x + Math.max(0, (dw - width) / 2)),
    y: Math.round(y + Math.max(0, (dh - height) / 2)),
    width,
    height,
  });
  win.webContents.once('did-finish-load', () => {
    if (!win.isDestroyed()) {
      win.show();
      win.focus();
      win.moveTop();
      broadcastTokenPathEditorTarget();
    }
  });
  win.on('closed', () => {
    pendingTokenPathEditorTarget = null;
  });
}

export function closeNpcsWindow(): void {
  const win = windows.get('npcs');
  if (win && !win.isDestroyed()) {
    win.close();
  }
}

export function getSceneDescriptionContent(): string {
  return pendingSceneDescriptionHtml;
}

/** Одно окно описания: переиспользовать существующее или создать новое. */
export function openSceneDescriptionWindow(html: string): void {
  pendingSceneDescriptionHtml = html;
  const existing = windows.get('sceneDescription');
  if (existing && !existing.isDestroyed()) {
    if (existing.isMinimized()) existing.restore();
    existing.show();
    existing.focus();
    existing.moveTop();
    sendSceneDescriptionContent(existing, html);
    return;
  }

  // Держим поверх пульта/презентации, иначе окно уходит под полноэкранный экран просмотра.
  const parent = windows.get('control') ?? windows.get('presentation');
  const win = createWindow('sceneDescription', parent ? { parent } : undefined);
  const { width, height } = win.getBounds();
  const display = screen.getDisplayMatching(parent?.getBounds() ?? win.getBounds());
  const { x, y, width: dw, height: dh } = display.workArea;
  win.setBounds({
    x: Math.round(x + (dw - width) / 2),
    y: Math.round(y + (dh - height) / 2),
    width,
    height,
  });
  win.webContents.once('did-finish-load', () => {
    sendSceneDescriptionContent(win, pendingSceneDescriptionHtml);
    if (!win.isDestroyed()) {
      win.show();
      win.focus();
      win.moveTop();
    }
  });
}

/** Полоса материалов: фиксированная ширина плитки, переиспользование окна. */
export function openMaterialsWindow(): void {
  const existing = windows.get('materials');
  if (existing && !existing.isDestroyed()) {
    if (existing.isMinimized()) existing.restore();
    const b = existing.getBounds();
    existing.setBounds({
      x: b.x,
      y: b.y,
      width: MATERIALS_WINDOW_WIDTH,
      height: MATERIALS_WINDOW_HEIGHT,
    });
    existing.show();
    existing.focus();
    existing.moveTop();
    return;
  }

  const parent = windows.get('control') ?? windows.get('presentation');
  const win = createWindow('materials', parent ? { parent } : undefined);
  const { width, height } = win.getBounds();
  const display = screen.getDisplayMatching(parent?.getBounds() ?? win.getBounds());
  const { x, y, width: dw, height: dh } = display.workArea;
  win.setBounds({
    x: Math.round(x + Math.max(0, dw - width - 24)),
    y: Math.round(y + (dh - height) / 2),
    width,
    height,
  });
  win.webContents.once('did-finish-load', () => {
    if (!win.isDestroyed()) {
      win.show();
      win.focus();
      win.moveTop();
    }
  });
}

function positionNpcsEditorWindow(win: BrowserWindow): void {
  const parent = windows.get('editor');
  const { width, height } = win.getBounds();
  const display = screen.getDisplayMatching(parent?.getBounds() ?? win.getBounds());
  const { x, y, width: dw, height: dh } = display.workArea;
  win.setBounds({
    x: Math.round(x + Math.max(0, (dw - width) / 2)),
    y: Math.round(y + Math.max(0, (dh - height) / 2)),
    width,
    height,
  });
}

/** Прогрев окна НПС в фоне после открытия проекта — клик «НПС» не ждёт холодной загрузки. */
export function warmNpcsEditorWindow(): void {
  const existing = windows.get('npcsEditor');
  if (existing && !existing.isDestroyed()) return;
  const parent = windows.get('editor');
  createWindow('npcsEditor', {
    ...(parent ? { parent } : {}),
    deferVisibility: true,
  });
}

/** Редактор НПС: отдельное окно с графом и инспектором. */
export function openNpcsEditorWindow(): void {
  const existing = windows.get('npcsEditor');
  if (existing && !existing.isDestroyed()) {
    if (existing.isMinimized()) existing.restore();
    positionNpcsEditorWindow(existing);
    existing.show();
    existing.focus();
    existing.moveTop();
    return;
  }

  const parent = windows.get('editor');
  const win = createWindow('npcsEditor', {
    ...(parent ? { parent } : {}),
    deferVisibility: true,
  });
  positionNpcsEditorWindow(win);
  // Показываем сразу (тёмный фон), не дожидаясь полной загрузки React/ReactFlow.
  win.show();
  win.focus();
  win.moveTop();
  win.webContents.once('did-finish-load', () => {
    if (!win.isDestroyed()) {
      win.focus();
      win.moveTop();
    }
  });
}

/** Редактор сцены: расстановка ловушек и легенды материалов. */
export function openSceneEditorWindow(): void {
  const existing = windows.get('sceneEditor');
  if (existing && !existing.isDestroyed()) {
    if (existing.isMinimized()) existing.restore();
    existing.show();
    existing.focus();
    existing.moveTop();
    return;
  }

  const parent = windows.get('editor');
  const win = createWindow('sceneEditor', parent ? { parent } : undefined);
  const { width, height } = win.getBounds();
  const display = screen.getDisplayMatching(parent?.getBounds() ?? win.getBounds());
  const { x, y, width: dw, height: dh } = display.workArea;
  win.setBounds({
    x: Math.round(x + Math.max(0, (dw - width) / 2)),
    y: Math.round(y + Math.max(0, (dh - height) / 2)),
    width,
    height,
  });
  win.webContents.once('did-finish-load', () => {
    if (!win.isDestroyed()) {
      win.show();
      win.focus();
      win.moveTop();
    }
  });
}

/** Пульт НПС: список + описание выбранного; оверлей аватара на сцене. */
export function openNpcsWindow(): void {
  const existing = windows.get('npcs');
  if (existing && !existing.isDestroyed()) {
    if (existing.isMinimized()) existing.restore();
    const b = existing.getBounds();
    existing.setBounds({
      x: b.x,
      y: b.y,
      width: Math.min(NPCS_WINDOW_WIDTH, Math.max(560, b.width)),
      height: Math.max(NPCS_WINDOW_HEIGHT, b.height),
    });
    existing.show();
    existing.focus();
    existing.moveTop();
    return;
  }

  const parent = windows.get('control') ?? windows.get('presentation');
  const win = createWindow('npcs', parent ? { parent } : undefined);
  const { width, height } = win.getBounds();
  const display = screen.getDisplayMatching(parent?.getBounds() ?? win.getBounds());
  const { x, y, width: dw, height: dh } = display.workArea;
  win.setBounds({
    x: Math.round(x + Math.max(0, dw - width - 24)),
    y: Math.round(y + (dh - height) / 2),
    width,
    height,
  });
  win.webContents.once('did-finish-load', () => {
    if (!win.isDestroyed()) {
      win.show();
      win.focus();
      win.moveTop();
    }
  });
}

export function togglePresentationFullscreen(): boolean {
  const pres = windows.get('presentation');
  if (!pres) return false;
  const next = !pres.isFullScreen();
  pres.setFullScreen(next);
  return pres.isFullScreen();
}
