/*
  SubMiner - All-in-one sentence mining overlay
  Copyright (C) 2026 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 {
  app,
  BrowserWindow,
  clipboard,
  globalShortcut,
  ipcMain,
  shell,
  protocol,
  Extension,
  Session,
  Menu,
  nativeImage,
  Tray,
  dialog,
  screen,
} from 'electron';
import { applyControllerConfigUpdate } from './main/controller-config-update.js';
import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open';
import { createAniSkipRuntime } from './main/runtime/aniskip-runtime';
import { resolveAniSkipMetadataForFile } from './main/runtime/aniskip-metadata';
import { createDiscordRpcClient } from './main/runtime/discord-rpc-client.js';
import { startAppControlServer } from './main/runtime/app-control-server';
import { createEnsureBackgroundStatsServerHandler } from './main/runtime/background-stats-startup';
import {
  markJellyfinRemotePlaybackLoaded as markJellyfinRemotePlaybackLoadedState,
  shouldAutoLoadSecondarySubTrackForJellyfinPlayback,
} from './main/runtime/jellyfin-remote-playback';
import { getAppControlSocketPath } from './shared/app-control';
import {
  type CancelLinuxMpvFullscreenOverlayRefreshBurst,
  clearLinuxMpvFullscreenOverlayRefreshTimeouts,
  updateLinuxMpvFullscreenOverlayRefreshBurst,
} from './main/runtime/linux-mpv-fullscreen-overlay-refresh';
import {
  resolveLinuxVisibleOverlayWindowModeAction,
  type LinuxVisibleOverlayWindowMode,
} from './main/runtime/linux-visible-overlay-window-mode';
import { shouldRunLinuxOverlayZOrderKeepAlive } from './main/runtime/linux-overlay-zorder-keepalive';
import { focusMacOSOverlayWindow } from './main/runtime/macos-overlay-window-focus';
import { restoreMacOSMpvFocusAfterModalClose } from './main/runtime/macos-modal-focus-handoff';
import { resolveFreshPlaybackPaused } from './main/runtime/playback-paused-state';
import { mergeAiConfig } from './ai/config';
import {
  getDefaultPasswordStore,
  getPasswordStoreArg,
  normalizePasswordStoreArg,
} from './main/password-store-args';

protocol.registerSchemesAsPrivileged([
  {
    scheme: 'chrome-extension',
    privileges: {
      standard: true,
      secure: true,
      supportFetchAPI: true,
      corsEnabled: true,
      bypassCSP: true,
    },
  },
]);

import * as fs from 'fs';
import { spawn } from 'node:child_process';
import * as os from 'os';
import * as path from 'path';
import { MecabTokenizer } from './mecab-tokenizer';
import type {
  JimakuApiResponse,
  KikuFieldGroupingChoice,
  MpvSubtitleRenderMetrics,
  ResolvedConfig,
  SessionActionDispatchRequest,
  SecondarySubMode,
  SubtitleData,
  SubtitleMiningContext,
  SubtitlePosition,
  WindowGeometry,
} from './types';
import { OPEN_ANKI_CARD_ACTION_ID } from './types';
import { AnkiIntegration } from './anki-integration';
import { SubtitleTimingTracker } from './subtitle-timing-tracker';
import { RuntimeOptionsManager } from './runtime-options';
import { downloadToFile, isRemoteMediaPath, parseMediaInfo } from './jimaku/utils';
import {
  createLogger,
  setLogFileToggles,
  setLogLevel,
  setLogRotation,
  type LogLevelSource,
} from './logger';
import {
  isLogFileEnabled,
  pruneLogDirectoryForPath,
  resolveDefaultLogFilePath,
} from './shared/log-files';
import { createFatalErrorReporter } from './main/fatal-error';
import { ensureWindowsOverlayTransparency } from './window-trackers/windows-helper';
import {
  commandNeedsOverlayStartupPrereqs,
  commandNeedsOverlayRuntime,
  isHeadlessInitialCommand,
  parseArgs,
  shouldStartApp,
  type CliArgs,
  type CliCommandSource,
} from './cli/args';
import { printHelp } from './cli/help';
import { IPC_CHANNELS, type OverlayHostedModal } from './shared/ipc/contracts';
import { buildMpvLoggingArgs } from './shared/mpv-logging-args';
import {
  MPV_X11_BACKEND_ARGS,
  applyX11EnvOverrides,
  shouldForceX11WaylandSession,
} from './shared/mpv-x11-backend';
import { AnkiConnectClient } from './anki-connect';
import {
  getStartupModeFlags,
  shouldHandleInitialArgsBeforeDeferredOverlayWarmup,
  shouldRefreshAnilistOnConfigReload,
  shouldStartAutomaticUpdateChecks,
} from './main/runtime/startup-mode-flags';
import {
  buildConfigParseErrorDetails,
  buildConfigWarningDialogDetails,
  buildConfigWarningNotificationBody,
  failStartupFromConfig,
} from './main/config-validation';
import {
  buildAnilistSetupUrl,
  consumeAnilistSetupCallbackUrl,
  createAnilistStateRuntime,
  createBuildOpenAnilistSetupWindowMainDepsHandler,
  createMaybeFocusExistingAnilistSetupWindowHandler,
  createOpenAnilistSetupWindowHandler,
  findAnilistSetupDeepLinkArgvUrl,
  isAnilistTrackingEnabled,
  loadAnilistManualTokenEntry,
  openAnilistSetupInBrowser,
  rememberAnilistAttemptedUpdateKey,
} from './main/runtime/domains/anilist';
import { DEFAULT_MIN_WATCH_RATIO } from './shared/watch-threshold';
import { shouldShowTexthookerTrayEntry } from './main/runtime/tray-main-actions';
import {
  createApplyJellyfinMpvDefaultsHandler,
  createBuildApplyJellyfinMpvDefaultsMainDepsHandler,
  createBuildGetDefaultSocketPathMainDepsHandler,
  createGetDefaultSocketPathHandler,
  buildJellyfinSetupFormHtml,
  parseJellyfinSetupSubmissionUrl,
  getConfiguredJellyfinSession,
  mergeJellyfinRecentServers,
  persistJellyfinAuthSession,
  type ActiveJellyfinRemotePlaybackState,
} from './main/runtime/domains/jellyfin';
import {
  createBuildConfigHotReloadMessageMainDepsHandler,
  createBuildConfigHotReloadAppliedMainDepsHandler,
  createBuildConfigHotReloadRuntimeMainDepsHandler,
  createBuildWatchConfigPathMainDepsHandler,
  createWatchConfigPathHandler,
  createBuildOverlayContentMeasurementStoreMainDepsHandler,
  createBuildOverlayModalRuntimeMainDepsHandler,
  createBuildAppendClipboardVideoToQueueMainDepsHandler,
  createBuildHandleOverlayModalClosedMainDepsHandler,
  createBuildLoadSubtitlePositionMainDepsHandler,
  createBuildSaveSubtitlePositionMainDepsHandler,
  createBuildFieldGroupingOverlayMainDepsHandler,
  createBuildGetFieldGroupingResolverMainDepsHandler,
  createBuildSetFieldGroupingResolverMainDepsHandler,
  createBuildOverlayVisibilityRuntimeMainDepsHandler,
  createBuildBroadcastRuntimeOptionsChangedMainDepsHandler,
  createBuildGetRuntimeOptionsStateMainDepsHandler,
  createBuildOpenRuntimeOptionsPaletteMainDepsHandler,
  createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler,
  createBuildSendToActiveOverlayWindowMainDepsHandler,
  createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler,
  createTrayRuntimeHandlers,
  createOverlayVisibilityRuntime,
  createBroadcastRuntimeOptionsChangedHandler,
  createGetRuntimeOptionsStateHandler,
  createGetFieldGroupingResolverHandler,
  createSetFieldGroupingResolverHandler,
  createOpenRuntimeOptionsPaletteHandler,
  createRestorePreviousSecondarySubVisibilityHandler,
  createSendToActiveOverlayWindowHandler,
  createSetOverlayDebugVisualizationEnabledHandler,
  createLoadSubtitlePositionHandler,
  createSaveSubtitlePositionHandler,
  createAppendClipboardVideoToQueueHandler,
  createHandleOverlayModalClosedHandler,
  createConfigHotReloadMessageHandler,
  createConfigHotReloadAppliedHandler,
  buildTrayMenuTemplateRuntime,
  resolveTrayIconPathRuntime,
  createYomitanExtensionRuntime,
  createYomitanSettingsRuntime,
  buildRestartRequiredConfigMessage,
  resolveSubtitleStyleForRenderer,
} from './main/runtime/domains/overlay';
import {
  createBuildAnilistStateRuntimeMainDepsHandler,
  createBuildConfigDerivedRuntimeMainDepsHandler,
  createBuildImmersionMediaRuntimeMainDepsHandler,
  createBuildMainSubsyncRuntimeMainDepsHandler,
  createBuildSubtitleProcessingControllerMainDepsHandler,
  createBuildMediaRuntimeMainDepsHandler,
  createBuildDictionaryRootsMainHandler,
  createBuildFrequencyDictionaryRootsMainHandler,
  createBuildFrequencyDictionaryRuntimeMainDepsHandler,
  createBuildJlptDictionaryRuntimeMainDepsHandler,
  createImmersionMediaRuntime,
  createConfigDerivedRuntime,
  appendClipboardVideoToQueueRuntime,
  createMainSubsyncRuntime,
} from './main/runtime/domains/startup';
import {
  createMpvOsdRuntimeHandlers,
  createCycleSecondarySubModeRuntimeHandler,
} from './main/runtime/domains/mpv';
import { buildSubtitleTrackDiagnostics } from './main/runtime/mpv-track-diagnostics';
import {
  resolveCanonicalPrimarySubtitle,
  resolvePrimarySubtitle,
} from './main/runtime/primary-subtitle-text';
import {
  createBuildCopyCurrentSubtitleMainDepsHandler,
  createBuildHandleMineSentenceDigitMainDepsHandler,
  createBuildHandleMultiCopyDigitMainDepsHandler,
  createBuildMarkLastCardAsAudioCardMainDepsHandler,
  createBuildMineSentenceCardMainDepsHandler,
  createBuildRefreshKnownWordCacheMainDepsHandler,
  createBuildTriggerFieldGroupingMainDepsHandler,
  createBuildUpdateLastCardFromClipboardMainDepsHandler,
  createMarkLastCardAsAudioCardHandler,
  createMineSentenceCardHandler,
  createRefreshKnownWordCacheHandler,
  createTriggerFieldGroupingHandler,
  createUpdateLastCardFromClipboardHandler,
  createCopyCurrentSubtitleHandler,
  createHandleMineSentenceDigitHandler,
  createHandleMultiCopyDigitHandler,
} from './main/runtime/domains/mining';
import {
  enforceUnsupportedWaylandMode,
  forceX11Backend,
  generateDefaultConfigFile,
  resolveConfiguredShortcuts,
  resolveKeybindings,
  showDesktopNotification,
} from './core/utils';
import {
  ensureDefaultConfigBootstrap,
  getDefaultConfigFilePaths,
  resolveDefaultMpvInstallPaths,
} from './shared/setup-state';
import {
  ImmersionTrackerService,
  JellyfinRemoteSessionService,
  MpvIpcClient,
  SubtitleWebSocket,
  Texthooker,
  applyMpvSubtitleRenderMetricsPatch,
  authenticateWithPasswordRuntime,
  broadcastRuntimeOptionsChangedRuntime,
  captureLiveSubtitleMiningContext,
  copyCurrentSubtitle as copyCurrentSubtitleCore,
  createConfigHotReloadRuntime,
  createDiscordPresenceService,
  createFieldGroupingOverlayRuntime,
  createOverlayContentMeasurementStore,
  createOverlayManager,
  createOverlayWindow as createOverlayWindowCore,
  createSubtitleProcessingController,
  createTokenizerDepsRuntime,
  cycleSecondarySubMode as cycleSecondarySubModeCore,
  deleteYomitanDictionaryByTitle,
  destroyYomitanSettingsWindow,
  getYomitanDictionaryInfo,
  handleMineSentenceDigit as handleMineSentenceDigitCore,
  handleMultiCopyDigit as handleMultiCopyDigitCore,
  hasMpvWebsocketPlugin,
  importYomitanDictionaryFromZip,
  initializeOverlayAnkiIntegration as initializeOverlayAnkiIntegrationCore,
  initializeOverlayRuntime as initializeOverlayRuntimeCore,
  isSubtitleAnnotationUpgrade,
  isOverlayWindowContentReady,
  jellyfinTicksToSecondsRuntime,
  listJellyfinItemsRuntime,
  listJellyfinLibrariesRuntime,
  listJellyfinSubtitleTracksRuntime,
  loadSubtitlePosition as loadSubtitlePositionCore,
  loadYomitanExtension as loadYomitanExtensionCore,
  markLastCardAsAudioCard as markLastCardAsAudioCardCore,
  mineSentenceCard as mineSentenceCardCore,
  openYomitanSettingsWindow,
  playNextSubtitleRuntime,
  promoteSettingsWindowAboveOverlay,
  registerGlobalShortcuts as registerGlobalShortcutsCore,
  replayCurrentSubtitleRuntime,
  resolveSanitizedSubtitleSeekCommand,
  resolveJellyfinPlaybackPlanRuntime,
  runStartupBootstrapRuntime,
  saveSubtitlePosition as saveSubtitlePositionCore,
  clearYomitanParserCachesForWindow,
  getYomitanCurrentAnkiDeckName as getYomitanCurrentAnkiDeckNameCore,
  sendMpvCommandRuntime,
  setMpvSubVisibilityRuntime,
  setOverlayDebugVisualizationEnabledRuntime,
  setVisibleOverlayVisible as setVisibleOverlayVisibleCore,
  showMpvOsdRuntime,
  tokenizeSubtitle as tokenizeSubtitleCore,
  triggerFieldGrouping as triggerFieldGroupingCore,
  upsertYomitanDictionarySettings,
  updateLastCardFromClipboard as updateLastCardFromClipboardCore,
} from './core/services';
import {
  acquireYoutubeSubtitleTrack,
  acquireYoutubeSubtitleTracks,
} from './core/services/youtube/generate';
import { applyOverlayClickThrough } from './core/services/overlay-click-through';
import { createYoutubeMediaCacheService } from './core/services/youtube/media-cache';
import { resolveYoutubePlaybackUrl } from './core/services/youtube/playback-resolve';
import { probeYoutubeTracks } from './core/services/youtube/track-probe';
import {
  destroyStatsWindow,
  registerStatsOverlayToggle,
  toggleStatsOverlay as toggleStatsOverlayWindow,
  withStatsWindowLayerSuspendedForNativeDialog,
} from './core/services/stats-window.js';
import {
  createFirstRunSetupService,
  getFirstRunSetupCompletionMessage,
  isStandaloneFirstRunSetupCommand,
  shouldAutoOpenFirstRunSetup,
} from './main/runtime/first-run-setup-service';
import { createYoutubeFlowRuntime } from './main/runtime/youtube-flow';
import { createYoutubeMediaCachePlaybackRuntime } from './main/runtime/youtube-media-cache-playback';
import { createYoutubePlaybackRuntime } from './main/runtime/youtube-playback-runtime';
import {
  clearYoutubePrimarySubtitleNotificationTimer,
  createYoutubePrimarySubtitleNotificationRuntime,
} from './main/runtime/youtube-primary-subtitle-notification';
import {
  createAutoplayReadyGate,
  type AutoplayReadySignal,
} from './main/runtime/autoplay-ready-gate';
import { createAutoplaySubtitlePrimingRuntime } from './main/runtime/autoplay-subtitle-priming-runtime';
import { createAutoplayTokenizationWarmRelease } from './main/runtime/autoplay-tokenization-warm-release';
import { isVisibleOverlayAutoplayTargetReady } from './main/runtime/visible-overlay-autoplay-readiness';
import { createManagedLocalSubtitleSelectionRuntime } from './main/runtime/local-subtitle-selection';
import {
  buildFirstRunSetupHtml,
  createMaybeFocusExistingFirstRunSetupWindowHandler,
  createOpenFirstRunSetupWindowHandler,
  parseFirstRunSetupSubmissionUrl,
  type FirstRunSetupSubmission,
} from './main/runtime/first-run-setup-window';
import {
  detectInstalledFirstRunPlugin,
  detectInstalledFirstRunPluginCandidates,
  detectInstalledMpvPlugin,
  removeLegacyMpvPluginCandidates,
} from './main/runtime/first-run-setup-plugin';
import { createWindowsMpvPluginDetectionRuntime } from './main/runtime/windows-mpv-plugin-detection';
import {
  applyWindowsMpvShortcuts,
  detectWindowsMpvShortcuts,
  resolveWindowsMpvShortcutPaths,
} from './main/runtime/windows-mpv-shortcuts';
import {
  detectCommandLineLauncher,
  installBun as installCommandLineBun,
  installLauncher as installCommandLineLauncher,
} from './main/runtime/command-line-launcher';
import {
  createWindowsMpvLaunchDeps,
  getConfiguredWindowsMpvPathStatus,
  launchWindowsMpv,
} from './main/runtime/windows-mpv-launch';
import { createWaitForMpvConnectedHandler } from './main/runtime/jellyfin-remote-connection';
import {
  DEFAULT_JELLYFIN_CLIENT_NAME,
  DEFAULT_JELLYFIN_CLIENT_VERSION,
  createHostDerivedJellyfinDeviceId,
} from './main/runtime/jellyfin-device-identity';
import {
  clearJellyfinAuthSessionAndRefreshTray as clearJellyfinAuthSessionAndRefreshTrayRuntime,
  isJellyfinConfiguredForTray as isJellyfinConfiguredForTrayRuntime,
  toggleJellyfinDiscoveryFromTray as toggleJellyfinDiscoveryFromTrayRuntime,
} from './main/runtime/jellyfin-tray-discovery';
import { createPrepareYoutubePlaybackInMpvHandler } from './main/runtime/youtube-playback-launch';
import {
  shouldEnsureTrayOnStartupForInitialArgs,
  shouldQuitOnMpvShutdownForTrayState,
  shouldQuitOnWindowAllClosedForTrayState,
} from './main/runtime/startup-tray-policy';
import { createLogExportTrayRuntime } from './main/runtime/log-export-tray';
import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup';
import { createBuildImmersionTrackerStartupMainDepsHandler } from './main/runtime/immersion-startup-main-deps';
import {
  createRunStatsCliCommandHandler,
  writeStatsCliCommandResponse,
} from './main/runtime/stats-cli-command';
import { createStatsServerRuntime } from './main/runtime/stats-server-runtime';
import { resolveLegacyVocabularyPosFromTokens } from './core/services/immersion-tracker/legacy-vocabulary-pos';
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
import {
  guessAnilistMediaInfo,
  updateAnilistPostWatchProgress,
} from './core/services/anilist/anilist-updater';
import { createCoverArtFetcher } from './core/services/anilist/cover-art-fetcher';
import { createAnilistRateLimiter } from './core/services/anilist/rate-limiter';
import { createJellyfinTokenStore } from './core/services/jellyfin-token-store';
import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options-ipc';
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
import { dispatchSessionAction as dispatchSessionActionCore } from './core/services/session-actions';
import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/domains/shortcuts';
import { createMainRuntimeRegistry } from './main/runtime/registry';
import {
  createEnsureOverlayMpvSubtitlesHiddenHandler,
  createRestoreOverlayMpvSubtitlesHandler,
} from './main/runtime/overlay-mpv-sub-visibility';
import {
  composeAnilistSetupHandlers,
  composeAnilistTrackingHandlers,
  composeAppReadyRuntime,
  composeCliStartupHandlers,
  composeHeadlessStartupHandlers,
  composeIpcRuntimeHandlers,
  composeJellyfinRuntimeHandlers,
  composeMpvRuntimeHandlers,
  composeOverlayVisibilityRuntime,
  composeShortcutRuntimes,
  composeStartupLifecycleHandlers,
} from './main/runtime/composers';
import { createOverlayWindowRuntimeHandlers } from './main/runtime/overlay-window-runtime-handlers';
import { tryBeginVisibleOverlayNumericSelection } from './main/runtime/overlay-numeric-selection';
import { createStartupBootstrapRuntimeDeps } from './main/startup';
import { createAppLifecycleRuntimeRunner } from './main/startup-lifecycle';
import {
  registerSecondInstanceHandlerEarly,
  requestSingleInstanceLockEarly,
  shouldBypassSingleInstanceLockForArgv,
} from './main/early-single-instance';
import { handleMpvCommandFromIpcRuntime } from './main/ipc-mpv-command';
import { registerIpcRuntimeServices } from './main/ipc-runtime';
import { createAnkiJimakuIpcRuntimeServiceDeps } from './main/dependencies';
import { createMainBootServices, type MainBootServicesResult } from './main/boot/services';
import { handleCliCommandRuntimeServiceWithContext } from './main/cli-runtime';
import { createOverlayModalRuntimeService } from './main/overlay-runtime';
import { createOverlayModalInputState } from './main/runtime/overlay-modal-input-state';
import { MediaTimingPreviewSession } from './core/services/media-timing-preview';
import { getSharedRemoteMediaWindowCache } from './core/services/remote-media-window-cache';
import { resolveMediaGenerationInput } from './anki-integration/media-source';
import { generateSpeechWaveform } from './core/services/media-timing-waveform';
import {
  collectMediaTimingContextLines,
  createMediaTimingReviewRuntime,
} from './main/runtime/media-timing-review';
import { openMediaTimingReviewModal } from './main/runtime/media-timing-review-open';
import { openYoutubeTrackPicker } from './main/runtime/youtube-picker-open';
import { openRuntimeOptionsModal as openRuntimeOptionsModalRuntime } from './main/runtime/runtime-options-open';
import { openJimakuModal as openJimakuModalRuntime } from './main/runtime/jimaku-open';
import { openTsukihimeModal as openTsukihimeModalRuntime } from './main/runtime/tsukihime-open';
import { openSubsyncManualModal as openSubsyncManualModalRuntime } from './main/runtime/subsync-open';
import { openSessionHelpModal as openSessionHelpModalRuntime } from './main/runtime/session-help-open';
import { openChangelogModal as openChangelogModalRuntime } from './main/runtime/changelog-open';
import { createChangelogRuntime } from './main/runtime/changelog/changelog-runtime';
import { openCharacterDictionaryManagerModal as openCharacterDictionaryManagerModalRuntime } from './main/runtime/character-dictionary-open';
import { openControllerSelectModal as openControllerSelectModalRuntime } from './main/runtime/controller-select-open';
import { openControllerDebugModal as openControllerDebugModalRuntime } from './main/runtime/controller-debug-open';
import { createPlaylistBrowserIpcRuntime } from './main/runtime/playlist-browser-ipc';
import { createSessionBindingsRuntime } from './main/runtime/session-bindings-runtime';
import { createOverlayShortcutsRuntimeService } from './main/overlay-shortcuts-runtime';
import {
  createFrequencyDictionaryRuntimeService,
  getFrequencyDictionarySearchPaths,
} from './main/frequency-dictionary-runtime';
import {
  createJlptDictionaryRuntimeService,
  getJlptDictionarySearchPaths,
} from './main/jlpt-runtime';
import { createMediaRuntimeService } from './main/media-runtime';
import { createOverlayVisibilityRuntimeService } from './main/overlay-visibility-runtime';
import { createDiscordPresenceRuntime } from './main/runtime/discord-presence-runtime';
import { createCharacterDictionaryRuntimeService } from './main/character-dictionary-runtime';
import { createCharacterDictionaryImageLookup } from './main/character-dictionary-runtime/image-lookup';
import { createCharacterNameCandidateLookup } from './main/character-dictionary-runtime/name-candidates';
import {
  createCharacterDictionaryAutoSyncRuntimeService,
  getCharacterDictionaryManagerSnapshot,
  moveCharacterDictionaryManagedEntry,
  removeCharacterDictionaryManagedEntry,
  replaceCharacterDictionaryManagedEntry,
} from './main/runtime/character-dictionary-auto-sync';
import { handleCharacterDictionaryAutoSyncComplete } from './main/runtime/character-dictionary-auto-sync-completion';
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
import { openCharacterDictionaryManagerWithConfigGate } from './main/runtime/character-dictionary-manager-gate';
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
import { resolveCurrentSubtitleForRenderer } from './main/runtime/current-subtitle-snapshot';
import { createOverlayGeometryRuntime } from './main/runtime/overlay-geometry-runtime';
import { createVisibleOverlayInteractionRuntime } from './main/runtime/visible-overlay-interaction-runtime';
import { createJellyfinSubtitleCacheIo } from './main/runtime/jellyfin-subtitle-cache-io';
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
import {
  INSTALL_UPDATE_ACTION_ID,
  UPDATE_AVAILABLE_NOTIFICATION_ID,
  VIEW_CHANGELOG_ACTION_ID,
} from './main/runtime/update/update-notifications';
import { createOverlayNotificationsRuntime } from './main/runtime/overlay-notifications-runtime';
import {
  runUpdateCliCommand,
  writeUpdateCliCommandResponse,
} from './main/runtime/update/update-cli-command';
import {
  runEnsureLinuxRuntimePluginAssetsCliCommand,
  writeEnsureLinuxRuntimePluginAssetsCliCommandResponse,
} from './main/runtime/linux-runtime-plugin-assets-cli-command';
import { ensureLinuxRuntimePluginAssets } from './main/runtime/linux-runtime-plugin-assets';
import { createUpdateServiceRuntime } from './main/runtime/update/update-service-runtime';
import {
  createRefreshSubtitlePrefetchFromActiveTrackHandler,
  createResolveActiveSubtitleSidebarSourceHandler,
} from './main/runtime/subtitle-prefetch-runtime';
import { createSecondarySubtitleTrackController } from './main/runtime/secondary-subtitle-track';
import {
  createCreateAnilistSetupWindowHandler,
  createCreateConfigSettingsWindowHandler,
  createCreateFirstRunSetupWindowHandler,
  createCreateJellyfinSetupWindowHandler,
  createCreateSyncUiWindowHandler,
} from './main/runtime/setup-window-factory';
import { createConfigSettingsRuntime } from './main/runtime/config-settings-runtime';
import { createOpenConfigSettingsWindowHandler } from './main/runtime/config-settings-window';
import { createSyncUiRuntime } from './main/runtime/sync-ui-runtime';
import { createSyncAutoScheduler } from './main/runtime/sync-auto-scheduler';
import { resolveSyncLauncherCommand, runSyncLauncher } from './main/runtime/sync-launcher-client';
import { getSyncHostsPath } from './shared/sync/sync-hosts-store';
import { shouldSuppressVisibleOverlayRaiseForSeparateWindow } from './main/runtime/settings-window-z-order';
import {
  isSameYoutubeMediaPath,
  isYoutubeMediaPath,
  shouldUseCachedYoutubeParsedCues,
} from './main/runtime/youtube-playback';
import { createYomitanProfilePolicy } from './main/runtime/yomitan-profile-policy';
import { reloadOverlayWindowsForYomitanContentScripts } from './main/runtime/yomitan-extension-overlay-reload';
import { formatSkippedYomitanWriteAction } from './main/runtime/yomitan-read-only-log';
import { createYomitanAnkiServerSyncRuntime } from './main/runtime/yomitan-anki-server-sync';
import {
  type AnilistMediaGuessRuntimeState,
  type StartupState,
  applyStartupState,
  createAppState,
  createInitialAnilistMediaGuessRuntimeState,
  createInitialAnilistUpdateInFlightState,
  transitionAnilistClientSecretState,
  transitionAnilistMediaGuessRuntimeState,
  transitionAnilistRetryQueueLastAttemptAt,
  transitionAnilistRetryQueueLastError,
  transitionAnilistRetryQueueState,
  transitionAnilistUpdateInFlightState,
} from './main/state';
import {
  isAllowedAnilistExternalUrl,
  isAllowedAnilistSetupNavigationUrl,
} from './main/anilist-url-guard';
import {
  ConfigService,
  ConfigStartupParseError,
  DEFAULT_CONFIG,
  DEFAULT_KEYBINDINGS,
  generateConfigTemplate,
} from './config';
import { resolveConfigDir } from './config/path-resolution';
import { buildConfigSettingsRegistry } from './config/settings/registry';
import { parseSubtitleCues } from './core/services/subtitle-cue-parser';
import {
  createSubtitlePrefetchService,
  type SubtitlePrefetchService,
} from './core/services/subtitle-prefetch';
import { buildSubtitleSidebarSourceKey } from './main/runtime/subtitle-prefetch-source';
import { createSubtitlePrefetchInitController } from './main/runtime/subtitle-prefetch-init';
import {
  createCachedInternalSubtitleTrackExtractor,
  loadSubtitleSourceText,
} from './main/runtime/internal-subtitle-extraction';
import { createRemoteMediaPathDetector } from './main/runtime/network-media-path';
import { applyCharacterDictionarySelection } from './main/character-dictionary-selection';
import { getSubsyncConfig } from './subsync/utils';

if (process.platform === 'linux') {
  app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal');
  const passwordStore = normalizePasswordStoreArg(
    getPasswordStoreArg(process.argv) ?? getDefaultPasswordStore(),
  );
  app.commandLine.appendSwitch('password-store', passwordStore);
  createLogger('main').debug(`Applied --password-store ${passwordStore}`);
}

app.setName('SubMiner');

const DEFAULT_TEXTHOOKER_PORT = 5174;
const DEFAULT_MPV_LOG_FILE = resolveDefaultLogFilePath('mpv', {
  platform: process.platform,
  homeDir: os.homedir(),
  appDataDir: process.env.APPDATA,
});
const ANILIST_SETUP_CLIENT_ID_URL = 'https://anilist.co/api/v2/oauth/authorize';
const jellyfinSubtitleCacheIo = createJellyfinSubtitleCacheIo({
  tmpDir: () => os.tmpdir(),
  makeTempDir: (prefix) => fs.promises.mkdtemp(prefix),
  writeFile: (filePath, bytes) => fs.promises.writeFile(filePath, bytes),
  removeDir: (dir, options) => {
    fs.rmSync(dir, options);
  },
  fetch: (url) => fetch(url),
});
const ANILIST_SETUP_RESPONSE_TYPE = 'token';
const ANILIST_DEFAULT_CLIENT_ID = '36084';
const ANILIST_REDIRECT_URI = 'https://anilist.subminer.moe/';
const ANILIST_DEVELOPER_SETTINGS_URL = 'https://anilist.co/settings/developer';
const ANILIST_UPDATE_MIN_WATCH_SECONDS = 10 * 60;
const ANILIST_DURATION_RETRY_INTERVAL_MS = 15_000;
const ANILIST_MAX_ATTEMPTED_UPDATE_KEYS = 1000;
const TRAY_TOOLTIP = 'SubMiner';
const JELLYFIN_SETUP_PRELOAD_PATH = path.join(__dirname, 'preload-jellyfin-setup.js');

let anilistMediaGuessRuntimeState: AnilistMediaGuessRuntimeState =
  createInitialAnilistMediaGuessRuntimeState();
let anilistUpdateInFlightState = createInitialAnilistUpdateInFlightState();
const anilistAttemptedUpdateKeys = new Set<string>();
let anilistCachedAccessToken: string | null = null;
let jellyfinPlayQuitOnDisconnectArmed = false;
const JELLYFIN_LANG_PREF = 'ja,jp,jpn,japanese,en,eng,english,enUS,en-US';
const JELLYFIN_TICKS_PER_SECOND = 10_000_000;
const JELLYFIN_REMOTE_PROGRESS_INTERVAL_MS = 3000;
const JELLYFIN_REMOTE_STARTUP_STOP_GRACE_MS = 10_000;
const DISCORD_PRESENCE_APP_ID = '1475264834730856619';
const JELLYFIN_MPV_CONNECT_TIMEOUT_MS = 3000;
const JELLYFIN_MPV_AUTO_LAUNCH_TIMEOUT_MS = 20000;
const YOUTUBE_MPV_CONNECT_TIMEOUT_MS = 3000;
const YOUTUBE_MPV_AUTO_LAUNCH_TIMEOUT_MS = 10000;
const YOUTUBE_MPV_YTDL_FORMAT = 'bestvideo*+bestaudio/best';
const YOUTUBE_DIRECT_PLAYBACK_FORMAT = 'b';
const MPV_JELLYFIN_DEFAULT_ARGS = [
  '--sub-auto=no',
  '--sub-file-paths=.;subs;subtitles',
  '--sid=no',
  '--secondary-sid=no',
  '--sub-visibility=no',
  '--secondary-sub-visibility=no',
  '--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
  '--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
] as const;

/**
 * Spawn a SubMiner-managed mpv (Jellyfin/YouTube) detached. On unsupported Wayland
 * sessions it is pinned to XWayland — Wayland-hint env stripped and an X11 GPU context
 * appended — so the XWayland overlay can stay above it, matching the `subminer` launcher.
 */
function spawnManagedMpvProcess(args: string[]): ReturnType<typeof spawn> {
  if (!shouldForceX11WaylandSession(process.env)) {
    return spawn('mpv', args, { detached: true, stdio: 'ignore' });
  }
  return spawn('mpv', [...args, ...MPV_X11_BACKEND_ARGS], {
    detached: true,
    stdio: 'ignore',
    env: applyX11EnvOverrides({ ...process.env }),
  });
}

let activeJellyfinRemotePlayback: ActiveJellyfinRemotePlaybackState | null = null;
let jellyfinRemoteLastProgressAtMs = 0;
let jellyfinMpvAutoLaunchInFlight: Promise<boolean> | null = null;
let backgroundWarmupsStarted = false;
let yomitanLoadInFlight: Promise<Extension | null> | null = null;
let notifyAnilistTokenStoreWarning: (message: string) => void = () => {};

const buildApplyJellyfinMpvDefaultsMainDepsHandler =
  createBuildApplyJellyfinMpvDefaultsMainDepsHandler({
    sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client, command),
    jellyfinLangPref: JELLYFIN_LANG_PREF,
  });
const applyJellyfinMpvDefaultsMainDeps = buildApplyJellyfinMpvDefaultsMainDepsHandler();
const applyJellyfinMpvDefaultsHandler = createApplyJellyfinMpvDefaultsHandler(
  applyJellyfinMpvDefaultsMainDeps,
);

const isDev = process.argv.includes('--dev') || process.argv.includes('--debug');
const texthookerService = new Texthooker(() => {
  const config = configService.getConfig();
  const characterDictionaryEnabled =
    config.subtitleStyle.nameMatchEnabled && yomitanProfilePolicy.isCharacterDictionaryEnabled();
  const knownWordColoringEnabled = getRuntimeBooleanOption(
    'subtitle.annotation.knownWords.highlightEnabled',
    config.ankiConnect.knownWords.highlightEnabled,
  );
  const nPlusOneColoringEnabled = getRuntimeBooleanOption(
    'subtitle.annotation.nPlusOne',
    config.ankiConnect.nPlusOne.enabled,
  );

  return {
    enableKnownWordColoring: knownWordColoringEnabled,
    enableNPlusOneColoring: nPlusOneColoringEnabled,
    enableNameMatchColoring: config.subtitleStyle.nameMatchEnabled && characterDictionaryEnabled,
    enableFrequencyColoring: getRuntimeBooleanOption(
      'subtitle.annotation.frequency',
      config.subtitleStyle.frequencyDictionary.enabled,
    ),
    enableJlptColoring: getRuntimeBooleanOption(
      'subtitle.annotation.jlpt',
      config.subtitleStyle.enableJlpt,
    ),
    characterDictionaryEnabled,
    knownWordColor: config.subtitleStyle.knownWordColor,
    nPlusOneColor: config.subtitleStyle.nPlusOneColor,
    nameMatchColor: config.subtitleStyle.nameMatchColor,
    hoverTokenColor: config.subtitleStyle.hoverTokenColor,
    hoverTokenBackgroundColor: config.subtitleStyle.hoverTokenBackgroundColor,
    jlptColors: config.subtitleStyle.jlptColors,
    frequencyDictionary: {
      singleColor: config.subtitleStyle.frequencyDictionary.singleColor,
      bandedColors: config.subtitleStyle.frequencyDictionary.bandedColors,
    },
  };
});
let syncOverlayShortcutsForModal: (isActive: boolean) => void = () => {};
let syncOverlayVisibilityForModal: () => void = () => {};
const buildGetDefaultSocketPathMainDepsHandler = createBuildGetDefaultSocketPathMainDepsHandler({
  platform: process.platform,
});
const getDefaultSocketPathMainDeps = buildGetDefaultSocketPathMainDepsHandler();
const getDefaultSocketPathHandler = createGetDefaultSocketPathHandler(getDefaultSocketPathMainDeps);

