/**
 * Shared contract between main and renderer.
 * IPC channel names + payload types live here so both sides stay in sync.
 */

export const IpcChannels = {
  /** the AeroFetch app version (package.json / app.getVersion) */
  appVersion: 'app:version',
  /** check the configured Gitea repo for a newer AeroFetch release */
  appUpdateCheck: 'app:update-check',
  /** download the latest release's installer to a temp file */
  appUpdateDownload: 'app:update-download',
  /** abort the in-flight installer download (B6) */
  appUpdateCancel: 'app:update-cancel',
  /** launch a downloaded installer and quit so it can replace the app */
  appUpdateRun: 'app:update-run',
  /** main → renderer push channel for installer download progress */
  appUpdateProgress: 'app:update-progress',
  ytdlpVersion: 'ytdlp:version',
  ffmpegVersion: 'ffmpeg:version',
  /** readiness (both binaries present) + versions of the managed ffmpeg/ffprobe */
  ffmpegSetupStatus: 'ffmpeg:setup-status',
  /** download + unpack ffmpeg/ffprobe from the pinned upstream archive (first-run setup) */
  ffmpegSetupDownload: 'ffmpeg:setup-download',
  /** abort the in-flight ffmpeg archive download */
  ffmpegSetupCancel: 'ffmpeg:setup-cancel',
  /** pick a folder that already holds ffmpeg.exe + ffprobe.exe and install from it */
  ffmpegSetupLocate: 'ffmpeg:setup-locate',
  /** main → renderer push channel for ffmpeg setup progress (download %, then extract) */
  ffmpegSetupProgress: 'ffmpeg:setup-progress',
  /** whether the optional aria2c.exe external downloader is present in the bin dir */
  aria2cAvailable: 'aria2c:available',
  probe: 'media:probe',
  downloadStart: 'download:start',
  downloadCancel: 'download:cancel',
  downloadPause: 'download:pause',
  chooseFolder: 'download:choose-folder',
  openPath: 'shell:open-path',
  openUrl: 'shell:open-url',
  showInFolder: 'shell:show-in-folder',
  clipboardRead: 'clipboard:read',
  settingsGet: 'settings:get',
  settingsSet: 'settings:set',
  historyList: 'history:list',
  historyAdd: 'history:add',
  historyRemove: 'history:remove',
  historyRemoveMany: 'history:remove-many',
  historyClear: 'history:clear',
  /** main → renderer push channel for live download events */
  downloadEvent: 'download:event',
  cookiesLogin: 'cookies:login',
  cookiesStatus: 'cookies:status',
  cookiesClear: 'cookies:clear',
  templatesList: 'templates:list',
  templatesSave: 'templates:save',
  templatesRemove: 'templates:remove',
  /** Media profiles: named download presets a Source can point at (PINCHFLAT) */
  profilesList: 'profiles:list',
  profilesSave: 'profiles:save',
  profilesRemove: 'profiles:remove',
  commandPreview: 'command:preview',
  ytdlpUpdate: 'ytdlp:update',
  /** main → renderer push channel for background yt-dlp auto-update status */
  ytdlpAutoUpdateStatus: 'ytdlp:auto-update-status',
  errorLogList: 'errorlog:list',
  errorLogClear: 'errorlog:clear',
  /** Persisted download queue (M4): saved/scheduled/pending items survive a quit. */
  queueList: 'queue:list',
  queueSave: 'queue:save',
  backupExport: 'backup:export',
  backupImport: 'backup:import',
  systemThemeGet: 'system-theme:get',
  /** main → renderer push channel for OS theme/contrast changes (nativeTheme 'updated') */
  systemThemeUpdate: 'system-theme:update',
  openHighContrastSettings: 'shell:open-high-contrast-settings',
  /** preload → main: the contextBridge failed to expose the API; main shows a
   *  hard error instead of letting the renderer silently fall back to mock mode (M30) */
  preloadBridgeFailure: 'preload:bridge-failure',
  /** renderer → main fire-and-forget: forward a renderer-side failure to the main
   *  diagnostic log so it survives into a packaged build (CC8; the M29 sink half). */
  logWrite: 'log:write',
  /** main → renderer push channel: a URL handed to AeroFetch from outside the app
   *  (the aerofetch:// protocol, or a .url file via Explorer's "Send to" menu) */
  externalUrl: 'app:external-url',
  // --- Sources & media-manager indexing (Pinchflat-style) ---
  sourcesList: 'sources:list',
  sourceIndex: 'sources:index',
  sourceReindex: 'sources:reindex',
  /** renderer → main: abort the in-flight index/reindex walk (B2) */
  sourceIndexCancel: 'sources:index-cancel',
  sourceRemove: 'sources:remove',
  sourceItems: 'sources:items',
  /** mark a media item downloaded once its collection download completes */
  sourceItemDownloaded: 'sources:item-downloaded',
  /** toggle whether a source is watched for new uploads (Phase J) */
  sourceSetWatched: 'sources:set-watched',
  /** point a source at a MediaProfile (or clear it) */
  sourceSetProfile: 'sources:set-profile',
  /** prune (delete files of) the given media items for a source (retention) */
  sourcePruneItems: 'sources:prune-items',
  /** re-index all watched sources and return the videos that are new */
  sourcesSync: 'sources:sync',
  /** get / set the Windows scheduled-sync task (Task Scheduler) */
  scheduledSyncGet: 'sources:scheduled-sync-get',
  scheduledSyncSet: 'sources:scheduled-sync-set',
  /** main → renderer push channel for live indexing progress */
  indexProgress: 'sources:index-progress',
  /** main → renderer: a scheduled `--sync` relaunch hit the running instance — run the
   *  watched-source sync here instead of no-opping (Phase J) */
  runWatchedSync: 'sources:run-sync',
  // --- Built-in yt-dlp terminal (Phase N) ---
  terminalRun: 'terminal:run',
  terminalCancel: 'terminal:cancel',
  /** main → renderer push channel for live terminal output lines */
  terminalOutput: 'terminal:output',
  /** renderer → main: reflect overall queue progress on the Windows taskbar (Phase O) */
  taskbarProgress: 'taskbar:progress',
  /** renderer → main: open a YouTube WebView and extract a PO token (Phase P) */
  youtubePoTokenMint: 'youtube:po-token-mint'
} as const

