/**
 * IPC surface for the main process. Every `ipcMain.handle` (and the one
 * fire-and-forget `taskbarProgress` receiver) lives here, extracted from index.ts
 * so that module is just app lifecycle + window creation and this is the single
 * place the renderer's IPC contract is wired (L2).
 *
 * The handful of handlers that must touch the window (folder picker parenting,
 * theme-synced background, taskbar progress) get the current window through the
 * `getMainWindow` accessor rather than a captured reference, so they always act on
 * the live window. The small theme helpers they share are defined and exported
 * here too, since index's system-theme bridge uses them as well.
 */

import { app, shell, BrowserWindow, ipcMain, dialog, clipboard, nativeTheme } from 'electron'
import { existsSync } from 'fs'
import {
  IpcChannels,
  type StartDownloadOptions,
  type Settings,
  type HistoryEntry,
  type CommandTemplate,
  type MediaProfile,
  type YtdlpUpdateChannel,
  type SystemThemeInfo,
  type TaskbarProgress,
  type PersistedQueueItem
} from '@shared/ipc'
import { PAGE_BACKGROUND } from '@shared/theme'
import { getYtdlpVersion, updateYtdlp } from './ytdlp'
import { getFfmpegVersions } from './ffmpeg'
import {
  getFfmpegSetupStatus,
  downloadFfmpeg,
  cancelFfmpegDownload,
  locateFfmpegManually
} from './ffmpegSetup'
import { getAria2cPath } from './binaries'
import { checkForAppUpdate, downloadAppUpdate, runAppUpdate, cancelAppUpdate } from './updater'
import { probeMedia } from './probe'
import { startDownload, cancelDownload, pauseDownload, previewCommand } from './download'
import { whenYtdlpUpdateSettled } from './ytdlpLock'
import { runTerminal, cancelTerminal } from './terminal'
import { getSettings, setSettings } from './settings'
import { listHistory, addHistory, removeHistory, removeManyHistory, clearHistory } from './history'
import { listTemplates, saveTemplate, removeTemplate } from './templates'
import { listProfiles, saveProfile, removeProfile } from './profiles'
import { safeOpenPath, safeShowInFolder } from './reveal'
import { openCookieLoginWindow, getCookiesStatus, clearCookies } from './cookies'
import { listErrorLog, addErrorLog, clearErrorLog } from './errorlog'
import { listQueue, saveQueue } from './queue'
import { exportBackup, importBackup } from './backup'
import {
  listSources,
  getSource,
  removeSource,
  listMediaItems,
  setMediaItemDownloaded,
  setSourceWatched,
  setSourceProfile,
  pruneMediaItems
} from './sources'
import { indexSourceCancelable, cancelIndexing } from './indexer'
import { syncWatchedSources } from './sync'
import { getScheduledSync, setScheduledSync } from './schedule'
import { getActiveBadge, getErrorBadge } from './badge'
import { openPoTokenWindow } from './poToken'
import { logger } from './logger'

// --- Theme helpers (shared with index's system-theme bridge) ----------------

/** Resolve 'system' against the OS preference so callers always get a concrete color. */
export function resolveBackgroundMode(theme: Settings['theme']): 'light' | 'dark' {
  return theme === 'system' ? (nativeTheme.shouldUseDarkColors ? 'dark' : 'light') : theme
}

/**
 * Keep the OS-drawn title bar consistent with the in-app theme (W3). Setting
 * themeSource to 'light'/'dark' forces the caption/chrome to match; 'system'
 * restores OS control. Called at startup and on every theme change.
 */
export function applyNativeTheme(theme: Settings['theme']): void {
  nativeTheme.themeSource = theme
}

export function getSystemThemeInfo(): SystemThemeInfo {
  return {
    shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
    shouldUseHighContrastColors: nativeTheme.shouldUseHighContrastColors
  }
}

// --- IPC registration -------------------------------------------------------