type BootServices = MainBootServicesResult<
  ConfigService,
  ReturnType<typeof createAnilistTokenStore>,
  ReturnType<typeof createJellyfinTokenStore>,
  ReturnType<typeof createAnilistUpdateQueue>,
  SubtitleWebSocket,
  ReturnType<typeof createLogger>,
  ReturnType<typeof createMainRuntimeRegistry>,
  ReturnType<typeof createOverlayManager>,
  ReturnType<typeof createOverlayModalInputState>,
  ReturnType<typeof createOverlayContentMeasurementStore>,
  ReturnType<typeof createOverlayModalRuntimeService>,
  ReturnType<typeof createAppState>,
  {
    requestSingleInstanceLock: () => boolean;
    quit: () => void;
    exit: (code?: number) => void;
    on: (event: string, listener: (...args: unknown[]) => void) => unknown;
    whenReady: () => Promise<void>;
  }
>;

const bootServices = createMainBootServices({
  platform: process.platform,
  argv: process.argv,
  appDataDir: process.env.APPDATA,
  xdgConfigHome: process.env.XDG_CONFIG_HOME,
  homeDir: os.homedir(),
  defaultMpvLogFile: DEFAULT_MPV_LOG_FILE,
  envMpvLog: process.env.SUBMINER_MPV_LOG,
  defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
  getDefaultSocketPath: () => getDefaultSocketPathHandler(),
  resolveConfigDir,
  existsSync: fs.existsSync,
  mkdirSync: fs.mkdirSync,
  joinPath: (...parts) => path.join(...parts),
  app,
  shouldBypassSingleInstanceLock: () => shouldBypassSingleInstanceLockForArgv(process.argv),
  requestSingleInstanceLockEarly: () => requestSingleInstanceLockEarly(app),
  registerSecondInstanceHandlerEarly: (listener) => {
    registerSecondInstanceHandlerEarly(app, listener);
  },
  onConfigStartupParseError: (error) => {
    failStartupFromConfig(
      'SubMiner config parse error',
      buildConfigParseErrorDetails(error.path, error.parseError),
      {
        logError: (details) => console.error(details),
        showErrorBox: (title, details) => dialog.showErrorBox(title, details),
        quit: () => requestAppQuit(),
      },
    );
  },
  createConfigService: (configDir) => new ConfigService(configDir),
  createAnilistTokenStore: (targetPath) =>
    createAnilistTokenStore(targetPath, {
      info: (message: string) => console.info(message),
      warn: (message: string, details?: unknown) => console.warn(message, details),
      error: (message: string, details?: unknown) => console.error(message, details),
      warnUser: (message: string) => notifyAnilistTokenStoreWarning(message),
    }),
  createJellyfinTokenStore: (targetPath) =>
    createJellyfinTokenStore(targetPath, {
      info: (message: string) => console.info(message),
      warn: (message: string, details?: unknown) => console.warn(message, details),
      error: (message: string, details?: unknown) => console.error(message, details),
    }),
  createAnilistUpdateQueue: (targetPath) =>
    createAnilistUpdateQueue(targetPath, {
      info: (message: string) => console.info(message),
      warn: (message: string, details?: unknown) => console.warn(message, details),
      error: (message: string, details?: unknown) => console.error(message, details),
    }),
  createSubtitleWebSocket: (payloadMode) => new SubtitleWebSocket(payloadMode),
  createLogger,
  createMainRuntimeRegistry,
  createOverlayManager: () =>
    createOverlayManager({
      shouldPromoteWindowOnBoundsUpdate: (window) =>
        !shouldSuppressVisibleOverlayRaiseForSeparateWindow({
          window,
          mainWindow: overlayManager.getMainWindow(),
          separateWindows: getOverlayForegroundSeparateWindows(),
        }),
    }),
  createOverlayModalInputState,
  createOverlayContentMeasurementStore: ({ logger }) => {
    const buildHandler = createBuildOverlayContentMeasurementStoreMainDepsHandler({
      now: () => Date.now(),
      warn: (message: string) => logger.warn(message),
    });
    return createOverlayContentMeasurementStore(buildHandler());
  },
  getSyncOverlayShortcutsForModal: () => syncOverlayShortcutsForModal,
  getSyncOverlayVisibilityForModal: () => syncOverlayVisibilityForModal,
  createOverlayModalRuntime: ({ overlayManager, overlayModalInputState }) => {
    const buildHandler = createBuildOverlayModalRuntimeMainDepsHandler({
      getMainWindow: () => overlayManager.getMainWindow(),
      getModalWindow: () => overlayManager.getModalWindow(),
      createModalWindow: () => createModalWindow(),
      getModalGeometry: () => overlayGeometryRuntime.getCurrentOverlayGeometry(),
      setModalWindowBounds: (geometry) => overlayManager.setModalWindowBounds(geometry),
    });
    return createOverlayModalRuntimeService(buildHandler(), {
      onModalStateChange: (isActive: boolean) =>
        overlayModalInputState.handleModalInputStateChange(isActive),
      // On macOS, after the last modal closes the post-close visibility sync hides the overlay
      // when neither it nor mpv is focused, and keyboard focus is left in limbo (mpv keys like
      // shift-to-unpause stop working). Programmatically focusing mpv from our background helper
      // is refused by macOS (most visibly in native fullscreen), so instead resign SubMiner's
      // active status — exactly what a manual click does — handing focus back to the previously
      // active app (mpv). The overlay is already hidden at this point, so app.hide() hides nothing
      // visible; once mpv is focused the tracker re-shows the overlay above it.
      onFinalModalClosed: () => {
        void restoreMacOSMpvFocusAfterModalClose({
          platform: process.platform,
          focusMpv: async () => {
            app.hide();
          },
          getWindowTracker: () => appState.windowTracker,
          updateVisibleOverlayVisibility: () =>
            overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
          warn: (message, details) => logger.warn(message, details),
        });
      },
    });
  },
  createAppState,
}) as BootServices;
const {
  configDir: CONFIG_DIR,
  userDataPath: USER_DATA_PATH,
  defaultMpvLogPath: DEFAULT_MPV_LOG_PATH,
  defaultImmersionDbPath: DEFAULT_IMMERSION_DB_PATH,
  configService,
  anilistTokenStore,
  jellyfinTokenStore,
  anilistUpdateQueue,
  subtitleWsService,
  annotationSubtitleWsService,
  logger,
  runtimeRegistry,
  overlayManager,
  overlayModalInputState,
  overlayContentMeasurementStore,
  overlayModalRuntime,
  appState,
  appLifecycleApp,
} = bootServices;
let pendingSubtitleMiningContext: SubtitleMiningContext | null = null;
const configSettingsFields = buildConfigSettingsRegistry(DEFAULT_CONFIG);

function getOverlayForegroundSeparateWindows(): BrowserWindow[] {
  return [
    appState.configSettingsWindow,
    appState.syncUiWindow,
    appState.yomitanSettingsWindow,
    appState.anilistSetupWindow,
    appState.jellyfinSetupWindow,
    appState.firstRunSetupWindow,
  ].filter((window): window is BrowserWindow => Boolean(window));
}

notifyAnilistTokenStoreWarning = (message: string) => {
  logger.warn(`[AniList] ${message}`);
  try {
    showDesktopNotification('SubMiner AniList', {
      body: message,
    });
  } catch {
    // Notification may fail if desktop notifications are unavailable early in startup.
  }
};
const appLogger = {
  logInfo: (message: string) => {
    logger.info(message);
  },
  logDebug: (message: string) => {
    logger.debug(message);
  },
  logWarning: (message: string) => {
    logger.warn(message);
  },
  logError: (message: string, details: unknown) => {
    logger.error(message, details);
  },
  logNoRunningInstance: () => {
    logger.error('No running instance. Use --start to launch the app.');
  },
  logConfigWarning: (warning: {
    path: string;
    message: string;
    value: unknown;
    fallback: unknown;
  }) => {
    logger.warn(
      `[config] ${warning.path}: ${warning.message} value=${JSON.stringify(warning.value)} fallback=${JSON.stringify(warning.fallback)}`,
    );
  },
};
const reportFatalError = createFatalErrorReporter({
  showErrorBox: (title, details) => dialog.showErrorBox(title, details),
  consoleError: (message, error) => logger.error(message, error),
});

let forceQuitTimer: ReturnType<typeof setTimeout> | null = null;
const statsDistPath = path.join(__dirname, '..', 'stats', 'dist');
const statsPreloadPath = path.join(__dirname, 'preload-stats.js');
const statsServerRuntime = createStatsServerRuntime({
  userDataPath: USER_DATA_PATH,
  statsDistPath,
  getResolvedConfig: () => configService.getConfig(),
  getImmersionTracker: () => appState.immersionTracker,
  setAppStateStatsServer: (server) => {
    appState.statsServer = server;
  },
  getMpvSocketPath: () => appState.mpvSocketPath,
  getYomitanExt: () => appState.yomitanExt,
  getYomitanSession: () => appState.yomitanSession,
  getYomitanParserWindow: () => appState.yomitanParserWindow,
  setYomitanParserWindow: (w) => {
    appState.yomitanParserWindow = w;
  },
  getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
  setYomitanParserReadyPromise: (p) => {
    appState.yomitanParserReadyPromise = p;
  },
  getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
  setYomitanParserInitPromise: (p) => {
    appState.yomitanParserInitPromise = p;
  },
  getYomitanAnkiDeckName: () => getCurrentYomitanAnkiDeckNameForRuntime(),
  getAnilistRateLimiter: () => anilistRateLimiter,
  resolveAnkiNoteId: (noteId) => appState.ankiIntegration?.resolveCurrentNoteId(noteId) ?? noteId,
  trackDuplicateNoteIdsForNote: (noteId, duplicateNoteIds) => {
    appState.ankiIntegration?.trackDuplicateNoteIdsForNote(noteId, duplicateNoteIds);
  },
  resolveSentenceSearchHeadwords: (term) => resolveSentenceSearchHeadwords(term),
  ensureImmersionTrackerStarted: () => ensureImmersionTrackerStarted(),
  setStatsStartupInProgress: (inProgress) => {
    appState.statsStartupInProgress = inProgress;
  },
});
const {
  stopStatsServer,
  ensureStatsServerStarted,
  ensureBackgroundStatsServerStarted,
  stopBackgroundStatsServer,
} = statsServerRuntime;

function requestAppQuit(): void {
  destroyYomitanSettingsWindow(appState.yomitanSettingsWindow);
  appState.yomitanSettingsWindow = null;
  destroyStatsWindow();
  stopStatsServer();
  if (!forceQuitTimer) {
    forceQuitTimer = setTimeout(() => {
      logger.warn('App quit timed out; forcing process exit.');
      app.exit(0);
    }, 2000);
  }
  app.quit();
}

process.on('SIGINT', () => {
  requestAppQuit();
});
process.on('SIGTERM', () => {
  requestAppQuit();
});

const youtubeFlowRuntime = createYoutubeFlowRuntime({
  probeYoutubeTracks: (url: string) => probeYoutubeTracks(url),
  acquireYoutubeSubtitleTrack: (input) => acquireYoutubeSubtitleTrack(input),
  acquireYoutubeSubtitleTracks: (input) => acquireYoutubeSubtitleTracks(input),
  openPicker: async (payload) => {
    return await openYoutubeTrackPicker(
      {
        sendToActiveOverlayWindow: (channel, nextPayload, runtimeOptions) =>
          overlayModalRuntime.sendToActiveOverlayWindow(channel, nextPayload, runtimeOptions),
        waitForModalOpen: (modal, timeoutMs) =>
          overlayModalRuntime.waitForModalOpen(modal, timeoutMs),
        logWarn: (message) => logger.warn(message),
      },
      payload,
    );
  },
  pauseMpv: () => {
    sendMpvCommandRuntime(appState.mpvClient, ['set_property', 'pause', 'yes']);
  },
  resumeMpv: () => {
    sendMpvCommandRuntime(appState.mpvClient, ['set_property', 'pause', 'no']);
  },
  sendMpvCommand: (command) => {
    sendMpvCommandRuntime(appState.mpvClient, command);
  },
  requestMpvProperty: async (name: string) => {
    const client = appState.mpvClient;
    if (!client) return null;
    return await client.requestProperty(name);
  },
  refreshCurrentSubtitle: (text: string) => {
    subtitleProcessingController.refreshCurrentSubtitle(text);
  },
  refreshSubtitleSidebarSource: async (sourcePath: string, mediaPath?: string) => {
    await subtitlePrefetchRuntime.refreshSubtitleSidebarFromSource(sourcePath, mediaPath);
  },
  startTokenizationWarmups: async () => {
    await startTokenizationWarmups();
  },
  waitForTokenizationReady: async () => {
    await currentMediaTokenizationGate.waitUntilReady(
      appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null,
    );
  },
  waitForAnkiReady: async () => {
    const integration = appState.ankiIntegration;
    if (!integration) {
      return;
    }
    try {
      await Promise.race([
        integration.waitUntilReady(),
        new Promise<never>((_, reject) => {
          setTimeout(
            () => reject(new Error('Timed out waiting for AnkiConnect integration')),
            2500,
          );
        }),
      ]);
    } catch (error) {
      logger.warn(
        'Continuing YouTube playback before AnkiConnect integration reported ready:',
        error instanceof Error ? error.message : String(error),
      );
    }
  },
  wait: (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)),
  waitForPlaybackWindowReady: async () => {
    const deadline = Date.now() + 4000;
    let stableGeometry: WindowGeometry | null = null;
    let stableSinceMs = 0;
    while (Date.now() < deadline) {
      const tracker = appState.windowTracker;
      const trackerGeometry = tracker?.getGeometry() ?? null;
      const mediaPath =
        appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || '';
      const trackerFocused = tracker?.isTargetWindowFocused() ?? false;
      if (tracker && tracker.isTracking() && trackerGeometry && trackerFocused && mediaPath) {
        if (!overlayGeometryRuntime.geometryMatches(stableGeometry, trackerGeometry)) {
          stableGeometry = trackerGeometry;
          stableSinceMs = Date.now();
        } else if (Date.now() - stableSinceMs >= 200) {
          return;
        }
      } else {
        stableGeometry = null;
        stableSinceMs = 0;
      }
      await new Promise((resolve) => setTimeout(resolve, 100));
    }
    logger.warn(
      'Timed out waiting for tracked playback window focus/media readiness before opening YouTube subtitle picker.',
    );
  },
  waitForOverlayGeometryReady: async () => {
    const deadline = Date.now() + 4000;
    while (Date.now() < deadline) {
      const tracker = appState.windowTracker;
      const trackerGeometry = tracker?.getGeometry() ?? null;
      if (
        trackerGeometry &&
        overlayGeometryRuntime.geometryMatches(
          overlayGeometryRuntime.getLastOverlayWindowGeometry(),
          trackerGeometry,
        )
      ) {
        return;
      }
      await new Promise((resolve) => setTimeout(resolve, 50));
    }
    logger.warn('Timed out waiting for overlay geometry to match tracked playback window.');
  },
  focusOverlayWindow: () => {
    const mainWindow = overlayManager.getMainWindow();
    if (!mainWindow || mainWindow.isDestroyed()) {
      return;
    }
    mainWindow.setIgnoreMouseEvents(false);
    if (!mainWindow.isFocused()) {
      mainWindow.focus();
    }
    if (!mainWindow.webContents.isFocused()) {
      mainWindow.webContents.focus();
    }
  },
  showMpvOsd: (text: string) => overlayNotificationsRuntime.showYoutubeFlowStatusNotification(text),
  reportSubtitleFailure: (message: string) => reportYoutubeSubtitleFailure(message),
  notifyPrimarySubtitleLoaded: () =>
    youtubePrimarySubtitleNotificationRuntime.markCurrentMediaPrimarySubtitleLoaded(),
  warn: (message: string) => logger.warn(message),
  log: (message: string) => logger.info(message),
  getYoutubeOutputDir: () => path.join(os.homedir(), '.cache', 'subminer', 'youtube-subs'),
  createSubtitleTempDir: () =>
    fs.promises.mkdtemp(path.join(os.tmpdir(), 'subminer-youtube-subtitles-')),
  cleanupSubtitleTempDirs: (dirs) => {
    for (const dir of dirs) {
      fs.rmSync(dir, { recursive: true, force: true });
    }
  },
});
const prepareYoutubePlaybackInMpv = createPrepareYoutubePlaybackInMpvHandler({
  requestPath: async () => {
    const client = appState.mpvClient;
    if (!client) return null;
    const value = await client.requestProperty('path').catch(() => null);
    return typeof value === 'string' ? value : null;
  },
  requestProperty: async (name) => {
    const client = appState.mpvClient;
    if (!client) return null;
    return await client.requestProperty(name);
  },
  sendMpvCommand: (command) => {
    sendMpvCommandRuntime(appState.mpvClient, command);
  },
  wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
});
const youtubeMediaCache = createYoutubeMediaCacheService({
  onDownloadStarted: (event) => {
    overlayNotificationsRuntime.showConfiguredStatusNotification(
      'YouTube media cache is downloading.',
      {
        id: 'youtube-media-cache-status',
        title: 'YouTube media cache',
        variant: 'progress',
        persistent: true,
      },
    );
    logger.info(`YouTube media cache download notification shown for ${event.url}`);
  },
  onReady: (event) => {
    overlayNotificationsRuntime.showConfiguredStatusNotification('YouTube media cache ready.', {
      id: 'youtube-media-cache-status',
      title: 'YouTube media cache',
      variant: 'success',
      persistent: false,
    });
    void appState.ankiIntegration
      ?.handleYoutubeMediaCacheReady(event.url, event.path, { notifyNoQueued: false })
      .catch((error) => {
        logger.warn(
          `Failed to apply queued YouTube media updates: ${
            error instanceof Error ? error.message : String(error)
          }`,
        );
      });
  },
  onFailed: (event) => {
    overlayNotificationsRuntime.showConfiguredStatusNotification('YouTube media cache failed.', {
      id: 'youtube-media-cache-status',
      title: 'YouTube media cache',
      variant: 'error',
      persistent: false,
    });
    void appState.ankiIntegration
      ?.handleYoutubeMediaCacheFailed(event.url, { notifyStatus: false })
      .catch((error) => {
        logger.warn(
          `Failed to drain queued YouTube media updates after cache failure: ${
            error instanceof Error ? error.message : String(error)
          }`,
        );
      });
  },
  logInfo: (message) => logger.info(message),
  logWarn: (message) => logger.warn(message),
});
const youtubeMediaCachePlaybackRuntime = createYoutubeMediaCachePlaybackRuntime({
  getMediaCacheConfig: () => configService.getConfig().youtube.mediaCache,
  requestMpvProperty: async (name) => {
    const client = appState.mpvClient;
    if (!client) return null;
    return await client.requestProperty(name);
  },
  startYoutubeMediaCache: (url, options) => {
    youtubeMediaCache.start(url, options);
  },
  logWarn: (message) => logger.warn(message),
});
const waitForYoutubeMpvConnected = createWaitForMpvConnectedHandler({
  getMpvClient: () => appState.mpvClient,
  now: () => Date.now(),
  sleep: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
});
const POST_WARM_AUTOPLAY_SUBTITLE_PRIME_DELAYS_MS = [0, 75, 200, 500, 1_000] as const;

function schedulePostWarmAutoplaySubtitlePrime(signal: AutoplayReadySignal): void {
  if (signal.payload.text.trim() !== '__warm__') {
    return;
  }

  const mediaPath = signal.mediaPath;
  for (const delayMs of POST_WARM_AUTOPLAY_SUBTITLE_PRIME_DELAYS_MS) {
    const timer = setTimeout(() => {
      const currentMediaPath =
        appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null;
      if (currentMediaPath !== mediaPath || !overlayManager.getVisibleOverlayVisible()) {
        return;
      }
      void autoplaySubtitlePrimingRuntime
        .primeCurrentSubtitleForAutoplay(mediaPath)
        .catch((error) => {
          logger.debug(
            `[autoplay-subtitle-prime] failed to prime current subtitle after warm readiness: ${
              error instanceof Error ? error.message : String(error)
            }`,
          );
        });
    }, delayMs);
    timer.unref?.();
  }
}

const autoplayReadyGate = createAutoplayReadyGate({
  isAppOwnedFlowInFlight: () => youtubePrimarySubtitleNotificationRuntime.isAppOwnedFlowInFlight(),
  getCurrentMediaPath: () => appState.currentMediaPath,
  getCurrentVideoPath: () => appState.mpvClient?.currentVideoPath ?? null,
  getPlaybackPaused: () => appState.playbackPaused,
  getMpvClient: () => appState.mpvClient,
  signalPluginAutoplayReady: () => {
    sendMpvCommandRuntime(appState.mpvClient, ['script-message', 'subminer-autoplay-ready']);
  },
  // Deferred: isTokenizationWarmupReady is assigned during composeMpvRuntimeHandlers below.
  isTokenizationReady: () => isTokenizationWarmupReady(),
  requestOverlayPointerRecovery: () => {
    if (process.platform !== 'darwin' || !overlayManager.getVisibleOverlayVisible()) {
      return;
    }
    // Renderer-side recovery alone cannot wake subtitle hover on macOS: the overlay only
    // receives mouse-moved events while it is the key window of the frontmost app, and mpv
    // is frontmost once playback starts. Activate the overlay window first so the broadcast's
    // setIgnoreMouseEvents toggling actually takes effect, mirroring a manual subtitle click.
    focusMacOSOverlayWindow({
      platform: process.platform,
      getOverlayWindow: () => overlayManager.getMainWindow(),
      stealAppFocus: () => app.focus({ steal: true }),
      warn: (message, details) => logger.warn(message, details),
    });
    overlayManager.broadcastToOverlayWindows(IPC_CHANNELS.event.overlayPointerRecoveryRequest);
  },
  onAutoplayReadyReleased: (signal) => {
    schedulePostWarmAutoplaySubtitlePrime(signal);
  },
  isSignalTargetReady: (signal) =>
    isVisibleOverlayAutoplayTargetReady(
      {
        getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
        isOverlayWindowReady: () => {
          const overlayWindow = overlayManager.getMainWindow();
          return Boolean(overlayWindow && isOverlayWindowContentReady(overlayWindow));
        },
        getLatestVisibleMeasurement: () =>
          overlayContentMeasurementStore.getLatestByLayer('visible'),
      },
      signal,
    ),
  schedule: (callback, delayMs) => setTimeout(callback, delayMs),
  logDebug: (message) => logger.debug(message),
});
const managedLocalSubtitleSelectionRuntime = createManagedLocalSubtitleSelectionRuntime({
  getCurrentMediaPath: () => appState.currentMediaPath,
  getMpvClient: () => appState.mpvClient,
  getPrimarySubtitleLanguages: () => configService.getConfig().youtube.primarySubLanguages,
  getSecondarySubtitleLanguages: () => configService.getConfig().secondarySub.secondarySubLanguages,
  sendMpvCommand: (command) => {
    sendMpvCommandRuntime(appState.mpvClient, command);
  },
  schedule: (callback, delayMs) => setTimeout(callback, delayMs),
  clearScheduled: (timer) => clearTimeout(timer),
});

const {
  resolveBundledMpvRuntimePluginEntrypoint,
  detectWindowsInstalledMpvPlugin,
  logInstalledMpvPluginDetected,
  promptForLegacyMpvPluginRemovalBeforeWindowsLaunch,
} = createWindowsMpvPluginDetectionRuntime({
  mainDirname: __dirname,
  logWarn: (message) => logger.warn(message),
});

const youtubePlaybackRuntime = createYoutubePlaybackRuntime({
  platform: process.platform,
  directPlaybackFormat: YOUTUBE_DIRECT_PLAYBACK_FORMAT,
  mpvYtdlFormat: YOUTUBE_MPV_YTDL_FORMAT,
  autoLaunchTimeoutMs: YOUTUBE_MPV_AUTO_LAUNCH_TIMEOUT_MS,
  connectTimeoutMs: YOUTUBE_MPV_CONNECT_TIMEOUT_MS,
  getSocketPath: () => appState.mpvSocketPath,
  getMpvConnected: () => Boolean(appState.mpvClient?.connected),
  invalidatePendingAutoplayReadyFallbacks: () =>
    autoplayReadyGate.invalidatePendingAutoplayReadyFallbacks(),
  setAppOwnedFlowInFlight: (next) => {
    youtubePrimarySubtitleNotificationRuntime.setAppOwnedFlowInFlight(next);
  },
  ensureYoutubePlaybackRuntimeReady: async () => {
    await ensureYoutubePlaybackRuntimeReady();
  },
  resolveYoutubePlaybackUrl: (url, format) => resolveYoutubePlaybackUrl(url, format),
  launchWindowsMpv: (playbackUrl, args) => {
    const config = configService.getConfig();
    setLogFileToggles(config.logging.files);
    const mpvLogPath = isLogFileEnabled('mpv') ? resolveDefaultLogFilePath('mpv') : '';
    if (mpvLogPath) {
      pruneLogDirectoryForPath(mpvLogPath, config.logging.rotation);
    }
    const mpvArgs = [...args, ...buildMpvLoggingArgs(config.logging.level, mpvLogPath, args)];
    return launchWindowsMpv(
      [playbackUrl],
      createWindowsMpvLaunchDeps({
        showError: (title, content) => dialog.showErrorBox(title, content),
      }),
      mpvArgs,
      process.execPath,
      resolveBundledMpvRuntimePluginEntrypoint(),
      config.mpv.executablePath,
      config.mpv.launchMode,
      {
        detectInstalledMpvPlugin: detectWindowsInstalledMpvPlugin,
        notifyInstalledPluginDetected: logInstalledMpvPluginDetected,
        resolveInstalledPluginBeforeLaunch: (detection, mpvPath) =>
          promptForLegacyMpvPluginRemovalBeforeWindowsLaunch(mpvPath, detection),
      },
      getMpvPluginRuntimeConfig(),
    );
  },
  waitForYoutubeMpvConnected: (timeoutMs) => waitForYoutubeMpvConnected(timeoutMs),
  prepareYoutubePlaybackInMpv: (request) => prepareYoutubePlaybackInMpv(request),
  startYoutubeMediaCache: (url) => {
    const mediaCacheConfig = configService.getConfig().youtube.mediaCache;
    youtubeMediaCache.start(url, {
      mode: mediaCacheConfig.mode,
      maxHeight: mediaCacheConfig.maxHeight,
    });
  },
  runYoutubePlaybackFlow: (request) => youtubeFlowRuntime.runYoutubePlaybackFlow(request),
  logInfo: (message) => logger.info(message),
  logWarn: (message) => logger.warn(message),
  schedule: (callback, delayMs) => setTimeout(callback, delayMs),
  clearScheduled: (timer) => clearTimeout(timer),
});

function getMpvPluginRuntimeConfig() {
  const config = configService.getConfig();
  return {
    socketPath: appState.mpvSocketPath,
    binaryPath: config.mpv.subminerBinaryPath,
    backend: config.mpv.backend,
    autoStart: config.mpv.autoStartSubMiner,
    autoStartVisibleOverlay: config.auto_start_overlay,
    autoStartPauseUntilReady: config.mpv.pauseUntilOverlayReady,
    osdMessages:
      config.ankiConnect.behavior.notificationType === 'osd' ||
      config.ankiConnect.behavior.notificationType === 'osd-system',
    texthookerEnabled: config.texthooker.launchAtStartup,
  };
}

let firstRunSetupMessage: string | null = null;
const resolveWindowsMpvShortcutRuntimePaths = () =>
  resolveWindowsMpvShortcutPaths({
    appDataDir: app.getPath('appData'),
    desktopDir: app.getPath('desktop'),
  });
const createCommandLineLauncherRuntimeOptions = () => ({
  platform: process.platform,
  env: process.env,
  homeDir: os.homedir(),
  localAppData: process.env.LOCALAPPDATA,
  userProfile: process.env.USERPROFILE,
  cwd: process.cwd(),
  resourcesPath: process.resourcesPath,
  appExePath: process.execPath,
});
const firstRunSetupService = createFirstRunSetupService({
  platform: process.platform,
  configDir: CONFIG_DIR,
  getYomitanDictionaryCount: async () => {
    await ensureYomitanExtensionLoaded();
    const dictionaries = await getYomitanDictionaryInfo(getYomitanParserRuntimeDeps(), {
      error: (message, ...args) => logger.error(message, ...args),
      info: (message, ...args) => logger.info(message, ...args),
    });
    return dictionaries.length;
  },
  isExternalYomitanConfigured: () =>
    configService.getConfig().yomitan.externalProfilePath.trim().length > 0,
  detectPluginInstalled: () => {
    const candidates = detectInstalledFirstRunPluginCandidates({
      platform: process.platform,
      homeDir: os.homedir(),
      xdgConfigHome: process.env.XDG_CONFIG_HOME,
      appDataDir: app.getPath('appData'),
      mpvExecutablePath: configService.getConfig().mpv.executablePath,
    });
    if (candidates.length > 0) {
      return true;
    }
    const installPaths = resolveDefaultMpvInstallPaths(
      process.platform,
      os.homedir(),
      process.env.XDG_CONFIG_HOME,
    );
    return detectInstalledFirstRunPlugin(installPaths);
  },
  detectLegacyMpvPluginCandidates: () =>
    detectInstalledFirstRunPluginCandidates({
      platform: process.platform,
      homeDir: os.homedir(),
      xdgConfigHome: process.env.XDG_CONFIG_HOME,
      appDataDir: app.getPath('appData'),
      mpvExecutablePath: configService.getConfig().mpv.executablePath,
    }),
  removeLegacyMpvPlugins: (candidates) =>
    removeLegacyMpvPluginCandidates({
      candidates,
      trashItem: (candidatePath) => shell.trashItem(candidatePath),
    }),
  detectWindowsMpvShortcuts: () => {
    if (process.platform !== 'win32') {
      return {
        startMenuInstalled: false,
        desktopInstalled: false,
      };
    }
    return detectWindowsMpvShortcuts(resolveWindowsMpvShortcutRuntimePaths());
  },
  applyWindowsMpvShortcuts: async (preferences) => {
    if (process.platform !== 'win32') {
      return {
        ok: true,
        status: 'unknown' as const,
        message: '',
      };
    }
    return applyWindowsMpvShortcuts({
      preferences,
      paths: resolveWindowsMpvShortcutRuntimePaths(),
      exePath: process.execPath,
      writeShortcutLink: (shortcutPath, operation, details) =>
        shell.writeShortcutLink(shortcutPath, operation, details),
    });
  },
  detectCommandLineLauncher: () =>
    detectCommandLineLauncher(createCommandLineLauncherRuntimeOptions()),
  installBun: async () => {
    const snapshot = await installCommandLineBun(createCommandLineLauncherRuntimeOptions());
    return {
      ok: snapshot.status === 'ready',
      message:
        snapshot.message ??
        (snapshot.status === 'ready'
          ? 'Bun is ready. Open a new terminal.'
          : 'Bun installation failed.'),
    };
  },
  installCommandLineLauncher: async () => {
    const snapshot = await installCommandLineLauncher(createCommandLineLauncherRuntimeOptions());
    const ok = snapshot.status === 'ready' || snapshot.status === 'installed_bun_missing';
    return {
      ok,
      installPath: snapshot.installPath,
      message:
        snapshot.message ??
        (ok
          ? 'Command-line launcher installed. Open a new terminal.'
          : 'Command-line launcher installation failed.'),
    };
  },
  onStateChanged: (state) => {
    appState.firstRunSetupCompleted = state.status === 'completed';
    if (appTray) {
      ensureTrayHandler();
    }
  },
});
const discordPresenceSessionStartedAtMs = Date.now();
let discordPresenceMediaDurationSec: number | null = null;
const discordPresenceRuntime = createDiscordPresenceRuntime({
  getDiscordPresenceService: () => appState.discordPresenceService,
  isDiscordPresenceEnabled: () => configService.getConfig().discordPresence.enabled === true,
  getMpvClient: () => appState.mpvClient,
  getCurrentMediaTitle: () => appState.currentMediaTitle,
  getCurrentMediaPath: () => appState.currentMediaPath,
  getCurrentSubtitleText: () => appState.currentSubText,
  getPlaybackPaused: () => appState.playbackPaused,
  getFallbackMediaDurationSec: () => anilistMediaGuessRuntimeState.mediaDurationSec,
  getSessionStartedAtMs: () => discordPresenceSessionStartedAtMs,
  getMediaDurationSec: () => discordPresenceMediaDurationSec,
  setMediaDurationSec: (next) => {
    discordPresenceMediaDurationSec = next;
  },
});

async function initializeDiscordPresenceService(): Promise<void> {
  if (configService.getConfig().discordPresence.enabled !== true) {
    appState.discordPresenceService = null;
    return;
  }

  appState.discordPresenceService = createDiscordPresenceService({
    config: configService.getConfig().discordPresence,
    createClient: () => createDiscordRpcClient(DISCORD_PRESENCE_APP_ID),
    logDebug: (message, meta) => logger.debug(message, meta),
  });
  await appState.discordPresenceService.start();
  discordPresenceRuntime.publishDiscordPresence();
}
const ensureOverlayMpvSubtitlesHidden = createEnsureOverlayMpvSubtitlesHiddenHandler({
  getMpvClient: () => appState.mpvClient,
  getSavedSubVisibility: () => appState.overlaySavedMpvSubVisibility,
  setSavedSubVisibility: (visible) => {
    appState.overlaySavedMpvSubVisibility = visible;
  },
  getRevision: () => appState.overlayMpvSubVisibilityRevision,
  setRevision: (revision) => {
    appState.overlayMpvSubVisibilityRevision = revision;
  },
  setMpvSubVisibility: (visible) => {
    setMpvSubVisibilityRuntime(appState.mpvClient, visible);
  },
  logWarn: (message, error) => {
    logger.warn(message, error);
  },
});
const restoreOverlayMpvSubtitles = createRestoreOverlayMpvSubtitlesHandler({
  getSavedSubVisibility: () => appState.overlaySavedMpvSubVisibility,
  setSavedSubVisibility: (visible) => {
    appState.overlaySavedMpvSubVisibility = visible;
  },
  getRevision: () => appState.overlayMpvSubVisibilityRevision,
  setRevision: (revision) => {
    appState.overlayMpvSubVisibilityRevision = revision;
  },
  isMpvConnected: () => Boolean(appState.mpvClient?.connected),
  shouldKeepSuppressedFromVisibleOverlayBinding: () => overlayManager.getVisibleOverlayVisible(),
  setMpvSubVisibility: (visible) => {
    setMpvSubVisibilityRuntime(appState.mpvClient, visible);
  },
});