/** Sentinel format id meaning "let yt-dlp pick the best video+audio". */
export const BEST_FORMAT_ID = '__best__'

export type MediaKind = 'video' | 'audio'

// --- Post-processing / format options ---------------------------------------

/** Audio extraction target. 'best' keeps the source codec without re-encoding. */
export type AudioFormat = 'best' | 'mp3' | 'm4a' | 'opus' | 'flac' | 'wav' | 'aac'
export const AUDIO_FORMATS: AudioFormat[] = ['best', 'mp3', 'm4a', 'opus', 'flac', 'wav', 'aac']

/** Container the merged video is muxed into (`--merge-output-format`). */
export type VideoContainer = 'mp4' | 'mkv' | 'webm'
export const VIDEO_CONTAINERS: VideoContainer[] = ['mp4', 'mkv', 'webm']

/** Preferred video codec, applied as a `-S vcodec:…` sort tiebreaker. */
export type VideoCodecPref = 'any' | 'h264' | 'vp9' | 'av1'
export const VIDEO_CODECS: VideoCodecPref[] = ['any', 'h264', 'vp9', 'av1']

/** Whether SponsorBlock cuts the segments out or just marks them as chapters. */
export type SponsorBlockMode = 'remove' | 'mark'

/**
 * Where yt-dlp's auth cookies come from. 'browser' reads a logged-in browser's
 * cookie store directly; 'login' uses a cookie file exported from AeroFetch's
 * own built-in sign-in window (see src/main/cookies.ts).
 */
export type CookieSource = 'none' | 'browser' | 'login'

/** Browsers yt-dlp's --cookies-from-browser supports that ship on Windows. */
export const COOKIE_BROWSERS = ['chrome', 'edge', 'firefox', 'brave', 'opera', 'vivaldi'] as const
export type CookieBrowser = (typeof COOKIE_BROWSERS)[number]

/** Accent color presets for the brand ramp (see src/renderer/src/theme.ts). */
export const ACCENT_COLORS = ['rose', 'coral', 'amber', 'teal'] as const
export type AccentColor = (typeof ACCENT_COLORS)[number]

/** Quality/format labels for the video download selector. */
export const VIDEO_QUALITY_OPTIONS = ['Best available', '1080p', '720p', '480p', '360p'] as const
export type VideoQuality = (typeof VIDEO_QUALITY_OPTIONS)[number]

/** Quality/format labels for the audio download selector. */
// Format-agnostic: the audio container/codec is chosen separately (downloadOptions
// .audioFormat); these are just the target quality. Bitrate values are ignored for
// lossless formats (flac/wav) -- see buildArgs (M18/M21).
export const AUDIO_QUALITY_OPTIONS = ['Best', '320 kbps', '192 kbps', '128 kbps'] as const
export type AudioQuality = (typeof AUDIO_QUALITY_OPTIONS)[number]

/**
 * Rank a quality label so two can be compared for the retention quality-upgrade
 * (higher = better). Works across the video and audio ladders: "Best*" tops each,
 * then the numeric resolution/bitrate descends. An unknown label ranks 0 (worst),
 * so it never blocks an upgrade to a known-better target. Shared so the selector
 * (main) and any UI use one ordering.
 */
export function qualityRank(label: string): number {
  const l = label.trim().toLowerCase()
  if (l.startsWith('best')) return 100_000
  // First number in the label is the resolution (1080) or bitrate (320) — both
  // sort correctly as "bigger is better" within their own ladder.
  const n = parseInt(l.replace(/[^\d]/g, ''), 10)
  return Number.isFinite(n) ? n : 0
}

/** UI color theme: an explicit mode, or 'system' to follow the OS preference. */
export type ThemeMode = 'light' | 'dark' | 'system'

/** OS-level theme signal, read from Electron's nativeTheme in the main process. */
export interface SystemThemeInfo {
  /** whether Windows is currently in dark mode */
  shouldUseDarkColors: boolean
  /** whether a Windows high-contrast theme is active */
  shouldUseHighContrastColors: boolean
}

/** SponsorBlock segment categories (subset of yt-dlp's, the ones Seal exposes). */
export const SPONSORBLOCK_CATEGORIES = [
  'sponsor',
  'intro',
  'outro',
  'selfpromo',
  'preview',
  'filler',
  'interaction',
  'music_offtopic'
] as const
export type SponsorBlockCategory = (typeof SPONSORBLOCK_CATEGORIES)[number]

/**
 * Post-processing / format choices applied to a download. The persisted
 * defaults live in `Settings.downloadOptions`; an individual download may
 * override them via `StartDownloadOptions.options`.
 */