export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null): void {
  // M30: a broken contextBridge causes the renderer to silently run in preview/mock
  // mode. Catch it here and show a hard error so the failure is never invisible.
  ipcMain.once(IpcChannels.preloadBridgeFailure, (_e, detail: unknown) => {
    dialog.showErrorBox(
      'AeroFetch could not start',
      `The renderer bridge failed to initialize. Please reinstall the application.\n\n${String(detail)}`
    )
    app.quit()
  })

  // CC8: persist renderer-side failures (the M29 `logError` sites) to the main log
  // file — the renderer console is unreachable in a packaged build. Fire-and-forget.
  ipcMain.on(IpcChannels.logWrite, (_e, op: string, detail: string) =>
    logger.error(`[renderer] ${op}`, detail)
  )

  ipcMain.handle(IpcChannels.appVersion, () => app.getVersion())

  ipcMain.handle(IpcChannels.appUpdateCheck, () => checkForAppUpdate())
  ipcMain.handle(IpcChannels.appUpdateDownload, (e, url: string) =>
    downloadAppUpdate(e.sender, url)
  )
  ipcMain.handle(IpcChannels.appUpdateCancel, () => cancelAppUpdate())
  ipcMain.handle(IpcChannels.appUpdateRun, (_e, filePath: string) => runAppUpdate(filePath))

  ipcMain.handle(IpcChannels.ytdlpVersion, () => getYtdlpVersion())

  ipcMain.handle(IpcChannels.ffmpegVersion, () => getFfmpegVersions())

  // First-run ffmpeg/ffprobe acquisition (they're no longer bundled). Status gates the
  // setup screen; download streams the pinned archive to e.sender; locate is the offline
  // fallback (folder picker parented to the window, like chooseFolder).
  ipcMain.handle(IpcChannels.ffmpegSetupStatus, () => getFfmpegSetupStatus())
  ipcMain.handle(IpcChannels.ffmpegSetupDownload, (e) => downloadFfmpeg(e.sender))
  ipcMain.handle(IpcChannels.ffmpegSetupCancel, () => cancelFfmpegDownload())
  ipcMain.handle(IpcChannels.ffmpegSetupLocate, (e) =>
    locateFfmpegManually(BrowserWindow.fromWebContents(e.sender) ?? undefined)
  )

  // Whether the optional aria2c external downloader shipped with this build, so the
  // Settings toggle can disable itself instead of silently doing nothing (the flag
  // is a no-op without the binary — see download.ts's existsSync gate).
  ipcMain.handle(IpcChannels.aria2cAvailable, () => existsSync(getAria2cPath()))

  ipcMain.handle(IpcChannels.probe, (_e, url: string) => probeMedia(url))

  ipcMain.handle(IpcChannels.downloadStart, async (e, opts: StartDownloadOptions) => {
    // L139: hold the spawn until any in-progress yt-dlp self-update has finished
    // swapping the exe, so a launch-time (auto-resumed) download waits it out instead
    // of racing the rewrite. Resolves immediately when no update is running.
    await whenYtdlpUpdateSettled()
    const result = startDownload(e.sender, opts)
    // Pre-spawn failures (missing yt-dlp.exe, bad URL, duplicate id) never reach
    // download.ts's own close/error handlers, so log them here instead — unless the
    // download is incognito, which is never recorded (L136).
    if (!result.ok && !opts.incognito) {
      addErrorLog({
        id: opts.id,
        url: opts.url,
        kind: opts.kind,
        error: result.error ?? 'Unknown error',
        occurredAt: Date.now()
      })
    }
    return result
  })
  ipcMain.handle(IpcChannels.downloadCancel, (_e, id: string) => cancelDownload(id))
  ipcMain.handle(IpcChannels.downloadPause, (_e, id: string) => pauseDownload(id))
  ipcMain.handle(IpcChannels.terminalRun, async (e, id: string, args: string) => {
    await whenYtdlpUpdateSettled() // L139: hold behind an in-progress exe rewrite
    return runTerminal(e.sender, id, args)
  })
  ipcMain.handle(IpcChannels.terminalCancel, (_e, id: string) => cancelTerminal(id))

  ipcMain.handle(IpcChannels.chooseFolder, async (e, current?: string) => {
    const win = BrowserWindow.fromWebContents(e.sender) ?? undefined
    // Seed the picker at the currently-configured folder so it opens where the
    // user already points, not a generic default (W5). 'createDirectory' is a
    // macOS-only property and a no-op on Windows, so it's dropped (L58).
    const defaultPath = current && existsSync(current) ? current : undefined
    // Use the parented overload only when we actually have a window — passing a
    // forced non-null window that's gone can throw (L53).
    const res = win
      ? await dialog.showOpenDialog(win, { properties: ['openDirectory'], defaultPath })
      : await dialog.showOpenDialog({ properties: ['openDirectory'], defaultPath })
    return res.canceled || !res.filePaths[0] ? null : res.filePaths[0]
  })

  ipcMain.handle(IpcChannels.openPath, (_e, p: string) => safeOpenPath(p))
  ipcMain.handle(IpcChannels.openUrl, (_e, url: string) => {
    try {
      const { protocol } = new URL(url)
      if (protocol === 'http:' || protocol === 'https:') void shell.openExternal(url)
    } catch {
      // Ignore malformed URLs.
    }
  })
  ipcMain.handle(IpcChannels.showInFolder, (_e, p: string) => safeShowInFolder(p))

  ipcMain.handle(IpcChannels.clipboardRead, () => clipboard.readText())

  ipcMain.handle(IpcChannels.settingsGet, () => getSettings())
  ipcMain.handle(IpcChannels.settingsSet, (e, partial: Partial<Settings>) => {
    const result = setSettings(partial)
    // Keep the window's native background and title bar in sync with the theme
    // so a compositor repaint never flashes a mismatched color, and the OS-drawn
    // caption always matches the in-app theme (W3). Use the validated result.
    if (partial.theme) {
      BrowserWindow.fromWebContents(e.sender)?.setBackgroundColor(
        PAGE_BACKGROUND[resolveBackgroundMode(result.theme)]
      )
      applyNativeTheme(result.theme)
    }
    return result
  })

  ipcMain.handle(IpcChannels.systemThemeGet, () => getSystemThemeInfo())

  ipcMain.handle(IpcChannels.openHighContrastSettings, () =>
    shell.openExternal('ms-settings:easeofaccess-highcontrast')
  )

  ipcMain.handle(IpcChannels.historyList, () => listHistory())
  ipcMain.handle(IpcChannels.historyAdd, (_e, entry: HistoryEntry) => addHistory(entry))
  ipcMain.handle(IpcChannels.historyRemove, (_e, id: string) => removeHistory(id))
  ipcMain.handle(IpcChannels.historyRemoveMany, (_e, ids: string[]) => removeManyHistory(ids))
  ipcMain.handle(IpcChannels.historyClear, () => clearHistory())

  ipcMain.handle(IpcChannels.cookiesLogin, (e, url: string) =>
    // Parent the sign-in window to the app window (W6) so it groups under
    // AeroFetch instead of spawning a second taskbar button; theme its
    // background to the app's Light/Dark (UI23).
    openCookieLoginWindow(
      url,
      BrowserWindow.fromWebContents(e.sender) ?? undefined,
      PAGE_BACKGROUND[resolveBackgroundMode(getSettings().theme)]
    )
  )
  ipcMain.handle(IpcChannels.cookiesStatus, () => getCookiesStatus())
  ipcMain.handle(IpcChannels.cookiesClear, () => clearCookies())

  ipcMain.handle(IpcChannels.templatesList, () => listTemplates())
  ipcMain.handle(IpcChannels.templatesSave, (_e, template: CommandTemplate) =>
    saveTemplate(template)
  )
  ipcMain.handle(IpcChannels.templatesRemove, (_e, id: string) => removeTemplate(id))

  ipcMain.handle(IpcChannels.profilesList, () => listProfiles())
  ipcMain.handle(IpcChannels.profilesSave, (_e, profile: MediaProfile) => saveProfile(profile))
  ipcMain.handle(IpcChannels.profilesRemove, (_e, id: string) => removeProfile(id))

  ipcMain.handle(IpcChannels.commandPreview, (_e, opts: StartDownloadOptions) =>
    previewCommand(opts)
  )

  ipcMain.handle(IpcChannels.ytdlpUpdate, (_e, channel: YtdlpUpdateChannel) => updateYtdlp(channel))

  ipcMain.handle(IpcChannels.errorLogList, () => listErrorLog())
  ipcMain.handle(IpcChannels.errorLogClear, () => clearErrorLog())

  // Persisted download queue (M4): the renderer mirrors its durable items here and
  // rehydrates them on launch, so saved/scheduled/pending downloads survive a quit.
  ipcMain.handle(IpcChannels.queueList, () => listQueue())
  ipcMain.handle(IpcChannels.queueSave, (_e, items: PersistedQueueItem[]) => saveQueue(items))

  ipcMain.handle(IpcChannels.backupExport, (e) =>
    exportBackup(BrowserWindow.fromWebContents(e.sender) ?? undefined)
  )
  ipcMain.handle(IpcChannels.backupImport, async (e) => {
    const win = BrowserWindow.fromWebContents(e.sender) ?? undefined
    const result = await importBackup(win)
    // A restored backup may have changed the theme; keep the native window
    // background and title bar in sync the same way settingsSet does (W3).
    if (result.ok) {
      const theme = getSettings().theme
      win?.setBackgroundColor(PAGE_BACKGROUND[resolveBackgroundMode(theme)])
      applyNativeTheme(theme)
    }
    return result
  })

  // --- Media-manager sources (Pinchflat-style index) ---
  ipcMain.handle(IpcChannels.sourcesList, () => listSources())
  ipcMain.handle(IpcChannels.sourceItems, (_e, sourceId: string) => listMediaItems(sourceId))
  ipcMain.handle(IpcChannels.sourceRemove, (_e, id: string) => removeSource(id))
  ipcMain.handle(
    IpcChannels.sourceItemDownloaded,
    (_e, id: string, filePath?: string, quality?: string) =>
      setMediaItemDownloaded(id, filePath, quality)
  )
  // Indexing pushes live progress to the requesting renderer over `indexProgress`
  // and resolves with the final result.
  ipcMain.handle(IpcChannels.sourceIndex, (e, url: string) =>
    indexSourceCancelable((p) => {
      if (!e.sender.isDestroyed()) e.sender.send(IpcChannels.indexProgress, p)
    }, url)
  )
  ipcMain.handle(IpcChannels.sourceReindex, (e, id: string) => {
    const src = getSource(id)
    if (!src) return { ok: false, error: 'Source not found.' }
    return indexSourceCancelable((p) => {
      if (!e.sender.isDestroyed()) e.sender.send(IpcChannels.indexProgress, p)
    }, src.url)
  })
  // Abort the in-flight index/reindex walk (kills the current probe child). (B2)
  ipcMain.handle(IpcChannels.sourceIndexCancel, () => cancelIndexing())
  ipcMain.handle(IpcChannels.sourceSetWatched, (_e, id: string, watched: boolean) =>
    setSourceWatched(id, watched)
  )
  ipcMain.handle(IpcChannels.sourceSetProfile, (_e, id: string, profileId: string | null) =>
    setSourceProfile(id, profileId)
  )
  ipcMain.handle(IpcChannels.sourcePruneItems, (_e, sourceId: string, itemIds: string[]) =>
    pruneMediaItems(sourceId, itemIds)
  )
  ipcMain.handle(IpcChannels.sourcesSync, (e) =>
    syncWatchedSources((p) => {
      if (!e.sender.isDestroyed()) e.sender.send(IpcChannels.indexProgress, p)
    })
  )
  ipcMain.handle(IpcChannels.scheduledSyncGet, () => getScheduledSync())
  // `time` (24h HH:MM) is validated inside setScheduledSync before touching schtasks (L51).
  ipcMain.handle(IpcChannels.scheduledSyncSet, (_e, enabled: boolean, time?: string) =>
    setScheduledSync(enabled, time)
  )

  // Reflect overall queue progress on the Windows taskbar and window title (SR8).
  ipcMain.handle(IpcChannels.taskbarProgress, (_e, p: TaskbarProgress) => {
    const win = getMainWindow()
    if (!win || win.isDestroyed()) return
    if (p.mode === 'none') {
      win.setProgressBar(-1)
      win.setOverlayIcon(null, '')
      win.setTitle('AeroFetch')
    } else {
      win.setProgressBar(Math.max(0, Math.min(1, p.fraction)), { mode: p.mode })
      const badge = p.mode === 'error' ? getErrorBadge() : getActiveBadge()
      const n = p.badgeCount ?? 0
      const label =
        p.mode === 'error' ? 'Download error' : `${n} download${n !== 1 ? 's' : ''} active`
      win.setOverlayIcon(badge, label)
      win.setTitle(p.mode === 'error' ? 'AeroFetch — Error' : `AeroFetch — ${label}`)
    }
  })

  // Open a YouTube WebView and extract a PO token for bot-check bypass (Phase P).
  ipcMain.handle(IpcChannels.youtubePoTokenMint, async () => {
    const token = await openPoTokenWindow(
      PAGE_BACKGROUND[resolveBackgroundMode(getSettings().theme)]
    )
    if (token) await setSettings({ youtubePoToken: token })
    return token
  })
}