function syncOverlayMpvSubtitleSuppression(): void {
  if (overlayManager.getVisibleOverlayVisible()) {
    void ensureOverlayMpvSubtitlesHidden();
    return;
  }

  restoreOverlayMpvSubtitles();
}

const buildImmersionMediaRuntimeMainDepsHandler = createBuildImmersionMediaRuntimeMainDepsHandler({
  getResolvedConfig: () => configService.getConfig(),
  defaultImmersionDbPath: DEFAULT_IMMERSION_DB_PATH,
  getTracker: () => appState.immersionTracker,
  getMpvClient: () => appState.mpvClient,
  getCurrentMediaPath: () => appState.currentMediaPath,
  getCurrentMediaTitle: () => appState.currentMediaTitle,
  logDebug: (message) => logger.debug(message),
  logInfo: (message) => logger.info(message),
});
const buildAnilistStateRuntimeMainDepsHandler = createBuildAnilistStateRuntimeMainDepsHandler({
  getClientSecretState: () => appState.anilistClientSecretState,
  setClientSecretState: (next) => {
    appState.anilistClientSecretState = transitionAnilistClientSecretState(
      appState.anilistClientSecretState,
      next,
    );
  },
  getRetryQueueState: () => appState.anilistRetryQueueState,
  setRetryQueueState: (next) => {
    appState.anilistRetryQueueState = transitionAnilistRetryQueueState(
      appState.anilistRetryQueueState,
      next,
    );
  },
  getUpdateQueueSnapshot: () => anilistUpdateQueue.getSnapshot(),
  clearStoredToken: () => anilistTokenStore.clearToken(),
  clearCachedAccessToken: () => {
    anilistCachedAccessToken = null;
  },
});
const buildConfigDerivedRuntimeMainDepsHandler = createBuildConfigDerivedRuntimeMainDepsHandler({
  getResolvedConfig: () => configService.getConfig(),
  getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
  defaultJimakuLanguagePreference: DEFAULT_CONFIG.jimaku.languagePreference,
  defaultJimakuMaxEntryResults: DEFAULT_CONFIG.jimaku.maxEntryResults,
  defaultJimakuApiBaseUrl: DEFAULT_CONFIG.jimaku.apiBaseUrl,
});
const buildMainSubsyncRuntimeMainDepsHandler = createBuildMainSubsyncRuntimeMainDepsHandler({
  getMpvClient: () => appState.mpvClient,
  getResolvedConfig: () => configService.getConfig(),
  getSubsyncInProgress: () => appState.subsyncInProgress,
  setSubsyncInProgress: (inProgress) => {
    appState.subsyncInProgress = inProgress;
  },
  showMpvOsd: (text) => overlayNotificationsRuntime.showSubsyncStatusNotification(text),
  openManualPicker: (payload) => {
    openOverlayHostedModalWithOsd(
      (deps) => openSubsyncManualModalRuntime(deps, payload),
      'Subsync overlay unavailable.',
      'Failed to open subsync overlay.',
    );
  },
});
const immersionMediaRuntime = createImmersionMediaRuntime(
  buildImmersionMediaRuntimeMainDepsHandler(),
);
const anilistRateLimiter = createAnilistRateLimiter();
const statsCoverArtFetcher = createCoverArtFetcher(
  anilistRateLimiter,
  createLogger('main:stats-cover-art'),
);
const anilistStateRuntime = createAnilistStateRuntime(buildAnilistStateRuntimeMainDepsHandler());
const configDerivedRuntime = createConfigDerivedRuntime(buildConfigDerivedRuntimeMainDepsHandler());
const subsyncRuntime = createMainSubsyncRuntime(buildMainSubsyncRuntimeMainDepsHandler());
const currentMediaTokenizationGate = createCurrentMediaTokenizationGate();
const startupOsdSequencer = createStartupOsdSequencer({
  getNotificationType: () => overlayNotificationsRuntime.getConfiguredStatusNotificationType(),
  showOsd: (message) => showMpvOsd(message),
  showOverlayNotification: (payload) =>
    overlayNotificationsRuntime.showOverlayNotification(payload),
  showDesktopNotification: (title, options) => showDesktopNotification(title, options),
});
const youtubePrimarySubtitleNotificationRuntime = createYoutubePrimarySubtitleNotificationRuntime({
  getPrimarySubtitleLanguages: () => configService.getConfig().youtube.primarySubLanguages,
  notifyFailure: (message) => reportYoutubeSubtitleFailure(message),
  schedule: (fn, delayMs) => setTimeout(fn, delayMs),
  clearSchedule: clearYoutubePrimarySubtitleNotificationTimer,
  getCurrentSubtitleState: async () => {
    const client = appState.mpvClient;
    if (!client?.connected) {
      return null;
    }
    const [sid, trackList] = await Promise.all([
      client.requestProperty('sid').catch(() => null),
      client.requestProperty('track-list').catch(() => null),
    ]);
    return {
      sid,
      trackList: Array.isArray(trackList) ? trackList : null,
    };
  },
});

function isYoutubePlaybackActiveNow(): boolean {
  return Boolean(getCurrentYoutubeMediaCacheSourceUrlSnapshot());
}

function getCurrentYoutubeMediaCacheSourceUrlSnapshot(): string | null {
  const currentMediaPath = appState.currentMediaPath?.trim() || null;
  if (isYoutubeMediaPath(currentMediaPath)) {
    return currentMediaPath;
  }

  const currentVideoPath = appState.mpvClient?.currentVideoPath?.trim() || null;
  if (isYoutubeMediaPath(currentVideoPath)) {
    return currentVideoPath;
  }

  return youtubeMediaCachePlaybackRuntime.getActiveYoutubeSourceUrlSnapshot();
}

async function getCurrentYoutubeMediaCacheSourceUrl(): Promise<string | null> {
  const currentMediaPath = appState.currentMediaPath?.trim() || null;
  if (isYoutubeMediaPath(currentMediaPath)) {
    return currentMediaPath;
  }

  const currentVideoPath = appState.mpvClient?.currentVideoPath?.trim() || null;
  if (isYoutubeMediaPath(currentVideoPath)) {
    return currentVideoPath;
  }

  return await youtubeMediaCachePlaybackRuntime.getActiveYoutubeSourceUrl();
}

function shouldRequireYoutubeMediaCacheForCurrentPlayback(): boolean {
  return (
    configService.getConfig().youtube.mediaCache.mode === 'background' &&
    isYoutubePlaybackActiveNow()
  );
}

async function getCachedYoutubeMediaPathForCurrentPlayback(
  currentVideoPath: string,
  _kind: 'audio' | 'video',
): Promise<string | null> {
  if (configService.getConfig().youtube.mediaCache.mode !== 'background') {
    return null;
  }
  const cacheSourceUrl = isYoutubeMediaPath(currentVideoPath)
    ? currentVideoPath
    : await getCurrentYoutubeMediaCacheSourceUrl();
  if (!cacheSourceUrl) {
    return null;
  }
  // mpv can expose the resolved stream URL here while the cache key uses the original page URL.
  // Keep the active-cache fallback so current playback can still resolve the ready cached file.
  return (
    (await youtubeMediaCache.getCachedMediaPath(cacheSourceUrl)) ??
    (await youtubeMediaCache.getActiveCachedMediaPath())
  );
}

function reportYoutubeSubtitleFailure(message: string): void {
  const type = overlayNotificationsRuntime.getConfiguredStatusNotificationType();
  if (type === 'none') {
    return;
  }
  if (type === 'overlay' || type === 'both') {
    overlayNotificationsRuntime.showOverlayNotification({
      title: 'SubMiner',
      body: message,
      variant: 'warning',
    });
  }
  if (type === 'osd' || type === 'osd-system') {
    showMpvOsd(message);
  }
  if (type === 'system' || type === 'both' || type === 'osd-system') {
    try {
      showDesktopNotification('SubMiner', { body: message });
    } catch {
      logger.warn(`Unable to show desktop notification: ${message}`);
    }
  }
}

async function openYoutubeTrackPickerFromPlayback(): Promise<void> {
  if (youtubeFlowRuntime.hasActiveSession()) {
    overlayNotificationsRuntime.showConfiguredStatusNotification(
      'YouTube subtitle flow already in progress.',
      {
        title: 'YouTube subtitles',
        variant: 'warning',
      },
    );
    return;
  }
  const currentMediaPath =
    appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || '';
  if (!isYoutubePlaybackActiveNow() || !currentMediaPath) {
    overlayNotificationsRuntime.showConfiguredStatusNotification(
      'YouTube subtitle picker is only available during YouTube playback.',
      {
        title: 'YouTube subtitles',
        variant: 'warning',
      },
    );
    return;
  }
  await youtubeFlowRuntime.openManualPicker({
    url: currentMediaPath,
  });
}

let appTray: Tray | null = null;
let tokenizeSubtitleDeferred: ((text: string) => Promise<SubtitleData>) | null = null;
function withCurrentSubtitleTiming(payload: SubtitleData): SubtitleData {
  const canonical = resolveCanonicalPrimarySubtitle({
    liveText: payload.text,
    currentTimeSec: Number(appState.mpvClient?.currentTimePos),
    cues: appState.activeParsedSubtitleCues,
  });
  return {
    ...payload,
    startTime: canonical?.startTime ?? appState.mpvClient?.currentSubStart ?? null,
    endTime: canonical?.endTime ?? appState.mpvClient?.currentSubEnd ?? null,
  };
}

function captureCurrentPrimarySubtitleMiningContext(): SubtitleMiningContext | null {
  // Mine what the overlay shows, not raw mpv `sub-text`: the raw text lists every active
  // event, so a finished caption row lingering beside a fresh line would end up on the
  // card. The parsed view also carries the cue's own timings for the clip range.
  const resolved = resolvePrimarySubtitle({
    liveText: appState.mpvClient?.currentSubText ?? '',
    currentTimeSec: Number(appState.mpvClient?.currentTimePos),
    cues: appState.activeParsedSubtitleCues,
  });
  // Same validity bar as the live capture path: an unusable resolved span must fall
  // back rather than hand mining an empty line or an inverted range.
  const resolvedText = resolved?.text.replace(/\n{2,}/g, '\n').trim();
  if (
    !resolved ||
    !resolvedText ||
    !Number.isFinite(resolved.startTime) ||
    !Number.isFinite(resolved.endTime) ||
    resolved.endTime <= resolved.startTime
  ) {
    return captureLiveSubtitleMiningContext(appState.mpvClient);
  }
  return {
    source: 'overlay',
    text: resolvedText,
    startTime: resolved.startTime,
    endTime: resolved.endTime,
    capturedAtMs: Date.now(),
  };
}
function emitSubtitlePayload(payload: SubtitleData, options?: { resumePrefetch?: boolean }): void {
  const timedPayload = withCurrentSubtitleTiming(payload);
  const currentSubtitleData = appState.currentSubtitleData;
  const isAnnotationUpgrade = isSubtitleAnnotationUpgrade(currentSubtitleData, timedPayload);
  const frequencyDictionary = configService.getConfig().subtitleStyle.frequencyDictionary;
  const frequencyOptions = {
    enabled: frequencyDictionary.enabled,
    topX: frequencyDictionary.topX,
    mode: frequencyDictionary.mode,
  };
  appState.currentSubtitleData = timedPayload;
  overlayManager.broadcastToOverlayWindows('subtitle:set', timedPayload);
  if (!isAnnotationUpgrade) {
    subtitleWsService.broadcast(timedPayload, frequencyOptions);
  }
  annotationSubtitleWsService.broadcast(timedPayload, frequencyOptions);
  autoplayReadyGate.maybeSignalPluginAutoplayReady(timedPayload, { forceWhilePaused: true });
  // resumePrefetch: false marks an emit that is not the end of the work for
  // this line; prefetch stays paused until the subtitle processing controller
  // settles so it does not compete with the on-screen line for the single
  // Yomitan parser window.
  if (options?.resumePrefetch !== false) {
    subtitlePrefetchService?.resume();
  }
}
function getCurrentAutoplaySubtitlePayload(): SubtitleData | null {
  const payload = appState.currentSubtitleData;
  if (!payload?.text.trim()) {
    return null;
  }
  if (payload.text !== appState.currentSubText) {
    return null;
  }
  return payload;
}

async function resolveSentenceSearchHeadwords(term: string): Promise<string[]> {
  const fallback = term.trim() ? [term.trim()] : [];
  try {
    const tokenized = tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(term) : null;
    const tokens = tokenized?.tokens ?? [];
    if (tokens.length === 0) return fallback;

    const seen = new Set<string>();
    const headwords: string[] = [];
    for (const token of tokens) {
      const headword = (token.headword || token.surface).trim();
      if (!headword || seen.has(headword)) continue;
      seen.add(headword);
      headwords.push(headword);
    }
    return headwords.length > 0 ? headwords : fallback;
  } catch (error) {
    logger.debug(
      'Failed to resolve sentence-search headwords:',
      error instanceof Error ? error.message : String(error),
    );
    return fallback;
  }
}

function signalCurrentSubtitleAutoplayReady(): void {
  autoplayReadyGate.flushPendingAutoplayReadySignal();
  const payload = getCurrentAutoplaySubtitlePayload();
  if (payload) {
    autoplayReadyGate.maybeSignalPluginAutoplayReady(payload, { forceWhilePaused: true });
    return;
  }
  if (!appState.currentSubText.trim()) {
    autoplayReadyGate.maybeSignalPluginAutoplayReady(
      { text: '__warm__', tokens: null },
      { forceWhilePaused: true },
    );
  }
}
const buildSubtitleProcessingControllerMainDepsHandler =
  createBuildSubtitleProcessingControllerMainDepsHandler({
    tokenizeSubtitle: async (text: string) =>
      tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(text) : { text, tokens: null },
    // Controller emits never release the prefetch pause: the first emit for an
    // uncached line is the provisional plain payload, sent before tokenization
    // starts, so resuming on it would put prefetch back in contention with the
    // on-screen line for the single parser window.
    emitSubtitle: (payload) => emitSubtitlePayload(payload, { resumePrefetch: false }),
    // The pause is released once the controller has no work left, which covers
    // the runs that end without an emit (suppressed duplicate, failed
    // tokenization) as well as the ones that deliver a payload.
    onProcessingSettled: () => {
      subtitlePrefetchService?.resume();
    },
    logDebug: (message) => {
      logger.debug(`[subtitle-processing] ${message}`);
    },
    now: () => Date.now(),
  });
const subtitleProcessingControllerMainDeps = buildSubtitleProcessingControllerMainDepsHandler();
const subtitleProcessingController = createSubtitleProcessingController(
  subtitleProcessingControllerMainDeps,
);

let subtitlePrefetchService: SubtitlePrefetchService | null = null;
let lastObservedTimePos = 0;
let lastObservedPrimarySubtitleTrackId: number | null = null;
let cancelLinuxMpvFullscreenOverlayRefreshBurst: CancelLinuxMpvFullscreenOverlayRefreshBurst | null =
  null;
let linuxVisibleOverlayWindowMode: LinuxVisibleOverlayWindowMode = 'managed';
let linuxTrackedMpvFullscreen = false;
let linuxTrackedMpvFullscreenChangedAtMs = 0;
let linuxVisibleOverlayOwnerBindingKey: string | null = null;
let linuxVisibleOverlayWindowModeSwitchToken = 0;
let subtitleSidebarRequestedOpen = false;
const SEEK_THRESHOLD_SECONDS = 3;
const EXPLICIT_SEEK_INTENT_TTL_MS = 2000;
let explicitSeekIntentExpiresAtMs = 0;

function isExplicitMpvSeekCommand(command: readonly (string | number)[]): boolean {
  return command[0] === 'seek' || command[0] === 'sub-seek';
}

function sendRendererMpvCommand(rawCommand: (string | number)[]): void {
  const command =
    resolveSanitizedSubtitleSeekCommand(
      rawCommand,
      appState.activeParsedSubtitleCues,
      appState.mpvClient?.currentTimePos ?? Number.NaN,
    ) ?? rawCommand;
  if (isExplicitMpvSeekCommand(command)) {
    explicitSeekIntentExpiresAtMs = Date.now() + EXPLICIT_SEEK_INTENT_TTL_MS;
  }
  sendMpvCommandRuntime(appState.mpvClient, command);
}

function consumeExplicitSeekIntent(): boolean {
  const pending = explicitSeekIntentExpiresAtMs >= Date.now();
  explicitSeekIntentExpiresAtMs = 0;
  return pending;
}

const autoplaySubtitlePrimingRuntime = createAutoplaySubtitlePrimingRuntime({
  getCurrentMediaPath: () => appState.currentMediaPath,
  getMpvClient: () => appState.mpvClient,
  setCurrentSubText: (text) => {
    appState.currentSubText = text;
  },
  getCurrentSubText: () => appState.currentSubText,
  getCurrentSubtitleData: () => appState.currentSubtitleData,
  getActiveParsedSubtitleCues: () => appState.activeParsedSubtitleCues,
  setActiveParsedSubtitleMediaPath: (mediaPath) => {
    appState.activeParsedSubtitleMediaPath = mediaPath;
  },
  subtitleProcessingController,
  emitSubtitlePayload: (payload, options) => emitSubtitlePayload(payload, options),
  getSubtitlePrefetchService: () => subtitlePrefetchService,
  getLastObservedTimePos: () => lastObservedTimePos,
  getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
  emitSecondarySubtitle: (text) => {
    secondarySubtitleTrackController.handleLiveText(text);
  },
  initSubtitlePrefetch: (sourcePath, currentTimePos, sourceKey) =>
    subtitlePrefetchInitController.initSubtitlePrefetch(sourcePath, currentTimePos, sourceKey),
  refreshSubtitlePrefetchFromActiveTrack: () => refreshSubtitlePrefetchFromActiveTrackHandler(),
  logDebug: (message) => {
    logger.debug(message);
  },
});

function cancelPendingLinuxMpvFullscreenOverlayRefreshBurst(): void {
  cancelLinuxMpvFullscreenOverlayRefreshBurst?.();
  cancelLinuxMpvFullscreenOverlayRefreshBurst = null;
}

const subtitlePrefetchInitController = createSubtitlePrefetchInitController({
  getCurrentService: () => subtitlePrefetchService,
  setCurrentService: (service) => {
    subtitlePrefetchService = service;
  },
  loadSubtitleSourceText,
  parseSubtitleCues: (content, filename) => parseSubtitleCues(content, filename),
  createSubtitlePrefetchService: (deps) => createSubtitlePrefetchService(deps),
  tokenizeSubtitle: async (text) =>
    tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(text) : null,
  preCacheTokenization: (text, data) => {
    subtitleProcessingController.preCacheTokenization(text, data);
  },
  hasCachedTokenization: (text) => subtitleProcessingController.hasCachedSubtitle(text),
  logInfo: (message) => logger.info(message),
  logWarn: (message) => logger.warn(message),
  onParsedSubtitleCuesChanged: (cues, sourceKey) => {
    appState.activeParsedSubtitleCues = cues ?? [];
    appState.activeParsedSubtitleSource = sourceKey;
    if (!cues?.length) {
      appState.activeParsedSubtitleMediaPath = null;
    }
    const mediaPath = autoplaySubtitlePrimingRuntime.getCurrentAutoplayMediaPath();
    if (mediaPath && cues?.length) {
      void autoplaySubtitlePrimingRuntime
        .primeAutoplaySubtitleFromParsedCues(mediaPath, cues)
        .catch((error) => {
          logger.debug(
            `[autoplay-subtitle-prime] failed to prime from parsed cues: ${
              error instanceof Error ? error.message : String(error)
            }`,
          );
        });
    }
  },
});
const cachedInternalSubtitleTrackExtractor = createCachedInternalSubtitleTrackExtractor();
const detectRemoteMediaPath = createRemoteMediaPathDetector();
const resolveActiveSubtitleSidebarSourceHandler = createResolveActiveSubtitleSidebarSourceHandler({
  getFfmpegPath: () => configService.getConfig().subsync.ffmpeg_path.trim() || 'ffmpeg',
  extractInternalSubtitleTrack: (ffmpegPath, videoPath, track) =>
    cachedInternalSubtitleTrackExtractor.extract(ffmpegPath, videoPath, track),
  logDebug: (message) => logger.debug(message),
});

const secondarySubtitleTrackController = createSecondarySubtitleTrackController({
  getMpvClient: () => appState.mpvClient,
  getCurrentTimePos: () => appState.mpvClient?.currentTimePos ?? lastObservedTimePos,
  resolveSubtitleSource: (input) => resolveActiveSubtitleSidebarSourceHandler(input),
  loadSubtitleSourceText,
  parseSubtitleCues: (content, filename) => parseSubtitleCues(content, filename),
  setCurrentSecondaryText: (text) => {
    if (appState.mpvClient) {
      appState.mpvClient.currentSecondarySubText = text;
    }
  },
  broadcastSecondaryText: (text) => {
    overlayManager.broadcastToOverlayWindows('secondary-subtitle:set', text);
  },
  logDebug: (message) => logger.debug(message),
  logWarn: (message, error) => logger.warn(message, error),
});

const refreshSubtitlePrefetchFromActiveTrackHandler =
  createRefreshSubtitlePrefetchFromActiveTrackHandler({
    getMpvClient: () => appState.mpvClient,
    getLastObservedTimePos: () => lastObservedTimePos,
    // Remote media has no extractable on-disk track to fall back to, so a transient
    // resolve miss (sid briefly 'no', a cycle onto an embedded stream track) would
    // otherwise drop a working cue list for the rest of the episode.
    shouldKeepExistingCuesOnMissingSource: async (videoPath) =>
      isYoutubeMediaPath(videoPath) || (await detectRemoteMediaPath(videoPath)),
    subtitlePrefetchInitController,
    resolveActiveSubtitleSidebarSource: (input) => resolveActiveSubtitleSidebarSourceHandler(input),
    logDebug: (message) => logger.debug(message),
    logWarn: (message) => logger.warn(message),
  });

const subtitlePrefetchRuntime = {
  cancelPendingInit: () => subtitlePrefetchInitController.cancelPendingInit(),
  initSubtitlePrefetch: subtitlePrefetchInitController.initSubtitlePrefetch,
  refreshSubtitleSidebarFromSource: (sourcePath: string, mediaPath?: string) =>
    autoplaySubtitlePrimingRuntime.refreshSubtitleSidebarFromSource(sourcePath, mediaPath),
  refreshSubtitlePrefetchFromActiveTrack: () => refreshSubtitlePrefetchFromActiveTrackHandler(),
  scheduleSubtitlePrefetchRefresh: (delayMs?: number) =>
    autoplaySubtitlePrimingRuntime.scheduleSubtitlePrefetchRefresh(delayMs),
  clearScheduledSubtitlePrefetchRefresh: () =>
    autoplaySubtitlePrimingRuntime.clearScheduledSubtitlePrefetchRefresh(),
} as const;

const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService(
  createBuildOverlayShortcutsRuntimeMainDepsHandler({
    getConfiguredShortcuts: () => getConfiguredShortcuts(),
    getShortcutsRegistered: () => appState.shortcutsRegistered,
    setShortcutsRegistered: (registered: boolean) => {
      appState.shortcutsRegistered = registered;
    },
    isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
    isOverlayShortcutContextActive: () => {
      if (process.platform !== 'win32') {
        return true;
      }

      if (!overlayManager.getVisibleOverlayVisible()) {
        return false;
      }

      const windowTracker = appState.windowTracker;
      if (!windowTracker || !windowTracker.isTracking()) {
        return false;
      }

      return windowTracker.isTargetWindowFocused();
    },
    showMpvOsd: (text: string) =>
      overlayNotificationsRuntime.showConfiguredStatusNotification(text),
    openRuntimeOptionsPalette: () => {
      openRuntimeOptionsPalette();
    },
    openCharacterDictionaryManager: () => {
      openCharacterDictionaryManagerOverlay();
    },
    openJimaku: () => {
      openJimakuOverlay();
    },
    openTsukihime: () => {
      openTsukihimeOverlay();
    },
    markAudioCard: () => markLastCardAsAudioCardHandler(),
    copySubtitleMultiple: (timeoutMs: number) => {
      startPendingMultiCopy(timeoutMs);
    },
    copySubtitle: () => {
      copyCurrentSubtitleHandler();
    },
    toggleSecondarySubMode: () => cycleSecondarySubMode(),
    updateLastCardFromClipboard: () => updateLastCardFromClipboardHandler(),
    triggerFieldGrouping: () => triggerFieldGroupingHandler(),
    triggerSubsyncFromConfig: () => subsyncRuntime.triggerFromConfig(),
    mineSentenceCard: () => mineSentenceCardHandler(),
    mineSentenceMultiple: (timeoutMs: number) => {
      startPendingMineSentenceMultiple(timeoutMs);
    },
    cancelPendingMultiCopy: () => {
      cancelPendingMultiCopy();
    },
    cancelPendingMineSentenceMultiple: () => {
      cancelPendingMineSentenceMultiple();
    },
  })(),
);
syncOverlayShortcutsForModal = (isActive: boolean): void => {
  if (isActive) {
    overlayShortcutsRuntime.unregisterOverlayShortcuts();
  } else {
    overlayShortcutsRuntime.syncOverlayShortcuts();
  }
};

const buildConfigHotReloadMessageMainDepsHandler = createBuildConfigHotReloadMessageMainDepsHandler(
  {
    getNotificationType: () => overlayNotificationsRuntime.getConfiguredStatusNotificationType(),
    showMpvOsd: (message) => showMpvOsd(message),
    showOverlayNotification: (payload) =>
      overlayNotificationsRuntime.showOverlayNotification(payload),
    showDesktopNotification: (title, options) => showDesktopNotification(title, options),
  },
);
const configHotReloadMessageMainDeps = buildConfigHotReloadMessageMainDepsHandler();
const notifyConfigHotReloadMessage = createConfigHotReloadMessageHandler(
  configHotReloadMessageMainDeps,
);
const buildWatchConfigPathMainDepsHandler = createBuildWatchConfigPathMainDepsHandler({
  fileExists: (targetPath) => fs.existsSync(targetPath),
  dirname: (targetPath) => path.dirname(targetPath),
  watchPath: (targetPath, listener) => fs.watch(targetPath, listener),
});
const watchConfigPathHandler = createWatchConfigPathHandler(buildWatchConfigPathMainDepsHandler());
const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadAppliedMainDepsHandler(
  {
    setKeybindings: (keybindings) => {
      appState.keybindings = keybindings;
    },
    setSessionBindings: (sessionBindings, sessionBindingWarnings) => {
      persistSessionBindings(sessionBindings, sessionBindingWarnings);
    },
    refreshGlobalAndOverlayShortcuts: () => {
      refreshGlobalAndOverlayShortcuts();
    },
    setSecondarySubMode: (mode) => {
      setSecondarySubMode(mode);
    },
    broadcastToOverlayWindows: (channel, payload) => {
      overlayManager.broadcastToOverlayWindows(channel, payload);
    },
    applyAnkiRuntimeConfigPatch: (patch) => {
      if (appState.ankiIntegration) {
        appState.ankiIntegration.applyRuntimeConfigPatch(patch);
      }
    },
    invalidateTokenizationCache: () => {
      subtitleProcessingController.invalidateTokenizationCache();
    },
    refreshSubtitlePrefetch: () => {
      subtitlePrefetchService?.onSeek(lastObservedTimePos);
    },
    refreshCurrentSubtitle: () => {
      subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
    },
    setLogLevel: (level) => {
      setLogLevel(level, 'config');
    },
    setLogRotation: (rotation) => {
      setLogRotation(rotation);
    },
    setLogFileToggles: (files) => {
      setLogFileToggles(files);
    },
    applyAniSkipConfig: () => {
      aniSkipRuntime.applyConfigChange();
    },
  },
);
const applyConfigHotReloadDiff = createConfigHotReloadAppliedHandler(
  buildConfigHotReloadAppliedMainDepsHandler(),
);
const buildConfigHotReloadRuntimeMainDepsHandler = createBuildConfigHotReloadRuntimeMainDepsHandler(
  {
    getCurrentConfig: () => configService.getConfig(),
    reloadConfigStrict: () => configService.reloadConfigStrict(),
    watchConfigPath: (configPath, onChange) => watchConfigPathHandler(configPath, onChange),
    setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
    clearTimeout: (timeout) => clearTimeout(timeout),
    debounceMs: 250,
    onHotReloadApplied: applyConfigHotReloadDiff,
    onRestartRequired: (fields) =>
      notifyConfigHotReloadMessage(buildRestartRequiredConfigMessage(fields)),
    onInvalidConfig: notifyConfigHotReloadMessage,
    onValidationWarnings: (configPath, warnings) => {
      showDesktopNotification('SubMiner', {
        body: buildConfigWarningNotificationBody(configPath, warnings),
      });
      if (process.platform === 'darwin') {
        dialog.showErrorBox(
          'SubMiner config validation warning',
          buildConfigWarningDialogDetails(configPath, warnings),
        );
      }
    },
  },
);
const configHotReloadRuntime = createConfigHotReloadRuntime(
  buildConfigHotReloadRuntimeMainDepsHandler(),
);

async function getCurrentYomitanAnkiDeckNameForRuntime(): Promise<string> {
  await yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
  return getYomitanCurrentAnkiDeckNameCore(getYomitanParserRuntimeDeps(), {
    error: (message, ...args) => {
      logger.error(message, ...args);
    },
    info: (message, ...args) => {
      logger.info(message, ...args);
    },
  });
}

const configSettingsRuntime = createConfigSettingsRuntime({
  fields: configSettingsFields,
  getConfigPath: () => configService.getConfigPath(),
  getRawConfig: () => configService.getRawConfig(),
  getConfig: () => configService.getConfig(),
  getWarnings: () => configService.getWarnings(),
  reloadConfigStrict: () => configService.reloadConfigStrict(),
  onHotReloadApplied: applyConfigHotReloadDiff,
  defaultAnkiConnectUrl: DEFAULT_CONFIG.ankiConnect.url,
  createAnkiClient: (url) => new AnkiConnectClient(url),
  getYomitanAnkiDeckName: getCurrentYomitanAnkiDeckNameForRuntime,
  getSettingsWindow: () => appState.configSettingsWindow,
  setSettingsWindow: (window) => {
    appState.configSettingsWindow = window as BrowserWindow | null;
  },
  createSettingsWindow: createCreateConfigSettingsWindowHandler({
    createBrowserWindow: (options) => new BrowserWindow(options),
    preloadPath: path.join(__dirname, 'preload-settings.js'),
  }),
  settingsHtmlPath: path.join(__dirname, 'settings', 'index.html'),
  promoteSettingsWindowAboveOverlay: (window) =>
    promoteSettingsWindowAboveOverlay(window as BrowserWindow),
  openPath: (targetPath) => shell.openPath(targetPath),
  ipcMain,
  ipcChannels: IPC_CHANNELS.request,
  log: (message) => logger.error(message),
});

configSettingsRuntime.registerHandlers();

const openSyncUiWindowHandler = createOpenConfigSettingsWindowHandler({
  getSettingsWindow: () => appState.syncUiWindow,
  setSettingsWindow: (window) => {
    appState.syncUiWindow = window as BrowserWindow | null;
  },
  createSettingsWindow: createCreateSyncUiWindowHandler({
    createBrowserWindow: (options) => new BrowserWindow(options),
    preloadPath: path.join(__dirname, 'preload-syncui.js'),
  }),
  settingsHtmlPath: path.join(__dirname, 'syncui', 'index.html'),
  promoteSettingsWindowAboveOverlay: (window) =>
    promoteSettingsWindowAboveOverlay(window as BrowserWindow),
  onClosed: () => {
    // requestAppQuit (not app.quit) so the forced-exit fallback covers a
    // stalled quit; a standalone sync window is the app's only reason to live.
    if (appState.initialArgs?.syncWindow) requestAppQuit();
  },
  log: (message) => logger.error(message),
});

const syncUiRuntime = createSyncUiRuntime({
  ipcMain,
  hostsFilePath: getSyncHostsPath(USER_DATA_PATH),
  snapshotsDir: path.join(os.tmpdir(), 'subminer-db-snapshots'),
  getDbPath: () => {
    const configured = configService.getConfig().immersionTracking?.dbPath?.trim();
    return configured || DEFAULT_IMMERSION_DB_PATH;
  },
  resolveLauncherCommand: () =>
    resolveSyncLauncherCommand({
      execPath: process.execPath,
      appPath: app.isPackaged ? null : app.getAppPath(),
    }),
  runLauncher: runSyncLauncher,
  getWindow: () => appState.syncUiWindow,
  pickSnapshotFile: async () => {
    const result = await dialog.showOpenDialog({
      title: 'Choose a SubMiner stats snapshot',
      filters: [{ name: 'SQLite snapshots', extensions: ['sqlite'] }],
      properties: ['openFile'],
    });
    return result.canceled ? null : (result.filePaths[0] ?? null);
  },
  revealPath: (targetPath) => shell.showItemInFolder(targetPath),
  nowMs: () => Date.now(),
  log: (message) => logger.info(message),
  notify: (payload) =>
    overlayNotificationsRuntime.showOverlayNotification({
      title: payload.title,
      body: payload.body,
      variant: payload.variant,
    }),
});
syncUiRuntime.registerHandlers();

// Live sync uses a consistent WAL snapshot plus a transactional merge, and it
// skips unfinished sessions, so resident stats writers do not block scheduling.
const syncAutoScheduler = createSyncAutoScheduler({
  readState: () => syncUiRuntime.readState(),
  isRunning: () => syncUiRuntime.isRunning(),
  triggerHostSync: (host, direction) => {
    void syncUiRuntime.runHostSync({ host, direction }, { notify: true });
  },
  nowMs: () => Date.now(),
  log: (message) => logger.info(message),
});
syncAutoScheduler.start();

const buildDictionaryRootsHandler = createBuildDictionaryRootsMainHandler({
  platform: process.platform,
  dirname: __dirname,
  appPath: app.getAppPath(),
  resourcesPath: process.resourcesPath,
  userDataPath: USER_DATA_PATH,
  appUserDataPath: app.getPath('userData'),
  homeDir: os.homedir(),
  appDataDir: process.env.APPDATA,
  cwd: process.cwd(),
  joinPath: (...parts) => path.join(...parts),
});
const buildFrequencyDictionaryRootsHandler = createBuildFrequencyDictionaryRootsMainHandler({
  platform: process.platform,
  dirname: __dirname,
  appPath: app.getAppPath(),
  resourcesPath: process.resourcesPath,
  userDataPath: USER_DATA_PATH,
  appUserDataPath: app.getPath('userData'),
  homeDir: os.homedir(),
  appDataDir: process.env.APPDATA,
  cwd: process.cwd(),
  joinPath: (...parts) => path.join(...parts),
});

const jlptDictionaryRuntime = createJlptDictionaryRuntimeService(
  createBuildJlptDictionaryRuntimeMainDepsHandler({
    isJlptEnabled: () => configService.getConfig().subtitleStyle.enableJlpt,
    getDictionaryRoots: () => buildDictionaryRootsHandler(),
    getJlptDictionarySearchPaths,
    setJlptLevelLookup: (lookup) => {
      appState.jlptLevelLookup = lookup;
    },
    logInfo: (message) => logger.info(message),
  })(),
);