export interface DownloadOptions {
  /** audio extraction target (audio downloads only) */
  audioFormat: AudioFormat
  /** container for merged video downloads */
  videoContainer: VideoContainer
  /** preferred video codec (sort tiebreaker, not a hard filter) */
  preferredVideoCodec: VideoCodecPref
  /**
   * Advanced: a raw yt-dlp `-S` format-sort string (e.g. 'res:1080,vcodec:av01,size').
   * When non-empty it replaces the codec-derived sort, letting power users rank
   * resolution / codec / container / size by weighted priority. Empty = use the
   * preferredVideoCodec tiebreaker.
   */
  formatSort: string
  /** download + embed subtitles into video */
  embedSubtitles: boolean
  /** comma-separated yt-dlp sub-lang selectors, e.g. 'en' or 'en.*,es' */
  subtitleLanguages: string
  /** also pull auto-generated (ASR) captions */
  autoSubtitles: boolean
  /** enable SponsorBlock */
  sponsorBlock: boolean
  sponsorBlockMode: SponsorBlockMode
  sponsorBlockCategories: SponsorBlockCategory[]
  /** embed chapter markers */
  embedChapters: boolean
  /** split the download into one file per chapter (--split-chapters) */
  splitChapters: boolean
  /** embed metadata (title/artist/etc.) */
  embedMetadata: boolean
  /** override the title tag before embedding; blank = keep the extracted value */
  metadataTitle?: string
  /** override the artist tag before embedding (audio downloads) */
  metadataArtist?: string
  /** override the album tag before embedding (audio downloads) */
  metadataAlbum?: string
  /** embed the thumbnail (cover art for audio, poster for mp4/mkv video) */
  embedThumbnail: boolean
  /** crop embedded audio artwork to a centred square (Seal's "crop artwork") */
  cropThumbnail: boolean
  /** write a .info.json metadata sidecar (Jellyfin/Plex/Kodi ingest — Phase K) */
  writeInfoJson: boolean
  /** write the thumbnail as a separate image file alongside the video */
  writeThumbnailFile: boolean
  /** write the video description to a .description sidecar file */
  writeDescription: boolean
}

export const DEFAULT_DOWNLOAD_OPTIONS: DownloadOptions = {
  audioFormat: 'mp3',
  videoContainer: 'mp4',
  preferredVideoCodec: 'any',
  formatSort: '',
  embedSubtitles: false,
  subtitleLanguages: 'en',
  autoSubtitles: false,
  sponsorBlock: false,
  sponsorBlockMode: 'remove',
  sponsorBlockCategories: ['sponsor', 'selfpromo', 'interaction'],
  embedChapters: false,
  splitChapters: false,
  embedMetadata: true,
  embedThumbnail: true,
  cropThumbnail: false,
  writeInfoJson: false,
  writeThumbnailFile: false,
  writeDescription: false
}

export interface YtdlpVersionResult {
  ok: boolean
  /** yt-dlp version string, present when ok is true */
  version?: string
  /** human-readable error, present when ok is false */
  error?: string
}

/**
 * Versions of the managed ffmpeg + ffprobe, for display in Settings. Each is the
 * parsed version token, or null when that binary is missing or unreadable. These
 * are fetched on first run (not bundled) and aren't auto-updated (ffmpeg has no self-update and,
 * unlike yt-dlp, doesn't break as sites change), so there's no ok/error here —
 * just "what's installed".
 */
export interface FfmpegVersionResult {
  ffmpeg: string | null
  ffprobe: string | null
}

/** Readiness of the managed ffmpeg/ffprobe: `ready` gates the first-run setup screen. */
export interface FfmpegSetupStatus {
  /** true once both ffmpeg.exe and ffprobe.exe are present in the managed dir */
  ready: boolean
  /** installed versions (null when the binary is absent), for the About card */
  ffmpeg: string | null
  ffprobe: string | null
}

/** Live progress of the first-run ffmpeg acquisition (main → renderer). */
export interface FfmpegSetupProgress {
  /** 'downloading' the archive, then a brief indeterminate 'extracting' phase */
  phase: 'downloading' | 'extracting'
  /** bytes received so far (downloading phase) */
  received?: number
  /** total archive bytes, when Content-Length is known */
  total?: number
  /** 0..1 download fraction, when total is known */
  fraction?: number
}

/** Result of a download / locate setup action. */
export interface FfmpegSetupResult {
  ok: boolean
  error?: string
}

/** Result of checking the configured Gitea repo for a newer AeroFetch release. */
export interface AppUpdateInfo {
  ok: boolean
  /** true when latestVersion is newer than the running app */
  available: boolean
  /** the running app's version, e.g. '0.4.0' */
  currentVersion: string
  /** the latest release's version (tag without a leading 'v'), when ok */
  latestVersion?: string
  /** the release notes / changelog body (Markdown), shown before updating */
  notes?: string
  /** the release's web page on Gitea (for "View release") */
  htmlUrl?: string
  /** direct download URL of the Windows installer asset, when the release has one */
  downloadUrl?: string
  /** the installer asset's file name */
  assetName?: string
  /** human-readable error, when ok is false */
  error?: string
}

/** Result of downloading the update installer to a temp file. */
export interface AppUpdateDownload {
  ok: boolean
  /** absolute path to the downloaded installer, when ok */
  filePath?: string
  error?: string
}

/** Live progress of the installer download (main → renderer). */
export interface AppUpdateProgress {
  /** bytes downloaded so far */
  received: number
  /** total bytes, when the server reports Content-Length */
  total?: number
  /** 0..1 fraction, when total is known */
  fraction?: number
}

