/*
 * SubMiner - All-in-one sentence mining overlay
 * Copyright (C) 2024 sudacode
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import type {
  KikuDuplicateCardInfo,
  RuntimeOptionState,
  SecondarySubMode,
  SubtitleData,
  SubtitlePosition,
  SubsyncManualPayload,
  ConfigHotReloadPayload,
} from '../types';
import type { OverlayHostedModal } from '../shared/ipc/contracts';
import { createKeyboardHandlers } from './handlers/keyboard.js';
import { createGamepadController } from './handlers/gamepad-controller.js';
import { createMouseHandlers } from './handlers/mouse.js';
import { createControllerStatusIndicator } from './controller-status-indicator.js';
import { createControllerDebugModal } from './modals/controller-debug.js';
import { createControllerSelectModal } from './modals/controller-select.js';
import { createJimakuModal } from './modals/jimaku.js';
import { createTsukihimeModal } from './modals/tsukihime.js';
import { createKikuModal } from './modals/kiku.js';
import { prepareForKikuFieldGroupingOpen } from './kiku-open.js';
import { createPlaylistBrowserModal } from './modals/playlist-browser.js';
import { createSessionHelpModal } from './modals/session-help.js';
import { createChangelogModal } from './modals/changelog.js';
import { createSubtitleSidebarModal } from './modals/subtitle-sidebar.js';
import { isControllerInteractionBlocked } from './controller-interaction-blocking.js';
import { createCharacterDictionaryModal } from './modals/character-dictionary.js';
import { createRuntimeOptionsModal } from './modals/runtime-options.js';
import { createSubsyncModal } from './modals/subsync.js';
import { createYoutubeTrackPickerModal } from './modals/youtube-track-picker.js';
import { createMediaTimingReviewModal } from './modals/media-timing-review.js';
import { createPositioningController } from './positioning.js';
import { createOverlayContentMeasurementReporter } from './overlay-content-measurement.js';
import { syncOverlayMouseIgnoreState } from './overlay-mouse-ignore.js';
import {
  createOverlayNotificationRenderer,
  handleOverlayNotificationEvent,
  overlayNotificationPositionClass,
} from './overlay-notifications.js';
import { createOverlayNotificationHistoryPanel } from './overlay-notification-history.js';
import { createRendererState } from './state.js';
import { createModalRegistry, type ModalDescriptor } from './modal-registry.js';
import { createSubtitleRenderer } from './subtitle-render.js';
import { isYomitanPopupVisible, registerYomitanLookupListener } from './yomitan-popup.js';
import {
  createRendererRecoveryController,
  registerRendererGlobalErrorHandlers,
} from './error-recovery.js';
import { resolveRendererDom } from './utils/dom.js';
import { resolvePlatformInfo } from './utils/platform.js';
import {
  buildMpvLoadfileCommands,
  buildMpvSubtitleAddCommands,
  collectDroppedSubtitlePaths,
  collectDroppedVideoPaths,
} from '../core/services/overlay-drop.js';

const ctx = {
  dom: resolveRendererDom(),
  platform: resolvePlatformInfo(),
  state: createRendererState(),
};

const modalDescriptors = [
  {
    id: 'controller-select',
    isOpen: () => ctx.state.controllerSelectModalOpen,
    close: () => controllerSelectModal.closeControllerSelectModal(),
    suppressesSubtitles: true,
  },
  {
    id: 'controller-debug',
    isOpen: () => ctx.state.controllerDebugModalOpen,
    close: () => controllerDebugModal.closeControllerDebugModal(),
    suppressesSubtitles: true,
  },
  {
    id: 'subtitle-sidebar',
    isOpen: () => ctx.state.subtitleSidebarModalOpen,
    close: () => subtitleSidebarModal.closeSubtitleSidebarModal(),
    suppressesSubtitles: false,
  },
  {
    id: 'jimaku',
    isOpen: () => ctx.state.jimakuModalOpen,
    close: () => jimakuModal.closeJimakuModal(),
    suppressesSubtitles: true,
  },
  {
    id: 'tsukihime',
    isOpen: () => ctx.state.tsukihimeModalOpen,
    close: () => tsukihimeModal.closeTsukihimeModal(),
    suppressesSubtitles: false,
  },
  {
    id: 'youtube-track-picker',
    isOpen: () => ctx.state.youtubePickerModalOpen,
    close: () => youtubePickerModal.closeYoutubePickerModal(),
    suppressesSubtitles: true,
  },
  {
    id: 'media-timing-review',
    isOpen: () => ctx.state.mediaTimingReviewModalOpen,
    close: () => mediaTimingReviewModal.requestCancel(),
    suppressesSubtitles: true,
  },
  {
    id: 'playlist-browser',
    isOpen: () => ctx.state.playlistBrowserModalOpen,
    close: () => playlistBrowserModal.closePlaylistBrowserModal(),
    suppressesSubtitles: true,
  },
  {
    id: 'kiku',
    isOpen: () => ctx.state.kikuModalOpen,
    close: () => kikuModal.cancelKikuFieldGrouping(),
    suppressesSubtitles: true,
  },
  {
    id: 'runtime-options',
    isOpen: () => ctx.state.runtimeOptionsModalOpen,
    close: () => runtimeOptionsModal.closeRuntimeOptionsModal(),
    suppressesSubtitles: true,
  },
  {
    id: 'character-dictionary',
    isOpen: () => ctx.state.characterDictionaryModalOpen,
    close: () => characterDictionaryModal.closeCharacterDictionaryModal(),
    suppressesSubtitles: true,
  },
  {
    id: 'subsync',
    isOpen: () => ctx.state.subsyncModalOpen,
    close: () => subsyncModal.closeSubsyncModal(),
    suppressesSubtitles: true,
  },
  {
    id: 'session-help',
    isOpen: () => ctx.state.sessionHelpModalOpen,
    close: () => sessionHelpModal.closeSessionHelpModal(),
    suppressesSubtitles: true,
  },
  {
    id: 'changelog',
    isOpen: () => ctx.state.changelogModalOpen,
    close: () => changelogModal.closeChangelogModal(),
    suppressesSubtitles: true,
  },
] satisfies readonly ModalDescriptor<OverlayHostedModal>[];

const modalRegistry = createModalRegistry(modalDescriptors);

function isAnySettingsModalOpen(): boolean {
  return modalRegistry.isAnySuppressingSubtitlesOpen();
}

function isAnyModalOpen(): boolean {
  return modalRegistry.isAnyOpen();
}

function isControllerInputBlocked(): boolean {
  return isControllerInteractionBlocked(ctx.state);
}

function syncSettingsModalSubtitleSuppression(): void {
  const suppressSubtitles = isAnySettingsModalOpen();
  document.body.classList.toggle('settings-modal-open', suppressSubtitles);
  if (suppressSubtitles) {
    ctx.state.isOverSubtitle = false;
  }
}

const subtitleRenderer = createSubtitleRenderer(ctx);
const measurementReporter = createOverlayContentMeasurementReporter(ctx);
const notificationHistory = createOverlayNotificationHistoryPanel(ctx, {
  onChanged: () => measurementReporter.schedule(),
});
const overlayNotifications = createOverlayNotificationRenderer(ctx, {
  onChanged: () => {
    notificationHistory.syncSide();
    measurementReporter.schedule();
  },
  onShow: (entry) => notificationHistory.record(entry),
});
const positioning = createPositioningController(ctx);
const runtimeOptionsModal = createRuntimeOptionsModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
});
const characterDictionaryModal = createCharacterDictionaryModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
});
const subsyncModal = createSubsyncModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
});
const controllerSelectModal = createControllerSelectModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
  notifyControllerDisabled: showControllerDisabledNotice,
});
const controllerDebugModal = createControllerDebugModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
  notifyControllerDisabled: showControllerDisabledNotice,
});
const controllerStatusIndicator = createControllerStatusIndicator(ctx.dom);
const sessionHelpModal = createSessionHelpModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
});
const changelogModal = createChangelogModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
});
const subtitleSidebarModal = createSubtitleSidebarModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  shouldRestoreOpenOnStartup: async () =>
    ctx.platform.overlayLayer === 'visible' && (await window.electronAPI.getSubtitleSidebarOpen()),
  onVisibilityChanged: () => {
    measurementReporter.emitNow();
  },
});
const kikuModal = createKikuModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
});
const jimakuModal = createJimakuModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
});
const tsukihimeModal = createTsukihimeModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
});
const mouseHandlers = createMouseHandlers(ctx, {
  modalStateReader: { isAnySettingsModalOpen, isAnyModalOpen },
  applyYPercent: positioning.applyYPercent,
  getCurrentYPercent: positioning.getCurrentYPercent,
  persistSubtitlePositionPatch: positioning.persistSubtitlePositionPatch,
  getSubtitleHoverAutoPauseEnabled: () => ctx.state.autoPauseVideoOnSubtitleHover,
  getYomitanPopupAutoPauseEnabled: () => ctx.state.autoPauseVideoOnYomitanPopup,
  getPlaybackPaused: () => window.electronAPI.getPlaybackPaused(),
  sendMpvCommand: (command) => {
    window.electronAPI.sendMpvCommand(command);
  },
});
const youtubePickerModal = createYoutubeTrackPickerModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  restorePointerInteractionState: mouseHandlers.restorePointerInteractionState,
  syncSettingsModalSubtitleSuppression,
});
const playlistBrowserModal = createPlaylistBrowserModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
});
const mediaTimingReviewModal = createMediaTimingReviewModal(ctx, {
  modalStateReader: { isAnyModalOpen },
  syncSettingsModalSubtitleSuppression,
});
const keyboardHandlers = createKeyboardHandlers(ctx, {
  handleRuntimeOptionsKeydown: runtimeOptionsModal.handleRuntimeOptionsKeydown,
  handleCharacterDictionaryKeydown: characterDictionaryModal.handleCharacterDictionaryKeydown,
  handleSubsyncKeydown: subsyncModal.handleSubsyncKeydown,
  handleKikuKeydown: kikuModal.handleKikuKeydown,
  handleJimakuKeydown: jimakuModal.handleJimakuKeydown,
  handleTsukihimeKeydown: tsukihimeModal.handleTsukihimeKeydown,
  handleYoutubePickerKeydown: youtubePickerModal.handleYoutubePickerKeydown,
  handleMediaTimingReviewKeydown: mediaTimingReviewModal.handleMediaTimingReviewKeydown,
  handlePlaylistBrowserKeydown: playlistBrowserModal.handlePlaylistBrowserKeydown,
  handleControllerSelectKeydown: controllerSelectModal.handleControllerSelectKeydown,
  handleControllerDebugKeydown: controllerDebugModal.handleControllerDebugKeydown,
  handleSessionHelpKeydown: sessionHelpModal.handleSessionHelpKeydown,
  handleChangelogKeydown: changelogModal.handleChangelogKeydown,
  openSessionHelpModal: sessionHelpModal.openSessionHelpModal,
  openControllerSelectModal: () => {
    if (controllerSelectModal.openControllerSelectModal()) {
      window.electronAPI.notifyOverlayModalOpened('controller-select');
    }
  },
  openControllerDebugModal: () => {
    if (controllerDebugModal.openControllerDebugModal()) {
      window.electronAPI.notifyOverlayModalOpened('controller-debug');
    }
  },
  getPlaybackPaused: () => window.electronAPI.getPlaybackPaused(),
  toggleSubtitleSidebarModal: () => {
    void subtitleSidebarModal.toggleSubtitleSidebarModal();
  },
});

let lastSubtitlePreview = '';
let lastSecondarySubtitlePreview = '';
let overlayErrorToastTimeout: ReturnType<typeof setTimeout> | null = null;
let controllerAnimationFrameId: number | null = null;

function truncateForErrorLog(text: string): string {
  const normalized = text.replace(/\s+/g, ' ').trim();
  if (normalized.length <= 180) {
    return normalized;
  }
  return `${normalized.slice(0, 177)}...`;
}

function getSubtitleTextForPreview(data: SubtitleData | string): string {
  if (typeof data === 'string') {
    return data;
  }
  if (data && typeof data.text === 'string') {
    return data.text;
  }
  return '';
}

function getActiveModal(): OverlayHostedModal | null {
  return modalRegistry.getActive();
}

function dismissActiveUiAfterError(): void {
  modalRegistry.dismissOpen();
  syncSettingsModalSubtitleSuppression();
}

function applyControllerSnapshot(snapshot: {
  connectedGamepads: Array<{ id: string; index: number; mapping: string; connected: boolean }>;
  activeGamepadId: string | null;
  rawAxes: number[];
  rawButtons: Array<{ value: number; pressed: boolean; touched?: boolean }>;
}): void {
  controllerStatusIndicator.update({
    connectedGamepads: snapshot.connectedGamepads,
    activeGamepadId: snapshot.activeGamepadId,
  });
  ctx.state.connectedGamepads = snapshot.connectedGamepads;
  ctx.state.activeGamepadId = snapshot.activeGamepadId;
  ctx.state.controllerRawAxes = snapshot.rawAxes;
  ctx.state.controllerRawButtons = snapshot.rawButtons;
  controllerSelectModal.updateDevices();
  controllerDebugModal.updateSnapshot();
}

function showControllerDisabledNotice(): void {
  controllerStatusIndicator.show(
    'Controller support disabled. Set controller.enabled to true in config to use controller tools.',
  );
}

function emitControllerPopupScroll(deltaPixels: number): void {
  if (deltaPixels === 0) return;
  keyboardHandlers.scrollPopupByController(0, deltaPixels);
}

function emitControllerPopupJump(deltaPixels: number): void {
  if (deltaPixels === 0) return;
  keyboardHandlers.scrollPopupByController(0, deltaPixels * 4);
}

function startControllerPolling(): void {
  if (controllerAnimationFrameId !== null) {
    cancelAnimationFrame(controllerAnimationFrameId);
    controllerAnimationFrameId = null;
  }

  const gamepadController = createGamepadController({
    getGamepads: () => Array.from(navigator.getGamepads?.() ?? []),
    getConfig: () =>
      ctx.state.controllerConfig ?? {
        enabled: false,
        preferredGamepadId: '',
        preferredGamepadLabel: '',
        smoothScroll: true,
        scrollPixelsPerSecond: 900,
        horizontalJumpPixels: 160,
        stickDeadzone: 0.2,
        triggerInputMode: 'auto',
        triggerDeadzone: 0.5,
        repeatDelayMs: 320,
        repeatIntervalMs: 120,
        buttonIndices: {
          select: 6,
          buttonSouth: 0,
          buttonEast: 1,
          buttonWest: 2,
          buttonNorth: 3,
          leftShoulder: 4,
          rightShoulder: 5,
          leftStickPress: 9,
          rightStickPress: 10,
          leftTrigger: 6,
          rightTrigger: 7,
        },
        bindings: {
          toggleLookup: { kind: 'button', buttonIndex: 0 },
          closeLookup: { kind: 'button', buttonIndex: 1 },
          toggleKeyboardOnlyMode: { kind: 'button', buttonIndex: 3 },
          mineCard: { kind: 'button', buttonIndex: 2 },
          quitMpv: { kind: 'button', buttonIndex: 6 },
          previousAudio: { kind: 'none' },
          nextAudio: { kind: 'button', buttonIndex: 5 },
          playCurrentAudio: { kind: 'button', buttonIndex: 4 },
          toggleMpvPause: { kind: 'button', buttonIndex: 9 },
          leftStickHorizontal: { kind: 'axis', axisIndex: 0, dpadFallback: 'horizontal' },
          leftStickVertical: { kind: 'axis', axisIndex: 1, dpadFallback: 'vertical' },
          rightStickHorizontal: { kind: 'axis', axisIndex: 3, dpadFallback: 'none' },
          rightStickVertical: { kind: 'axis', axisIndex: 4, dpadFallback: 'none' },
        },
        profiles: {},
      },
    getKeyboardModeEnabled: () => ctx.state.keyboardDrivenModeEnabled,
    getLookupWindowOpen: () => ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document),
    getInteractionBlocked: () => isControllerInputBlocked(),
    toggleKeyboardMode: () => keyboardHandlers.handleKeyboardModeToggleRequested(),
    toggleLookup: () => keyboardHandlers.handleLookupWindowToggleRequested(),
    closeLookup: () => {
      keyboardHandlers.closeLookupWindow();
    },
    moveSelection: (delta) => {
      keyboardHandlers.moveSelectionForController(delta);
    },
    mineCard: () => {
      keyboardHandlers.mineSelectedFromController();
    },
    quitMpv: () => {
      window.electronAPI.sendMpvCommand(['quit']);
    },
    previousAudio: () => {
      keyboardHandlers.cyclePopupAudioSourceForController(-1);
    },
    nextAudio: () => {
      keyboardHandlers.cyclePopupAudioSourceForController(1);
    },
    playCurrentAudio: () => {
      keyboardHandlers.playCurrentAudioForController();
    },
    toggleMpvPause: () => {
      window.electronAPI.sendMpvCommand(['cycle', 'pause']);
    },
    scrollPopup: (deltaPixels) => {
      emitControllerPopupScroll(deltaPixels);
    },
    jumpPopup: (deltaPixels) => {
      emitControllerPopupJump(deltaPixels);
    },
    onState: (snapshot) => {
      applyControllerSnapshot(snapshot);
    },
  });

  const poll = (now: number): void => {
    gamepadController.poll(now);
    controllerAnimationFrameId = requestAnimationFrame(poll);
  };

  controllerAnimationFrameId = requestAnimationFrame(poll);
}

function restoreOverlayInteractionAfterError(): void {
  ctx.state.isOverSubtitle = false;
  ctx.dom.overlay.classList.remove('interactive');
  if (ctx.platform.shouldToggleMouseIgnore) {
    window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
  }
}

const OVERLAY_TOAST_POSITION_CLASSES = [
  'position-top-left',
  'position-top',
  'position-top-right',
] as const;

// Mirror the notification stack's current position onto a toast so error/status toasts honor the
// configured `notifications.overlayPosition` instead of always pinning to the top-right corner.
function applyConfiguredToastPosition(toast: HTMLElement): void {
  const stackClasses = ctx.dom.overlayNotificationStack.classList;
  const active =
    OVERLAY_TOAST_POSITION_CLASSES.find((cls) => stackClasses.contains(cls)) ??
    'position-top-right';
  toast.classList.remove(...OVERLAY_TOAST_POSITION_CLASSES);
  toast.classList.add(active);
}

function showOverlayErrorToast(message: string): void {
  if (overlayErrorToastTimeout) {
    clearTimeout(overlayErrorToastTimeout);
    overlayErrorToastTimeout = null;
  }
  ctx.dom.overlayErrorToast.textContent = message;
  applyConfiguredToastPosition(ctx.dom.overlayErrorToast);
  ctx.dom.overlayErrorToast.classList.remove('hidden');
  overlayErrorToastTimeout = setTimeout(() => {
    ctx.dom.overlayErrorToast.classList.add('hidden');
    ctx.dom.overlayErrorToast.textContent = '';
    overlayErrorToastTimeout = null;
  }, 3200);
}

const recovery = createRendererRecoveryController({
  dismissActiveUi: dismissActiveUiAfterError,
  restoreOverlayInteraction: restoreOverlayInteractionAfterError,
  showToast: showOverlayErrorToast,
  getSnapshot: () => ({
    activeModal: getActiveModal(),
    subtitlePreview: lastSubtitlePreview,
    secondarySubtitlePreview: lastSecondarySubtitlePreview,
    isOverlayInteractive: ctx.dom.overlay.classList.contains('interactive'),
    isOverSubtitle: ctx.state.isOverSubtitle,
    overlayLayer: ctx.platform.overlayLayer,
  }),
  logError: (payload) => {
    console.error('renderer overlay recovery', payload);
  },
});

registerRendererGlobalErrorHandlers(window, recovery);

function registerModalOpenHandlers(): void {
  window.electronAPI.onOpenRuntimeOptions(() => {
    runGuarded('runtime-options:open', () => {
      runtimeOptionsModal.openRuntimeOptionsModal();
      window.electronAPI.notifyOverlayModalOpened('runtime-options');
    });
  });
  window.electronAPI.onOpenCharacterDictionaryManager(() => {
    runGuardedAsync('character-dictionary-manager:open', async () => {
      await characterDictionaryModal.openCharacterDictionaryManagerModal();
    });
  });
  window.electronAPI.onOpenSessionHelp(() => {
    runGuarded('session-help:open', () => {
      sessionHelpModal.openSessionHelpModal(keyboardHandlers.getSessionHelpOpeningInfo());
      window.electronAPI.notifyOverlayModalOpened('session-help');
    });
  });
  window.electronAPI.onOpenChangelog(() => {
    runGuarded('changelog:open', () => {
      changelogModal.openChangelogModal();
      window.electronAPI.notifyOverlayModalOpened('changelog');
    });
  });
  window.electronAPI.onOpenControllerSelect(() => {
    runGuarded('controller-select:open', () => {
      if (controllerSelectModal.openControllerSelectModal()) {
        window.electronAPI.notifyOverlayModalOpened('controller-select');
      }
    });
  });
  window.electronAPI.onOpenControllerDebug(() => {
    runGuarded('controller-debug:open', () => {
      if (controllerDebugModal.openControllerDebugModal()) {
        window.electronAPI.notifyOverlayModalOpened('controller-debug');
      }
    });
  });
  window.electronAPI.onOpenJimaku(() => {
    runGuarded('jimaku:open', () => {
      jimakuModal.openJimakuModal();
      window.electronAPI.notifyOverlayModalOpened('jimaku');
    });
  });
  window.electronAPI.onOpenTsukihime(() => {
    runGuarded('tsukihime:open', () => {
      tsukihimeModal.openTsukihimeModal();
      window.electronAPI.notifyOverlayModalOpened('tsukihime');
    });
  });
  window.electronAPI.onOpenYoutubeTrackPicker((payload) => {
    runGuarded('youtube:picker-open', () => {
      youtubePickerModal.openYoutubePickerModal(payload);
    });
  });
  window.electronAPI.onOpenMediaTimingReview((payload) => {
    runGuarded('media-timing-review:open', () => {
      mediaTimingReviewModal.openMediaTimingReviewModal(payload);
    });
  });
  window.electronAPI.onMediaTimingReviewPreviewEnded((reviewId) => {
    runGuarded('media-timing-review:preview-ended', () => {
      mediaTimingReviewModal.handlePreviewEnded(reviewId);
    });
  });
  window.electronAPI.onOpenPlaylistBrowser(() => {
    runGuardedAsync('playlist-browser:open', async () => {
      await playlistBrowserModal.openPlaylistBrowserModal();
    });
  });
  window.electronAPI.onCancelYoutubeTrackPicker(() => {
    runGuarded('youtube:picker-cancel', () => {
      youtubePickerModal.closeYoutubePickerModal();
    });
  });
  window.electronAPI.onSubsyncManualOpen((payload: SubsyncManualPayload) => {
    runGuarded('subsync:manual-open', () => {
      subsyncModal.openSubsyncModal(payload);
      window.electronAPI.notifyOverlayModalOpened('subsync');
    });
  });
  window.electronAPI.onKikuFieldGroupingRequest(
    (data: { original: KikuDuplicateCardInfo; duplicate: KikuDuplicateCardInfo }) => {
      runGuarded('kiku:field-grouping-open', () => {
        prepareForKikuFieldGroupingOpen({
          closeLookupWindow: () => keyboardHandlers.closeLookupWindow(),
          pausePlayback: () => {
            window.electronAPI.sendMpvCommand(['set_property', 'pause', 'yes']);
          },
        });
        kikuModal.openKikuFieldGroupingModal(data);
        window.electronAPI.notifyOverlayModalOpened('kiku');
      });
    },
  );
  window.electronAPI.onKikuFieldGroupingCancel(() => {
    runGuarded('kiku:field-grouping-cancel', () => {
      // Main already settled the choice (timeout/failure); just close the dialog. Using the
      // plain close path avoids sending a second, redundant response back to main.
      kikuModal.closeKikuFieldGroupingModal();
    });
  });
}

function registerKeyboardCommandHandlers(): void {
  window.electronAPI.onSessionNumericSelectionStart((payload) => {
    runGuarded('session:numeric-selection-start', () => {
      keyboardHandlers.beginSessionNumericSelection(payload.actionId, payload.timeoutMs);
    });
  });

  window.electronAPI.onKeyboardModeToggleRequested(() => {
    runGuarded('keyboard-mode-toggle:requested', () => {
      keyboardHandlers.handleKeyboardModeToggleRequested();
    });
  });

  window.electronAPI.onLookupWindowToggleRequested(() => {
    runGuarded('lookup-window-toggle:requested', () => {
      keyboardHandlers.handleLookupWindowToggleRequested();
    });
  });

  window.electronAPI.onSubtitleSidebarToggle(() => {
    runGuardedAsync('subtitle-sidebar:toggle', async () => {
      await subtitleSidebarModal.toggleSubtitleSidebarModal();
    });
  });

  window.electronAPI.onPrimarySubtitleBarToggle(() => {
    runGuarded('primary-subtitle-bar:toggle', () => {
      keyboardHandlers.togglePrimarySubtitleBarVisibility();
    });
  });
}

function runGuarded(action: string, fn: () => void): void {
  try {
    fn();
  } catch (error) {
    recovery.handleError(error, { source: 'callback', action });
  }
}

function runGuardedAsync(action: string, fn: () => Promise<void> | void): void {
  Promise.resolve()
    .then(fn)
    .catch((error) => {
      recovery.handleError(error, { source: 'callback', action });
    });
}

registerModalOpenHandlers();
registerKeyboardCommandHandlers();
registerYomitanLookupListener(window, () => {
  runGuarded('yomitan:lookup', () => {
    window.electronAPI.recordYomitanLookup(subtitleSidebarModal.getSubtitleSidebarMiningContext());
  });
});

async function init(): Promise<void> {
  document.body.classList.add(`layer-${ctx.platform.overlayLayer}`);
  if (ctx.platform.isMacOSPlatform) {
    document.body.classList.add('platform-macos');
  }
  if (ctx.platform.isWindowsPlatform) {
    document.body.classList.add('platform-windows');
  }
  if (ctx.platform.shouldToggleMouseIgnore) {
    syncOverlayMouseIgnoreState(ctx);
  }

  // Seed the notification stack position from config before subscribing to history toggles, so the
  // closed history panel starts on the same side it will slide in from.
  try {
    const overlayNotificationPosition = await window.electronAPI.getOverlayNotificationPosition();
    ctx.dom.overlayNotificationStack.classList.remove(...OVERLAY_TOAST_POSITION_CLASSES);
    ctx.dom.overlayNotificationStack.classList.add(
      overlayNotificationPositionClass(overlayNotificationPosition),
    );
    notificationHistory.syncSide();
  } catch {
    // Non-fatal: keep the default position class from index.html.
  }

  window.electronAPI.onOverlayPointerRecoveryRequested(() => {
    runGuarded('overlay:pointer-recovery', () => {
      if (!ctx.platform.isMacOSPlatform || !ctx.platform.shouldToggleMouseIgnore) {
        return;
      }
      if (isAnyModalOpen()) {
        return;
      }
      mouseHandlers.restorePointerInteractionState();
    });
  });
  window.electronAPI.onOverlayNotification((payload) => {
    runGuarded('overlay:notification', () => {
      handleOverlayNotificationEvent(overlayNotifications, payload);
    });
  });
  window.electronAPI.onNotificationHistoryToggle(() => {
    runGuarded('notification-history:toggle', () => {
      notificationHistory.toggle();
    });
  });

  await keyboardHandlers.setupMpvInputForwarding();

  const initialSubtitleStyle = await window.electronAPI.getSubtitleStyle();
  subtitleRenderer.applySubtitleStyle(initialSubtitleStyle);
  subtitleRenderer.updatePrimarySubMode(initialSubtitleStyle?.primaryDefaultMode ?? 'visible');
  positioning.applyStoredSubtitlePosition(
    await window.electronAPI.getSubtitlePosition(),
    'startup',
  );
  measurementReporter.schedule();

  ctx.dom.subtitleContainer.addEventListener('mouseenter', mouseHandlers.handlePrimaryMouseEnter);
  ctx.dom.subtitleContainer.addEventListener('mouseleave', mouseHandlers.handlePrimaryMouseLeave);
  ctx.dom.secondarySubContainer.addEventListener(
    'mouseenter',
    mouseHandlers.handleSecondaryMouseEnter,
  );
  ctx.dom.secondarySubContainer.addEventListener(
    'mouseleave',
    mouseHandlers.handleSecondaryMouseLeave,
  );

  mouseHandlers.setupResizeHandler();
  mouseHandlers.setupPointerTracking();
  mouseHandlers.setupSelectionObserver();
  mouseHandlers.setupYomitanObserver();

  window.electronAPI.onSubtitlePosition((position: SubtitlePosition | null) => {
    runGuarded('subtitle-position:update', () => {
      positioning.applyStoredSubtitlePosition(position, 'media-change');
      measurementReporter.schedule();
      if (ctx.state.playlistBrowserModalOpen) {
        runGuardedAsync('playlist-browser:refresh-on-media-change', async () => {
          await playlistBrowserModal.refreshSnapshot();
        });
      }
    });
  });

  let initialSubtitle: SubtitleData | string = '';
  try {
    initialSubtitle = await window.electronAPI.getCurrentSubtitle();
  } catch {
    initialSubtitle = await window.electronAPI.getCurrentSubtitleRaw();
  }
  lastSubtitlePreview = truncateForErrorLog(getSubtitleTextForPreview(initialSubtitle));
  keyboardHandlers.handleSubtitleContentUpdated();
  subtitleRenderer.renderSubtitle(initialSubtitle);
  positioning.applyYPercent(positioning.getCurrentYPercent());
  measurementReporter.emitNow();

  window.electronAPI.onSubtitle((data: SubtitleData) => {
    runGuarded('subtitle:update', () => {
      lastSubtitlePreview = truncateForErrorLog(getSubtitleTextForPreview(data));
      keyboardHandlers.handleSubtitleContentUpdated();
      subtitleRenderer.renderSubtitle(data);
      positioning.applyYPercent(positioning.getCurrentYPercent());
      measurementReporter.emitNow();
      subtitleSidebarModal.handleSubtitleUpdated(data);
      measurementReporter.schedule();
    });
  });

  window.electronAPI.onSecondarySub((text: string) => {
    runGuarded('secondary-subtitle:update', () => {
      lastSecondarySubtitlePreview = truncateForErrorLog(text);
      subtitleRenderer.renderSecondarySub(text);
      measurementReporter.schedule();
    });
  });
  window.electronAPI.onSecondarySubMode((mode: SecondarySubMode) => {
    runGuarded('secondary-subtitle-mode:update', () => {
      subtitleRenderer.updateSecondarySubMode(mode);
      measurementReporter.schedule();
    });
  });

  subtitleRenderer.updateSecondarySubMode(await window.electronAPI.getSecondarySubMode());
  subtitleRenderer.renderSecondarySub(await window.electronAPI.getCurrentSecondarySub());
  measurementReporter.schedule();

  setupDragDropToMpvQueue();
  window.addEventListener('resize', () => {
    measurementReporter.schedule();
  });

  jimakuModal.wireDomEvents();
  tsukihimeModal.wireDomEvents();
  youtubePickerModal.wireDomEvents();
  mediaTimingReviewModal.wireDomEvents();
  playlistBrowserModal.wireDomEvents();
  kikuModal.wireDomEvents();
  runtimeOptionsModal.wireDomEvents();
  subsyncModal.wireDomEvents();
  controllerSelectModal.wireDomEvents();
  controllerDebugModal.wireDomEvents();
  sessionHelpModal.wireDomEvents();
  changelogModal.wireDomEvents();
  subtitleSidebarModal.wireDomEvents();
  characterDictionaryModal.wireDomEvents();
  window.addEventListener('beforeunload', () => {
    subtitleSidebarModal.disposeDomEvents();
  });

  window.electronAPI.onRuntimeOptionsChanged((options: RuntimeOptionState[]) => {
    runGuarded('runtime-options:changed', () => {
      runtimeOptionsModal.updateRuntimeOptions(options);
    });
  });
  window.electronAPI.onConfigHotReload((payload: ConfigHotReloadPayload) => {
    runGuarded('config:hot-reload', () => {
      keyboardHandlers.updateSessionBindings(payload.sessionBindings);
      void keyboardHandlers.refreshConfiguredShortcuts();
      subtitleRenderer.applySubtitleStyle(payload.subtitleStyle);
      subtitleRenderer.updatePrimarySubMode(payload.primarySubMode);
      subtitleRenderer.updateSecondarySubMode(payload.secondarySubMode);
      ctx.state.subtitleSidebarConfig = payload.subtitleSidebar;
      ctx.state.subtitleSidebarToggleKey = payload.subtitleSidebar.toggleKey;
      ctx.state.subtitleSidebarPauseVideoOnHover = payload.subtitleSidebar.pauseVideoOnHover;
      ctx.state.subtitleSidebarAutoScroll = payload.subtitleSidebar.autoScroll;
      void subtitleSidebarModal.refreshSubtitleSidebarSnapshot();
      measurementReporter.schedule();
    });
  });
  mouseHandlers.setupDragging();
  try {
    ctx.state.controllerConfig = await window.electronAPI.getControllerConfig();
  } catch (error) {
    console.error('Failed to load controller config.', error);
    ctx.state.controllerConfig = null;
  }
  startControllerPolling();

  await subtitleSidebarModal.refreshSubtitleSidebarSnapshot();
  await subtitleSidebarModal.autoOpenSubtitleSidebarOnStartup();

  measurementReporter.emitNow();
}

function setupDragDropToMpvQueue(): void {
  let dragDepth = 0;

  const setDropInteractive = (): void => {
    ctx.dom.overlay.classList.add('interactive');
    if (ctx.platform.shouldToggleMouseIgnore) {
      window.electronAPI.setIgnoreMouseEvents(false);
    }
  };

  const clearDropInteractive = (): void => {
    dragDepth = 0;
    if (isAnyModalOpen() || ctx.state.isOverSubtitle) {
      return;
    }
    ctx.dom.overlay.classList.remove('interactive');
    if (ctx.platform.shouldToggleMouseIgnore) {
      window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
    }
  };

  document.addEventListener('dragenter', (event: DragEvent) => {
    if (!event.dataTransfer) return;
    dragDepth += 1;
    setDropInteractive();
  });

  document.addEventListener('dragover', (event: DragEvent) => {
    if (dragDepth <= 0 || !event.dataTransfer) return;
    event.preventDefault();
    event.dataTransfer.dropEffect = 'copy';
  });

  document.addEventListener('dragleave', () => {
    if (dragDepth <= 0) return;
    dragDepth -= 1;
    if (dragDepth === 0) {
      clearDropInteractive();
    }
  });

  document.addEventListener('drop', (event: DragEvent) => {
    if (!event.dataTransfer) return;
    event.preventDefault();

    const resolvedFilePaths = Array.from(event.dataTransfer.files, (file) =>
      window.electronAPI.getPathForFile(file),
    );
    const droppedVideoPaths = collectDroppedVideoPaths(event.dataTransfer, resolvedFilePaths);
    const droppedSubtitlePaths = collectDroppedSubtitlePaths(event.dataTransfer, resolvedFilePaths);
    const appendDroppedVideos = event.shiftKey;
    const loadCommands = buildMpvLoadfileCommands(droppedVideoPaths, appendDroppedVideos);
    const subtitleCommands = buildMpvSubtitleAddCommands(droppedSubtitlePaths);
    for (const command of loadCommands) {
      window.electronAPI.sendMpvCommand(command);
    }
    for (const command of subtitleCommands) {
      window.electronAPI.sendMpvCommand(command);
    }
    const osdParts: string[] = [];
    if (loadCommands.length > 0) {
      const action = appendDroppedVideos ? 'Queued' : 'Loaded';
      osdParts.push(`${action} ${loadCommands.length} file${loadCommands.length === 1 ? '' : 's'}`);
    }
    if (subtitleCommands.length > 0) {
      osdParts.push(
        `Loaded ${subtitleCommands.length} subtitle file${subtitleCommands.length === 1 ? '' : 's'}`,
      );
    }
    if (osdParts.length > 0) {
      window.electronAPI.sendMpvCommand(['show-text', osdParts.join(' | '), '1500']);
    }

    clearDropInteractive();
  });
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => {
    runGuardedAsync('bootstrap:init', init);
  });
} else {
  runGuardedAsync('bootstrap:init', init);
}