const frequencyDictionaryRuntime = createFrequencyDictionaryRuntimeService(
  createBuildFrequencyDictionaryRuntimeMainDepsHandler({
    isFrequencyDictionaryEnabled: () =>
      configService.getConfig().subtitleStyle.frequencyDictionary.enabled,
    getDictionaryRoots: () => buildFrequencyDictionaryRootsHandler(),
    getFrequencyDictionarySearchPaths,
    getSourcePath: () => configService.getConfig().subtitleStyle.frequencyDictionary.sourcePath,
    setFrequencyRankLookup: (lookup) => {
      appState.frequencyRankLookup = lookup;
    },
    logInfo: (message) => logger.info(message),
  })(),
);

const buildGetFieldGroupingResolverMainDepsHandler =
  createBuildGetFieldGroupingResolverMainDepsHandler({
    getResolver: () => appState.fieldGroupingResolver,
  });
const getFieldGroupingResolverMainDeps = buildGetFieldGroupingResolverMainDepsHandler();
const getFieldGroupingResolverHandler = createGetFieldGroupingResolverHandler(
  getFieldGroupingResolverMainDeps,
);

const buildSetFieldGroupingResolverMainDepsHandler =
  createBuildSetFieldGroupingResolverMainDepsHandler({
    setResolver: (resolver) => {
      appState.fieldGroupingResolver = resolver;
    },
    nextSequence: () => {
      appState.fieldGroupingResolverSequence += 1;
      return appState.fieldGroupingResolverSequence;
    },
    getSequence: () => appState.fieldGroupingResolverSequence,
  });
const setFieldGroupingResolverMainDeps = buildSetFieldGroupingResolverMainDepsHandler();
const setFieldGroupingResolverHandler = createSetFieldGroupingResolverHandler(
  setFieldGroupingResolverMainDeps,
);

const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>(
  createBuildFieldGroupingOverlayMainDepsHandler<OverlayHostedModal>({
    getMainWindow: () => overlayManager.getMainWindow(),
    getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
    setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
    getResolver: () => getFieldGroupingResolverHandler(),
    setResolver: (resolver) => setFieldGroupingResolverHandler(resolver),
    getRestoreVisibleOverlayOnModalClose: () =>
      overlayModalRuntime.getRestoreVisibleOverlayOnModalClose(),
    waitForModalOpen: (modal, timeoutMs) => overlayModalRuntime.waitForModalOpen(modal, timeoutMs),
    handleOverlayModalClosed: (modal) => overlayModalRuntime.handleOverlayModalClosed(modal),
    logWarn: (message) => logger.warn(message),
    ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
    ensureOverlayWindowsReadyForVisibilityActions: () =>
      ensureOverlayWindowsReadyForVisibilityActions(),
    sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
      overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions),
  })(),
);
const createFieldGroupingCallback = fieldGroupingOverlayRuntime.createFieldGroupingCallback;

const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, 'subtitle-positions');

const mediaRuntime = createMediaRuntimeService(
  createBuildMediaRuntimeMainDepsHandler({
    isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
    loadSubtitlePosition: () => loadSubtitlePosition(),
    getCurrentMediaPath: () => appState.currentMediaPath,
    getPendingSubtitlePosition: () => appState.pendingSubtitlePosition,
    getSubtitlePositionsDir: () => SUBTITLE_POSITIONS_DIR,
    setCurrentMediaPath: (nextPath: string | null) => {
      appState.currentMediaPath = nextPath;
    },
    clearPendingSubtitlePosition: () => {
      appState.pendingSubtitlePosition = null;
    },
    setSubtitlePosition: (position: SubtitlePosition | null) => {
      appState.subtitlePosition = position;
    },
    broadcastToOverlayWindows: (channel, payload) => {
      overlayManager.broadcastToOverlayWindows(channel, payload);
    },
    getCurrentMediaTitle: () => appState.currentMediaTitle,
    setCurrentMediaTitle: (title) => {
      appState.currentMediaTitle = title;
    },
  })(),
);

const characterDictionaryRuntime = createCharacterDictionaryRuntimeService({
  userDataPath: USER_DATA_PATH,
  getCurrentMediaPath: () => appState.currentMediaPath,
  getCurrentVideoPath: () => appState.mpvClient?.currentVideoPath,
  getCurrentMediaTitle: () => appState.currentMediaTitle,
  resolveMediaPathForJimaku: (mediaPath) => mediaRuntime.resolveMediaPathForJimaku(mediaPath),
  guessAnilistMediaInfo: (mediaPath, mediaTitle) => guessAnilistMediaInfo(mediaPath, mediaTitle),
  getNameMatchImagesEnabled: () => configService.getConfig().subtitleStyle.nameMatchImagesEnabled,
  getCollapsibleSectionOpenState: (section) =>
    configService.getConfig().anilist.characterDictionary.collapsibleSections[section],
  tokenizeJapaneseName: async (text) => (await appState.mecabTokenizer?.tokenize(text)) ?? null,
  getJapaneseNameTokenizerAvailable: () => {
    const status = appState.mecabTokenizer?.getStatus();
    return status?.available === true && status.enabled === true;
  },
  now: () => Date.now(),
  logInfo: (message) => logger.info(message),
  logWarn: (message) => logger.warn(message),
});

const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRuntimeService({
  userDataPath: USER_DATA_PATH,
  getConfig: () => {
    const config = configService.getConfig().anilist.characterDictionary;
    return {
      enabled:
        configService.getConfig().subtitleStyle.nameMatchEnabled &&
        yomitanProfilePolicy.isCharacterDictionaryEnabled() &&
        !isYoutubePlaybackActiveNow(),
      maxLoaded: config.maxLoaded,
      profileScope: config.profileScope,
    };
  },
  getOrCreateCurrentSnapshot: (targetPath, progress) =>
    characterDictionaryRuntime.getOrCreateCurrentSnapshot(targetPath, progress),
  buildMergedDictionary: (mediaIds) => characterDictionaryRuntime.buildMergedDictionary(mediaIds),
  waitForYomitanMutationReady: () =>
    currentMediaTokenizationGate.waitUntilReady(
      appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null,
    ),
  getYomitanDictionaryInfo: async () => {
    await ensureYomitanExtensionLoaded();
    return await getYomitanDictionaryInfo(getYomitanParserRuntimeDeps(), {
      error: (message, ...args) => logger.error(message, ...args),
      info: (message, ...args) => logger.info(message, ...args),
    });
  },
  importYomitanDictionary: async (zipPath) => {
    if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
      yomitanProfilePolicy.logSkippedWrite(
        formatSkippedYomitanWriteAction('importYomitanDictionary', zipPath),
      );
      return false;
    }
    await ensureYomitanExtensionLoaded();
    return await importYomitanDictionaryFromZip(zipPath, getYomitanParserRuntimeDeps(), {
      error: (message, ...args) => logger.error(message, ...args),
      info: (message, ...args) => logger.info(message, ...args),
    });
  },
  deleteYomitanDictionary: async (dictionaryTitle) => {
    if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
      yomitanProfilePolicy.logSkippedWrite(
        formatSkippedYomitanWriteAction('deleteYomitanDictionary', dictionaryTitle),
      );
      return false;
    }
    await ensureYomitanExtensionLoaded();
    return await deleteYomitanDictionaryByTitle(dictionaryTitle, getYomitanParserRuntimeDeps(), {
      error: (message, ...args) => logger.error(message, ...args),
      info: (message, ...args) => logger.info(message, ...args),
    });
  },
  upsertYomitanDictionarySettings: async (dictionaryTitle, profileScope) => {
    if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
      yomitanProfilePolicy.logSkippedWrite(
        formatSkippedYomitanWriteAction('upsertYomitanDictionarySettings', dictionaryTitle),
      );
      return false;
    }
    await ensureYomitanExtensionLoaded();
    return await upsertYomitanDictionarySettings(
      dictionaryTitle,
      profileScope,
      getYomitanParserRuntimeDeps(),
      {
        error: (message, ...args) => logger.error(message, ...args),
        info: (message, ...args) => logger.info(message, ...args),
      },
    );
  },
  now: () => Date.now(),
  schedule: (fn, delayMs) => setTimeout(fn, delayMs),
  clearSchedule: (timer) => clearTimeout(timer),
  logInfo: (message) => logger.info(message),
  logWarn: (message) => logger.warn(message),
  onSyncStatus: (event) => {
    notifyCharacterDictionaryAutoSyncStatus(event, {
      getNotificationType: () => overlayNotificationsRuntime.getConfiguredStatusNotificationType(),
      showOsd: (message) => showMpvOsd(message),
      showOverlayNotification: (payload) =>
        overlayNotificationsRuntime.showOverlayNotification(payload),
      showDesktopNotification: (title, options) => showDesktopNotification(title, options),
      startupOsdSequencer,
    });
  },
  onSyncComplete: ({ mediaId, mediaTitle, changed }) => {
    handleCharacterDictionaryAutoSyncComplete(
      {
        mediaId,
        mediaTitle,
        changed,
      },
      {
        hasParserWindow: () => Boolean(appState.yomitanParserWindow),
        invalidateCharacterDictionaryLookups: () => {
          characterDictionaryImageLookup.invalidate();
          characterNameCandidateLookup.invalidate();
        },
        clearParserCaches: () => {
          if (appState.yomitanParserWindow) {
            clearYomitanParserCachesForWindow(appState.yomitanParserWindow);
          }
        },
        invalidateTokenizationCache: () => {
          subtitleProcessingController.invalidateTokenizationCache();
        },
        refreshSubtitlePrefetch: () => {
          subtitlePrefetchService?.onSeek(lastObservedTimePos);
        },
        refreshCurrentSubtitle: () => {
          subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
        },
        logInfo: (message) => logger.info(message),
      },
    );
  },
});

const characterDictionaryImageLookup = createCharacterDictionaryImageLookup({
  userDataPath: USER_DATA_PATH,
  getCurrentMediaId: () => characterDictionaryAutoSyncRuntime.getCurrentMediaId(),
  onIndexReady: () => refreshCurrentSubtitleAnnotations(),
  onIndexReadyError: (error) =>
    logger.warn(
      'Failed to refresh subtitle annotations after character portrait index became ready.',
      error,
    ),
});

// Lets the Yomitan scan runtime skip name lookups at positions where no
// character name can start; absent candidates just mean the exhaustive scan.
const characterNameCandidateLookup = createCharacterNameCandidateLookup({
  userDataPath: USER_DATA_PATH,
  getCurrentMediaId: () => characterDictionaryAutoSyncRuntime.getCurrentMediaId(),
});

const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
  createBuildOverlayVisibilityRuntimeMainDepsHandler({
    getMainWindow: () => overlayManager.getMainWindow(),
    getModalActive: () => overlayModalInputState.getModalInputExclusive(),
    getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
    getForceMousePassthrough: () => appState.statsOverlayVisible,
    getNonNativeInputRegionActive: () =>
      process.platform === 'linux' &&
      visibleOverlayInteractionRuntime.getLinuxOverlayInputShapeActive(),
    getSuspendVisibleOverlay: () => appState.statsOverlayVisible,
    getOverlayInteractionActive: () =>
      visibleOverlayInteractionRuntime.getVisibleOverlayInteractionActive(),
    getWindowTracker: () => appState.windowTracker,
    getLastKnownWindowsForegroundProcessName: () =>
      visibleOverlayInteractionRuntime.getLastWindowsVisibleOverlayForegroundProcessName(),
    getWindowsOverlayProcessName: () => path.parse(process.execPath).name.toLowerCase(),
    getWindowsFocusHandoffGraceActive: () =>
      visibleOverlayInteractionRuntime.hasWindowsVisibleOverlayFocusHandoffGrace(),
    getMacOSForegroundProbeActive: () =>
      visibleOverlayInteractionRuntime.getMacOSVisibleOverlayForegroundProbeActive(),
    getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown,
    setTrackerNotReadyWarningShown: (shown: boolean) => {
      appState.trackerNotReadyWarningShown = shown;
    },
    updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
      overlayGeometryRuntime.updateVisibleOverlayBounds(geometry),
    ensureOverlayWindowLevel: (window) => {
      overlayGeometryRuntime.ensureOverlayWindowLevel(window);
    },
    syncWindowsOverlayToMpvZOrder: (_window) => {
      visibleOverlayInteractionRuntime.requestWindowsVisibleOverlayZOrderSync();
    },
    syncPrimaryOverlayWindowLayer: (layer) => {
      overlayGeometryRuntime.syncPrimaryOverlayWindowLayer(layer);
    },
    enforceOverlayLayerOrder: () => {
      overlayGeometryRuntime.enforceOverlayLayerOrder();
    },
    syncOverlayShortcuts: () => {
      overlayShortcutsRuntime.syncOverlayShortcuts();
    },
    isMacOSPlatform: () => process.platform === 'darwin',
    isWindowsPlatform: () => process.platform === 'win32',
    showOverlayLoadingOsd: (_message: string) => {
      overlayNotificationsRuntime.showOverlayLoadingStatusNotification();
    },
    dismissOverlayLoadingOsd: () => {
      overlayNotificationsRuntime.dismissOverlayLoadingStatusNotification();
    },
    hideNonNativeOverlayWhenTargetUnfocused: () =>
      shouldRunLinuxOverlayZOrderKeepAlive() &&
      linuxVisibleOverlayWindowMode === 'fullscreen-override',
    resolveFallbackBounds: () => {
      const cursorPoint = screen.getCursorScreenPoint();
      const display = screen.getDisplayNearestPoint(cursorPoint);
      const fallbackBounds = display.workArea;
      return {
        x: fallbackBounds.x,
        y: fallbackBounds.y,
        width: fallbackBounds.width,
        height: fallbackBounds.height,
      };
    },
  })(),
);

const visibleOverlayInteractionRuntime = createVisibleOverlayInteractionRuntime({
  overlayManager: {
    getMainWindow: () => overlayManager.getMainWindow(),
    getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
  },
  overlayContentMeasurementStore: {
    clear: (layer) => overlayContentMeasurementStore.clear(layer),
    getLatestByLayer: (layer) => overlayContentMeasurementStore.getLatestByLayer(layer),
  },
  logger: {
    info: (message, ...args) => logger.info(message, ...args),
    warn: (message, ...args) => logger.warn(message, ...args),
    debug: (message, ...args) => logger.debug(message, ...args),
  },
  updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
  getModalInputExclusive: () => overlayModalInputState.getModalInputExclusive(),
  getStatsOverlayVisible: () => appState.statsOverlayVisible,
  setStatsOverlayVisible: (visible) => {
    appState.statsOverlayVisible = visible;
  },
  getWindowTracker: () => appState.windowTracker,
  setWindowTracker: (tracker) => {
    appState.windowTracker = tracker;
  },
  setTrackerNotReadyWarningShown: (shown) => {
    appState.trackerNotReadyWarningShown = shown;
  },
  getMpvSocketPath: () => appState.mpvSocketPath,
  getBackendOverride: () => appState.backendOverride,
  getInitialArgs: () => appState.initialArgs,
  getOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
  getLinuxVisibleOverlayWindowMode: () => linuxVisibleOverlayWindowMode,
  setLinuxVisibleOverlayOwnerBindingKey: (key) => {
    linuxVisibleOverlayOwnerBindingKey = key;
  },
  bindVisibleOverlayToTrackedX11Window: (window) =>
    overlayGeometryRuntime.bindVisibleOverlayToTrackedX11Window(window),
  updateVisibleOverlayBounds: (geometry) =>
    overlayGeometryRuntime.updateVisibleOverlayBounds(geometry),
  refreshCurrentSubtitle: () => {
    subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
  },
  getOverlayWindows: () => overlayManager.getOverlayWindows(),
  syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
  resetLastOverlayWindowGeometry: () => overlayGeometryRuntime.resetLastOverlayWindowGeometry(),
  enforceOverlayLayerOrder: () => {
    overlayGeometryRuntime.enforceOverlayLayerOrder();
  },
  getOverlayForegroundSeparateWindows: () => getOverlayForegroundSeparateWindows(),
});

const buildGetRuntimeOptionsStateMainDepsHandler = createBuildGetRuntimeOptionsStateMainDepsHandler(
  {
    getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
  },
);
const getRuntimeOptionsStateMainDeps = buildGetRuntimeOptionsStateMainDepsHandler();
const getRuntimeOptionsStateHandler = createGetRuntimeOptionsStateHandler(
  getRuntimeOptionsStateMainDeps,
);

const buildRestorePreviousSecondarySubVisibilityMainDepsHandler =
  createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler({
    getMpvClient: () => appState.mpvClient,
  });
syncOverlayVisibilityForModal = () => {
  overlayVisibilityRuntime.updateVisibleOverlayVisibility();
};

const overlayNotificationsRuntime = createOverlayNotificationsRuntime({
  getResolvedConfig: () => configService.getConfig(),
  getMainOverlayWindow: () => overlayManager.getMainWindow(),
  getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
  broadcastToOverlayWindows: (channel, ...args) =>
    overlayManager.broadcastToOverlayWindows(channel, ...args),
  showMpvOsd: (message) => showMpvOsd(message),
  getMpvClient: () => appState.mpvClient,
  getAnkiIntegration: () => appState.ankiIntegration,
  getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
});
const {
  flushQueuedOverlayNotifications,
  flushQueuedMpvOsdNotifications,
  openAnkiCardFromNotification,
  toggleNotificationHistoryPanel,
  showConfiguredPlaybackFeedback,
  maybeStartOverlayLoadingOsd,
} = overlayNotificationsRuntime;

const buildBroadcastRuntimeOptionsChangedMainDepsHandler =
  createBuildBroadcastRuntimeOptionsChangedMainDepsHandler({
    broadcastRuntimeOptionsChangedRuntime,
    getRuntimeOptionsState: () => getRuntimeOptionsStateHandler(),
    broadcastToOverlayWindows: (channel, ...args) =>
      overlayManager.broadcastToOverlayWindows(channel, ...args),
  });

const buildSendToActiveOverlayWindowMainDepsHandler =
  createBuildSendToActiveOverlayWindowMainDepsHandler({
    sendToActiveOverlayWindowRuntime: (channel, payload, runtimeOptions) =>
      overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions),
  });

const buildSetOverlayDebugVisualizationEnabledMainDepsHandler =
  createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler({
    setOverlayDebugVisualizationEnabledRuntime,
    getCurrentEnabled: () => appState.overlayDebugVisualizationEnabled,
    setCurrentEnabled: (next) => {
      appState.overlayDebugVisualizationEnabled = next;
    },
  });

const buildOpenRuntimeOptionsPaletteMainDepsHandler =
  createBuildOpenRuntimeOptionsPaletteMainDepsHandler({
    openRuntimeOptionsPaletteRuntime: () => overlayModalRuntime.openRuntimeOptionsPalette(),
  });
const overlayVisibilityComposer = composeOverlayVisibilityRuntime({
  overlayVisibilityRuntime,
  restorePreviousSecondarySubVisibilityMainDeps:
    buildRestorePreviousSecondarySubVisibilityMainDepsHandler(),
  broadcastRuntimeOptionsChangedMainDeps: buildBroadcastRuntimeOptionsChangedMainDepsHandler(),
  sendToActiveOverlayWindowMainDeps: buildSendToActiveOverlayWindowMainDepsHandler(),
  setOverlayDebugVisualizationEnabledMainDeps:
    buildSetOverlayDebugVisualizationEnabledMainDepsHandler(),
  openRuntimeOptionsPaletteMainDeps: buildOpenRuntimeOptionsPaletteMainDepsHandler(),
});

function createOverlayHostedModalOpenDeps(): {
  ensureOverlayStartupPrereqs: () => void;
  ensureOverlayWindowsReadyForVisibilityActions: () => void;
  sendToActiveOverlayWindow: (
    channel: string,
    payload?: unknown,
    runtimeOptions?: {
      restoreOnModalClose?: OverlayHostedModal;
      preferModalWindow?: boolean;
    },
  ) => boolean;
  waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
  logWarn: (message: string) => void;
} {
  return {
    ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
    ensureOverlayWindowsReadyForVisibilityActions: () =>
      ensureOverlayWindowsReadyForVisibilityActions(),
    sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
      overlayVisibilityComposer.sendToActiveOverlayWindow(channel, payload, runtimeOptions),
    waitForModalOpen: (modal, timeoutMs) => overlayModalRuntime.waitForModalOpen(modal, timeoutMs),
    logWarn: (message) => logger.warn(message),
  };
}

const mediaTimingReviewRuntime = createMediaTimingReviewRuntime({
  getMpvClient: () => appState.mpvClient,
  getCurrentMediaPath: () =>
    appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null,
  getMpvExecutablePath: () =>
    configService.getConfig().mpv.executablePath || process.env.SUBMINER_MPV_PATH?.trim() || '',
  createPreviewSession: () => new MediaTimingPreviewSession(),
  generateWaveform: (options) => generateSpeechWaveform(options),
  resolveMediaSource: async () => {
    const resolved = await resolveMediaGenerationInput(appState.mpvClient, 'audio', {
      getCachedMediaPath: (currentVideoPath, kind) =>
        getCachedYoutubeMediaPathForCurrentPlayback(currentVideoPath, kind),
      remoteCacheMode: shouldRequireYoutubeMediaCacheForCurrentPlayback() ? 'required' : 'optional',
    });
    return resolved
      ? {
          path: resolved.path,
          ...(resolved.inputOptions ? { inputOptions: resolved.inputOptions } : {}),
          singleResolvedStream: resolved.singleResolvedStream,
        }
      : null;
  },
  acquireMediaWindow: (source, range) => getSharedRemoteMediaWindowCache().acquire(source, range),
  getSubtitleContextLines: (range) =>
    collectMediaTimingContextLines({
      cues: appState.activeParsedSubtitleCues,
      fallbackPrevious: appState.subtitleTimingTracker?.getRecentEntries(40) ?? [],
      startTime: range.startTime,
      endTime: range.endTime,
    }),
  openModal: (payload) => openMediaTimingReviewModal(createOverlayHostedModalOpenDeps(), payload),
  onPreviewEnded: (reviewId) => {
    // The review may live in either overlay window; the renderer ignores foreign review ids.
    for (const window of [overlayManager.getMainWindow(), overlayManager.getModalWindow()]) {
      if (window && !window.isDestroyed()) {
        window.webContents.send(IPC_CHANNELS.event.mediaTimingReviewPreviewEnded, reviewId);
      }
    }
  },
  showStatus: (message) =>
    overlayNotificationsRuntime.showConfiguredStatusNotification(message, { variant: 'warning' }),
});

function openOverlayHostedModalWithOsd(
  openModal: (deps: ReturnType<typeof createOverlayHostedModalOpenDeps>) => Promise<boolean>,
  unavailableMessage: string,
  failureLogMessage: string,
): void {
  void openModal(createOverlayHostedModalOpenDeps())
    .then((opened) => {
      if (!opened) {
        overlayNotificationsRuntime.showConfiguredStatusNotification(unavailableMessage, {
          variant: 'warning',
        });
      }
    })
    .catch((error) => {
      logger.error(failureLogMessage, error);
      overlayNotificationsRuntime.showConfiguredStatusNotification(unavailableMessage, {
        variant: 'error',
      });
    });
}

function openRuntimeOptionsPalette(): void {
  openOverlayHostedModalWithOsd(
    openRuntimeOptionsModalRuntime,
    'Runtime options overlay unavailable.',
    'Failed to open runtime options overlay.',
  );
}

function openJimakuOverlay(): void {
  openOverlayHostedModalWithOsd(
    openJimakuModalRuntime,
    'Jimaku overlay unavailable.',
    'Failed to open Jimaku overlay.',
  );
}

function openTsukihimeOverlay(): void {
  openOverlayHostedModalWithOsd(
    openTsukihimeModalRuntime,
    'TsukiHime overlay unavailable.',
    'Failed to open TsukiHime overlay.',
  );
}

function openSessionHelpOverlay(): void {
  openOverlayHostedModalWithOsd(
    openSessionHelpModalRuntime,
    'Session help overlay unavailable.',
    'Failed to open session help overlay.',
  );
}

function openChangelogOverlay(): void {
  openOverlayHostedModalWithOsd(
    openChangelogModalRuntime,
    'Changelog overlay unavailable.',
    'Failed to open changelog overlay.',
  );
}

function openCharacterDictionaryManagerOverlay(): void {
  openCharacterDictionaryManagerWithConfigGate({
    isCharacterDictionaryEnabled: () => configService.getConfig().subtitleStyle.nameMatchEnabled,
    getNotificationType: () => overlayNotificationsRuntime.getConfiguredStatusNotificationType(),
    openManager: () => {
      openOverlayHostedModalWithOsd(
        openCharacterDictionaryManagerModalRuntime,
        'Character dictionary manager unavailable.',
        'Failed to open character dictionary manager.',
      );
    },
    showOsd: (message) => showMpvOsd(message),
    showOverlayNotification: (payload) =>
      overlayNotificationsRuntime.showOverlayNotification(payload),
    showDesktopNotification: (title, options) => showDesktopNotification(title, options),
    logWarn: (message, error) => logger.warn(message, error),
  });
}

function openControllerSelectOverlay(): void {
  openOverlayHostedModalWithOsd(
    openControllerSelectModalRuntime,
    'Controller select overlay unavailable.',
    'Failed to open controller select overlay.',
  );
}

function openControllerDebugOverlay(): void {
  openOverlayHostedModalWithOsd(
    openControllerDebugModalRuntime,
    'Controller debug overlay unavailable.',
    'Failed to open controller debug overlay.',
  );
}

function openPlaylistBrowser(): void {
  if (!appState.mpvClient?.connected) {
    overlayNotificationsRuntime.showConfiguredStatusNotification(
      'Playlist browser requires active playback.',
      {
        title: 'Playlist browser',
        variant: 'warning',
      },
    );
    return;
  }
  openOverlayHostedModalWithOsd(
    openPlaylistBrowserRuntime,
    'Playlist browser overlay unavailable.',
    'Failed to open playlist browser overlay.',
  );
}

function getRuntimeBooleanOption(
  id:
    | 'subtitle.annotation.knownWords.highlightEnabled'
    | 'subtitle.annotation.nPlusOne'
    | 'subtitle.annotation.jlpt'
    | 'subtitle.annotation.frequency',
  fallback: boolean,
): boolean {
  const value = appState.runtimeOptionsManager?.getOptionValue(id);
  return typeof value === 'boolean' ? value : fallback;
}

function shouldInitializeMecabForAnnotations(): boolean {
  const config = configService.getConfig();
  const knownWordsEnabled = getRuntimeBooleanOption(
    'subtitle.annotation.knownWords.highlightEnabled',
    config.ankiConnect.knownWords.highlightEnabled,
  );
  const nPlusOneEnabled = getRuntimeBooleanOption(
    'subtitle.annotation.nPlusOne',
    config.ankiConnect.nPlusOne.enabled,
  );
  const jlptEnabled = getRuntimeBooleanOption(
    'subtitle.annotation.jlpt',
    config.subtitleStyle.enableJlpt,
  );
  const frequencyEnabled = getRuntimeBooleanOption(
    'subtitle.annotation.frequency',
    config.subtitleStyle.frequencyDictionary.enabled,
  );
  return knownWordsEnabled || nPlusOneEnabled || jlptEnabled || frequencyEnabled;
}

const {
  getResolvedJellyfinConfig,
  reportJellyfinRemoteProgress,
  reportJellyfinRemoteStopped,
  startJellyfinRemoteSession,
  stopJellyfinRemoteSession,
  cleanupJellyfinSubtitleCache,
  runJellyfinCommand,
  openJellyfinSetupWindow,
  getJellyfinClientInfo,
} = composeJellyfinRuntimeHandlers({
  getResolvedJellyfinConfigMainDeps: {
    getResolvedConfig: () => configService.getConfig(),
    loadStoredSession: () => jellyfinTokenStore.loadSession(),
    getEnv: (name) => process.env[name],
  },
  getJellyfinClientInfoMainDeps: {
    getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(),
    getHostName: () => os.hostname(),
    defaultClientName: DEFAULT_JELLYFIN_CLIENT_NAME,
    defaultClientVersion: DEFAULT_JELLYFIN_CLIENT_VERSION,
  },
  waitForMpvConnectedMainDeps: {
    getMpvClient: () => appState.mpvClient,
    now: () => Date.now(),
    sleep: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
  },
  launchMpvIdleForJellyfinPlaybackMainDeps: {
    getSocketPath: () => appState.mpvSocketPath,
    getLaunchMode: () => configService.getConfig().mpv.launchMode,
    platform: process.platform,
    execPath: process.execPath,
    getRuntimePluginEntrypoint: () => resolveBundledMpvRuntimePluginEntrypoint(),
    getInstalledPluginDetection: () =>
      detectInstalledMpvPlugin({
        platform: process.platform,
        homeDir: os.homedir(),
        xdgConfigHome: process.env.XDG_CONFIG_HOME,
        appDataDir: app.getPath('appData'),
        mpvExecutablePath: configService.getConfig().mpv.executablePath,
      }),
    getPluginRuntimeConfig: () => getMpvPluginRuntimeConfig(),
    getDefaultMpvLogPath: () => (isLogFileEnabled('mpv') ? DEFAULT_MPV_LOG_PATH : ''),
    defaultMpvArgs: MPV_JELLYFIN_DEFAULT_ARGS,
    removeSocketPath: (socketPath) => {
      fs.rmSync(socketPath, { force: true });
    },
    spawnMpv: (args) => spawnManagedMpvProcess(args),
    logWarn: (message, error) => logger.warn(message, error),
    logInfo: (message) => logger.info(message),
  },
  ensureMpvConnectedForJellyfinPlaybackMainDeps: {
    getMpvClient: () => appState.mpvClient,
    setMpvClient: (client) => {
      appState.mpvClient = client as MpvIpcClient | null;
    },
    createMpvClient: () => createMpvClientRuntimeService(),
    getAutoLaunchInFlight: () => jellyfinMpvAutoLaunchInFlight,
    setAutoLaunchInFlight: (promise) => {
      jellyfinMpvAutoLaunchInFlight = promise;
    },
    connectTimeoutMs: JELLYFIN_MPV_CONNECT_TIMEOUT_MS,
    autoLaunchTimeoutMs: JELLYFIN_MPV_AUTO_LAUNCH_TIMEOUT_MS,
  },
  preloadJellyfinExternalSubtitlesMainDeps: {
    listJellyfinSubtitleTracks: (session, clientInfo, itemId) =>
      listJellyfinSubtitleTracksRuntime(session, clientInfo, itemId),
    getMpvClient: () => appState.mpvClient,
    sendMpvCommand: (command) => {
      sendMpvCommandRuntime(appState.mpvClient, command);
    },
    wait: (ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)),
    cacheSubtitleTrack: (track) => jellyfinSubtitleCacheIo.cacheSubtitleTrack(track),
    cleanupCachedSubtitles: (dirs) => jellyfinSubtitleCacheIo.cleanupCachedSubtitles(dirs),
    initSubtitlePrefetch: (sourcePath) =>
      subtitlePrefetchRuntime.refreshSubtitleSidebarFromSource(sourcePath),
    logDebug: (message, error) => {
      logger.debug(message, error);
    },
  },
  playJellyfinItemInMpvMainDeps: {
    getMpvClient: () => appState.mpvClient,
    resolvePlaybackPlan: (params) =>
      resolveJellyfinPlaybackPlanRuntime(
        params.session,
        params.clientInfo,
        params.jellyfinConfig as ReturnType<typeof getResolvedJellyfinConfig>,
        {
          itemId: params.itemId,
          audioStreamIndex: params.audioStreamIndex ?? undefined,
          subtitleStreamIndex: params.subtitleStreamIndex ?? undefined,
        },
      ),
    applyJellyfinMpvDefaults: (mpvClient) => applyJellyfinMpvDefaultsHandler(mpvClient),
    showVisibleOverlay: () => setVisibleOverlayVisible(true),
    sendMpvCommand: (command) => sendMpvCommandRuntime(appState.mpvClient, command),
    armQuitOnDisconnect: () => {
      jellyfinPlayQuitOnDisconnectArmed = false;
      setTimeout(() => {
        jellyfinPlayQuitOnDisconnectArmed = true;
      }, 3000);
    },
    schedule: (callback, delayMs) => {
      setTimeout(callback, delayMs);
    },
    convertTicksToSeconds: (ticks) => jellyfinTicksToSecondsRuntime(ticks),
    setActivePlayback: (state) => {
      activeJellyfinRemotePlayback = {
        ...(state as ActiveJellyfinRemotePlaybackState),
        stopReportsAfterMs:
          state.stopReportsAfterMs ?? Date.now() + JELLYFIN_REMOTE_STARTUP_STOP_GRACE_MS,
      };
    },
    setLastProgressAtMs: (value) => {
      jellyfinRemoteLastProgressAtMs = value;
    },
    reportPlaying: (payload) => {
      void appState.jellyfinRemoteSession?.reportPlaying(payload);
    },
    showMpvOsd: (text) => {
      overlayNotificationsRuntime.showConfiguredStatusNotification(text, { title: 'Jellyfin' });
    },
    updateCurrentMediaTitle: (title) => {
      mediaRuntime.updateCurrentMediaTitle(title);
    },
    recordJellyfinPlaybackMetadata: (metadata) => {
      ensureImmersionTrackerStarted();
      appState.immersionTracker?.recordJellyfinPlaybackMetadata(metadata);
    },
  },
  remoteComposerOptions: {
    getConfiguredSession: () => getConfiguredJellyfinSession(getResolvedJellyfinConfig()),
    logWarn: (message) => logger.warn(message),
    getMpvClient: () => appState.mpvClient,
    sendMpvCommand: (client, command) => sendMpvCommandRuntime(client as MpvIpcClient, command),
    jellyfinTicksToSeconds: (ticks) => jellyfinTicksToSecondsRuntime(ticks),
    getActivePlayback: () => activeJellyfinRemotePlayback,
    clearActivePlayback: () => {
      activeJellyfinRemotePlayback = null;
    },
    getSession: () => appState.jellyfinRemoteSession,
    getNow: () => Date.now(),
    getLastProgressAtMs: () => jellyfinRemoteLastProgressAtMs,
    setLastProgressAtMs: (value) => {
      jellyfinRemoteLastProgressAtMs = value;
    },
    progressIntervalMs: JELLYFIN_REMOTE_PROGRESS_INTERVAL_MS,
    ticksPerSecond: JELLYFIN_TICKS_PER_SECOND,
    logDebug: (message, error) => logger.debug(message, error),
  },
  handleJellyfinAuthCommandsMainDeps: {
    patchRawConfig: (patch) => {
      configService.patchRawConfig(patch);
      refreshTrayMenuIfPresent();
    },
    authenticateWithPassword: (serverUrl, username, password, clientInfo) =>
      authenticateWithPasswordRuntime(serverUrl, username, password, clientInfo),
    saveStoredSession: (session) => jellyfinTokenStore.saveSession(session),
    clearStoredSession: () =>
      clearJellyfinAuthSessionAndRefreshTrayRuntime(getJellyfinTrayDiscoveryDeps()),
    logInfo: (message) => logger.info(message),
  },
  handleJellyfinListCommandsMainDeps: {
    listJellyfinLibraries: (session, clientInfo) =>
      listJellyfinLibrariesRuntime(session, clientInfo),
    listJellyfinItems: (session, clientInfo, params) =>
      listJellyfinItemsRuntime(session, clientInfo, params),
    listJellyfinSubtitleTracks: (session, clientInfo, itemId) =>
      listJellyfinSubtitleTracksRuntime(session, clientInfo, itemId),
    writeJellyfinPreviewAuth: (responsePath, payload) => {
      fs.mkdirSync(path.dirname(responsePath), { recursive: true });
      fs.writeFileSync(responsePath, JSON.stringify(payload, null, 2), 'utf-8');
    },
    logInfo: (message) => logger.info(message),
  },
  handleJellyfinPlayCommandMainDeps: {
    logWarn: (message) => logger.warn(message),
  },
  handleJellyfinRemoteAnnounceCommandMainDeps: {
    getRemoteSession: () => appState.jellyfinRemoteSession,
    logInfo: (message) => logger.info(message),
    logWarn: (message) => logger.warn(message),
  },
  startJellyfinRemoteSessionMainDeps: {
    getCurrentSession: () => appState.jellyfinRemoteSession,
    setCurrentSession: (session) => {
      appState.jellyfinRemoteSession = session as typeof appState.jellyfinRemoteSession;
    },
    createRemoteSessionService: (options) => new JellyfinRemoteSessionService(options),
    getHostName: () => os.hostname(),
    defaultDeviceId: createHostDerivedJellyfinDeviceId(os.hostname()),
    defaultClientName: DEFAULT_JELLYFIN_CLIENT_NAME,
    defaultClientVersion: DEFAULT_JELLYFIN_CLIENT_VERSION,
    logInfo: (message) => logger.info(message),
    logWarn: (message, details) => logger.warn(message, details),
    onSessionStateChanged: () => refreshTrayMenuIfPresent(),
  },
  stopJellyfinRemoteSessionMainDeps: {
    getCurrentSession: () => appState.jellyfinRemoteSession,
    setCurrentSession: (session) => {
      appState.jellyfinRemoteSession = session as typeof appState.jellyfinRemoteSession;
    },
    clearActivePlayback: () => {
      activeJellyfinRemotePlayback = null;
    },
    onSessionStateChanged: () => refreshTrayMenuIfPresent(),
  },
  runJellyfinCommandMainDeps: {
    defaultServerUrl: DEFAULT_CONFIG.jellyfin.serverUrl,
  },
  maybeFocusExistingJellyfinSetupWindowMainDeps: {
    getSetupWindow: () => appState.jellyfinSetupWindow,
  },
  openJellyfinSetupWindowMainDeps: {
    createSetupWindow: createCreateJellyfinSetupWindowHandler({
      createBrowserWindow: (options) => new BrowserWindow(options),
      preloadPath: JELLYFIN_SETUP_PRELOAD_PATH,
    }),
    buildSetupFormHtml: (state) => buildJellyfinSetupFormHtml(state),
    parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl),
    authenticateWithPassword: (server, username, password, clientInfo) =>
      authenticateWithPasswordRuntime(server, username, password, clientInfo),
    saveStoredSession: (session) => jellyfinTokenStore.saveSession(session),
    clearStoredSession: () =>
      clearJellyfinAuthSessionAndRefreshTrayRuntime(getJellyfinTrayDiscoveryDeps()),
    patchJellyfinConfig: (session) => {
      const recentServers = mergeJellyfinRecentServers(
        session.serverUrl,
        configService.getConfig().jellyfin.recentServers || [],
      );
      configService.patchRawConfig({
        jellyfin: {
          enabled: true,
          serverUrl: session.serverUrl,
          username: session.username,
          recentServers,
        },
      });
      refreshTrayMenuIfPresent();
    },
    persistAuthenticatedSession: (session, clientInfo) =>
      persistJellyfinAuthSession({
        session,
        clientInfo,
        existingRecentServers: configService.getConfig().jellyfin.recentServers || [],
        saveStoredSession: (storedSession) => jellyfinTokenStore.saveSession(storedSession),
        patchRawConfig: (patch) => {
          configService.patchRawConfig(patch);
          refreshTrayMenuIfPresent();
        },
      }),
    logInfo: (message) => logger.info(message),
    logError: (message, error) => logger.error(message, error),
    showMpvOsd: (message) =>
      overlayNotificationsRuntime.showConfiguredStatusNotification(message, { title: 'Jellyfin' }),
    clearSetupWindow: () => {
      appState.jellyfinSetupWindow = null;
    },
    setSetupWindow: (window) => {
      appState.jellyfinSetupWindow = window as BrowserWindow;
    },
    registerSetupIpcHandler: (handler) => {
      const channel = IPC_CHANNELS.request.jellyfinSetupSubmit;
      ipcMain.removeHandler(channel);
      ipcMain.handle(channel, async (event, payload) => {
        const setupWindow = appState.jellyfinSetupWindow;
        if (!setupWindow || event.sender !== setupWindow.webContents) {
          return {
            handled: false,
            statusMessage: 'This Jellyfin setup window is no longer active.',
            statusKind: 'error',
          };
        }
        return handler(payload);
      });
      return () => {
        ipcMain.removeHandler(channel);
      };
    },
    encodeURIComponent: (value) => encodeURIComponent(value),
    defaultServerUrl: DEFAULT_CONFIG.jellyfin.serverUrl || 'http://127.0.0.1:8096',
    hasStoredSession: () => Boolean(jellyfinTokenStore.loadSession()),
  },
});