/**
 * yt-dlp's self-update release channels (`--update-to <channel>`).
 *
 * SECURITY (audit F1): `--update-to` also accepts an `OWNER/REPO@TAG` spec, which
 * makes yt-dlp download a release binary from an ARBITRARY GitHub repo and
 * overwrite the running yt-dlp.exe — i.e. arbitrary, persistent code execution.
 * The TypeScript type is erased at runtime and is no defence over IPC, so the
 * value must be checked against this allowlist before it ever reaches the flag
 * (see isYtdlpUpdateChannel; enforced in src/main/ytdlp.ts).
 */
export const YTDLP_UPDATE_CHANNELS = ['stable', 'nightly'] as const
export type YtdlpUpdateChannel = (typeof YTDLP_UPDATE_CHANNELS)[number]

/** Runtime guard for an untrusted update-channel value crossing the IPC boundary. */
export function isYtdlpUpdateChannel(v: unknown): v is YtdlpUpdateChannel {
  return typeof v === 'string' && (YTDLP_UPDATE_CHANNELS as readonly string[]).includes(v)
}

/**
 * Runtime guard for a 24h 'HH:MM' sync time crossing the IPC boundary (L51) —
 * the value reaches `schtasks /ST`, so anything malformed must never be forwarded.
 * Shared so main's settings validation and schedule.ts use one definition.
 */
export function isValidSyncTime(v: unknown): v is string {
  return typeof v === 'string' && /^([01]\d|2[0-3]):[0-5]\d$/.test(v)
}

export interface YtdlpUpdateResult {
  ok: boolean
  /** yt-dlp's own update output, e.g. "Updated to ... " or "yt-dlp is up to date" */
  output?: string
  error?: string
}

/**
 * Status of a background (startup) yt-dlp auto-update, pushed main → renderer on
 * IpcChannels.ytdlpAutoUpdateStatus so the Settings UI can reflect a check that
 * ran on its own — no button click needed.
 */
export interface YtdlpAutoUpdateStatus {
  /** 'checking' when the update starts; the rest are terminal */
  phase: 'checking' | 'updated' | 'current' | 'error'
  /** which channel was checked */
  channel: YtdlpUpdateChannel
  /** yt-dlp version after the run, when known (phases 'updated' | 'current') */
  version?: string
  /** epoch ms the check finished (terminal phases) — feeds the "last checked" line */
  checkedAt?: number
  /** error text when phase === 'error' */
  error?: string
}

/** A single selectable output format, derived from `yt-dlp -J`. */
export interface FormatOption {
  /** yt-dlp format_id, or BEST_FORMAT_ID for the auto-best option */
  id: string
  /** human label, e.g. '1080p60 · mp4 · 124 MB' */
  label: string
  height?: number
  ext?: string
  filesizeLabel?: string
  /** true when this format already carries an audio track */
  hasAudio: boolean
}

/** Metadata + available formats returned by a probe. */
export interface MediaInfo {
  title: string
  channel?: string
  durationLabel?: string
  thumbnail?: string
  /** video format options, best-first (audio is transcoded, so not listed) */
  formats: FormatOption[]
}

/** One entry of a probed playlist (flat — no per-entry format list). */
export interface PlaylistEntry {
  /** 1-based position in the playlist */
  index: number
  id: string
  title: string
  /** canonical URL for the entry, enqueued as its own single-video download */
  url: string
  durationLabel?: string
  uploader?: string
}

/** A probed playlist: lightweight metadata for each entry. */
export interface PlaylistInfo {
  title: string
  uploader?: string
  count: number
  entries: PlaylistEntry[]
}

/**
 * A single probe can resolve to either one video (with formats) or a playlist
 * (with entries). `kind` discriminates which field is populated.
 */
export interface ProbeResult {
  ok: boolean
  kind?: 'video' | 'playlist'
  info?: MediaInfo
  playlist?: PlaylistInfo
  error?: string
}

/**
 * Media-manager folder context for a collection download. When present on a
 * StartDownloadOptions, the file is filed into
 * `<outputDir>/<channel>/<playlist>/<NNN> - <title>.<ext>` instead of the flat
 * filenameTemplate. The directory segments are sanitized for Windows at argv-build
 * time, and `index` (the 1-based playlist position from the persisted MediaItem)
 * drives the NNN prefix — not yt-dlp's %(playlist_index)s, which is empty under
 * the per-video `--no-playlist` path these downloads still take.
 */
export interface CollectionContext {
  /** channel / uploader name — the top folder level */
  channel: string
  /** playlist title — the second folder level ('Uploads' for the catch-all) */
  playlist: string
  /** 1-based position within the playlist, for the NNN filename prefix */
  index: number
}

/** Sent renderer → main to kick off a download. The renderer owns the id. */
export interface StartDownloadOptions {
  id: string
  url: string
  kind: MediaKind
  /** one of the UI quality labels (e.g. '1080p', 'Best') */
  quality: string
  /** absolute output directory; main falls back to the OS Downloads folder */
  outputDir?: string
  /** exact yt-dlp format_id chosen from a probe; omit for the preset path */
  formatId?: string
  /** whether the chosen format already includes audio (skips +bestaudio) */
  formatHasAudio?: boolean
  /**
   * Incognito ("private") download. Main enforces the promise the UI makes ("no
   * logging, no history, no cookies"): it writes no errorlog entry, reveals no
   * title/URL in an OS notification, and attaches no saved login / browser
   * cookies — not just the renderer's history skip (L136 / M6).
   */
  incognito?: boolean
  /** per-download post-processing override; main falls back to settings defaults */
  options?: DownloadOptions
  /**
   * Per-download custom-command override: raw extra yt-dlp flags appended to
   * the generated argv (see CommandTemplate). Omit to fall back to the
   * persisted settings default (enableCustomCommands + defaultTemplateId);
   * pass an empty string to explicitly run with no extra args for just this
   * download even when the settings default is enabled.
   */
  extraArgs?: string
  /**
   * Optional trim: keep only these time ranges. Raw user text — ranges like
   * '1:30-2:00', comma- or newline-separated — normalised to yt-dlp
   * `--download-sections` specs in buildArgs (parseTrimSections). Empty or
   * undefined downloads the whole media.
   */
  trim?: string
  /**
   * Metadata the renderer already fetched when probing the URL (title/channel/
   * duration). When present, main uses it directly instead of spawning a second
   * yt-dlp `--skip-download` probe — avoiding doubled network traffic and the
   * race where the late probe overwrites a good title the renderer supplied.
   */
  meta?: DownloadMeta
  /**
   * When set, this is a media-manager (collection) download — file it into
   * `<channel>/<playlist>/<NNN> - <title>` folders instead of the flat
   * filenameTemplate. See CollectionContext.
   */
  collection?: CollectionContext
  /**
   * Per-download collection folder-layout override (a Media Profile's
   * `outputTemplate`). When set, it overrides `Settings.collectionOutputTemplate`
   * for this collection download; omit to fall back to the global setting.
   */
  collectionOutputTemplate?: string
}

export interface StartDownloadResult {
  ok: boolean
  error?: string
}

export interface DownloadMeta {
  title?: string
  channel?: string
  durationLabel?: string
}

export interface DownloadProgress {
  /** yt-dlp progress status: 'downloading' | 'finished' | … */
  status: string
  /** 0..1 (0 when total size is unknown) */
  progress: number
  /** raw transfer rate in bytes/sec; the renderer formats + aggregates it (H4) */
  speedBytesPerSec?: number
  /** raw time remaining in seconds; the renderer formats + aggregates it (H4) */
  etaSeconds?: number
  /** human-readable total size, e.g. '512 MB' */
  sizeLabel?: string
  /** true when yt-dlp reports no total/estimated size (livestreams, some sites),
   *  so `progress` stays 0 the whole time — the bar should go indeterminate
   *  instead of reading a frozen 0% (L137) */
  sizeUnknown?: boolean
  /** latched true once the first download stream has finished, so the remaining
   *  stream/merge/post-processing shows as an indeterminate "Finishing…" state
   *  rather than a second 0→100% fill of the bar (SR7) */
  finishing?: boolean
}

/** Discriminated union pushed on IpcChannels.downloadEvent. */
export type DownloadEvent =
  | { type: 'meta'; id: string; meta: DownloadMeta }
  | { type: 'progress'; id: string; progress: DownloadProgress }
  | { type: 'done'; id: string; filePath?: string }
  | { type: 'error'; id: string; error: string }

/** The subset of a persisted queue item's status worth surviving a quit. */
export type PersistedQueueStatus = 'queued' | 'downloading' | 'paused' | 'saved' | 'error'

/**
 * A queue item persisted across restarts (M4) so saved/scheduled and other pending
 * downloads return after a quit. This is the *durable* subset of the renderer's
 * `DownloadItem`: runtime-only fields (progress/speed/eta/sizeLabel/finishing) are
 * dropped since a restored item re-derives them. Transient results ('completed' /
 * 'canceled') are never persisted; on load a 'downloading' item is normalized back
 * to 'queued' because its process died with the previous session.
 */
export interface PersistedQueueItem {
  id: string
  url: string
  title: string
  channel?: string
  durationLabel?: string
  thumbnail?: string
  kind: MediaKind
  quality: string
  status: PersistedQueueStatus
  filePath?: string
  error?: string
  formatId?: string
  formatHasAudio?: boolean
  options?: DownloadOptions
  extraArgs?: string
  trim?: string
  scheduledFor?: number
  incognito?: boolean
  probedMeta?: DownloadMeta
  collection?: CollectionContext
  mediaItemId?: string
}