const maybeFocusExistingFirstRunSetupWindow = createMaybeFocusExistingFirstRunSetupWindowHandler({
  getSetupWindow: () => appState.firstRunSetupWindow,
});
const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
  maybeFocusExistingSetupWindow: maybeFocusExistingFirstRunSetupWindow,
  createSetupWindow: createCreateFirstRunSetupWindowHandler({
    createBrowserWindow: (options) => new BrowserWindow(options),
  }),
  getSetupSnapshot: async () => {
    const snapshot = await firstRunSetupService.getSetupStatus();
    const mpvExecutablePath = configService.getConfig().mpv.executablePath;
    return {
      configReady: snapshot.configReady,
      dictionaryCount: snapshot.dictionaryCount,
      canFinish: snapshot.canFinish,
      externalYomitanConfigured: snapshot.externalYomitanConfigured,
      pluginStatus: snapshot.pluginStatus,
      pluginInstallPathSummary: snapshot.pluginInstallPathSummary,
      legacyMpvPluginPaths: snapshot.legacyMpvPluginPaths,
      mpvExecutablePath,
      mpvExecutablePathStatus: getConfiguredWindowsMpvPathStatus(mpvExecutablePath),
      windowsMpvShortcuts: snapshot.windowsMpvShortcuts,
      commandLineLauncher: snapshot.commandLineLauncher,
      message: firstRunSetupMessage,
    };
  },
  buildSetupHtml: (model) => buildFirstRunSetupHtml(model),
  parseSubmissionUrl: (rawUrl) => parseFirstRunSetupSubmissionUrl(rawUrl),
  handleAction: async (submission: FirstRunSetupSubmission) => {
    if (submission.action === 'remove-legacy-plugin') {
      const snapshot = await firstRunSetupService.removeLegacyMpvPlugin();
      firstRunSetupMessage = snapshot.message;
      return;
    }
    if (submission.action === 'configure-mpv-executable-path') {
      const mpvExecutablePath = submission.mpvExecutablePath?.trim() ?? '';
      const pathStatus = getConfiguredWindowsMpvPathStatus(mpvExecutablePath);
      configService.patchRawConfig({
        mpv: {
          executablePath: mpvExecutablePath,
        },
      });
      firstRunSetupMessage =
        pathStatus === 'invalid'
          ? `Saved mpv executable path, but the file was not found: ${mpvExecutablePath}`
          : mpvExecutablePath
            ? `Saved mpv executable path: ${mpvExecutablePath}`
            : 'Cleared mpv executable path. SubMiner will auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.';
      return;
    }
    if (submission.action === 'configure-windows-mpv-shortcuts') {
      const snapshot = await firstRunSetupService.configureWindowsMpvShortcuts({
        startMenuEnabled: submission.startMenuEnabled === true,
        desktopEnabled: submission.desktopEnabled === true,
      });
      firstRunSetupMessage = snapshot.message;
      return;
    }
    if (submission.action === 'install-bun') {
      const snapshot = await firstRunSetupService.installBun();
      firstRunSetupMessage = snapshot.message;
      return;
    }
    if (submission.action === 'install-command-line-launcher') {
      const snapshot = await firstRunSetupService.installCommandLineLauncher();
      firstRunSetupMessage = snapshot.message;
      return;
    }
    if (submission.action === 'open-yomitan-settings') {
      firstRunSetupMessage = openYomitanSettings()
        ? 'Opened Yomitan settings. Install dictionaries, then refresh status.'
        : 'Yomitan settings are unavailable while external read-only profile mode is enabled.';
      return;
    }
    if (submission.action === 'open-config-settings') {
      const opened = configSettingsRuntime.openWindow();
      firstRunSetupMessage = opened
        ? 'Opened SubMiner settings.'
        : 'SubMiner settings are unavailable.';
      if (opened) {
        return { skipRender: true };
      }
      return;
    }
    if (submission.action === 'refresh') {
      const snapshot = await firstRunSetupService.refreshStatus('Status refreshed.');
      firstRunSetupMessage = snapshot.message;
      return;
    }

    const snapshot = await firstRunSetupService.markSetupCompleted();
    if (snapshot.state.status === 'completed') {
      firstRunSetupMessage = null;
      return { closeWindow: true };
    }
    firstRunSetupMessage =
      getFirstRunSetupCompletionMessage(snapshot) ??
      'Finish setup requires the mpv plugin and Yomitan dictionaries.';
    return;
  },
  markSetupInProgress: async () => {
    firstRunSetupMessage = null;
    await firstRunSetupService.markSetupInProgress();
  },
  markSetupCancelled: async () => {
    firstRunSetupMessage = null;
    await firstRunSetupService.markSetupCancelled();
  },
  isSetupCompleted: () => firstRunSetupService.isSetupCompleted(),
  shouldQuitWhenClosedIncomplete: () => !appState.backgroundMode,
  shouldQuitWhenClosedCompleted: () =>
    Boolean(appState.initialArgs && isStandaloneFirstRunSetupCommand(appState.initialArgs)),
  quitApp: () => requestAppQuit(),
  clearSetupWindow: () => {
    appState.firstRunSetupWindow = null;
  },
  setSetupWindow: (window) => {
    appState.firstRunSetupWindow = window as BrowserWindow;
  },
  encodeURIComponent: (value) => encodeURIComponent(value),
  logError: (message, error) => logger.error(message, error),
});

function openFirstRunSetupWindow(force = false): void {
  if (!force && firstRunSetupService.isSetupCompleted()) {
    return;
  }
  openFirstRunSetupWindowHandler();
}

const {
  notifyAnilistSetup,
  consumeAnilistSetupTokenFromUrl,
  handleAnilistSetupProtocolUrl,
  registerSubminerProtocolClient,
} = composeAnilistSetupHandlers({
  notifyDeps: {
    getNotificationType: () => overlayNotificationsRuntime.getConfiguredStatusNotificationType(),
    hasMpvClient: () => Boolean(appState.mpvClient),
    showMpvOsd: (message) =>
      overlayNotificationsRuntime.showConfiguredStatusNotification(message, { title: 'AniList' }),
    showOverlayNotification: (payload) =>
      overlayNotificationsRuntime.showOverlayNotification(payload),
    showDesktopNotification: (title, options) => showDesktopNotification(title, options),
    logInfo: (message) => logger.info(message),
  },
  consumeTokenDeps: {
    consumeAnilistSetupCallbackUrl,
    saveToken: (token) => anilistTokenStore.saveToken(token),
    setCachedToken: (token) => {
      anilistCachedAccessToken = token;
    },
    setResolvedState: (resolvedAt) => {
      anilistStateRuntime.setClientSecretState({
        status: 'resolved',
        source: 'stored',
        message: 'saved token from AniList login',
        resolvedAt,
        errorAt: null,
      });
    },
    setSetupPageOpened: (opened) => {
      appState.anilistSetupPageOpened = opened;
    },
    onSuccess: () => {
      notifyAnilistSetup('AniList login success');
    },
    closeWindow: () => {
      if (appState.anilistSetupWindow && !appState.anilistSetupWindow.isDestroyed()) {
        appState.anilistSetupWindow.close();
      }
    },
  },
  handleProtocolDeps: {
    consumeAnilistSetupTokenFromUrl: (rawUrl) => consumeAnilistSetupTokenFromUrl(rawUrl),
    logWarn: (message, details) => logger.warn(message, details),
  },
  registerProtocolClientDeps: {
    isDefaultApp: () => Boolean(process.defaultApp),
    getArgv: () => process.argv,
    execPath: process.execPath,
    resolvePath: (value) => path.resolve(value),
    setAsDefaultProtocolClient: (scheme, appPath, args) =>
      appPath
        ? app.setAsDefaultProtocolClient(scheme, appPath, args)
        : app.setAsDefaultProtocolClient(scheme),
    logDebug: (message, details) => logger.debug(message, details),
  },
});

const maybeFocusExistingAnilistSetupWindow = createMaybeFocusExistingAnilistSetupWindowHandler({
  getSetupWindow: () => appState.anilistSetupWindow,
});
const buildOpenAnilistSetupWindowMainDepsHandler = createBuildOpenAnilistSetupWindowMainDepsHandler(
  {
    maybeFocusExistingSetupWindow: maybeFocusExistingAnilistSetupWindow,
    createSetupWindow: createCreateAnilistSetupWindowHandler({
      createBrowserWindow: (options) => new BrowserWindow(options),
    }),
    buildAuthorizeUrl: () =>
      buildAnilistSetupUrl({
        authorizeUrl: ANILIST_SETUP_CLIENT_ID_URL,
        clientId: ANILIST_DEFAULT_CLIENT_ID,
        responseType: ANILIST_SETUP_RESPONSE_TYPE,
      }),
    consumeCallbackUrl: (rawUrl) => consumeAnilistSetupTokenFromUrl(rawUrl),
    openSetupInBrowser: (authorizeUrl) =>
      openAnilistSetupInBrowser({
        authorizeUrl,
        openExternal: (url) => shell.openExternal(url),
        logError: (message, error) => logger.error(message, error),
      }),
    loadManualTokenEntry: (setupWindow, authorizeUrl) =>
      loadAnilistManualTokenEntry({
        setupWindow: setupWindow as BrowserWindow,
        authorizeUrl,
        developerSettingsUrl: ANILIST_DEVELOPER_SETTINGS_URL,
        logWarn: (message, data) => logger.warn(message, data),
      }),
    redirectUri: ANILIST_REDIRECT_URI,
    developerSettingsUrl: ANILIST_DEVELOPER_SETTINGS_URL,
    isAllowedExternalUrl: (url) => isAllowedAnilistExternalUrl(url),
    isAllowedNavigationUrl: (url) => isAllowedAnilistSetupNavigationUrl(url),
    logWarn: (message, details) => logger.warn(message, details),
    logError: (message, details) => logger.error(message, details),
    clearSetupWindow: () => {
      appState.anilistSetupWindow = null;
    },
    setSetupPageOpened: (opened) => {
      appState.anilistSetupPageOpened = opened;
    },
    setSetupWindow: (setupWindow) => {
      appState.anilistSetupWindow = setupWindow as BrowserWindow;
    },
    openExternal: (url) => {
      void shell.openExternal(url);
    },
  },
);

const openAnilistSetupWindowHandler = createOpenAnilistSetupWindowHandler(
  buildOpenAnilistSetupWindowMainDepsHandler(),
);

const {
  refreshAnilistClientSecretState,
  getCurrentAnilistMediaKey,
  resetAnilistMediaTracking,
  getAnilistMediaGuessRuntimeState,
  setAnilistMediaGuessRuntimeState,
  recordAnilistMediaDuration,
  resetAnilistMediaGuessState,
  maybeProbeAnilistDuration,
  ensureAnilistMediaGuess,
  processNextAnilistRetryUpdate,
  maybeRunAnilistPostWatchUpdate,
} = composeAnilistTrackingHandlers({
  refreshClientSecretMainDeps: {
    getResolvedConfig: () => configService.getConfig(),
    isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config as ResolvedConfig),
    getCachedAccessToken: () => anilistCachedAccessToken,
    setCachedAccessToken: (token) => {
      anilistCachedAccessToken = token;
    },
    saveStoredToken: (token) => {
      anilistTokenStore.saveToken(token);
    },
    loadStoredToken: () => anilistTokenStore.loadToken(),
    setClientSecretState: (state) => {
      anilistStateRuntime.setClientSecretState(state);
    },
    getAnilistSetupPageOpened: () => appState.anilistSetupPageOpened,
    setAnilistSetupPageOpened: (opened) => {
      appState.anilistSetupPageOpened = opened;
    },
    openAnilistSetupWindow: () => {
      openAnilistSetupWindowHandler();
    },
    now: () => Date.now(),
  },
  getCurrentMediaKeyMainDeps: {
    getCurrentMediaPath: () => appState.currentMediaPath,
  },
  resetMediaTrackingMainDeps: {
    setMediaKey: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { mediaKey: value },
      );
    },
    setMediaDurationSec: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { mediaDurationSec: value },
      );
    },
    setMediaGuess: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { mediaGuess: value },
      );
    },
    setMediaGuessPromise: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { mediaGuessPromise: value },
      );
    },
    setLastDurationProbeAtMs: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { lastDurationProbeAtMs: value },
      );
    },
  },
  getMediaGuessRuntimeStateMainDeps: {
    getMediaKey: () => anilistMediaGuessRuntimeState.mediaKey,
    getMediaDurationSec: () => anilistMediaGuessRuntimeState.mediaDurationSec,
    getMediaGuess: () => anilistMediaGuessRuntimeState.mediaGuess,
    getMediaGuessPromise: () => anilistMediaGuessRuntimeState.mediaGuessPromise,
    getLastDurationProbeAtMs: () => anilistMediaGuessRuntimeState.lastDurationProbeAtMs,
  },
  setMediaGuessRuntimeStateMainDeps: {
    setMediaKey: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { mediaKey: value },
      );
    },
    setMediaDurationSec: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { mediaDurationSec: value },
      );
    },
    setMediaGuess: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { mediaGuess: value },
      );
    },
    setMediaGuessPromise: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { mediaGuessPromise: value },
      );
    },
    setLastDurationProbeAtMs: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { lastDurationProbeAtMs: value },
      );
    },
  },
  recordMediaDurationMainDeps: {
    getCurrentMediaKey: () => getCurrentAnilistMediaKey(),
    getState: () => getAnilistMediaGuessRuntimeState(),
    setState: (state) => {
      setAnilistMediaGuessRuntimeState(state);
    },
  },
  resetMediaGuessStateMainDeps: {
    setMediaGuess: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { mediaGuess: value },
      );
    },
    setMediaGuessPromise: (value) => {
      anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
        anilistMediaGuessRuntimeState,
        { mediaGuessPromise: value },
      );
    },
  },
  maybeProbeDurationMainDeps: {
    getState: () => getAnilistMediaGuessRuntimeState(),
    setState: (state) => {
      setAnilistMediaGuessRuntimeState(state);
    },
    durationRetryIntervalMs: ANILIST_DURATION_RETRY_INTERVAL_MS,
    now: () => Date.now(),
    requestMpvDuration: async () => appState.mpvClient?.requestProperty('duration'),
    logWarn: (message, error) => logger.warn(message, error),
  },
  ensureMediaGuessMainDeps: {
    getState: () => getAnilistMediaGuessRuntimeState(),
    setState: (state) => {
      setAnilistMediaGuessRuntimeState(state);
    },
    resolveMediaPathForJimaku: (currentMediaPath) =>
      mediaRuntime.resolveMediaPathForJimaku(currentMediaPath),
    getCurrentMediaPath: () => appState.currentMediaPath,
    getCurrentMediaTitle: () => appState.currentMediaTitle,
    guessAnilistMediaInfo: (mediaPath, mediaTitle) => guessAnilistMediaInfo(mediaPath, mediaTitle),
  },
  processNextRetryUpdateMainDeps: {
    nextReady: () => anilistUpdateQueue.nextReady(),
    refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(),
    setLastAttemptAt: (value) => {
      appState.anilistRetryQueueState = transitionAnilistRetryQueueLastAttemptAt(
        appState.anilistRetryQueueState,
        value,
      );
    },
    setLastError: (value) => {
      appState.anilistRetryQueueState = transitionAnilistRetryQueueLastError(
        appState.anilistRetryQueueState,
        value,
      );
    },
    refreshAnilistClientSecretState: () => refreshAnilistClientSecretState(),
    updateAnilistPostWatchProgress: (accessToken, title, episode, season, mediaId) =>
      updateAnilistPostWatchProgress(accessToken, title, episode, {
        rateLimiter: anilistRateLimiter,
        season,
        mediaId,
        logInfo: (message) => logger.info(message),
      }),
    markSuccess: (key) => {
      anilistUpdateQueue.markSuccess(key);
    },
    rememberAttemptedUpdateKey: (key) => {
      rememberAnilistAttemptedUpdate(key);
    },
    markFailure: (key, message) => {
      anilistUpdateQueue.markFailure(key, message);
    },
    logInfo: (message) => logger.info(message),
    now: () => Date.now(),
  },
  maybeRunPostWatchUpdateMainDeps: {
    getInFlight: () => anilistUpdateInFlightState.inFlight,
    setInFlight: (value) => {
      anilistUpdateInFlightState = transitionAnilistUpdateInFlightState(
        anilistUpdateInFlightState,
        value,
      );
    },
    getResolvedConfig: () => configService.getConfig(),
    isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config as ResolvedConfig),
    getCurrentMediaKey: () => getCurrentAnilistMediaKey(),
    hasMpvClient: () => Boolean(appState.mpvClient),
    getTrackedMediaKey: () => anilistMediaGuessRuntimeState.mediaKey,
    resetTrackedMedia: (mediaKey) => {
      resetAnilistMediaTracking(mediaKey);
    },
    getWatchedSeconds: () => appState.mpvClient?.currentTimePos ?? Number.NaN,
    maybeProbeAnilistDuration: (mediaKey, options) => maybeProbeAnilistDuration(mediaKey, options),
    ensureAnilistMediaGuess: (mediaKey) => ensureAnilistMediaGuess(mediaKey),
    hasAttemptedUpdateKey: (key) => anilistAttemptedUpdateKeys.has(key),
    processNextAnilistRetryUpdate: () => processNextAnilistRetryUpdate(),
    refreshAnilistClientSecretState: () => refreshAnilistClientSecretState(),
    enqueueRetry: (key, title, episode, season, mediaId) => {
      anilistUpdateQueue.enqueue(key, title, episode, season, mediaId);
    },
    getCurrentMediaTitle: () => appState.currentMediaTitle,
    resolvePinnedAnilistMediaId: ({ mediaPath, mediaTitle, guess }) =>
      characterDictionaryRuntime.resolvePinnedMediaId({ mediaPath, mediaTitle, guess }),
    markRetryFailure: (key, message) => {
      anilistUpdateQueue.markFailure(key, message);
    },
    markRetrySuccess: (key) => {
      anilistUpdateQueue.markSuccess(key);
    },
    refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(),
    updateAnilistPostWatchProgress: (accessToken, title, episode, season, mediaId) =>
      updateAnilistPostWatchProgress(accessToken, title, episode, {
        rateLimiter: anilistRateLimiter,
        season,
        mediaId,
        logInfo: (message) => logger.info(message),
      }),
    rememberAttemptedUpdateKey: (key) => {
      rememberAnilistAttemptedUpdate(key);
    },
    showMpvOsd: (message) =>
      overlayNotificationsRuntime.showConfiguredStatusNotification(message, { title: 'AniList' }),
    logInfo: (message) => logger.info(message),
    logWarn: (message) => logger.warn(message),
    minWatchSeconds: ANILIST_UPDATE_MIN_WATCH_SECONDS,
    minWatchRatio: DEFAULT_MIN_WATCH_RATIO,
  },
});

function refreshAnilistClientSecretStateIfEnabled(options?: {
  force?: boolean;
  allowSetupPrompt?: boolean;
}): Promise<string | null> {
  if (!isAnilistTrackingEnabled(configService.getConfig())) {
    return Promise.resolve(null);
  }
  return refreshAnilistClientSecretState(options);
}

const rememberAnilistAttemptedUpdate = (key: string): void => {
  rememberAnilistAttemptedUpdateKey(
    anilistAttemptedUpdateKeys,
    key,
    ANILIST_MAX_ATTEMPTED_UPDATE_KEYS,
  );
};

const buildLoadSubtitlePositionMainDepsHandler = createBuildLoadSubtitlePositionMainDepsHandler({
  loadSubtitlePositionCore: () =>
    loadSubtitlePositionCore({
      currentMediaPath: appState.currentMediaPath,
      fallbackPosition: configService.getConfig().subtitlePosition,
      subtitlePositionsDir: SUBTITLE_POSITIONS_DIR,
    }),
  setSubtitlePosition: (position) => {
    appState.subtitlePosition = position;
  },
});
const loadSubtitlePositionMainDeps = buildLoadSubtitlePositionMainDepsHandler();
const loadSubtitlePosition = createLoadSubtitlePositionHandler(loadSubtitlePositionMainDeps);

const buildSaveSubtitlePositionMainDepsHandler = createBuildSaveSubtitlePositionMainDepsHandler({
  saveSubtitlePositionCore: (position) => {
    saveSubtitlePositionCore({
      position,
      currentMediaPath: appState.currentMediaPath,
      subtitlePositionsDir: SUBTITLE_POSITIONS_DIR,
      onQueuePending: (queued) => {
        appState.pendingSubtitlePosition = queued;
      },
      onPersisted: () => {
        appState.pendingSubtitlePosition = null;
      },
    });
  },
  setSubtitlePosition: (position) => {
    appState.subtitlePosition = position;
  },
});
const saveSubtitlePositionMainDeps = buildSaveSubtitlePositionMainDepsHandler();
const saveSubtitlePosition = createSaveSubtitlePositionHandler(saveSubtitlePositionMainDeps);

registerSubminerProtocolClient();
let flushPendingMpvLogWrites = (): void => {};
const {
  registerProtocolUrlHandlers: registerProtocolUrlHandlersHandler,
  onWillQuitCleanup: onWillQuitCleanupHandler,
  shouldRestoreWindowsOnActivate: shouldRestoreWindowsOnActivateHandler,
  restoreWindowsOnActivate: restoreWindowsOnActivateHandler,
} = composeStartupLifecycleHandlers({
  registerProtocolUrlHandlersMainDeps: {
    registerOpenUrl: (listener) => {
      app.on('open-url', listener);
    },
    registerSecondInstance: (listener) => {
      registerSecondInstanceHandlerEarly(app, listener);
    },
    handleAnilistSetupProtocolUrl: (rawUrl) => handleAnilistSetupProtocolUrl(rawUrl),
    findAnilistSetupDeepLinkArgvUrl: (argv) => findAnilistSetupDeepLinkArgvUrl(argv),
    logUnhandledOpenUrl: (rawUrl) => {
      logger.warn('Unhandled app protocol URL', { rawUrl });
    },
    logUnhandledSecondInstanceUrl: (rawUrl) => {
      logger.warn('Unhandled second-instance protocol URL', { rawUrl });
    },
  },
  onWillQuitCleanupMainDeps: {
    destroyTray: () => destroyTrayHandler(),
    stopConfigHotReload: () => configHotReloadRuntime.stop(),
    restorePreviousSecondarySubVisibility: () =>
      overlayVisibilityComposer.restorePreviousSecondarySubVisibility(),
    restoreMpvSubVisibility: () => {
      restoreOverlayMpvSubtitles({ force: true });
    },
    isAppReady: () => app.isReady(),
    unregisterAllGlobalShortcuts: () => globalShortcut.unregisterAll(),
    stopSubtitleWebsocket: () => {
      subtitleWsService.stop();
      annotationSubtitleWsService.stop();
    },
    stopTexthookerService: () => texthookerService.stop(),
    stopSyncAutoScheduler: async () => {
      syncAutoScheduler.stop();
      await syncUiRuntime.shutdown();
    },
    clearWindowsVisibleOverlayForegroundPollLoop: () =>
      visibleOverlayInteractionRuntime.clearWindowsVisibleOverlayForegroundPollLoop(),
    clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => {
      cancelLinuxMpvFullscreenOverlayRefreshBurst = null;
      clearLinuxMpvFullscreenOverlayRefreshTimeouts();
    },
    getMainOverlayWindow: () => overlayManager.getMainWindow(),
    clearMainOverlayWindow: () => overlayManager.setMainWindow(null),
    getModalOverlayWindow: () => overlayManager.getModalWindow(),
    clearModalOverlayWindow: () => overlayManager.setModalWindow(null),
    getYomitanParserWindow: () => appState.yomitanParserWindow,
    clearYomitanParserState: () => {
      appState.yomitanParserWindow = null;
      appState.yomitanParserReadyPromise = null;
      appState.yomitanParserInitPromise = null;
      appState.yomitanSession = null;
    },
    getWindowTracker: () => appState.windowTracker,
    flushMpvLog: () => flushPendingMpvLogWrites(),
    getMpvSocket: () => appState.mpvClient?.socket ?? null,
    getReconnectTimer: () => appState.reconnectTimer,
    clearReconnectTimerRef: () => {
      appState.reconnectTimer = null;
    },
    getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
    getImmersionTracker: () => appState.immersionTracker,
    clearImmersionTracker: () => {
      stopStatsServer();
      appState.statsServer = null;
      appState.immersionTracker = null;
    },
    getAnkiIntegration: () => appState.ankiIntegration,
    getAnilistSetupWindow: () => appState.anilistSetupWindow,
    clearAnilistSetupWindow: () => {
      appState.anilistSetupWindow = null;
    },
    getJellyfinSetupWindow: () => appState.jellyfinSetupWindow,
    clearJellyfinSetupWindow: () => {
      appState.jellyfinSetupWindow = null;
    },
    getFirstRunSetupWindow: () => appState.firstRunSetupWindow,
    clearFirstRunSetupWindow: () => {
      appState.firstRunSetupWindow = null;
    },
    getYomitanSettingsWindow: () => appState.yomitanSettingsWindow,
    clearYomitanSettingsWindow: () => {
      appState.yomitanSettingsWindow = null;
    },
    stopJellyfinRemoteSession: () => stopJellyfinRemoteSession(),
    cleanupInternalSubtitleTrackCache: () => cachedInternalSubtitleTrackExtractor.clear(),
    cleanupYoutubeSubtitleTempDirs: () => youtubeFlowRuntime.cleanupSubtitleTempDirs(),
    cleanupYoutubeMediaCache: () => youtubeMediaCache.cleanup(),
    cleanupRemoteMediaWindows: () => getSharedRemoteMediaWindowCache().cleanup(),
    cleanupJellyfinSubtitleCache: () => cleanupJellyfinSubtitleCache(),
    stopDiscordPresenceService: () => {
      void appState.discordPresenceService?.stop();
      appState.discordPresenceService = null;
    },
  },
  shouldRestoreWindowsOnActivateMainDeps: {
    isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
    getAllWindowCount: () => BrowserWindow.getAllWindows().length,
  },
  restoreWindowsOnActivateMainDeps: {
    createMainWindow: () => {
      createMainWindow();
    },
    updateVisibleOverlayVisibility: () => {
      overlayVisibilityRuntime.updateVisibleOverlayVisibility();
    },
    syncOverlayMpvSubtitleSuppression: () => {
      syncOverlayMpvSubtitleSuppression();
    },
  },
});
registerProtocolUrlHandlersHandler();

const resolveLegacyVocabularyPos = async (row: {
  headword: string;
  word: string;
  reading: string | null;
}) => {
  const tokenizer = appState.mecabTokenizer;
  if (!tokenizer) {
    return null;
  }

  const lookupTexts = [...new Set([row.headword, row.word, row.reading ?? ''])]
    .map((value) => value.trim())
    .filter((value) => value.length > 0);

  for (const lookupText of lookupTexts) {
    const tokens = await tokenizer.tokenize(lookupText);
    const resolved = resolveLegacyVocabularyPosFromTokens(lookupText, tokens);
    if (resolved) {
      return resolved;
    }
  }

  return null;
};

const immersionTrackerStartupMainDeps: Parameters<
  typeof createBuildImmersionTrackerStartupMainDepsHandler
>[0] = {
  getResolvedConfig: () => configService.getConfig(),
  getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(),
  createTrackerService: (params) =>
    new ImmersionTrackerService({
      ...params,
      resolveLegacyVocabularyPos,
    }),
  setTracker: (tracker) => {
    const trackerHasChanged =
      appState.immersionTracker !== null && appState.immersionTracker !== tracker;
    if (trackerHasChanged && appState.statsServer) {
      stopStatsServer();
      appState.statsServer = null;
    }

    appState.immersionTracker = tracker as ImmersionTrackerService | null;
    appState.immersionTracker?.setCoverArtFetcher(statsCoverArtFetcher);
    if (tracker) {
      // Start HTTP stats server
      if (!appState.statsServer) {
        const config = configService.getConfig();
        if (config.stats.autoStartServer) {
          ensureStatsServerStarted();
        }
      }

      // Register stats overlay toggle IPC handler (idempotent)
      registerStatsOverlayToggle({
        staticDir: statsDistPath,
        preloadPath: statsPreloadPath,
        getApiBaseUrl: () => ensureStatsServerStarted().url,
        getToggleKey: () => configService.getConfig().stats.toggleKey,
        resolveBounds: () => overlayGeometryRuntime.getCurrentOverlayGeometry(),
        onVisibilityChanged: (visible) => {
          visibleOverlayInteractionRuntime.handleStatsOverlayVisibilityChanged(visible);
        },
      });
    }
  },
  getMpvClient: () => appState.mpvClient,
  shouldAutoConnectMpv: () => !appState.statsStartupInProgress,
  seedTrackerFromCurrentMedia: () => {
    void immersionMediaRuntime.seedFromCurrentMedia();
  },
  logInfo: (message) => logger.info(message),
  logDebug: (message) => logger.debug(message),
  logWarn: (message, details) => logger.warn(message, details),
};
const createImmersionTrackerStartup = createImmersionTrackerStartupHandler(
  createBuildImmersionTrackerStartupMainDepsHandler(immersionTrackerStartupMainDeps)(),
);
const recordSubtitleMiningContext = (context: SubtitleMiningContext | null): void => {
  pendingSubtitleMiningContext = context;
};
const consumePendingSubtitleMiningContext = (): SubtitleMiningContext | null => {
  const context = pendingSubtitleMiningContext;
  pendingSubtitleMiningContext = null;
  return context;
};
const recordTrackedCardsMined = (count: number, noteIds?: number[]): void => {
  ensureImmersionTrackerStarted();
  appState.immersionTracker?.recordCardsMined(count, noteIds);
};
function refreshCurrentSubtitleAnnotations(): void {
  const hasCurrentSubtitle = appState.currentSubText.trim().length > 0;
  if (hasCurrentSubtitle) {
    subtitlePrefetchService?.pause();
  }
  subtitleProcessingController.invalidateTokenizationCache();
  subtitlePrefetchService?.onSeek(lastObservedTimePos);
  if (!subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText)) {
    // Idle controller: no settle is coming to release the pause above.
    subtitlePrefetchService?.resume();
  }
}
let hasAttemptedImmersionTrackerStartup = false;
const ensureImmersionTrackerStarted = (): void => {
  if (hasAttemptedImmersionTrackerStartup || appState.immersionTracker) {
    return;
  }
  hasAttemptedImmersionTrackerStartup = true;
  createImmersionTrackerStartup();
};
const statsStartupRuntime = {
  ensureStatsServerStarted: () => ensureStatsServerStarted(),
  ensureBackgroundStatsServerStarted: () => ensureBackgroundStatsServerStarted(),
  stopBackgroundStatsServer: () => stopBackgroundStatsServer(),
  ensureImmersionTrackerStarted: () => {
    appState.statsStartupInProgress = true;
    try {
      ensureImmersionTrackerStarted();
    } finally {
      appState.statsStartupInProgress = false;
    }
  },
} as const;
const ensureBackgroundStatsServer = createEnsureBackgroundStatsServerHandler({
  isStatsAutoStartEnabled: () => configService.getConfig().stats.autoStartServer,
  isImmersionTrackingEnabled: () => configService.getConfig().immersionTracking?.enabled !== false,
  ensureBackgroundStatsServerStarted: () =>
    statsStartupRuntime.ensureBackgroundStatsServerStarted(),
  logInfo: (message) => logger.info(message),
  logWarn: (message, error) => logger.warn(message, error),
});