/** Persisted user settings (electron-store). */
export interface Settings {
  /** where video downloads are saved; empty string = the default Documents\Video folder */
  videoFolder: string
  /** where audio downloads are saved; empty string = the default Documents\Audio folder */
  audioFolder: string
  defaultKind: MediaKind
  defaultVideoQuality: string
  defaultAudioQuality: string
  /** how many downloads run at once */
  maxConcurrent: number
  /** yt-dlp output template, e.g. '%(title)s.%(ext)s' */
  filenameTemplate: string
  /**
   * Raw yt-dlp `-o` template for COLLECTION downloads (a channel/playlist from
   * the Library), overriding the default `Channel/Playlist/NNN - Title` folder
   * layout. Empty = the built-in default. A relative template (no absolute path,
   * no `..`); yt-dlp expands its own fields (`%(uploader)s`, `%(playlist)s`,
   * `%(playlist_index)s`, `%(title)s`, `%(ext)s`, …). PINCHFLAT output-path setting.
   */
  collectionOutputTemplate: string
  /** UI color theme: explicit light/dark, or 'system' to follow the OS */
  theme: ThemeMode
  /** brand accent preset (buttons, links, selected nav item) */
  accentColor: AccentColor
  /** auto-detect video links from the clipboard on window focus */
  watchClipboard: boolean
  /** default post-processing / format options for new downloads */
  downloadOptions: DownloadOptions
  /** yt-dlp --proxy value, e.g. 'socks5://127.0.0.1:1080'. Empty disables it. */
  proxy: string
  /** yt-dlp --limit-rate value, e.g. '2M' or '500K'. Empty means unlimited. */
  rateLimit: string
  /**
   * Request throttle (seconds), to avoid YouTube's "rate-limited for up to an
   * hour" / HTTP 403 wall when pulling a whole channel. All default to 0 = off
   * (yt-dlp's as-fast-as-possible behaviour):
   *  - sleepRequests: `--sleep-requests`, a pause between the metadata/extraction
   *    HTTP requests — the single most effective knob for large channel pulls.
   *  - sleepInterval / maxSleepInterval: `--sleep-interval` (min) and
   *    `--max-sleep-interval` (max) bracket a random pause before each download.
   *    max only applies when it's set above a non-zero min (yt-dlp requires the
   *    min to be present, and a max below it is meaningless).
   */
  sleepRequests: number
  sleepInterval: number
  maxSleepInterval: number
  /** use bundled aria2c (resources/bin/aria2c.exe) as the external downloader */
  useAria2c: boolean
  /** aria2c parallel connections per download (-x/-s), 1–16; only used when useAria2c is on (L64) */
  aria2cConnections: number
  /** where auth cookies come from: none, a browser's store, or the sign-in window */
  cookieSource: CookieSource
  /** which browser to read cookies from when cookieSource is 'browser' */
  cookiesBrowser: CookieBrowser
  /**
   * YouTube reliability (Phase P). When set, emitted as
   * `--extractor-args "youtube:player_client=…;po_token=…"`:
   *  - playerClient: an alternate extraction client (e.g. 'web_safari', 'tv',
   *    'mweb') — a common workaround for YouTube throttling/extraction breakage.
   *  - poToken: a manually-supplied Proof-of-Origin token for the bot check.
   * Both empty = yt-dlp's defaults. (Automatic WebView token minting is deferred;
   * this is the argv plumbing it would feed.)
   */
  youtubePlayerClient: string
  youtubePoToken: string
  /** sanitize output filenames to a portable ASCII-only subset (--restrict-filenames) */
  restrictFilenames: boolean
  /** record completed downloads in a yt-dlp --download-archive file to skip repeats */
  useDownloadArchive: boolean
  /** keep the managed yt-dlp.exe auto-updated in the background on launch */
  autoUpdateYtdlp: boolean
  /** channel the auto-updater (and the manual "Update yt-dlp" button) updates to */
  ytdlpChannel: YtdlpUpdateChannel
  /** epoch ms of the last automatic yt-dlp update check; 0 = never run */
  ytdlpLastUpdateCheck: number
  /** apply a named custom-command template's extra args to every new download */
  enableCustomCommands: boolean
  /** id of the CommandTemplate applied when enableCustomCommands is on; null = none picked yet */
  defaultTemplateId: string | null
  /** show a native OS notification when a download finishes or fails */
  notifyOnComplete: boolean
  /** auto-enqueue newly-found videos from watched sources during a sync (Phase J) */
  autoDownloadNew: boolean
  /** daily watched-source sync start time, 24h 'HH:MM' (Task Scheduler /ST) (L51) */
  syncTime: string
  /** false until the first-run welcome screen has been dismissed */
  hasCompletedOnboarding: boolean
  /** keep AeroFetch running in the system tray when its window is closed (Phase O) */
  minimizeToTray: boolean
  /** launch AeroFetch automatically when the user signs in to Windows */
  launchAtStartup: boolean
  /** whether the navigation sidebar is collapsed to icon-only mode */
  isSidebarCollapsed: boolean
  /**
   * Opt in to GPU compositing (W15; takes effect on next launch). Off by
   * default: software rendering is the safe choice for this app's target
   * hardware — on some old/locked-down GPUs (the documented GeForce GT 625
   * case) Chromium's GPU compositor paints the whole window blank. Users on
   * modern GPUs can turn this on for smoother high-DPI/large-window rendering.
   */
  useHardwareAcceleration: boolean
}

/**
 * The canonical default Settings — the single source of truth shared by the main
 * electron-store defaults (main/settings.ts), the renderer's pre-load FALLBACK
 * (store/settings.ts), and the browser-preview mock (mockApi.ts), so the full
 * Settings object is no longer hand-maintained in three places (C1). Preview-only
 * tweaks (placeholder folders, skip onboarding, demo channel) are layered on top
 * of this at each call site.
 */
export const DEFAULT_SETTINGS: Settings = {
  // Both blank by default → downloads land in Documents\Video / Documents\Audio
  // (see getDefaultMediaDir). A non-empty value is an explicit per-kind override.
  videoFolder: '',
  audioFolder: '',
  defaultKind: 'video',
  defaultVideoQuality: 'Best available',
  defaultAudioQuality: 'Best',
  maxConcurrent: 2,
  filenameTemplate: '%(title)s.%(ext)s',
  collectionOutputTemplate: '',
  // Follow the OS theme on first launch (SR1) so a dark-mode Windows user isn't
  // greeted by a bright white window despite full system-theme support.
  theme: 'system',
  accentColor: 'teal',
  // Off on first launch (SR4): reading the clipboard on every window focus is a
  // privacy surprise for a brand-new user. It's a one-toggle opt-in in Settings
  // (the onboarding tip points there). Existing installs keep their saved value.
  watchClipboard: false,
  downloadOptions: DEFAULT_DOWNLOAD_OPTIONS,
  proxy: '',
  rateLimit: '',
  sleepRequests: 0,
  sleepInterval: 0,
  maxSleepInterval: 0,
  useAria2c: false,
  aria2cConnections: 16,
  cookieSource: 'none',
  cookiesBrowser: 'chrome',
  youtubePlayerClient: '',
  youtubePoToken: '',
  restrictFilenames: false,
  useDownloadArchive: false,
  // yt-dlp is self-managed: a writable copy under userData, auto-updated on the
  // configured channel so a stale binary can't silently cause YouTube 403s.
  autoUpdateYtdlp: true,
  // Default to stable so a nightly regression doesn't break downloads for
  // everyone out of the box (SR2). Users who want the latest YouTube fixes
  // can switch to nightly in Settings → Software.
  ytdlpChannel: 'stable',
  ytdlpLastUpdateCheck: 0,
  enableCustomCommands: false,
  defaultTemplateId: null,
  notifyOnComplete: true,
  // Default off so adding a watched channel doesn't silently fill the user's
  // disk on first use (SR3). Enable explicitly once they know what it does.
  autoDownloadNew: false,
  syncTime: '09:00',
  hasCompletedOnboarding: false,
  minimizeToTray: false,
  launchAtStartup: false,
  isSidebarCollapsed: false,
  useHardwareAcceleration: false
}

/**
 * A named, reusable set of extra yt-dlp CLI flags (Phase C "custom commands"),
 * e.g. name: 'Write thumbnail + no mtime', args: '--write-thumbnail --no-mtime'.
 * Shell-split (see parseExtraArgs) and appended to the generated argv just
 * before the `--` URL terminator, so they can override earlier flags.
 */
export interface CommandTemplate {
  id: string
  name: string
  args: string
  /**
   * Optional regex (matched case-insensitively against the download URL). When
   * set and custom commands are enabled, this template auto-applies to any URL it
   * matches — taking precedence over the global defaultTemplateId. Empty/undefined
   * means the template is only applied when explicitly chosen as the default.
   */
  urlPattern?: string
}

/** Result of building the exact yt-dlp command line for the current form state, without running it. */
export interface CommandPreviewResult {
  ok: boolean
  /** full command line (binary + quoted args), for display/copy only */
  command?: string
  error?: string
}

/** Whether the built-in sign-in window has ever saved a cookie file, and when. */
export interface CookiesStatus {
  exists: boolean
  /** epoch milliseconds the cookie file was last written */
  savedAt?: number
}

/** Result of a built-in sign-in window session, resolved once the window closes. */
export interface CookiesLoginResult {
  ok: boolean
  /** number of cookies captured into the exported file */
  cookieCount?: number
  error?: string
}

/** A completed download, persisted to history.json (newest-first). */
export interface HistoryEntry {
  id: string
  title: string
  channel?: string
  url: string
  kind: MediaKind
  /** Quality preset ('720p', 'Best available', 'Best', …) or the probed
   *  format's display label. Used for display and, when no formatId is present,
   *  as the fallback quality selector on re-download (H5). */
  quality: string
  /** Exact yt-dlp format id from the probe picker, carried so re-downloading
   *  always uses the same format rather than falling back to a preset (H5). */
  formatId?: string
  /** Whether the probed format already carries its own audio track (H5). */
  formatHasAudio?: boolean
  filePath?: string
  sizeLabel?: string
  thumbnail?: string
  /** The per-download post-processing options used originally, carried so a
   *  re-download reproduces the same output (subtitles, SponsorBlock, container,
   *  codec, …) instead of silently falling back to the current global defaults (L87). */
  options?: DownloadOptions
  /** epoch milliseconds */
  completedAt: number
}

/**
 * Cap on persisted history rows. Single-sourced here so the main store (history.ts)
 * and the renderer's optimistic in-memory list (store/history.ts, L141) share one
 * value instead of two hand-synced copies.
 */
export const HISTORY_MAX_ENTRIES = 500

/**
 * A failed download or download-start attempt, persisted to errorlog.json
 * (newest-first) so the report survives the queue item being cleared —
 * Seal's "debug report" equivalent.
 */
export interface ErrorLogEntry {
  id: string
  /** resolved video title when known; falls back to the URL in the UI */
  title?: string
  url: string
  kind: MediaKind
  error: string
  /**
   * Fuller non-noise stderr context behind the Diagnostics card's "Show
   * details" toggle — undefined when `error` already says everything (most
   * failures), present for the generic "Postprocessing: Conversion failed!"
   * wrapper where the useful detail is the tagged step lines that preceded it.
   */
  detail?: string
  /** epoch milliseconds */
  occurredAt: number
}

/** Result of exporting settings + custom-command templates to a JSON file. */
export interface BackupExportResult {
  ok: boolean
  /** absolute path written to, present when ok is true */
  path?: string
  error?: string
}

/** Result of restoring settings + custom-command templates from a JSON file. */
export interface BackupImportResult {
  ok: boolean
  error?: string
}

// --- Sources & media-manager indexing (Pinchflat-style) ---------------------
// A Source (channel/playlist) is indexed once into a
// persisted list of MediaItem records. When the user downloads pending items,
// all selected entries are enqueued at once — add a batch cap here if large
// channel queues (thousands of items) cause performance issues (M33).

/** Whether a Source is a whole channel or a single playlist. */
export type SourceKind = 'channel' | 'playlist'