const runStatsCliCommand = createRunStatsCliCommandHandler({
  getResolvedConfig: () => configService.getConfig(),
  ensureImmersionTrackerStarted: () => statsStartupRuntime.ensureImmersionTrackerStarted(),
  ensureVocabularyCleanupTokenizerReady: async () => {
    await createMecabTokenizerAndCheck();
  },
  getImmersionTracker: () => appState.immersionTracker,
  ensureStatsServerStarted: () => statsStartupRuntime.ensureStatsServerStarted().url,
  ensureBackgroundStatsServerStarted: () =>
    statsStartupRuntime.ensureBackgroundStatsServerStarted(),
  stopBackgroundStatsServer: () => statsStartupRuntime.stopBackgroundStatsServer(),
  openExternal: (url: string) => shell.openExternal(url),
  writeResponse: (responsePath, payload) => {
    writeStatsCliCommandResponse(responsePath, payload);
  },
  exitAppWithCode: (code) => {
    process.exitCode = code;
    requestAppQuit();
  },
  logInfo: (message) => logger.info(message),
  logWarn: (message, error) => logger.warn(message, error),
  logError: (message, error) => logger.error(message, error),
});

async function runHeadlessInitialCommand(): Promise<void> {
  if (!appState.initialArgs?.refreshKnownWords) {
    handleInitialArgs();
    return;
  }

  const resolvedConfig = configService.getConfig();
  if (resolvedConfig.ankiConnect.enabled !== true) {
    logger.error('Headless known-word refresh failed: AnkiConnect integration not enabled');
    process.exitCode = 1;
    requestAppQuit();
    return;
  }

  const effectiveAnkiConfig =
    appState.runtimeOptionsManager?.getEffectiveAnkiConnectConfig(resolvedConfig.ankiConnect) ??
    resolvedConfig.ankiConnect;
  const integration = new AnkiIntegration(
    effectiveAnkiConfig,
    new SubtitleTimingTracker(),
    { send: () => undefined } as never,
    undefined,
    undefined,
    async () => ({
      keepNoteId: 0,
      deleteNoteId: 0,
      deleteDuplicate: false,
      cancelled: true,
    }),
    path.join(USER_DATA_PATH, 'known-words-cache.json'),
    mergeAiConfig(resolvedConfig.ai, resolvedConfig.ankiConnect?.ai),
  );

  try {
    await integration.refreshKnownWordCache();
  } catch (error) {
    logger.error('Headless known-word refresh failed:', error);
    process.exitCode = 1;
  } finally {
    integration.stop();
    requestAppQuit();
  }
}

const { appReadyRuntimeRunner } = composeAppReadyRuntime({
  reloadConfigMainDeps: {
    reloadConfigStrict: () => configService.reloadConfigStrict(),
    logInfo: (message) => appLogger.logInfo(message),
    logDebug: (message) => appLogger.logDebug(message),
    logWarning: (message) => appLogger.logWarning(message),
    showDesktopNotification: (title, options) => showDesktopNotification(title, options),
    startConfigHotReload: () => configHotReloadRuntime.start(),
    shouldRefreshAnilistClientSecretState: () =>
      shouldRefreshAnilistOnConfigReload(appState.initialArgs),
    refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretStateIfEnabled(options),
    failHandlers: {
      logError: (details) => logger.error(details),
      showErrorBox: (title, details) => dialog.showErrorBox(title, details),
      quit: () => requestAppQuit(),
    },
  },
  criticalConfigErrorMainDeps: {
    getConfigPath: () => configService.getConfigPath(),
    failHandlers: {
      logError: (message) => logger.error(message),
      showErrorBox: (title, message) => dialog.showErrorBox(title, message),
      quit: () => requestAppQuit(),
    },
  },
  appReadyRuntimeMainDeps: {
    ensureDefaultConfigBootstrap: () => {
      ensureDefaultConfigBootstrap({
        configDir: CONFIG_DIR,
        configFilePaths: getDefaultConfigFilePaths(CONFIG_DIR),
        generateTemplate: () => generateConfigTemplate(DEFAULT_CONFIG),
      });
    },
    loadSubtitlePosition: () => loadSubtitlePosition(),
    resolveKeybindings: () => {
      appState.keybindings = resolveKeybindings(configService.getConfig(), DEFAULT_KEYBINDINGS);
      refreshCurrentSessionBindings();
    },
    createMpvClient: () => {
      appState.mpvClient = createMpvClientRuntimeService();
    },
    getResolvedConfig: () => configService.getConfig(),
    getConfigWarnings: () => configService.getWarnings(),
    logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
    setLogLevel: (level: string, source: LogLevelSource) => setLogLevel(level, source),
    setLogRotation: (rotation: number) => setLogRotation(rotation),
    setLogFileToggles: (files) => setLogFileToggles(files),
    initRuntimeOptionsManager: () => {
      appState.runtimeOptionsManager = new RuntimeOptionsManager(
        () => configService.getConfig().ankiConnect,
        {
          applyAnkiPatch: (patch) => {
            if (appState.ankiIntegration) {
              appState.ankiIntegration.applyRuntimeConfigPatch(patch);
            }
          },
          getSubtitleStyleConfig: () => configService.getConfig().subtitleStyle,
          onOptionsChanged: () => {
            subtitleProcessingController.invalidateTokenizationCache();
            subtitlePrefetchService?.onSeek(lastObservedTimePos);
            overlayVisibilityComposer.broadcastRuntimeOptionsChanged();
            refreshOverlayShortcuts();
          },
        },
      );
    },
    setSecondarySubMode: (mode: SecondarySubMode) => {
      setSecondarySubMode(mode);
    },
    defaultSecondarySubMode: 'hover',
    defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
    defaultAnnotationWebsocketPort: DEFAULT_CONFIG.annotationWebsocket.port,
    defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
    hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
    startSubtitleWebsocket: (port: number) => {
      subtitleWsService.start(
        port,
        () =>
          appState.currentSubtitleData ??
          (appState.currentSubText
            ? {
                text: appState.currentSubText,
                tokens: null,
                startTime: appState.mpvClient?.currentSubStart ?? null,
                endTime: appState.mpvClient?.currentSubEnd ?? null,
              }
            : null),
        () => {
          const frequencyDictionary = configService.getConfig().subtitleStyle.frequencyDictionary;
          return {
            enabled: frequencyDictionary.enabled,
            topX: frequencyDictionary.topX,
            mode: frequencyDictionary.mode,
          };
        },
      );
    },
    startAnnotationWebsocket: (port: number) => {
      annotationSubtitleWsService.start(
        port,
        () =>
          appState.currentSubtitleData ??
          (appState.currentSubText
            ? {
                text: appState.currentSubText,
                tokens: null,
                startTime: appState.mpvClient?.currentSubStart ?? null,
                endTime: appState.mpvClient?.currentSubEnd ?? null,
              }
            : null),
        () => {
          const frequencyDictionary = configService.getConfig().subtitleStyle.frequencyDictionary;
          return {
            enabled: frequencyDictionary.enabled,
            topX: frequencyDictionary.topX,
            mode: frequencyDictionary.mode,
          };
        },
      );
    },
    startTexthooker: (port: number, websocketUrl?: string) => {
      if (!texthookerService.isRunning()) {
        texthookerService.start(port, websocketUrl);
      }
    },
    log: (message) => appLogger.logInfo(message),
    createMecabTokenizerAndCheck: async () => {
      await createMecabTokenizerAndCheck();
    },
    createSubtitleTimingTracker: () => {
      const tracker = new SubtitleTimingTracker();
      appState.subtitleTimingTracker = tracker;
    },
    loadYomitanExtension: async () => {
      await loadYomitanExtension();
    },
    ensureYomitanExtensionLoaded: async () => {
      await ensureYomitanExtensionLoaded();
    },
    handleFirstRunSetup: async () => {
      const snapshot = await firstRunSetupService.ensureSetupStateInitialized();
      appState.firstRunSetupCompleted = snapshot.state.status === 'completed';
      const args = appState.initialArgs;
      if (args && shouldAutoOpenFirstRunSetup(args)) {
        const force = Boolean(args.setup);
        if (force || snapshot.state.status !== 'completed') {
          openFirstRunSetupWindow(force);
        }
      }
    },
    startJellyfinRemoteSession: async () => {
      await startJellyfinRemoteSession();
    },
    prewarmSubtitleDictionaries: async () => {
      await prewarmSubtitleDictionaries();
    },
    startBackgroundWarmups: () => {
      startBackgroundWarmups();
    },
    texthookerOnlyMode: appState.texthookerOnlyMode,
    shouldAutoInitializeOverlayRuntimeFromConfig: () =>
      appState.backgroundMode
        ? false
        : configDerivedRuntime.shouldAutoInitializeOverlayRuntimeFromConfig(),
    setVisibleOverlayVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
    initializeOverlayRuntime: () => initializeOverlayRuntime(),
    runHeadlessInitialCommand: () => runHeadlessInitialCommand(),
    handleInitialArgs: () => handleInitialArgs(),
    shouldRunHeadlessInitialCommand: () =>
      Boolean(appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)),
    shouldUseMinimalStartup: () =>
      getStartupModeFlags(appState.initialArgs).shouldUseMinimalStartup,
    shouldSkipHeavyStartup: () => getStartupModeFlags(appState.initialArgs).shouldSkipHeavyStartup,
    shouldHandleInitialArgsBeforeDeferredOverlayWarmup: () =>
      shouldHandleInitialArgsBeforeDeferredOverlayWarmup(appState.initialArgs),
    createImmersionTracker: () => {
      ensureImmersionTrackerStarted();
    },
    logDebug: (message: string) => {
      logger.debug(message);
    },
    now: () => Date.now(),
  },
  immersionTrackerStartupMainDeps,
});

async function runAppReadyRuntimeWithFatalReporting(): Promise<void> {
  try {
    await appReadyRuntimeRunner();
  } catch (error) {
    reportFatalError(error, {
      title: 'SubMiner startup failed',
      context: 'SubMiner failed during app-ready startup.',
    });
    process.exitCode = 1;
    requestAppQuit();
    return;
  }
}

function ensureOverlayStartupPrereqs(): void {
  if (appState.subtitlePosition === null) {
    loadSubtitlePosition();
  }
  if (appState.keybindings.length === 0) {
    appState.keybindings = resolveKeybindings(configService.getConfig(), DEFAULT_KEYBINDINGS);
    refreshCurrentSessionBindings();
  } else if (!appState.sessionBindingsInitialized) {
    refreshCurrentSessionBindings();
  }
  if (!appState.mpvClient) {
    appState.mpvClient = createMpvClientRuntimeService();
  }
  if (!appState.runtimeOptionsManager) {
    appState.runtimeOptionsManager = new RuntimeOptionsManager(
      () => configService.getConfig().ankiConnect,
      {
        applyAnkiPatch: (patch) => {
          if (appState.ankiIntegration) {
            appState.ankiIntegration.applyRuntimeConfigPatch(patch);
          }
        },
        getSubtitleStyleConfig: () => configService.getConfig().subtitleStyle,
        onOptionsChanged: () => {
          subtitleProcessingController.invalidateTokenizationCache();
          subtitlePrefetchService?.onSeek(lastObservedTimePos);
          overlayVisibilityComposer.broadcastRuntimeOptionsChanged();
          refreshOverlayShortcuts();
        },
      },
    );
  }
  if (!appState.subtitleTimingTracker) {
    appState.subtitleTimingTracker = new SubtitleTimingTracker();
  }
}

async function ensureYoutubePlaybackRuntimeReady(): Promise<void> {
  ensureOverlayStartupPrereqs();
  await ensureYomitanExtensionLoaded();
  if (!appState.overlayRuntimeInitialized) {
    initializeOverlayRuntime();
    return;
  }
  ensureOverlayWindowsReadyForVisibilityActions();
}

let signalAutoplayReadyFromWarmTokenization: ((path: string | null | undefined) => void) | null =
  null;

const {
  createMpvClientRuntimeService: createMpvClientRuntimeServiceHandler,
  updateMpvSubtitleRenderMetrics: updateMpvSubtitleRenderMetricsHandler,
  tokenizeSubtitle: tokenizeSubtitleRuntime,
  createMecabTokenizerAndCheck,
  prewarmSubtitleDictionaries,
  startBackgroundWarmups,
  startTokenizationWarmups,
  isTokenizationWarmupReady,
} = composeMpvRuntimeHandlers<
  MpvIpcClient,
  ReturnType<typeof createTokenizerDepsRuntime>,
  SubtitleData
>({
  bindMpvMainEventHandlersMainDeps: {
    appState,
    getQuitOnDisconnectArmed: () =>
      jellyfinPlayQuitOnDisconnectArmed || youtubePlaybackRuntime.getQuitOnDisconnectArmed(),
    scheduleQuitCheck: (callback) => {
      setTimeout(callback, 500);
    },
    quitApp: () => requestAppQuit(),
    reportJellyfinRemoteStopped: () => {
      void reportJellyfinRemoteStopped();
    },
    onMpvConnected: () => {
      maybeStartOverlayLoadingOsd();
      flushQueuedMpvOsdNotifications();
      secondarySubtitleTrackController.scheduleRefresh(0);
      if (appState.sessionBindingsInitialized) {
        sendMpvCommandRuntime(appState.mpvClient, [
          'script-message',
          'subminer-reload-session-bindings',
        ]);
      }
      if (appState.currentSubText.trim()) {
        subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
      }
    },
    maybeRunAnilistPostWatchUpdate: (options) => maybeRunAnilistPostWatchUpdate(options),
    recordAnilistMediaDuration: (durationSec) => {
      recordAnilistMediaDuration(durationSec);
    },
    logSubtitleTimingError: (message, error) => logger.error(message, error),
    broadcastToOverlayWindows: (channel, payload) => {
      overlayManager.broadcastToOverlayWindows(channel, payload);
    },
    onSecondarySubtitleChange: (text) => {
      secondarySubtitleTrackController.handleLiveText(text);
    },
    getImmediateSubtitlePayload: (text) => subtitleProcessingController.consumeCachedSubtitle(text),
    emitImmediateSubtitle: (payload) => {
      emitSubtitlePayload(payload);
    },
    onSubtitleChange: (text) => {
      // Pause only; restarting the prefetch run here would discard in-flight
      // tokenization work on every line. Real seeks restart via onTimePosUpdate.
      subtitlePrefetchService?.pause();
      if (!subtitleProcessingController.onSubtitleChange(text)) {
        // Repeat of the current text: the controller is idle, so no settle is
        // coming to release the pause. Resume now instead of idling prefetch
        // for the rest of the cue.
        subtitlePrefetchService?.resume();
      }
    },
    refreshDiscordPresence: () => {
      discordPresenceRuntime.publishDiscordPresence();
    },
    logSubtitleProcessingDebug: (message: string) => logger.debug(message),
    ensureImmersionTrackerInitialized: () => {
      ensureImmersionTrackerStarted();
    },
    tokenizeSubtitleForImmersion: async (text): Promise<SubtitleData | null> =>
      tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(text) : null,
    updateCurrentMediaPath: (path) => {
      const normalizedPath = path.trim();
      maybeStartOverlayLoadingOsd(normalizedPath);
      const previousPath = appState.currentMediaPath?.trim() || null;
      const preserveParsedSubtitleCues = isSameYoutubeMediaPath(
        normalizedPath,
        appState.activeParsedSubtitleMediaPath,
      );
      if ((normalizedPath || null) !== previousPath) {
        cachedInternalSubtitleTrackExtractor.clear();
        secondarySubtitleTrackController.reset();
        const resetSubtitlePayload = { text: '', tokens: null };
        const frequencyDictionary = configService.getConfig().subtitleStyle.frequencyDictionary;
        const frequencyOptions = {
          enabled: frequencyDictionary.enabled,
          topX: frequencyDictionary.topX,
          mode: frequencyDictionary.mode,
        };
        autoplaySubtitlePrimingRuntime.resetAutoplaySubtitlePrime();
        lastObservedTimePos = 0;
        appState.currentSubText = '';
        appState.currentSubAssText = '';
        appState.currentSubtitleData = null;
        if (!preserveParsedSubtitleCues) {
          appState.activeParsedSubtitleCues = [];
          appState.activeParsedSubtitleSource = null;
          appState.activeParsedSubtitleMediaPath = null;
        }
        overlayManager.broadcastToOverlayWindows('subtitle:set', resetSubtitlePayload);
        subtitleWsService.broadcast(resetSubtitlePayload, frequencyOptions);
        annotationSubtitleWsService.broadcast(resetSubtitlePayload, frequencyOptions);
        autoplayReadyGate.invalidatePendingAutoplayReadyFallbacks();
      }
      currentMediaTokenizationGate.updateCurrentMediaPath(path);
      managedLocalSubtitleSelectionRuntime.handleMediaPathChange(path);
      startupOsdSequencer.reset();
      subtitlePrefetchRuntime.clearScheduledSubtitlePrefetchRefresh();
      if (!preserveParsedSubtitleCues) {
        subtitlePrefetchRuntime.cancelPendingInit();
      }
      youtubePrimarySubtitleNotificationRuntime.handleMediaPathChange(path);
      void youtubeMediaCachePlaybackRuntime.handleMediaPathChange(path);
      if (path) {
        ensureImmersionTrackerStarted();
        secondarySubtitleTrackController.scheduleRefresh();
        void subtitlePrefetchRuntime.refreshSubtitlePrefetchFromActiveTrack();
        // Retry after a short delay because MPV can populate track-list after path.
        subtitlePrefetchRuntime.scheduleSubtitlePrefetchRefresh(500);
      }
      mediaRuntime.updateCurrentMediaPath(path);
    },
    restoreMpvSubVisibility: () => {
      restoreOverlayMpvSubtitles();
    },
    resetSubtitleSidebarEmbeddedLayout: () => {
      resetSubtitleSidebarEmbeddedLayoutRuntime();
    },
    getCurrentAnilistMediaKey: () => getCurrentAnilistMediaKey(),
    resetAnilistMediaTracking: (mediaKey) => {
      resetAnilistMediaTracking(mediaKey);
    },
    maybeProbeAnilistDuration: (mediaKey) => {
      void maybeProbeAnilistDuration(mediaKey);
    },
    ensureAnilistMediaGuess: (mediaKey) => {
      void ensureAnilistMediaGuess(mediaKey);
    },
    syncImmersionMediaState: () => {
      immersionMediaRuntime.syncFromCurrentMediaState();
    },
    signalAutoplayReadyIfWarm: (path) => signalAutoplayReadyFromWarmTokenization?.(path),
    markJellyfinRemotePlaybackLoaded: (path) => {
      markJellyfinRemotePlaybackLoadedState(activeJellyfinRemotePlayback, path);
    },
    scheduleCharacterDictionarySync: () => {
      if (
        !configService.getConfig().subtitleStyle.nameMatchEnabled ||
        !yomitanProfilePolicy.isCharacterDictionaryEnabled() ||
        isYoutubePlaybackActiveNow()
      ) {
        return;
      }
      characterDictionaryAutoSyncRuntime.scheduleSync();
    },
    updateCurrentMediaTitle: (title) => {
      mediaRuntime.updateCurrentMediaTitle(title);
    },
    resetAnilistMediaGuessState: () => {
      resetAnilistMediaGuessState();
    },
    reportJellyfinRemoteProgress: (forceImmediate) => {
      void reportJellyfinRemoteProgress(forceImmediate);
    },
    consumeExplicitSeek: () => consumeExplicitSeekIntent(),
    onTimePosUpdate: (time) => {
      const delta = time - lastObservedTimePos;
      if (subtitlePrefetchService && (delta > SEEK_THRESHOLD_SECONDS || delta < 0)) {
        subtitlePrefetchService.onSeek(time);
      }
      lastObservedTimePos = time;
      secondarySubtitleTrackController.handleTimePos(time);
    },
    onFullscreenChange: (fullscreen) => {
      cancelLinuxMpvFullscreenOverlayRefreshBurst = updateLinuxMpvFullscreenOverlayRefreshBurst(
        fullscreen,
        {
          overlayManager: {
            getMainWindow: () => overlayManager.getMainWindow(),
            getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
          },
          overlayVisibilityRuntime,
          syncVisibleOverlayMpvFullscreenMode: (nextFullscreen) =>
            syncLinuxVisibleOverlayMpvFullscreenMode(nextFullscreen),
          getOverlayInteractionActive: () =>
            visibleOverlayInteractionRuntime.getVisibleOverlayInteractionActive() ||
            visibleOverlayInteractionRuntime.getLinuxOverlayInputShapeActive(),
          ensureOverlayWindowLevel: (window) =>
            overlayGeometryRuntime.ensureOverlayWindowLevel(window),
        },
        cancelLinuxMpvFullscreenOverlayRefreshBurst,
      );
    },
    onSubtitleTrackChange: (sid) => {
      lastObservedPrimarySubtitleTrackId = sid;
      logger.info('[mpv-subtitles] primary subtitle track changed', { sid });
      autoplaySubtitlePrimingRuntime.scheduleSubtitlePrefetchRefresh();
      youtubePrimarySubtitleNotificationRuntime.handleSubtitleTrackChange(sid);
    },
    onSecondarySubtitleTrackChange: () => {
      secondarySubtitleTrackController.handleTrackChange();
      secondarySubtitleTrackController.scheduleRefresh(0);
    },
    onSecondarySubtitleDelayChange: (delay) => {
      secondarySubtitleTrackController.handleDelayChange(delay);
    },
    onSubtitleTrackListChange: (trackList) => {
      const diagnostics = buildSubtitleTrackDiagnostics(
        lastObservedPrimarySubtitleTrackId,
        trackList,
      );
      if (!diagnostics.trackListReadable) {
        logger.warn('[mpv-subtitles] mpv reported an unreadable subtitle track list', diagnostics);
      } else if (diagnostics.subtitleTrackCount === 0) {
        logger.warn('[mpv-subtitles] mpv reported no subtitle tracks', diagnostics);
      } else {
        logger.info('[mpv-subtitles] subtitle track list updated', diagnostics);
      }
      managedLocalSubtitleSelectionRuntime.handleSubtitleTrackListChange(trackList);
      secondarySubtitleTrackController.scheduleRefresh(0);
      autoplaySubtitlePrimingRuntime.scheduleSubtitlePrefetchRefresh();
      youtubePrimarySubtitleNotificationRuntime.handleSubtitleTrackListChange(trackList);
    },
    updateSubtitleRenderMetrics: (patch) => {
      updateMpvSubtitleRenderMetricsHandler(patch as Partial<MpvSubtitleRenderMetrics>);
    },
    syncOverlayMpvSubtitleSuppression: () => {
      syncOverlayMpvSubtitleSuppression();
    },
  },
  mpvClientRuntimeServiceFactoryMainDeps: {
    createClient: MpvIpcClient,
    getSocketPath: () => appState.mpvSocketPath,
    getResolvedConfig: () => configService.getConfig(),
    isAutoStartOverlayEnabled: () => appState.autoStartOverlay,
    setOverlayVisible: (visible: boolean) => setOverlayVisible(visible),
    isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
    getReconnectTimer: () => appState.reconnectTimer,
    setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => {
      appState.reconnectTimer = timer;
    },
    shouldAutoLoadSecondarySubTrack: (path: string) =>
      shouldAutoLoadSecondarySubTrackForJellyfinPlayback(activeJellyfinRemotePlayback, path),
    shouldQuitOnMpvShutdown: () =>
      shouldQuitOnMpvShutdownForTrayState({
        managedPlayback: appState.initialArgs?.managedPlayback === true,
        backgroundMode: appState.backgroundMode,
        hasTray: Boolean(appTray),
      }),
    requestAppQuit: () => requestAppQuit(),
  },
  updateMpvSubtitleRenderMetricsMainDeps: {
    getCurrentMetrics: () => appState.mpvSubtitleRenderMetrics,
    setCurrentMetrics: (metrics) => {
      appState.mpvSubtitleRenderMetrics = metrics;
    },
    applyPatch: (current, patch) => applyMpvSubtitleRenderMetricsPatch(current, patch),
    broadcastMetrics: () => {
      // no renderer consumer for subtitle render metrics updates at present
    },
  },
  tokenizer: {
    buildTokenizerDepsMainDeps: {
      getYomitanExt: () => appState.yomitanExt,
      getYomitanSession: () => appState.yomitanSession,
      getYomitanParserWindow: () => appState.yomitanParserWindow,
      setYomitanParserWindow: (window) => {
        appState.yomitanParserWindow = window as BrowserWindow | null;
      },
      getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
      setYomitanParserReadyPromise: (promise) => {
        appState.yomitanParserReadyPromise = promise;
      },
      getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
      setYomitanParserInitPromise: (promise) => {
        appState.yomitanParserInitPromise = promise;
      },
      isKnownWord: (text, reading, options) =>
        Boolean(appState.ankiIntegration?.isKnownWord(text, reading, options)),
      getKnownWordTier: (text, reading, options) =>
        appState.ankiIntegration?.getKnownWordTier(text, reading, options) ?? null,
      recordLookup: (hit) => {
        ensureImmersionTrackerStarted();
        appState.immersionTracker?.recordLookup(hit);
      },
      getKnownWordMatchMode: () =>
        appState.ankiIntegration?.getKnownWordMatchMode() ??
        configService.getConfig().ankiConnect.knownWords.matchMode,
      getKnownWordsEnabled: () =>
        getRuntimeBooleanOption(
          'subtitle.annotation.knownWords.highlightEnabled',
          configService.getConfig().ankiConnect.knownWords.highlightEnabled,
        ),
      getNPlusOneEnabled: () =>
        getRuntimeBooleanOption(
          'subtitle.annotation.nPlusOne',
          configService.getConfig().ankiConnect.nPlusOne.enabled,
        ),
      getMinSentenceWordsForNPlusOne: () =>
        configService.getConfig().ankiConnect.nPlusOne.minSentenceWords,
      getJlptLevel: (text) => appState.jlptLevelLookup(text),
      getJlptEnabled: () =>
        getRuntimeBooleanOption(
          'subtitle.annotation.jlpt',
          configService.getConfig().subtitleStyle.enableJlpt,
        ),
      getCharacterDictionaryEnabled: () =>
        configService.getConfig().subtitleStyle.nameMatchEnabled &&
        yomitanProfilePolicy.isCharacterDictionaryEnabled() &&
        !isYoutubePlaybackActiveNow(),
      getNameMatchEnabled: () => configService.getConfig().subtitleStyle.nameMatchEnabled,
      getNameMatchImagesEnabled: () =>
        configService.getConfig().subtitleStyle.nameMatchImagesEnabled,
      getCharacterNameImage: (term) => characterDictionaryImageLookup.get(term),
      getCurrentCharacterDictionaryMediaId: () =>
        characterDictionaryAutoSyncRuntime.getCurrentMediaId(),
      getCharacterNameCandidates: () => characterNameCandidateLookup.get(),
      getFrequencyDictionaryEnabled: () =>
        getRuntimeBooleanOption(
          'subtitle.annotation.frequency',
          configService.getConfig().subtitleStyle.frequencyDictionary.enabled,
        ),
      getFrequencyDictionaryMatchMode: () =>
        configService.getConfig().subtitleStyle.frequencyDictionary.matchMode,
      getFrequencyRank: (text) => appState.frequencyRankLookup(text),
      getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled,
      getMecabTokenizer: () => appState.mecabTokenizer,
      onTokenizationReady: () => {
        currentMediaTokenizationGate.markReady(
          appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null,
        );
        startupOsdSequencer.markTokenizationReady();
      },
    },
    createTokenizerRuntimeDeps: (deps) =>
      createTokenizerDepsRuntime(deps as Parameters<typeof createTokenizerDepsRuntime>[0]),
    tokenizeSubtitle: (text, deps) => tokenizeSubtitleCore(text, deps),
    createMecabTokenizerAndCheckMainDeps: {
      getMecabTokenizer: () => appState.mecabTokenizer,
      setMecabTokenizer: (tokenizer) => {
        appState.mecabTokenizer = tokenizer as MecabTokenizer | null;
      },
      createMecabTokenizer: () => new MecabTokenizer(),
      checkAvailability: async (tokenizer) => (tokenizer as MecabTokenizer).checkAvailability(),
    },
    prewarmSubtitleDictionariesMainDeps: {
      ensureJlptDictionaryLookup: () => jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
      ensureFrequencyDictionaryLookup: () =>
        frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
      showMpvOsd: (message: string) =>
        overlayNotificationsRuntime.showConfiguredStatusNotification(message),
      showLoadingOsd: (message: string) => startupOsdSequencer.showAnnotationLoading(message),
      showLoadedOsd: (message: string) =>
        startupOsdSequencer.markAnnotationLoadingComplete(message),
      shouldShowOsdNotification: () => {
        const type = overlayNotificationsRuntime.getConfiguredStatusNotificationType();
        return type === 'osd' || type === 'osd-system';
      },
    },
  },
  warmups: {
    launchBackgroundWarmupTaskMainDeps: {
      now: () => Date.now(),
      logDebug: (message) => logger.debug(message),
      logWarn: (message) => logger.warn(message),
    },
    startBackgroundWarmupsMainDeps: {
      getStarted: () => backgroundWarmupsStarted,
      setStarted: (started) => {
        backgroundWarmupsStarted = started;
      },
      isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
      ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}),
      shouldWarmupMecab: () => {
        const startupWarmups = configService.getConfig().startupWarmups;
        if (startupWarmups.lowPowerMode) {
          return false;
        }
        if (!startupWarmups.mecab) {
          return false;
        }
        return shouldInitializeMecabForAnnotations();
      },
      shouldWarmupYomitanExtension: () => configService.getConfig().startupWarmups.yomitanExtension,
      shouldWarmupSubtitleDictionaries: () => {
        const startupWarmups = configService.getConfig().startupWarmups;
        if (startupWarmups.lowPowerMode) {
          return false;
        }
        return startupWarmups.subtitleDictionaries;
      },
      shouldWarmupJellyfinRemoteSession: () => {
        const startupWarmups = configService.getConfig().startupWarmups;
        if (startupWarmups.lowPowerMode) {
          return false;
        }
        return startupWarmups.jellyfinRemoteSession;
      },
      shouldAutoConnectJellyfinRemote: () => {
        const jellyfin = configService.getConfig().jellyfin;
        return (
          jellyfin.enabled && jellyfin.remoteControlEnabled && jellyfin.remoteControlAutoConnect
        );
      },
      startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
      logDebug: (message) => logger.debug(message),
    },
  },
});

async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
  if (!isTokenizationWarmupReady()) {
    startupOsdSequencer.showTokenizationLoading('Loading subtitle tokenization...');
  }
  return await tokenizeSubtitleRuntime(text);
}

signalAutoplayReadyFromWarmTokenization = createAutoplayTokenizationWarmRelease({
  isTokenizationWarmupReady: () => isTokenizationWarmupReady(),
  startTokenizationWarmups: async () => {
    await startTokenizationWarmups();
  },
  getCurrentMediaPath: () =>
    appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null,
  primeCurrentSubtitle: (mediaPath) =>
    autoplaySubtitlePrimingRuntime.primeCurrentSubtitleForAutoplay(mediaPath),
  signalAutoplayReady: () => signalCurrentSubtitleAutoplayReady(),
  warn: (message, error) => logger.warn(message, error),
});
tokenizeSubtitleDeferred = tokenizeSubtitle;

const aniSkipRuntime = createAniSkipRuntime({
  getAniSkipConfig: () => ({
    aniskipEnabled: configService.getConfig().mpv.aniskipEnabled,
    aniskipButtonKey: configService.getConfig().mpv.aniskipButtonKey,
  }),
  resolveMetadataForFile: (mediaPath) => resolveAniSkipMetadataForFile(mediaPath),
  sendMpvCommand: (command) => {
    appState.mpvClient?.send({ command });
  },
  requestMpvProperty: (name) => {
    if (!appState.mpvClient) {
      return Promise.reject(new Error('MPV not connected'));
    }
    return appState.mpvClient.requestProperty(name);
  },
  isMpvConnected: () => appState.mpvClient?.connected === true,
  getCurrentTimePos: () => appState.mpvClient?.currentTimePos ?? Number.NaN,
  showMpvOsd: (text, durationMs) => {
    appState.mpvClient?.send({ command: ['show-text', text, durationMs] });
  },
  showPlaybackFeedback: (text) => showConfiguredPlaybackFeedback(text),
  logInfo: (message) => logger.info(message),
  logWarn: (message, error) => logger.warn(message, error),
  logDebug: (message) => logger.debug(message),
});

function createMpvClientRuntimeService(): MpvIpcClient {
  const client = createMpvClientRuntimeServiceHandler() as MpvIpcClient;
  client.on('connection-change', ({ connected }) => {
    if (connected) {
      return;
    }
    if (!youtubeFlowRuntime.hasActiveSession()) {
      return;
    }
    youtubeFlowRuntime.cancelActivePicker();
    overlayManager.broadcastToOverlayWindows(IPC_CHANNELS.event.youtubePickerCancel, null);
    overlayModalRuntime.handleOverlayModalClosed('youtube-track-picker');
  });
  client.on('connection-change', aniSkipRuntime.handleConnectionChange);
  client.on('media-path-change', aniSkipRuntime.handleMediaPathChange);
  client.on('time-pos-change', aniSkipRuntime.handleTimePosChange);
  client.on('client-message', aniSkipRuntime.handleClientMessage);
  return client;
}

function resetSubtitleSidebarEmbeddedLayoutRuntime(): void {
  sendMpvCommandRuntime(appState.mpvClient, ['set_property', 'video-margin-ratio-right', 0]);
  sendMpvCommandRuntime(appState.mpvClient, ['set_property', 'video-pan-x', 0]);
}