/**
 * A monitored channel or playlist that AeroFetch has indexed. Its full video
 * list lives as MediaItem records (one JSON store), keyed back by `id`.
 */
export interface Source {
  id: string
  /** the URL the user added (used for re-indexing) */
  url: string
  kind: SourceKind
  title: string
  /** channel / uploader name, when known */
  channel?: string
  /** epoch ms the source was first added */
  addedAt: number
  /** epoch ms of the last successful (re)index; undefined if never indexed */
  lastIndexedAt?: number
  /** number of MediaItems at the last index (cached for list display) */
  itemCount: number
  /** watched for new uploads — included in scheduled / startup sync (Phase J) */
  watched?: boolean
  /** YouTube RSS feed URL for cheap "anything new?" checks; undefined if unknown */
  feedUrl?: string
  /** id of the MediaProfile applied to this source's downloads; undefined = global defaults */
  profileId?: string
}

/**
 * A named download preset a Library Source can point at (PINCHFLAT "Media
 * Profile"): a reusable bundle of the per-download choices that would otherwise
 * come from the global defaults — kind, quality, post-processing options, the
 * collection folder layout, and extra yt-dlp args. A Source with a `profileId`
 * resolves those fields from the profile first, then falls back to the global
 * settings for any field the profile leaves unset (see resolveProfile).
 */
export interface MediaProfile {
  id: string
  name: string
  /** override the download kind (video/audio); unset = global default */
  kind?: MediaKind
  /** override the quality label ('1080p', 'Best', …); unset = global default */
  quality?: string
  /** override post-processing options; unset = global default */
  options?: DownloadOptions
  /** override the collection folder layout (raw yt-dlp -o template); unset = global default */
  outputTemplate?: string
  /** extra yt-dlp args appended to the argv (like a CommandTemplate's args); unset = none */
  extraArgs?: string
}

/**
 * One video discovered while indexing a Source. `playlistTitle`/`playlistIndex`
 * drive the on-disk folder layout (<Channel>/<Playlist>/<NNN> - <Title>); the
 * `downloaded` flag + `filePath` let the library view show per-item state and
 * let re-syncs skip what's already on disk.
 */
export interface MediaItem {
  /** globally unique, formed as `${sourceId}:${videoId}` */
  id: string
  sourceId: string
  /** the yt-dlp video id — the dedup key within a source */
  videoId: string
  title: string
  url: string
  /** the playlist this item is filed under; 'Uploads' for videos in no playlist */
  playlistTitle: string
  /** 1-based position within its playlist, for NNN numbering */
  playlistIndex: number
  durationLabel?: string
  downloaded: boolean
  downloadedAt?: number
  filePath?: string
  /** the quality label this item was last downloaded at, for the retention
   *  quality-upgrade check (undefined for items downloaded before this was tracked) */
  downloadedQuality?: string
}

/** Live progress pushed while a Source is being indexed (main → renderer). */
export interface IndexProgress {
  /** the Source URL being indexed (correlates events back to the request) */
  url: string
  phase: 'start' | 'playlists' | 'playlist' | 'uploads' | 'done' | 'error'
  /** human-readable status line */
  message: string
  /** for the per-playlist phase: playlists processed so far / total */
  current?: number
  total?: number
}

/** Result of indexing (or re-indexing) a Source. */
export interface IndexSourceResult {
  ok: boolean
  source?: Source
  itemCount?: number
  /** how many videos were new since the previous index (all of them on first index) */
  newCount?: number
  error?: string
}

/** Result of syncing all watched sources: the videos found new across them. */
export interface SyncResult {
  ok: boolean
  /** newly-discovered media items across all watched sources (empty if nothing new) */
  newItems: MediaItem[]
  error?: string
}

/** Whether the Windows scheduled-sync task is currently registered. */
export interface ScheduledSyncStatus {
  enabled: boolean
  error?: string
}

// --- Built-in yt-dlp terminal (Phase N) -------------------------------------
// A power-user console that runs the bundled yt-dlp with raw, user-typed args
// and streams its output. The binary is fixed to yt-dlp (never arbitrary exes),
// and the feature is gated on the same enableCustomCommands consent flag as the
// per-download extra args (extra args can run code via --exec — audit F2).

/** Live output pushed on IpcChannels.terminalOutput while a terminal command runs. */
export type TerminalEvent =
  | { type: 'output'; id: string; line: string; stream: 'stdout' | 'stderr' }
  | { type: 'done'; id: string; code: number | null }
  | { type: 'error'; id: string; error: string }

/** Result of starting a terminal command (pre-spawn validation only). */
export interface TerminalRunResult {
  ok: boolean
  error?: string
}

/**
 * Extract the `URL=` value from a Windows Internet Shortcut (.url) file's content.
 * Returns the raw URL string (untrimmed), or null if not found.
 * Callers validate the result with their own http/https check.
 */
export function parseUrlShortcutContent(text: string): string | null {
  const m = /^\s*URL\s*=\s*(\S+)/im.exec(text)
  return m?.[1]?.trim() ?? null
}

/** Overall queue state for the Windows taskbar progress bar (Phase O). */
export interface TaskbarProgress {
  /** 0..1 overall fraction; ignored when mode is 'none' */
  fraction: number
  /** taskbar bar mode: hidden, normal, or red (some failed) */
  mode: 'none' | 'normal' | 'error'
  /** active download count for the overlay badge; absent or 0 = clear badge */
  badgeCount?: number
}