const overlayGeometryRuntime = createOverlayGeometryRuntime({
  overlayManager: {
    getMainWindow: () => overlayManager.getMainWindow(),
    getModalWindow: () => overlayManager.getModalWindow(),
    getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
    setOverlayWindowBounds: (geometry) => overlayManager.setOverlayWindowBounds(geometry),
    setModalWindowBounds: (geometry) => overlayManager.setModalWindowBounds(geometry),
  },
  getTrackedWindowGeometry: () => appState.windowTracker?.getGeometry() ?? null,
  getTrackedWindowMediaSourceId: () => appState.windowTracker?.getTargetWindowMediaSourceId?.(),
  getTrackedWindowNativeId: () => appState.windowTracker?.getTargetWindowNativeId?.(),
  getStatsOverlayVisible: () => appState.statsOverlayVisible,
  getOverlayForegroundSeparateWindows: () => getOverlayForegroundSeparateWindows(),
  getLinuxVisibleOverlayWindowMode: () => linuxVisibleOverlayWindowMode,
  getLinuxTrackedMpvFullscreen: () => linuxTrackedMpvFullscreen,
  getLinuxTrackedMpvFullscreenChangedAtMs: () => linuxTrackedMpvFullscreenChangedAtMs,
  syncLinuxVisibleOverlayMpvFullscreenMode: (fullscreen) =>
    syncLinuxVisibleOverlayMpvFullscreenMode(fullscreen),
  getLinuxVisibleOverlayOwnerBindingKey: () => linuxVisibleOverlayOwnerBindingKey,
  setLinuxVisibleOverlayOwnerBindingKey: (key) => {
    linuxVisibleOverlayOwnerBindingKey = key;
  },
  clearVisibleOverlayX11OwnerBinding: (window) =>
    visibleOverlayInteractionRuntime.clearVisibleOverlayX11OwnerBinding(window),
  getNativeWindowHandleDecimal: (window) =>
    visibleOverlayInteractionRuntime.getNativeWindowHandleDecimal(window),
  enqueueVisibleOverlayX11OwnerBindingOperation: (window, args, onError) =>
    visibleOverlayInteractionRuntime.enqueueVisibleOverlayX11OwnerBindingOperation(
      window,
      args,
      onError,
    ),
  scheduleWindowsVisibleOverlayZOrderSyncBurst: () =>
    visibleOverlayInteractionRuntime.scheduleWindowsVisibleOverlayZOrderSyncBurst(),
  logDebug: (message, ...args) => logger.debug(message, ...args),
});

async function loadYomitanExtension(): Promise<Extension | null> {
  const extension = await yomitanExtensionRuntime.loadYomitanExtension();
  if (extension && !yomitanProfilePolicy.isExternalReadOnlyMode()) {
    await syncYomitanDefaultProfileAnkiServer();
  }
  return extension;
}

async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
  const extension = await yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
  if (extension && !yomitanProfilePolicy.isExternalReadOnlyMode()) {
    await syncYomitanDefaultProfileAnkiServer();
  }
  return extension;
}

const { syncYomitanDefaultProfileAnkiServer } = createYomitanAnkiServerSyncRuntime({
  isExternalReadOnlyMode: () => yomitanProfilePolicy.isExternalReadOnlyMode(),
  getResolvedConfig: () => configService.getConfig(),
  getYomitanParserRuntimeDeps: () => getYomitanParserRuntimeDeps(),
  logError: (message, ...args) => {
    logger.error(message, ...args);
  },
  logInfo: (message, ...args) => {
    logger.info(message, ...args);
  },
});

function getYomitanParserRuntimeDeps() {
  return {
    getYomitanExt: () => appState.yomitanExt,
    getYomitanSession: () => appState.yomitanSession,
    getYomitanParserWindow: () => appState.yomitanParserWindow,
    setYomitanParserWindow: (window: BrowserWindow | null) => {
      appState.yomitanParserWindow = window;
    },
    getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
    setYomitanParserReadyPromise: (promise: Promise<void> | null) => {
      appState.yomitanParserReadyPromise = promise;
    },
    getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
    setYomitanParserInitPromise: (promise: Promise<boolean> | null) => {
      appState.yomitanParserInitPromise = promise;
    },
  };
}

function createModalWindow(): BrowserWindow {
  const existingWindow = overlayManager.getModalWindow();
  if (existingWindow && !existingWindow.isDestroyed()) {
    return existingWindow;
  }
  const window = createModalWindowHandler();
  overlayManager.setModalWindowBounds(overlayGeometryRuntime.getCurrentOverlayGeometry());
  return window;
}

function createMainWindow(): BrowserWindow {
  const window = createMainWindowHandler();
  if (process.platform === 'win32') {
    const overlayHwnd = visibleOverlayInteractionRuntime.getWindowsNativeWindowHandleNumber(window);
    if (!ensureWindowsOverlayTransparency(overlayHwnd)) {
      logger.warn('Failed to eagerly extend Windows overlay transparency via koffi');
    }
  }
  return window;
}

function createLinuxVisibleOverlayWindowForCurrentMode(token: number, fullscreen: boolean): void {
  if (token !== linuxVisibleOverlayWindowModeSwitchToken) {
    return;
  }
  if (!overlayManager.getVisibleOverlayVisible()) {
    return;
  }
  const existingWindow = overlayManager.getMainWindow();
  if (existingWindow && !existingWindow.isDestroyed()) {
    return;
  }

  visibleOverlayInteractionRuntime.resetVisibleOverlayInputState();
  createMainWindow();
  const trackedGeometry = overlayGeometryRuntime.getCurrentTrackedOverlayGeometry();
  if (trackedGeometry) {
    overlayManager.setOverlayWindowBounds(trackedGeometry);
  }
  overlayVisibilityRuntime.updateVisibleOverlayVisibility();
  void ensureOverlayMpvSubtitlesHidden();
  if (appState.currentSubText.trim()) {
    subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
  }
  logger.debug(
    `Switched Linux visible overlay window mode to ${linuxVisibleOverlayWindowMode} for mpv fullscreen=${fullscreen}`,
  );
}

function syncLinuxVisibleOverlayMpvFullscreenMode(fullscreen: boolean): void {
  if (!shouldRunLinuxOverlayZOrderKeepAlive()) {
    return;
  }
  if (linuxTrackedMpvFullscreen !== fullscreen) {
    linuxTrackedMpvFullscreenChangedAtMs = Date.now();
  }
  linuxTrackedMpvFullscreen = fullscreen;
  const currentWindow = overlayManager.getMainWindow();
  const hasLiveWindow = Boolean(currentWindow && !currentWindow.isDestroyed());
  const action = resolveLinuxVisibleOverlayWindowModeAction({
    currentMode: linuxVisibleOverlayWindowMode,
    fullscreen,
    hasLiveWindow,
    visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
  });

  linuxVisibleOverlayWindowMode = action.nextMode;
  linuxVisibleOverlayOwnerBindingKey = null;
  linuxVisibleOverlayWindowModeSwitchToken += 1;
  const token = linuxVisibleOverlayWindowModeSwitchToken;
  if (!action.shouldCreateWindow && !action.shouldDestroyCurrentWindow) {
    return;
  }

  const previousWindow = currentWindow;
  if (action.shouldDestroyCurrentWindow && previousWindow && !previousWindow.isDestroyed()) {
    previousWindow.once('closed', () => {
      if (overlayManager.getMainWindow() === previousWindow) {
        overlayManager.setMainWindow(null);
      }
      if (action.createWindowTiming === 'after-current-destroyed') {
        createLinuxVisibleOverlayWindowForCurrentMode(token, fullscreen);
      }
    });
    previousWindow.hide();
    previousWindow.destroy();
  }

  if (!action.shouldCreateWindow) {
    logger.debug(
      `Recorded Linux visible overlay window mode ${action.nextMode} for hidden mpv fullscreen=${fullscreen}`,
    );
    return;
  }

  if (action.createWindowTiming === 'now') {
    createLinuxVisibleOverlayWindowForCurrentMode(token, fullscreen);
  }
}

function initializeOverlayRuntime(): void {
  initializeOverlayRuntimeHandler();
  if (!(appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs))) {
    overlayModalRuntime.primeModalWindow();
  }
  appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined);
  appState.ankiIntegration?.setKnownWordCacheUpdatedCallback(refreshCurrentSubtitleAnnotations);
  appState.ankiIntegration?.setSubtitleMiningContextConsumer(consumePendingSubtitleMiningContext);
  appState.ankiIntegration?.setMediaTimingReviewCallback(mediaTimingReviewRuntime.requestReview);
  syncOverlayMpvSubtitleSuppression();
}

function openYomitanSettings(): boolean {
  if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
    const message = 'Yomitan settings unavailable while using read-only external-profile mode.';
    logger.warn(
      'Yomitan settings window disabled while yomitan.externalProfilePath is configured because external profile mode is read-only.',
    );
    overlayNotificationsRuntime.showConfiguredStatusNotification(message, { variant: 'warning' });
    return false;
  }
  openYomitanSettingsHandler();
  return true;
}

const { exportLogsFromTray } = createLogExportTrayRuntime({
  flushMpvLog: () => flushMpvLog(),
  logInfo: (message) => logger.info(message),
  logWarn: (message, details) => logger.warn(message, details),
});

const {
  getConfiguredShortcuts,
  registerGlobalShortcuts,
  refreshGlobalAndOverlayShortcuts,
  cancelPendingMultiCopy,
  startPendingMultiCopy,
  cancelPendingMineSentenceMultiple,
  startPendingMineSentenceMultiple,
  syncOverlayShortcuts,
  refreshOverlayShortcuts,
} = composeShortcutRuntimes({
  globalShortcuts: {
    getConfiguredShortcutsMainDeps: {
      getResolvedConfig: () => configService.getConfig(),
      defaultConfig: DEFAULT_CONFIG,
      resolveConfiguredShortcuts,
    },
    buildRegisterGlobalShortcutsMainDeps: (getConfiguredShortcutsHandler) => ({
      getConfiguredShortcuts: () => getConfiguredShortcutsHandler(),
      registerGlobalShortcutsCore,
      toggleVisibleOverlay: () => toggleVisibleOverlay(),
      openYomitanSettings: () => openYomitanSettings(),
      isDev,
      getMainWindow: () => overlayManager.getMainWindow(),
    }),
    buildRefreshGlobalAndOverlayShortcutsMainDeps: (registerGlobalShortcutsHandler) => ({
      unregisterAllGlobalShortcuts: () => globalShortcut.unregisterAll(),
      registerGlobalShortcuts: () => registerGlobalShortcutsHandler(),
      syncOverlayShortcuts: () => syncOverlayShortcuts(),
    }),
  },
  numericShortcutRuntimeMainDeps: {
    globalShortcut,
    showMpvOsd: (text) => overlayNotificationsRuntime.showConfiguredStatusNotification(text),
    setTimer: (handler, timeoutMs) => setTimeout(handler, timeoutMs),
    clearTimer: (timer) => clearTimeout(timer),
  },
  numericSessions: {
    onMultiCopyDigit: (count) => handleMultiCopyDigitHandler(count),
    onMineSentenceDigit: (count) => handleMineSentenceDigitHandler(count),
    tryBeginMultiCopyOverlaySelection: (timeoutMs) =>
      tryBeginVisibleOverlayNumericSelection({
        actionId: 'copySubtitleMultiple',
        timeoutMs,
        getMainWindow: () => overlayManager.getMainWindow(),
        getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
      }),
    tryBeginMineSentenceOverlaySelection: (timeoutMs) =>
      tryBeginVisibleOverlayNumericSelection({
        actionId: 'mineSentenceMultiple',
        timeoutMs,
        getMainWindow: () => overlayManager.getMainWindow(),
        getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
      }),
  },
  overlayShortcutsRuntimeMainDeps: {
    overlayShortcutsRuntime,
  },
});

const { persistSessionBindings, refreshCurrentSessionBindings } = createSessionBindingsRuntime({
  configDir: CONFIG_DIR,
  getKeybindings: () => appState.keybindings,
  getConfiguredShortcuts: () => getConfiguredShortcuts(),
  getResolvedConfig: () => configService.getConfig(),
  getMpvClient: () => appState.mpvClient,
  setSessionBindings: (bindings) => {
    appState.sessionBindings = bindings;
  },
  setSessionBindingsInitialized: (initialized) => {
    appState.sessionBindingsInitialized = initialized;
  },
  logWarn: (message) => logger.warn(message),
});

const { flushMpvLog, showMpvOsd } = createMpvOsdRuntimeHandlers({
  appendToMpvLogMainDeps: {
    getLogPath: () => (isLogFileEnabled('mpv') ? DEFAULT_MPV_LOG_PATH : ''),
    dirname: (targetPath) => path.dirname(targetPath),
    mkdir: async (targetPath, options) => {
      await fs.promises.mkdir(targetPath, options);
    },
    appendFile: async (targetPath, data, options) => {
      await fs.promises.appendFile(targetPath, data, options);
    },
    now: () => new Date(),
  },
  buildShowMpvOsdMainDeps: (appendToMpvLogHandler) => ({
    appendToMpvLog: (message) => appendToMpvLogHandler(message),
    showMpvOsdRuntime: (mpvClient, text, fallbackLog) =>
      showMpvOsdRuntime(mpvClient, text, fallbackLog),
    getMpvClient: () => appState.mpvClient,
    logInfo: (line) => logger.info(line),
  }),
});
flushPendingMpvLogWrites = () => {
  void flushMpvLog();
};

const { getChangelogSnapshot } = createChangelogRuntime({
  getInstalledVersion: () => app.getVersion(),
  getUpdateChannel: () => configService.getConfig().updates.channel,
  resourcesPath: process.resourcesPath,
  appPath: app.getAppPath(),
  dirname: __dirname,
  joinPath: (...parts) => path.join(...parts),
  fileExists: (candidate) => fs.existsSync(candidate),
  readFile: (candidate) => fs.readFileSync(candidate, 'utf8'),
  logWarn: (message) => logger.warn(message),
});

const { getUpdateService } = createUpdateServiceRuntime({
  userDataPath: USER_DATA_PATH,
  getUpdatesConfig: () => configService.getConfig().updates,
  logInfo: (message) => logger.info(message),
  logWarn: (message, details) => logger.warn(message, details),
  showOverlayNotification: (payload) =>
    overlayNotificationsRuntime.showOverlayNotification(payload),
  showDesktopNotification: (title, options) => showDesktopNotification(title, options),
  showMpvOsd: (message) => {
    showMpvOsd(message);
  },
  withStatsWindowLayerSuspendedForNativeDialog: (showDialog) =>
    withStatsWindowLayerSuspendedForNativeDialog(showDialog),
});

const cycleSecondarySubMode = createCycleSecondarySubModeRuntimeHandler({
  cycleSecondarySubModeMainDeps: {
    getSecondarySubMode: () => appState.secondarySubMode,
    setSecondarySubMode: (mode: SecondarySubMode) => {
      setSecondarySubMode(mode);
    },
    getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
    setLastSecondarySubToggleAtMs: (timestampMs: number) => {
      appState.lastSecondarySubToggleAtMs = timestampMs;
    },
    broadcastToOverlayWindows: (channel, mode) => {
      overlayManager.broadcastToOverlayWindows(channel, mode);
    },
    showMpvOsd: (text: string) => showConfiguredPlaybackFeedback(text),
  },
  cycleSecondarySubMode: (deps) => cycleSecondarySubModeCore(deps),
});

function setSecondarySubMode(mode: SecondarySubMode): void {
  appState.secondarySubMode = mode;
}

const buildUpdateLastCardFromClipboardMainDepsHandler =
  createBuildUpdateLastCardFromClipboardMainDepsHandler({
    getAnkiIntegration: () => appState.ankiIntegration,
    readClipboardText: () => clipboard.readText(),
    showMpvOsd: (text) => overlayNotificationsRuntime.showConfiguredStatusNotification(text),
    updateLastCardFromClipboardCore,
  });
const updateLastCardFromClipboardMainDeps = buildUpdateLastCardFromClipboardMainDepsHandler();
const updateLastCardFromClipboardHandler = createUpdateLastCardFromClipboardHandler(
  updateLastCardFromClipboardMainDeps,
);

const buildRefreshKnownWordCacheMainDepsHandler = createBuildRefreshKnownWordCacheMainDepsHandler({
  getAnkiIntegration: () => appState.ankiIntegration,
  missingIntegrationMessage: 'AnkiConnect integration not enabled',
});
const refreshKnownWordCacheMainDeps = buildRefreshKnownWordCacheMainDepsHandler();
const refreshKnownWordCacheHandler = createRefreshKnownWordCacheHandler(
  refreshKnownWordCacheMainDeps,
);

const buildTriggerFieldGroupingMainDepsHandler = createBuildTriggerFieldGroupingMainDepsHandler({
  getAnkiIntegration: () => appState.ankiIntegration,
  showMpvOsd: (text) => overlayNotificationsRuntime.showConfiguredStatusNotification(text),
  triggerFieldGroupingCore,
});
const triggerFieldGroupingMainDeps = buildTriggerFieldGroupingMainDepsHandler();
const triggerFieldGroupingHandler = createTriggerFieldGroupingHandler(triggerFieldGroupingMainDeps);

const buildMarkLastCardAsAudioCardMainDepsHandler =
  createBuildMarkLastCardAsAudioCardMainDepsHandler({
    getAnkiIntegration: () => appState.ankiIntegration,
    showMpvOsd: (text) => overlayNotificationsRuntime.showConfiguredStatusNotification(text),
    markLastCardAsAudioCardCore,
  });
const markLastCardAsAudioCardMainDeps = buildMarkLastCardAsAudioCardMainDepsHandler();
const markLastCardAsAudioCardHandler = createMarkLastCardAsAudioCardHandler(
  markLastCardAsAudioCardMainDeps,
);

const buildMineSentenceCardMainDepsHandler = createBuildMineSentenceCardMainDepsHandler({
  getAnkiIntegration: () => appState.ankiIntegration,
  getMpvClient: () => appState.mpvClient,
  getPrimarySubtitle: () => captureCurrentPrimarySubtitleMiningContext(),
  showMpvOsd: (text) => overlayNotificationsRuntime.showConfiguredStatusNotification(text),
  mineSentenceCardCore,
  recordCardsMined: (count, noteIds) => {
    ensureImmersionTrackerStarted();
    appState.immersionTracker?.recordCardsMined(count, noteIds);
  },
});
const mineSentenceCardHandler = createMineSentenceCardHandler(
  buildMineSentenceCardMainDepsHandler(),
);

const buildHandleMultiCopyDigitMainDepsHandler = createBuildHandleMultiCopyDigitMainDepsHandler({
  getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
  writeClipboardText: (text) => clipboard.writeText(text),
  showMpvOsd: (text) => showConfiguredPlaybackFeedback(text),
  handleMultiCopyDigitCore,
});
const handleMultiCopyDigitMainDeps = buildHandleMultiCopyDigitMainDepsHandler();
const handleMultiCopyDigitHandler = createHandleMultiCopyDigitHandler(handleMultiCopyDigitMainDeps);

const buildCopyCurrentSubtitleMainDepsHandler = createBuildCopyCurrentSubtitleMainDepsHandler({
  getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
  writeClipboardText: (text) => clipboard.writeText(text),
  showMpvOsd: (text) => overlayNotificationsRuntime.showConfiguredStatusNotification(text),
  copyCurrentSubtitleCore,
});
const copyCurrentSubtitleMainDeps = buildCopyCurrentSubtitleMainDepsHandler();
const copyCurrentSubtitleHandler = createCopyCurrentSubtitleHandler(copyCurrentSubtitleMainDeps);

const buildHandleMineSentenceDigitMainDepsHandler =
  createBuildHandleMineSentenceDigitMainDepsHandler({
    getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
    getAnkiIntegration: () => appState.ankiIntegration,
    getCurrentSecondarySubText: () => appState.mpvClient?.currentSecondarySubText || undefined,
    showMpvOsd: (text) => overlayNotificationsRuntime.showConfiguredStatusNotification(text),
    logError: (message, err) => {
      logger.error(message, err);
    },
    onCardsMined: (cards) => {
      ensureImmersionTrackerStarted();
      appState.immersionTracker?.recordCardsMined(cards);
    },
    handleMineSentenceDigitCore,
  });
const handleMineSentenceDigitMainDeps = buildHandleMineSentenceDigitMainDepsHandler();
const handleMineSentenceDigitHandler = createHandleMineSentenceDigitHandler(
  handleMineSentenceDigitMainDeps,
);
const {
  setVisibleOverlayVisible: setVisibleOverlayVisibleHandler,
  toggleVisibleOverlay: toggleVisibleOverlayHandler,
  setOverlayVisible: setOverlayVisibleHandler,
} = createOverlayVisibilityRuntime({
  setVisibleOverlayVisibleDeps: {
    setVisibleOverlayVisibleCore,
    setVisibleOverlayVisibleState: (nextVisible) => {
      overlayManager.setVisibleOverlayVisible(nextVisible);
    },
    updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
  },
  getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
});

const buildHandleOverlayModalClosedMainDepsHandler =
  createBuildHandleOverlayModalClosedMainDepsHandler({
    handleOverlayModalClosedRuntime: (modal) => overlayModalRuntime.handleOverlayModalClosed(modal),
  });
const handleOverlayModalClosedMainDeps = buildHandleOverlayModalClosedMainDepsHandler();
const handleOverlayModalClosedHandler = createHandleOverlayModalClosedHandler(
  handleOverlayModalClosedMainDeps,
);

const buildAppendClipboardVideoToQueueMainDepsHandler =
  createBuildAppendClipboardVideoToQueueMainDepsHandler({
    appendClipboardVideoToQueueRuntime,
    getMpvClient: () => appState.mpvClient,
    readClipboardText: () => clipboard.readText(),
    showMpvOsd: (text) => overlayNotificationsRuntime.showConfiguredStatusNotification(text),
    sendMpvCommand: (command) => {
      sendMpvCommandRuntime(appState.mpvClient, command);
    },
  });
const appendClipboardVideoToQueueMainDeps = buildAppendClipboardVideoToQueueMainDepsHandler();
const appendClipboardVideoToQueueHandler = createAppendClipboardVideoToQueueHandler(
  appendClipboardVideoToQueueMainDeps,
);

async function dispatchSessionAction(request: SessionActionDispatchRequest): Promise<void> {
  await dispatchSessionActionCore(request, {
    toggleStatsOverlay: () =>
      toggleStatsOverlayWindow({
        staticDir: statsDistPath,
        preloadPath: statsPreloadPath,
        getApiBaseUrl: () => ensureStatsServerStarted().url,
        getToggleKey: () => configService.getConfig().stats.toggleKey,
        resolveBounds: () => overlayGeometryRuntime.getCurrentOverlayGeometry(),
        onVisibilityChanged: (visible) => {
          visibleOverlayInteractionRuntime.handleStatsOverlayVisibilityChanged(visible);
        },
      }),
    toggleVisibleOverlay: () => toggleVisibleOverlay(),
    copyCurrentSubtitle: () => copyCurrentSubtitleHandler(),
    copySubtitleCount: (count) => handleMultiCopyDigitHandler(count),
    updateLastCardFromClipboard: () => updateLastCardFromClipboardHandler(),
    triggerFieldGrouping: () => triggerFieldGroupingHandler(),
    triggerSubsyncFromConfig: () => subsyncRuntime.triggerFromConfig(),
    mineSentenceCard: () => mineSentenceCardHandler(),
    mineSentenceCount: (count) => handleMineSentenceDigitHandler(count),
    toggleSecondarySub: () => cycleSecondarySubMode(),
    toggleSubtitleSidebar: () =>
      overlayManager.broadcastToOverlayWindows(IPC_CHANNELS.event.subtitleSidebarToggle),
    toggleNotificationHistory: () => toggleNotificationHistoryPanel(),
    appendClipboardVideoToQueue: () => {
      appendClipboardVideoToQueueHandler();
    },
    markLastCardAsAudioCard: () => markLastCardAsAudioCardHandler(),
    markActiveVideoWatched: async () => {
      ensureImmersionTrackerStarted();
      const marked = (await appState.immersionTracker?.markActiveVideoWatched()) ?? false;
      if (marked) {
        try {
          await maybeRunAnilistPostWatchUpdate({ force: true });
        } catch (error) {
          logger.warn('Failed to run AniList post-watch update after manual watched mark:', error);
        }
      }
      return marked;
    },
    openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
    openJimaku: () => openJimakuOverlay(),
    openTsukihime: () => openTsukihimeOverlay(),
    openSessionHelp: () => openSessionHelpOverlay(),
    openCharacterDictionaryManager: () => openCharacterDictionaryManagerOverlay(),
    openControllerSelect: () => openControllerSelectOverlay(),
    openControllerDebug: () => openControllerDebugOverlay(),
    openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(),
    openPlaylistBrowser: () => openPlaylistBrowser(),
    replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
    playNextSubtitle: () => playNextSubtitleRuntime(appState.mpvClient),
    cycleRuntimeOption: (id, direction) => {
      if (!appState.runtimeOptionsManager) {
        return { ok: false, error: 'Runtime options manager unavailable' };
      }
      return applyRuntimeOptionResultRuntime(
        appState.runtimeOptionsManager.cycleOption(id, direction),
        (text) => showConfiguredPlaybackFeedback(text),
      );
    },
    playNextPlaylistItem: () =>
      sendMpvCommandRuntime(appState.mpvClient, ['playlist-next', 'force']),
    showMpvOsd: (text) => showConfiguredPlaybackFeedback(text),
  });
}

const { playlistBrowserMainDeps } = createPlaylistBrowserIpcRuntime(() => appState.mpvClient, {
  getPrimarySubtitleLanguages: () => configService.getConfig().youtube.primarySubLanguages,
  getSecondarySubtitleLanguages: () => configService.getConfig().secondarySub.secondarySubLanguages,
});

const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
  mpvCommandMainDeps: {
    triggerSubsyncFromConfig: () => subsyncRuntime.triggerFromConfig(),
    openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
    openJimaku: () => openJimakuOverlay(),
    openTsukihime: () => openTsukihimeOverlay(),
    openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(),
    openPlaylistBrowser: () => openPlaylistBrowser(),
    cycleRuntimeOption: (id, direction) => {
      if (!appState.runtimeOptionsManager) {
        return { ok: false, error: 'Runtime options manager unavailable' };
      }
      return applyRuntimeOptionResultRuntime(
        appState.runtimeOptionsManager.cycleOption(id, direction),
        (text) => showConfiguredPlaybackFeedback(text),
      );
    },
    showMpvOsd: (text: string) =>
      overlayNotificationsRuntime.showConfiguredStatusNotification(text),
    showRawMpvOsd: (text: string) => showMpvOsd(text),
    showPlaybackFeedback: (text: string) => showConfiguredPlaybackFeedback(text),
    replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
    playNextSubtitle: () => playNextSubtitleRuntime(appState.mpvClient),
    sendMpvCommand: (rawCommand: (string | number)[]) => sendRendererMpvCommand(rawCommand),
    getMpvClient: () => appState.mpvClient,
    isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
    hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
  },
  handleMpvCommandFromIpcRuntime,
  runSubsyncManualFromIpc: (request) => subsyncRuntime.runManualFromIpc(request),
  registration: {
    runtimeOptions: {
      getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
      showMpvOsd: (text: string) => showConfiguredPlaybackFeedback(text),
    },
    mainDeps: {
      previewMediaTimingReview: (request) => mediaTimingReviewRuntime.previewRange(request),
      getMediaTimingReviewWaveform: (request) => mediaTimingReviewRuntime.getWaveform(request),
      stopMediaTimingReviewPreview: (reviewId) => mediaTimingReviewRuntime.stopPreview(reviewId),
      resolveMediaTimingReview: (request) => mediaTimingReviewRuntime.resolveReview(request),
      getMainWindow: () => overlayManager.getMainWindow(),
      getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
      focusMainWindow: () => {
        const mainWindow = overlayManager.getMainWindow();
        if (!mainWindow || mainWindow.isDestroyed()) return;
        if (process.platform === 'darwin') {
          focusMacOSOverlayWindow({
            platform: process.platform,
            getOverlayWindow: () => mainWindow,
            stealAppFocus: () => app.focus({ steal: true }),
            warn: (message, details) => logger.warn(message, details),
          });
          return;
        }
        if (!mainWindow.isFocused()) {
          mainWindow.focus();
        }
      },
      activatePlaybackWindowForOverlayInteraction: async () => {
        try {
          const raised = (await appState.windowTracker?.raiseTargetWindow?.()) ?? false;
          overlayGeometryRuntime.enforceOverlayLayerOrder();
          return raised;
        } catch (error) {
          logger.debug(
            'Failed to raise tracked mpv window for overlay interaction:',
            error instanceof Error ? error.message : String(error),
          );
          overlayGeometryRuntime.enforceOverlayLayerOrder();
          return false;
        }
      },
      onOverlayModalClosed: (modal, senderWindow) => {
        if (modal === 'media-timing-review') {
          void mediaTimingReviewRuntime.dispose();
        }
        if (modal === 'subtitle-sidebar' && senderWindow === overlayManager.getMainWindow()) {
          subtitleSidebarRequestedOpen = false;
        }
        const modalWindow = overlayManager.getModalWindow();
        if (
          senderWindow &&
          modalWindow &&
          senderWindow === modalWindow &&
          !senderWindow.isDestroyed()
        ) {
          applyOverlayClickThrough(senderWindow);
          senderWindow.hide();
        }
        handleOverlayModalClosedHandler(modal);
      },
      onOverlayModalOpened: (modal, senderWindow) => {
        if (modal === 'subtitle-sidebar' && senderWindow === overlayManager.getMainWindow()) {
          subtitleSidebarRequestedOpen = true;
        }
        overlayModalRuntime.notifyOverlayModalOpened(modal);
      },
      onOverlayMouseInteractionChanged: (active, senderWindow) => {
        const mainWindow = overlayManager.getMainWindow();
        if (!mainWindow || senderWindow !== mainWindow) {
          return;
        }
        const previousActive =
          visibleOverlayInteractionRuntime.getVisibleOverlayInteractionActive();
        visibleOverlayInteractionRuntime.setVisibleOverlayInteractionActive(active);
        if (previousActive === active) {
          if (active && process.platform === 'darwin' && !mainWindow.isFocused()) {
            overlayVisibilityRuntime.updateVisibleOverlayVisibility();
          }
          return;
        }
        overlayVisibilityRuntime.updateVisibleOverlayVisibility();
      },
      onOverlayInteractiveHint: (interactive, senderWindow) => {
        const mainWindow = overlayManager.getMainWindow();
        if (!mainWindow || senderWindow !== mainWindow) {
          return;
        }
        visibleOverlayInteractionRuntime.setLinuxOverlayInteractiveHint(interactive);
        visibleOverlayInteractionRuntime.applyLinuxOverlayInputShapeFromLatestMeasurement();
      },
      handleOverlayNotificationAction: (notificationId, actionId, noteId) => {
        if (
          notificationId === UPDATE_AVAILABLE_NOTIFICATION_ID &&
          actionId === INSTALL_UPDATE_ACTION_ID
        ) {
          void getUpdateService()
            .checkForUpdates({
              source: 'manual',
              installWhenAvailable: true,
            })
            .catch((error) => {
              logger.warn('Failed to install update from overlay notification action:', error);
            });
        }
        if (
          notificationId === UPDATE_AVAILABLE_NOTIFICATION_ID &&
          actionId === VIEW_CHANGELOG_ACTION_ID
        ) {
          openChangelogOverlay();
        }
        if (actionId === OPEN_ANKI_CARD_ACTION_ID && noteId !== undefined) {
          void openAnkiCardFromNotification(noteId).catch((error) => {
            logger.warn('Failed to open Anki card from overlay notification action:', error);
            overlayNotificationsRuntime.showConfiguredStatusNotification(
              'Failed to open Anki card in Anki.',
              {
                id: 'open-anki-card-failed',
                variant: 'error',
              },
            );
          });
        }
      },
      onYoutubePickerResolve: (request) => youtubeFlowRuntime.resolveActivePicker(request),
      openYomitanSettings: () => openYomitanSettings(),
      // Overlay lookups carry no cue context of their own; fall back to snapshotting the
      // live mpv sub timings at lookup time so media generation clips the mined line even
      // when extraction finishes long after playback has moved on.
      recordSubtitleMiningContext: (context) =>
        recordSubtitleMiningContext(context ?? captureCurrentPrimarySubtitleMiningContext()),
      quitApp: () => requestAppQuit(),
      toggleVisibleOverlay: () => toggleVisibleOverlay(),
      tokenizeCurrentSubtitle: async () => {
        const tokenizeSubtitleForCurrent = tokenizeSubtitleDeferred;
        return resolveCurrentSubtitleForRenderer({
          currentSubText: appState.currentSubText,
          currentSubtitleData: appState.currentSubtitleData,
          withCurrentSubtitleTiming: (payload) => withCurrentSubtitleTiming(payload),
          tokenizeUncached: false,
          tokenizeSubtitle: tokenizeSubtitleForCurrent
            ? (text) => tokenizeSubtitleForCurrent(text)
            : undefined,
          onResolvedSubtitle: (payload) => {
            appState.currentSubtitleData = payload;
            autoplayReadyGate.maybeSignalPluginAutoplayReady(payload, { forceWhilePaused: true });
          },
        });
      },
      getCurrentSubtitleRaw: () => appState.currentSubText,
      getCurrentSubtitleAss: () => appState.currentSubAssText,
      getSubtitleSidebarOpen: () => subtitleSidebarRequestedOpen,
      getSubtitleSidebarSnapshot: async () => {
        const currentSubtitle = {
          text: appState.currentSubText,
          startTime: appState.mpvClient?.currentSubStart ?? null,
          endTime: appState.mpvClient?.currentSubEnd ?? null,
        };
        const currentTimeSec = appState.mpvClient?.currentTimePos ?? null;
        const config = configService.getConfig().subtitleSidebar;
        const client = appState.mpvClient;
        if (!client?.connected) {
          return {
            cues: appState.activeParsedSubtitleCues,
            currentTimeSec,
            currentSubtitle,
            config,
          };
        }

        try {
          const [currentExternalFilenameRaw, currentTrackRaw, trackListRaw, sidRaw, videoPathRaw] =
            await Promise.all([
              client.requestProperty('current-tracks/sub/external-filename').catch(() => null),
              client.requestProperty('current-tracks/sub').catch(() => null),
              client.requestProperty('track-list'),
              client.requestProperty('sid'),
              client.requestProperty('path'),
            ]);
          const videoPath = typeof videoPathRaw === 'string' ? videoPathRaw : '';
          if (!videoPath) {
            return {
              cues: appState.activeParsedSubtitleCues,
              currentTimeSec,
              currentSubtitle,
              config,
            };
          }
          if (
            shouldUseCachedYoutubeParsedCues({
              videoPath,
              cachedMediaPath: appState.activeParsedSubtitleMediaPath,
              cachedCueCount: appState.activeParsedSubtitleCues.length,
            })
          ) {
            return {
              cues: appState.activeParsedSubtitleCues,
              currentTimeSec,
              currentSubtitle,
              config,
            };
          }

          const resolvedSource = await resolveActiveSubtitleSidebarSourceHandler({
            currentExternalFilenameRaw,
            currentTrackRaw,
            trackListRaw,
            sidRaw,
            videoPath,
          });
          if (!resolvedSource) {
            return {
              cues: appState.activeParsedSubtitleCues,
              currentTimeSec,
              currentSubtitle,
              config,
            };
          }

          try {
            if (appState.activeParsedSubtitleSource === resolvedSource.sourceKey) {
              return {
                cues: appState.activeParsedSubtitleCues,
                currentTimeSec,
                currentSubtitle,
                config,
              };
            }

            const content = await loadSubtitleSourceText(resolvedSource.path);
            const cues = parseSubtitleCues(content, resolvedSource.path);
            appState.activeParsedSubtitleCues = cues;
            appState.activeParsedSubtitleSource = resolvedSource.sourceKey;
            appState.activeParsedSubtitleMediaPath = videoPath || null;
            return {
              cues,
              currentTimeSec,
              currentSubtitle,
              config,
            };
          } finally {
            await resolvedSource.cleanup?.();
          }
        } catch {
          return {
            cues: appState.activeParsedSubtitleCues,
            currentTimeSec,
            currentSubtitle,
            config,
          };
        }
      },
      getPlaybackPaused: () =>
        resolveFreshPlaybackPaused({
          getCachedPlaybackPaused: () => appState.playbackPaused,
          getMpvClient: () => appState.mpvClient,
        }),
      getSubtitlePosition: () => loadSubtitlePosition(),
      getSubtitleStyle: () => {
        const resolvedConfig = configService.getConfig();
        return resolveSubtitleStyleForRenderer(resolvedConfig);
      },
      saveSubtitlePosition: (position) => saveSubtitlePosition(position),
      getMecabTokenizer: () => appState.mecabTokenizer,
      getKeybindings: () => appState.keybindings,
      getSessionBindings: () => appState.sessionBindings,
      getConfiguredShortcuts: () => getConfiguredShortcuts(),
      dispatchSessionAction: (request) => dispatchSessionAction(request),
      getStatsToggleKey: () => configService.getConfig().stats.toggleKey,
      getMarkWatchedKey: () => configService.getConfig().stats.markWatchedKey,
      getOverlayNotificationPosition: () => configService.getConfig().notifications.overlayPosition,
      getControllerConfig: () => configService.getConfig().controller,
      saveControllerConfig: (update) => {
        const currentRawConfig = configService.getRawConfig();
        configService.patchRawConfig({
          controller: applyControllerConfigUpdate(currentRawConfig.controller, update),
        });
      },
      saveControllerPreference: ({ preferredGamepadId, preferredGamepadLabel }) => {
        configService.patchRawConfig({
          controller: {
            preferredGamepadId,
            preferredGamepadLabel,
          },
        });
      },
      getSecondarySubMode: () => appState.secondarySubMode,
      getMpvClient: () => appState.mpvClient,
      getAnkiConnectStatus: () => appState.ankiIntegration !== null,
      getRuntimeOptions: () => getRuntimeOptionsStateHandler(),
      reportOverlayContentBounds: (payload: unknown) => {
        if (overlayContentMeasurementStore.report(payload)) {
          visibleOverlayInteractionRuntime.tickLinuxOverlayPointerInteractionNow();
          visibleOverlayInteractionRuntime.tickWindowsOverlayPointerInteractionNow();
          visibleOverlayInteractionRuntime.primeLinuxOverlayPointerInteractionAfterFirstMeasurement();
          autoplayReadyGate.flushPendingAutoplayReadySignal();
          autoplaySubtitlePrimingRuntime.scheduleVisibleOverlaySubtitleRefreshAfterFirstPaint();
        }
      },
      getAnilistStatus: () => anilistStateRuntime.getStatusSnapshot(),
      clearAnilistToken: () => anilistStateRuntime.clearTokenState(),
      openAnilistSetup: () => openAnilistSetupWindowHandler(),
      getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(),
      retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
      runAnilistPostWatchUpdateOnManualMark: () => maybeRunAnilistPostWatchUpdate({ force: true }),
      getCharacterDictionarySelection: (searchTitle?: string) =>
        characterDictionaryRuntime.getManualSelectionSnapshot(undefined, searchTitle),
      setCharacterDictionarySelection: async (
        mediaId: number,
        replaceManagedMediaId?: number,
        mediaTitle?: string,
      ) => {
        if (replaceManagedMediaId !== undefined && mediaTitle) {
          const result = replaceCharacterDictionaryManagedEntry(
            USER_DATA_PATH,
            replaceManagedMediaId,
            {
              mediaId,
              mediaTitle,
            },
          );
          if (result.ok && result.rebuildRequired) {
            try {
              await characterDictionaryAutoSyncRuntime.runSyncNow();
            } catch (error) {
              logger.warn('Failed to rebuild character dictionary after manager override:', error);
            }
          }
          return result;
        }
        return await applyCharacterDictionarySelection(
          { mediaId },
          {
            setManualSelection: (request) => characterDictionaryRuntime.setManualSelection(request),
            resetAnilistMediaGuessState,
            runSyncNow: () => characterDictionaryAutoSyncRuntime.runSyncNow(),
            warn: (message, error) => logger.warn(message, error),
          },
        );
      },
      getCharacterDictionaryManagerSnapshot: async () =>
        getCharacterDictionaryManagerSnapshot(
          USER_DATA_PATH,
          characterDictionaryAutoSyncRuntime.getCurrentMediaId(),
        ),
      removeCharacterDictionaryManagedEntry: async (mediaId: number) => {
        const result = removeCharacterDictionaryManagedEntry(
          USER_DATA_PATH,
          mediaId,
          characterDictionaryAutoSyncRuntime.getCurrentMediaId(),
        );
        if (result.ok && result.rebuildRequired) {
          try {
            await characterDictionaryAutoSyncRuntime.runSyncNow();
          } catch (error) {
            logger.warn('Failed to rebuild character dictionary after manager removal:', error);
          }
        }
        return result;
      },
      moveCharacterDictionaryManagedEntry: async (mediaId: number, direction: 1 | -1) => {
        const result = moveCharacterDictionaryManagedEntry(
          USER_DATA_PATH,
          mediaId,
          direction,
          characterDictionaryAutoSyncRuntime.getCurrentMediaId(),
        );
        if (result.ok && result.rebuildRequired) {
          try {
            await characterDictionaryAutoSyncRuntime.runSyncNow();
          } catch (error) {
            logger.warn('Failed to rebuild character dictionary after manager reorder:', error);
          }
        }
        return result;
      },
      appendClipboardVideoToQueue: () => appendClipboardVideoToQueueHandler(),
      getChangelogSnapshot: (options) => getChangelogSnapshot(options),
      ...playlistBrowserMainDeps,
      getImmersionTracker: () => appState.immersionTracker,
    },
    ankiJimakuDeps: createAnkiJimakuIpcRuntimeServiceDeps({
      patchAnkiConnectEnabled: (enabled: boolean) => {
        configService.patchRawConfig({ ankiConnect: { enabled } });
      },
      getResolvedConfig: () => configService.getConfig(),
      getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
      getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
      getMpvClient: () => appState.mpvClient,
      getAnkiIntegration: () => appState.ankiIntegration,
      setAnkiIntegration: (integration: AnkiIntegration | null) => {
        appState.ankiIntegration = integration;
        appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined);
        appState.ankiIntegration?.setKnownWordCacheUpdatedCallback(
          refreshCurrentSubtitleAnnotations,
        );
        appState.ankiIntegration?.setSubtitleMiningContextConsumer(
          consumePendingSubtitleMiningContext,
        );
        appState.ankiIntegration?.setMediaTimingReviewCallback(
          mediaTimingReviewRuntime.requestReview,
        );
      },
      getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
      getCachedMediaPath: (currentVideoPath, kind) =>
        getCachedYoutubeMediaPathForCurrentPlayback(currentVideoPath, kind),
      shouldRequireRemoteMediaCache: () => shouldRequireYoutubeMediaCacheForCurrentPlayback(),
      getYoutubeMediaSourceUrl: () => getCurrentYoutubeMediaCacheSourceUrl(),
      showDesktopNotification,
      showOverlayNotification: (payload) =>
        overlayNotificationsRuntime.showOverlayNotification(payload),
      dismissOverlayNotification: (id) =>
        overlayNotificationsRuntime.dismissOverlayNotification(id),
      createFieldGroupingCallback: () => createFieldGroupingCallback(),
      broadcastRuntimeOptionsChanged: () =>
        overlayVisibilityComposer.broadcastRuntimeOptionsChanged(),
      getFieldGroupingResolver: () => getFieldGroupingResolverHandler(),
      setFieldGroupingResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) =>
        setFieldGroupingResolverHandler(resolver),
      parseMediaInfo: (mediaPath: string | null) =>
        parseMediaInfo(mediaRuntime.resolveMediaPathForJimaku(mediaPath)),
      getCurrentMediaPath: () => appState.currentMediaPath,
      jimakuFetchJson: <T>(
        endpoint: string,
        query?: Record<string, string | number | boolean | null | undefined>,
      ): Promise<JimakuApiResponse<T>> => configDerivedRuntime.jimakuFetchJson<T>(endpoint, query),
      getJimakuMaxEntryResults: () => configDerivedRuntime.getJimakuMaxEntryResults(),
      getJimakuLanguagePreference: () => configDerivedRuntime.getJimakuLanguagePreference(),
      resolveJimakuApiKey: () => configDerivedRuntime.resolveJimakuApiKey(),
      isRemoteMediaPath: (mediaPath: string) => isRemoteMediaPath(mediaPath),
      downloadToFile: (
        url: string,
        destPath: string,
        headers: Record<string, string>,
        downloadOptions?: { isAllowedRedirect?: (url: URL) => boolean },
      ) => downloadToFile(url, destPath, headers, downloadOptions),
    }),
    registerIpcRuntimeServices,
  },
});
const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({
  cliCommandContextMainDeps: {
    appState,
    setLogLevel: (level) => setLogLevel(level, 'cli'),
    onMpvSocketPathChanged: (nextSocketPath, previousSocketPath) =>
      visibleOverlayInteractionRuntime.retargetOverlayWindowTrackerForMpvSocket(
        nextSocketPath,
        previousSocketPath,
      ),
    texthookerService,
    getResolvedConfig: () => configService.getConfig(),
    defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
    defaultAnnotationWebsocketPort: DEFAULT_CONFIG.annotationWebsocket.port,
    hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
    openExternal: (url: string) => shell.openExternal(url),
    logBrowserOpenError: (url: string, error: unknown) =>
      logger.error(`Failed to open browser for texthooker URL: ${url}`, error),
    showMpvOsd: (text: string) =>
      overlayNotificationsRuntime.showConfiguredStatusNotification(text),
    showPlaybackFeedback: (text: string) => showConfiguredPlaybackFeedback(text),
    initializeOverlayRuntime: () => initializeOverlayRuntime(),
    toggleVisibleOverlay: () => toggleVisibleOverlay(),
    togglePrimarySubtitleBar: () =>
      overlayManager.broadcastToOverlayWindows(IPC_CHANNELS.event.primarySubtitleBarToggle),
    openFirstRunSetupWindow: (force?: boolean) => openFirstRunSetupWindow(force),
    setVisibleOverlayVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
    copyCurrentSubtitle: () => copyCurrentSubtitleHandler(),
    startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
    mineSentenceCard: () => mineSentenceCardHandler(),
    startPendingMineSentenceMultiple: (timeoutMs: number) =>
      startPendingMineSentenceMultiple(timeoutMs),
    updateLastCardFromClipboard: () => updateLastCardFromClipboardHandler(),
    refreshKnownWordCache: () => refreshKnownWordCacheHandler(),
    triggerFieldGrouping: () => triggerFieldGroupingHandler(),
    triggerSubsyncFromConfig: () => subsyncRuntime.triggerFromConfig(),
    markLastCardAsAudioCard: () => markLastCardAsAudioCardHandler(),
    getAnilistStatus: () => anilistStateRuntime.getStatusSnapshot(),
    clearAnilistToken: () => anilistStateRuntime.clearTokenState(),
    openAnilistSetupWindow: () => openAnilistSetupWindowHandler(),
    openJellyfinSetupWindow: () => openJellyfinSetupWindow(),
    getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(),
    processNextAnilistRetryUpdate: () => processNextAnilistRetryUpdate(),
    generateCharacterDictionary: async (targetPath?: string) => {
      const disabledReason = yomitanProfilePolicy.getCharacterDictionaryDisabledReason();
      if (disabledReason) {
        throw new Error(disabledReason);
      }
      return await characterDictionaryRuntime.generateForCurrentMedia(targetPath);
    },
    getCharacterDictionarySelection: async (targetPath?: string) =>
      characterDictionaryRuntime.getManualSelectionSnapshot(targetPath),
    setCharacterDictionarySelection: async (request) =>
      applyCharacterDictionarySelection(request, {
        setManualSelection: (selectionRequest) =>
          characterDictionaryRuntime.setManualSelection(selectionRequest),
        resetAnilistMediaGuessState,
        runSyncNow: () => characterDictionaryAutoSyncRuntime.runSyncNow(),
        warn: (message, error) => logger.warn(message, error),
      }),
    runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand),
    runStatsCommand: (argsFromCommand: CliArgs, source: CliCommandSource) =>
      runStatsCliCommand(argsFromCommand, source),
    runUpdateCommand: async (argsFromCommand: CliArgs, source: CliCommandSource) => {
      await runUpdateCliCommand(argsFromCommand, source, {
        checkForUpdates: (request) => getUpdateService().checkForUpdates(request),
        writeResponse: (responsePath, payload) =>
          writeUpdateCliCommandResponse(responsePath, payload),
        logWarn: (message, error) => logger.warn(message, error),
      });
    },
    runEnsureLinuxRuntimePluginAssetsCommand: async (
      argsFromCommand: CliArgs,
      source: CliCommandSource,
    ) => {
      await runEnsureLinuxRuntimePluginAssetsCliCommand(
        argsFromCommand,
        {
          ensureLinuxRuntimePluginAssets: () => ensureLinuxRuntimePluginAssets(),
          writeResponse: (responsePath, payload) =>
            writeEnsureLinuxRuntimePluginAssetsCliCommandResponse(responsePath, payload),
          logWarn: (message, error) => logger.warn(message, error),
        },
        source,
      );
    },
    runYoutubePlaybackFlow: (request) => youtubePlaybackRuntime.runYoutubePlaybackFlow(request),
    ensureBackgroundStatsServer: () => ensureBackgroundStatsServer(),
    openYomitanSettings: () => openYomitanSettings(),
    openConfigSettingsWindow: () => configSettingsRuntime.openWindow(),
    openSyncUiWindow: () => openSyncUiWindowHandler(),
    cycleSecondarySubMode: () => cycleSecondarySubMode(),
    openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
    printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
    stopApp: () => requestAppQuit(),
    hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
    dispatchSessionAction: (request: SessionActionDispatchRequest) =>
      dispatchSessionAction(request),
    getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
    schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
    logInfo: (message: string) => logger.info(message),
    logDebug: (message: string) => logger.debug(message),
    logWarn: (message: string) => logger.warn(message),
    logError: (message: string, err: unknown) => logger.error(message, err),
  },
  cliCommandRuntimeHandlerMainDeps: {
    handleTexthookerOnlyModeTransitionMainDeps: {
      isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
      ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
      setTexthookerOnlyMode: (enabled) => {
        appState.texthookerOnlyMode = enabled;
      },
      commandNeedsOverlayStartupPrereqs: (inputArgs) =>
        commandNeedsOverlayStartupPrereqs(inputArgs),
      startBackgroundWarmups: () => startBackgroundWarmups(),
      logInfo: (message: string) => logger.info(message),
    },
    ensureTrayForCommand: (args) => {
      if (args.background || args.managedPlayback) {
        ensureTrayHandler();
      }
    },
    handleCliCommandRuntimeServiceWithContext: (args, source, cliContext) =>
      handleCliCommandRuntimeServiceWithContext(args, source, cliContext),
  },
  initialArgsRuntimeHandlerMainDeps: {
    getInitialArgs: () => appState.initialArgs,
    isBackgroundMode: () => appState.backgroundMode,
    shouldEnsureTrayOnStartup: () =>
      shouldEnsureTrayOnStartupForInitialArgs(process.platform, appState.initialArgs),
    shouldRunHeadlessInitialCommand: (args) => isHeadlessInitialCommand(args),
    ensureTray: () => ensureTrayHandler(),
    isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
    hasImmersionTracker: () => Boolean(appState.immersionTracker),
    getMpvClient: () => appState.mpvClient,
    commandNeedsOverlayStartupPrereqs: (args) => commandNeedsOverlayStartupPrereqs(args),
    commandNeedsOverlayRuntime: (args) => commandNeedsOverlayRuntime(args),
    ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
    isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
    initializeOverlayRuntime: () => initializeOverlayRuntime(),
    logInfo: (message) => logger.info(message),
  },
});
const { runAndApplyStartupState } = composeHeadlessStartupHandlers<
  CliArgs,
  StartupState,
  ReturnType<typeof createStartupBootstrapRuntimeDeps>
>({
  startupRuntimeHandlersDeps: {
    appLifecycleRuntimeRunnerMainDeps: {
      app: appLifecycleApp,
      platform: process.platform,
      shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
      parseArgs: (argv: string[]) => parseArgs(argv),
      handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
        handleCliCommand(nextArgs, source),
      printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
      logNoRunningInstance: () => appLogger.logNoRunningInstance(),
      startControlServer: (handleArgv: (argv: string[]) => void) => {
        const server = startAppControlServer({
          socketPath: getAppControlSocketPath({ configDir: CONFIG_DIR }),
          platform: process.platform,
          handleArgv,
          logDebug: (message) => logger.debug(message),
          logWarn: (message, error) => logger.warn(message, error),
        });
        return () => server.close();
      },
      onReady: runAppReadyRuntimeWithFatalReporting,
      onWillQuitCleanup: () => onWillQuitCleanupHandler(),
      shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
      restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
      shouldQuitOnWindowAllClosed: () =>
        shouldQuitOnWindowAllClosedForTrayState({
          backgroundMode: appState.backgroundMode,
          hasTray: Boolean(appTray),
        }),
    },
    createAppLifecycleRuntimeRunner: (params) => createAppLifecycleRuntimeRunner(params),
    buildStartupBootstrapMainDeps: (startAppLifecycle) => ({
      argv: process.argv,
      parseArgs: (argv: string[]) => parseArgs(argv),
      setLogLevel: (level: string, source: LogLevelSource) => {
        setLogLevel(level, source);
      },
      forceX11Backend: (args: CliArgs) => {
        forceX11Backend(args);
      },
      enforceUnsupportedWaylandMode: (args: CliArgs) => {
        enforceUnsupportedWaylandMode(args);
      },
      shouldStartApp: (args: CliArgs) => shouldStartApp(args),
      getDefaultSocketPath: () =>
        configService.getConfig().mpv.socketPath || getDefaultSocketPathHandler(),
      defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
      configDir: CONFIG_DIR,
      defaultConfig: DEFAULT_CONFIG,
      generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
      generateDefaultConfigFile: (
        args: CliArgs,
        options: {
          configDir: string;
          defaultConfig: unknown;
          generateTemplate: (config: unknown) => string;
        },
      ) => generateDefaultConfigFile(args, options),
      setExitCode: (code) => {
        process.exitCode = code;
      },
      quitApp: () => requestAppQuit(),
      logGenerateConfigError: (message) => logger.error(message),
      startAppLifecycle,
    }),
    createStartupBootstrapRuntimeDeps: (deps) => createStartupBootstrapRuntimeDeps(deps),
    runStartupBootstrapRuntime,
    applyStartupState: (startupState) => applyStartupState(appState, startupState),
  },
});

runAndApplyStartupState();
void app.whenReady().then(() => {
  if (!shouldStartAutomaticUpdateChecks(appState.initialArgs)) {
    return;
  }
  getUpdateService().startAutomaticChecks();
});
const startupModeFlags = getStartupModeFlags(appState.initialArgs);
const shouldUseMinimalStartup = startupModeFlags.shouldUseMinimalStartup;
const shouldSkipHeavyStartup = startupModeFlags.shouldSkipHeavyStartup;
if (!appState.initialArgs || (!shouldUseMinimalStartup && !shouldSkipHeavyStartup)) {
  if (isAnilistTrackingEnabled(configService.getConfig())) {
    void refreshAnilistClientSecretStateIfEnabled({
      force: true,
      allowSetupPrompt: false,
    }).catch((error) => {
      logger.error('Failed to refresh AniList client secret state during startup', error);
    });
    anilistStateRuntime.refreshRetryQueueState();
  }
  void initializeDiscordPresenceService().catch((error) => {
    logger.error('Failed to initialize Discord presence service during startup', error);
  });
}
const { createMainWindow: createMainWindowHandler, createModalWindow: createModalWindowHandler } =
  createOverlayWindowRuntimeHandlers<BrowserWindow>({
    createOverlayWindowDeps: {
      createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
      isDev,
      ensureOverlayWindowLevel: (window) => overlayGeometryRuntime.ensureOverlayWindowLevel(window),
      onRuntimeOptionsChanged: () => overlayVisibilityComposer.broadcastRuntimeOptionsChanged(),
      setOverlayDebugVisualizationEnabled: (enabled) =>
        overlayVisibilityComposer.setOverlayDebugVisualizationEnabled(enabled),
      isOverlayVisible: (windowKind) =>
        windowKind === 'visible' ? overlayManager.getVisibleOverlayVisible() : false,
      getYomitanSession: () => appState.yomitanSession,
      tryHandleOverlayShortcutLocalFallback: (input) =>
        overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
      forwardTabToMpv: () => sendMpvCommandRuntime(appState.mpvClient, ['keypress', 'TAB']),
      getLinuxX11FullscreenOverlay: () =>
        shouldRunLinuxOverlayZOrderKeepAlive() &&
        linuxTrackedMpvFullscreen &&
        linuxVisibleOverlayWindowMode === 'fullscreen-override',
      onVisibleWindowBlurred: () =>
        visibleOverlayInteractionRuntime.scheduleVisibleOverlayBlurRefresh(),
      onVisibleWindowFocused: () =>
        visibleOverlayInteractionRuntime.requestLinuxOverlayZOrderFollow(),
      onWindowDidFinishLoad: () => {
        flushQueuedOverlayNotifications();
      },
      onWindowContentReady: () => {
        overlayNotificationsRuntime.dismissOverlayLoadingStatusNotification();
        flushQueuedOverlayNotifications();
        overlayVisibilityRuntime.updateVisibleOverlayVisibility();
        visibleOverlayInteractionRuntime.primeLinuxOverlayPointerInteractionAfterFirstMeasurement();
        autoplayReadyGate.flushPendingAutoplayReadySignal();
      },
      onWindowClosed: (windowKind, window) => {
        if (windowKind === 'visible') {
          if (overlayManager.getMainWindow() !== window) {
            return;
          }
          cancelPendingLinuxMpvFullscreenOverlayRefreshBurst();
          overlayManager.setMainWindow(null);
        } else {
          if (overlayManager.getModalWindow() !== window) {
            return;
          }
          void mediaTimingReviewRuntime.dispose();
          overlayManager.setModalWindow(null);
        }
      },
    },
    getMainWindow: () => overlayManager.getMainWindow(),
    isWindowDestroyed: (window) => window.isDestroyed(),
    setMainWindow: (window) => overlayManager.setMainWindow(window),
    setModalWindow: (window) => overlayManager.setModalWindow(window),
  });

function refreshTrayMenuIfPresent(): void {
  if (appTray) {
    ensureTrayHandler();
  }
}

function getJellyfinTrayDiscoveryDeps() {
  return {
    getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(),
    getRemoteSession: () => appState.jellyfinRemoteSession,
    clearStoredSession: () => jellyfinTokenStore.clearSession(),
    stopRemoteSession: () => stopJellyfinRemoteSession(),
    startRemoteSession: (options: { explicit: true }) => startJellyfinRemoteSession(options),
    refreshTrayMenu: () => refreshTrayMenuIfPresent(),
    logger,
    showMpvOsd: (message: string) =>
      overlayNotificationsRuntime.showConfiguredStatusNotification(message, { title: 'Jellyfin' }),
  };
}

const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
  createTrayRuntimeHandlers({
    resolveTrayIconPathDeps: {
      resolveTrayIconPathRuntime,
      platform: process.platform,
      resourcesPath: process.resourcesPath,
      appPath: app.getAppPath(),
      dirname: __dirname,
      joinPath: (...parts) => path.join(...parts),
      fileExists: (candidate) => fs.existsSync(candidate),
    },
    buildTrayMenuTemplateDeps: {
      buildTrayMenuTemplateRuntime,
      platform: process.platform,
      initializeOverlayRuntime: () => initializeOverlayRuntime(),
      isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
      openSessionHelpModal: () => openSessionHelpOverlay(),
      openChangelogModal: () => openChangelogOverlay(),
      openTexthookerInBrowser: () =>
        handleCliCommand(parseArgs(['--texthooker', '--open-browser'])),
      showTexthookerPage: () => shouldShowTexthookerTrayEntry(configService.getConfig()),
      showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(),
      openFirstRunSetupWindow: (force?: boolean) => openFirstRunSetupWindow(force),
      showWindowsMpvLauncherSetup: () => process.platform === 'win32',
      openYomitanSettings: () => openYomitanSettings(),
      openConfigSettingsWindow: () => configSettingsRuntime.openWindow(),
      openSyncUiWindow: () => openSyncUiWindowHandler(),
      exportLogs: () => {
        void exportLogsFromTray();
      },
      openJellyfinSetupWindow: () => openJellyfinSetupWindow(),
      isJellyfinConfigured: () =>
        isJellyfinConfiguredForTrayRuntime(getJellyfinTrayDiscoveryDeps()),
      isJellyfinDiscoveryActive: () => Boolean(appState.jellyfinRemoteSession),
      toggleJellyfinDiscovery: (checked: boolean) =>
        toggleJellyfinDiscoveryFromTrayRuntime(getJellyfinTrayDiscoveryDeps(), {
          desiredActive: checked,
        }),
      openAnilistSetupWindow: () => openAnilistSetupWindowHandler(),
      checkForUpdates: () => {
        void getUpdateService().checkForUpdates({ source: 'manual' });
      },
      quitApp: () => requestAppQuit(),
    },
    ensureTrayDeps: {
      getTray: () => appTray,
      setTray: (tray) => {
        appTray = tray as Tray | null;
      },
      createImageFromPath: (iconPath) => nativeImage.createFromPath(iconPath),
      createEmptyImage: () => nativeImage.createEmpty(),
      createTray: (icon) => new Tray(icon as ConstructorParameters<typeof Tray>[0]),
      trayTooltip: TRAY_TOOLTIP,
      platform: process.platform,
      logWarn: (message) => logger.warn(message),
      initializeOverlayRuntime: () => initializeOverlayRuntime(),
      isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
      setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
    },
    destroyTrayDeps: {
      getTray: () => appTray,
      setTray: (tray) => {
        appTray = tray as Tray | null;
      },
    },
    buildMenuFromTemplate: (template) => Menu.buildFromTemplate(template),
  });
const yomitanProfilePolicy = createYomitanProfilePolicy({
  externalProfilePath: configService.getConfig().yomitan.externalProfilePath,
  logInfo: (message) => logger.info(message),
});
const configuredExternalYomitanProfilePath = yomitanProfilePolicy.externalProfilePath;
const yomitanExtensionRuntime = createYomitanExtensionRuntime({
  loadYomitanExtensionCore,
  userDataPath: USER_DATA_PATH,
  externalProfilePath: configuredExternalYomitanProfilePath,
  getYomitanParserWindow: () => appState.yomitanParserWindow,
  setYomitanParserWindow: (window) => {
    appState.yomitanParserWindow = window as BrowserWindow | null;
  },
  setYomitanParserReadyPromise: (promise) => {
    appState.yomitanParserReadyPromise = promise;
  },
  setYomitanParserInitPromise: (promise) => {
    appState.yomitanParserInitPromise = promise;
  },
  setYomitanExtension: (extension) => {
    appState.yomitanExt = extension;
  },
  setYomitanSession: (nextSession) => {
    appState.yomitanSession = nextSession;
  },
  getYomitanExtension: () => appState.yomitanExt,
  getLoadInFlight: () => yomitanLoadInFlight,
  setLoadInFlight: (promise) => {
    yomitanLoadInFlight = promise;
  },
  onYomitanExtensionLoaded: () => {
    const reloaded = reloadOverlayWindowsForYomitanContentScripts(
      overlayManager.getOverlayWindows(),
      (message, error) => logger.warn(message, error),
    );
    if (reloaded > 0) {
      logger.debug(`Reloaded ${reloaded} overlay window(s) after Yomitan extension load.`);
    }
  },
});
const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
  runtimeRegistry.overlay.createOverlayRuntimeBootstrapHandlers({
    initializeOverlayRuntimeMainDeps: {
      appState,
      overlayManager: {
        getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
      },
      overlayVisibilityRuntime: {
        updateVisibleOverlayVisibility: () =>
          overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
      },
      refreshCurrentSubtitle: () => {
        subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
      },
      overlayShortcutsRuntime: {
        syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
      },
      createMainWindow: () => {
        if (appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)) {
          return;
        }
        createMainWindow();
      },
      registerGlobalShortcuts: () => {
        if (appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)) {
          return;
        }
        registerGlobalShortcuts();
      },
      createWindowTracker: (override, targetMpvSocketPath) =>
        visibleOverlayInteractionRuntime.createOverlayWindowTracker(override, targetMpvSocketPath),
      updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
        overlayGeometryRuntime.updateVisibleOverlayBounds(geometry),
      bindOverlayOwner: () => visibleOverlayInteractionRuntime.bindVisibleOverlayOwner(),
      releaseOverlayOwner: () => visibleOverlayInteractionRuntime.releaseVisibleOverlayOwner(),
      getOverlayWindows: () => overlayManager.getOverlayWindows(),
      getResolvedConfig: () => configService.getConfig(),
      showDesktopNotification,
      showOverlayNotification: (payload) =>
        overlayNotificationsRuntime.showOverlayNotification(payload),
      dismissOverlayNotification: (id) =>
        overlayNotificationsRuntime.dismissOverlayNotification(id),
      createFieldGroupingCallback: () => createFieldGroupingCallback(),
      getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
      getCachedMediaPath: (currentVideoPath, kind) =>
        getCachedYoutubeMediaPathForCurrentPlayback(currentVideoPath, kind),
      shouldRequireRemoteMediaCache: () => shouldRequireYoutubeMediaCacheForCurrentPlayback(),
      getYoutubeMediaSourceUrl: () => getCurrentYoutubeMediaCacheSourceUrl(),
      shouldStartAnkiIntegration: () =>
        !(appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)),
    },
    initializeOverlayRuntimeBootstrapDeps: {
      isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
      initializeOverlayRuntimeCore,
      setOverlayRuntimeInitialized: (initialized) => {
        appState.overlayRuntimeInitialized = initialized;
      },
      startBackgroundWarmups: () => {
        if (appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)) {
          return;
        }
        startBackgroundWarmups();
      },
    },
  });
const { openYomitanSettings: openYomitanSettingsHandler } = createYomitanSettingsRuntime({
  ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded(),
  getYomitanExtension: () => appState.yomitanExt,
  getYomitanExtensionLoadInFlight: () => yomitanLoadInFlight,
  getYomitanSession: () => appState.yomitanSession,
  openYomitanSettingsWindow: ({ yomitanExt, getExistingWindow, setWindow, yomitanSession }) => {
    openYomitanSettingsWindow({
      yomitanExt: yomitanExt as Extension,
      getExistingWindow: () => getExistingWindow() as BrowserWindow | null,
      setWindow: (window) => setWindow(window as BrowserWindow | null),
      yomitanSession: (yomitanSession as Session | null | undefined) ?? appState.yomitanSession,
      onWindowClosed: () => {
        if (appState.yomitanParserWindow) {
          clearYomitanParserCachesForWindow(appState.yomitanParserWindow);
        }
      },
    });
  },
  getExistingWindow: () => appState.yomitanSettingsWindow,
  setWindow: (window) => {
    appState.yomitanSettingsWindow = window as BrowserWindow | null;
  },
  logWarn: (message) => logger.warn(message),
  logError: (message, error) => logger.error(message, error),
});

function ensureOverlayWindowsReadyForVisibilityActions(): void {
  if (!appState.overlayRuntimeInitialized) {
    initializeOverlayRuntime();
    return;
  }

  const mainWindow = overlayManager.getMainWindow();
  if (!mainWindow || mainWindow.isDestroyed()) {
    createMainWindow();
  }
}

function notifyMpvPluginVisibleOverlayVisibility(visible: boolean): void {
  sendMpvCommandRuntime(appState.mpvClient, [
    'script-message',
    visible ? 'subminer-visible-overlay-shown' : 'subminer-visible-overlay-hidden',
  ]);
}

function setVisibleOverlayVisible(visible: boolean): void {
  ensureOverlayWindowsReadyForVisibilityActions();
  if (!visible) {
    overlayNotificationsRuntime.dismissOverlayLoadingStatusNotification();
    autoplayReadyGate.markCurrentMediaAutoplayReady();
    autoplaySubtitlePrimingRuntime.cancelVisibleOverlaySubtitleRefreshAfterFirstPaint();
    cancelPendingLinuxMpvFullscreenOverlayRefreshBurst();
    visibleOverlayInteractionRuntime.resetVisibleOverlayInputState();
  }
  if (visible) {
    maybeStartOverlayLoadingOsd();
    visibleOverlayInteractionRuntime.resetLinuxVisibleOverlayStartupInputPrimer();
    visibleOverlayInteractionRuntime.startLinuxVisibleOverlayStartupInputGrace();
    visibleOverlayInteractionRuntime.restoreVisibleOverlayWindowShapeForShow();
    void ensureOverlayMpvSubtitlesHidden();
    void autoplaySubtitlePrimingRuntime.primeCurrentSubtitleForVisibleOverlay();
  }
  setVisibleOverlayVisibleHandler(visible);
  notifyMpvPluginVisibleOverlayVisibility(visible);
  syncOverlayMpvSubtitleSuppression();
}

function toggleVisibleOverlay(): void {
  ensureOverlayWindowsReadyForVisibilityActions();
  const nextVisible = !overlayManager.getVisibleOverlayVisible();
  if (!nextVisible) {
    overlayNotificationsRuntime.dismissOverlayLoadingStatusNotification();
    autoplayReadyGate.markCurrentMediaAutoplayReady();
    autoplaySubtitlePrimingRuntime.cancelVisibleOverlaySubtitleRefreshAfterFirstPaint();
    cancelPendingLinuxMpvFullscreenOverlayRefreshBurst();
    visibleOverlayInteractionRuntime.resetVisibleOverlayInputState();
  } else {
    maybeStartOverlayLoadingOsd();
    visibleOverlayInteractionRuntime.resetLinuxVisibleOverlayStartupInputPrimer();
    visibleOverlayInteractionRuntime.startLinuxVisibleOverlayStartupInputGrace();
    visibleOverlayInteractionRuntime.restoreVisibleOverlayWindowShapeForShow();
    void ensureOverlayMpvSubtitlesHidden();
    void autoplaySubtitlePrimingRuntime.primeCurrentSubtitleForVisibleOverlay();
  }
  toggleVisibleOverlayHandler();
  notifyMpvPluginVisibleOverlayVisibility(nextVisible);
  syncOverlayMpvSubtitleSuppression();
}
function setOverlayVisible(visible: boolean): void {
  if (!visible) {
    overlayNotificationsRuntime.dismissOverlayLoadingStatusNotification();
    autoplaySubtitlePrimingRuntime.cancelVisibleOverlaySubtitleRefreshAfterFirstPaint();
    visibleOverlayInteractionRuntime.resetVisibleOverlayInputState();
    autoplayReadyGate.markCurrentMediaAutoplayReady();
    cancelPendingLinuxMpvFullscreenOverlayRefreshBurst();
  }
  if (visible) {
    maybeStartOverlayLoadingOsd();
    visibleOverlayInteractionRuntime.resetLinuxVisibleOverlayStartupInputPrimer();
    visibleOverlayInteractionRuntime.startLinuxVisibleOverlayStartupInputGrace();
    visibleOverlayInteractionRuntime.restoreVisibleOverlayWindowShapeForShow();
    void ensureOverlayMpvSubtitlesHidden();
    void autoplaySubtitlePrimingRuntime.primeCurrentSubtitleForVisibleOverlay();
  }
  setOverlayVisibleHandler(visible);
  notifyMpvPluginVisibleOverlayVisibility(visible);
  syncOverlayMpvSubtitleSuppression();
}

registerIpcRuntimeHandlers();
