import { useState, useEffect, useRef, type Dispatch, type SetStateAction } from 'react'
import {
  parseUrlShortcutContent,
  BEST_FORMAT_ID,
  type MediaInfo,
  type FormatOption,
  type PlaylistInfo,
  type DownloadOptions,
  DEFAULT_DOWNLOAD_OPTIONS,
  type CommandPreviewResult
} from '@shared/ipc'
import { useDownloads, type MediaKind } from '../../store/downloads'
import { type ChosenFormat } from '../../store/downloadTypes'
import { buildPlaylistAddEntries } from './playlistEntries'
import { useHistory } from '../../store/history'
import { QUALITY_OPTIONS } from '../../lib/qualityOptions'
import { sameVideo } from '../../lib/queueStats'
import { useSettings } from '../../store/settings'
import { useNav } from '../../store/nav'
import {
  useClipboardLink,
  looksLikeUrl,
  looksLikeChannelOrPlaylist,
  firstUrlInText,
  type SuggestionSource
} from '../../lib/useClipboardLink'
import { logError } from '../../reportError'

/** Pull the URL= target out of a dropped Windows .url Internet Shortcut file. */
function parseUrlFile(content: string): string | null {
  const url = parseUrlShortcutContent(content)
  return url && looksLikeUrl(url) ? url : null
}

// Format a Date as the `YYYY-MM-DDTHH:mm` value a datetime-local input expects,
// in LOCAL time -- used as the picker's `min` so a past time can't be chosen and
// then silently download immediately (L156/L57).
export function toLocalDatetimeValue(d: Date): string {
  const pad = (n: number): string => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
}

/** Everything the DownloadBar render needs — the hook's public surface. */
export interface DownloadBarController {
  // form state
  url: string
  kind: MediaKind
  quality: string
  // trim / schedule
  showTrim: boolean
  setShowTrim: Dispatch<SetStateAction<boolean>>
  trim: string
  showSchedule: boolean
  setShowSchedule: Dispatch<SetStateAction<boolean>>
  scheduleAt: string
  setScheduleAt: Dispatch<SetStateAction<string>>
  // drag / dup
  dragActive: boolean
  setDragActive: Dispatch<SetStateAction<boolean>>
  dup: string | null
  setDup: Dispatch<SetStateAction<string | null>>
  /** true when the duplicate was found in history (already downloaded) vs the live queue (L144). */
  dupInHistory: boolean
  // advanced / preview
  showAdvanced: boolean
  setShowAdvanced: Dispatch<SetStateAction<boolean>>
  downloadOptions: DownloadOptions
  incognito: boolean
  setIncognito: Dispatch<SetStateAction<boolean>>
  commandPreview: CommandPreviewResult | null
  previewLoading: boolean
  // probe (single video)
  info: MediaInfo | null
  thumbFailed: string | null
  setThumbFailed: Dispatch<SetStateAction<string | null>>
  probing: boolean
  probeError: string | null
  usingFormats: boolean
  selectedFormat: FormatOption | undefined
  // playlist
  playlist: PlaylistInfo | null
  selected: Set<number>
  allSelected: boolean
  // per-item format picker (video rows only)
  formatExpanded: Set<number>
  itemFormatOptions: Record<number, FormatOption[]>
  itemFormat: (index: number) => ChosenFormat | undefined
  formatProbing: Set<number>
  formatProbeError: Record<number, string>
  // suggestion banner
  suggestion: string | null
  suggestionSource: SuggestionSource
  dismissSuggestion: () => void
  // channel/playlist nudge → Library (UX3)
  channelHint: boolean
  openInLibrary: () => void
  dismissChannelHint: () => void
  // handlers
  onDragEnter: (e: React.DragEvent) => void
  onDragOver: (e: React.DragEvent) => void
  onDragLeave: () => void
  onDrop: (e: React.DragEvent) => void
  onUrlChange: (next: string) => void
  onKindChange: (next: MediaKind) => void
  onQualityChange: (v: string) => void
  onFormatIdChange: (v: string) => void
  onTrimChange: (v: string) => void
  onDownloadOptionsChange: (next: DownloadOptions) => void
  toggleCommandPreview: () => Promise<void>
  fetchFormats: () => Promise<void>
  paste: () => Promise<void>
  acceptSuggestion: () => void
  download: () => void
  downloadAnyway: () => void
  addPlaylist: () => void
  toggleEntry: (index: number, on: boolean) => void
  toggleAll: () => void
  effKind: (index: number) => MediaKind
  toggleItemKind: (index: number) => void
  setAllKinds: (k: MediaKind) => void
  toggleItemFormat: (index: number) => Promise<void>
  setItemFormat: (index: number, formatId: string) => void
}

/**
 * All state and behaviour for the DownloadBar. Extracted from the component so
 * the ~17 interdependent state hooks and their handlers live in one place and
 * the component is render-only.
 */
export function useDownloadBar(): DownloadBarController {
  const addFromUrl = useDownloads((s) => s.addFromUrl)
  const addMany = useDownloads((s) => s.addMany)
  const openLibraryWith = useNav((s) => s.openLibraryWith)
  const settingsLoaded = useSettings((s) => s.loaded)
  const defaultKind = useSettings((s) => s.defaultKind)
  const defaultVideoQuality = useSettings((s) => s.defaultVideoQuality)
  const defaultAudioQuality = useSettings((s) => s.defaultAudioQuality)

  const [url, setUrl] = useState('')
  const [kind, setKind] = useState<MediaKind>('video')
  const [quality, setQuality] = useState<string>(QUALITY_OPTIONS.video[0])

  // Optional trim: keep only certain time ranges. Raw text, normalised in main.
  const [showTrim, setShowTrim] = useState(false)
  const [trim, setTrim] = useState('')

  // Optional schedule: a datetime-local value; a future time parks the download.
  const [showSchedule, setShowSchedule] = useState(false)
  const [scheduleAt, setScheduleAt] = useState('')

  // Drag-and-drop: highlight while a link / .url file hovers the card.
  const [dragActive, setDragActive] = useState(false)

  // Channel/playlist nudge: a whole channel/playlist belongs in the Library, not
  // the one-off queue. Once dismissed for the current URL, stay quiet (UX3).
  const [channelHintDismissed, setChannelHintDismissed] = useState(false)

  // Duplicate guard: warn before enqueuing a URL already in the active queue.
  const [dup, setDup] = useState<string | null>(null)
  // Whether the duplicate matched a history entry (already downloaded) rather than
  // a live queue item (L144) — the banner wording differs.
  const [dupInHistory, setDupInHistory] = useState(false)
  const confirmDup = useRef(false)

  // Per-download options (M5/M6/UX1): advanced features.
  const [showAdvanced, setShowAdvanced] = useState(false)
  const [downloadOptions, setDownloadOptions] = useState<DownloadOptions>(DEFAULT_DOWNLOAD_OPTIONS)
  const [incognito, setIncognito] = useState(false)
  const [commandPreview, setCommandPreview] = useState<CommandPreviewResult | null>(null)
  const [previewLoading, setPreviewLoading] = useState(false)

  // Probe state for the single-video format picker.
  const [info, setInfo] = useState<MediaInfo | null>(null)
  const [thumbFailed, setThumbFailed] = useState<string | null>(null)
  const [probing, setProbing] = useState(false)
  const [probeError, setProbeError] = useState<string | null>(null)
  const [formatId, setFormatId] = useState<string>('')

  // Probe state for a playlist URL.
  const [playlist, setPlaylist] = useState<PlaylistInfo | null>(null)
  const [selected, setSelected] = useState<Set<number>>(new Set())
  // Per-entry kind override (entry.index → 'video' | 'audio'); absent = use the
  // bar's global kind. Lets a playlist mix video and audio downloads.
  const [itemKinds, setItemKinds] = useState<Record<number, MediaKind>>({})

  // Per-entry exact-format selection (video rows only). A playlist probe is flat —
  // it carries no per-entry formats — so each row's formats are fetched lazily on
  // first expand (never all up front: a playlist can hold thousands of entries and
  // per-entry probes are rate-limited). `formatExpanded` = rows showing the picker;
  // `itemFormatOptions` caches a row's probed formats; `itemFormats` = the chosen
  // exact format per row (absent = use the kind/quality preset path).
  const [formatExpanded, setFormatExpanded] = useState<Set<number>>(new Set())
  const [itemFormatOptions, setItemFormatOptions] = useState<Record<number, FormatOption[]>>({})
  const [itemFormats, setItemFormats] = useState<Record<number, ChosenFormat>>({})
  const [formatProbing, setFormatProbing] = useState<Set<number>>(new Set())
  const [formatProbeError, setFormatProbeError] = useState<Record<number, string>>({})

  // Drag highlight is gated on a depth counter, not raw enter/leave (L158): a
  // dragleave fires every time the cursor crosses onto a child element, so toggling
  // on leave made the dashed outline blink. Count enters vs leaves and only clear
  // the highlight when the drag has actually left the whole card (depth back to 0).
  const dragDepth = useRef(0)
  function onDragEnter(e: React.DragEvent): void {
    e.preventDefault()
    dragDepth.current += 1
    if (!dragActive) setDragActive(true)
  }
  function onDragOver(e: React.DragEvent): void {
    // Required so the element is a valid drop target; the highlight is owned by
    // onDragEnter/onDragLeave.
    e.preventDefault()
  }
  function onDragLeave(): void {
    dragDepth.current = Math.max(0, dragDepth.current - 1)
    if (dragDepth.current === 0) setDragActive(false)
  }
  function onDrop(e: React.DragEvent): void {
    e.preventDefault()
    dragDepth.current = 0
    setDragActive(false)
    const dt = e.dataTransfer
    const fromText = firstUrlInText(dt.getData('text/uri-list') || dt.getData('text/plain') || '')
    if (fromText) {
      onUrlChange(fromText)
      return
    }
    // A dropped Windows .url Internet Shortcut -- read its URL= line.
    const file = Array.from(dt.files).find((f) => f.name.toLowerCase().endsWith('.url'))
    if (file) {
      file
        .text()
        .then((content) => {
          const u = parseUrlFile(content)
          if (u) onUrlChange(u)
        })
        .catch(logError('dropped .url file read'))
    }
  }

  // When settings load or change, update kind/quality -- but only while the bar
  // is idle (URL empty) so an in-progress setup isn't unexpectedly reset.
  useEffect(() => {
    if (!settingsLoaded || url.trim()) return
    setKind(defaultKind)
    setQuality(defaultKind === 'audio' ? defaultAudioQuality : defaultVideoQuality)
  }, [settingsLoaded, defaultKind, defaultVideoQuality, defaultAudioQuality, url])

  // Clipboard auto-detect and links handed to AeroFetch from outside (the
  // aerofetch:// protocol or a "Send to" .url file) share one suggestion banner,
  // driven by the same hook the library's add-source field uses. An external link
  // always takes priority over a clipboard guess -- it's a direct request.
  const {
    suggestion,
    source: suggestionSource,
    accept: acceptLink,
    dismiss: dismissSuggestion,
    offer: offerLink
  } = useClipboardLink(url)
  useEffect(
    () => window.api.onExternalUrl((incoming) => offerLink(incoming, 'external')),
    [offerLink]
  )

  function acceptSuggestion(): void {
    const link = acceptLink()
    if (link) onUrlChange(link)
  }

  const usingFormats = kind === 'video' && info !== null && info.formats.length > 0
  const selectedFormat: FormatOption | undefined = usingFormats
    ? (info.formats.find((f) => f.id === formatId) ?? info.formats[0])
    : undefined

  // Show the "add this in the Library" nudge for a clear channel/playlist URL,
  // but only while the bar is still idle for it (nothing probed/resolved yet) and
  // the user hasn't waved it away (UX3).
  const channelHint =
    looksLikeChannelOrPlaylist(url) &&
    !channelHintDismissed &&
    info === null &&
    playlist === null &&
    probeError === null

  // Per-entry format state is keyed by playlist position, so it must be dropped
  // any time the playlist itself is dropped or re-fetched -- otherwise a stale
  // exact-format pick from a prior probe can silently reattach to a different
  // video that lands at the same index in the new result.
  function resetPlaylistItemState(): void {
    setSelected(new Set())
    setItemKinds({})
    setFormatExpanded(new Set())
    setItemFormatOptions({})
    setItemFormats({})
    setFormatProbing(new Set())
    setFormatProbeError({})
  }

  function clearProbe(): void {
    setInfo(null)
    setProbeError(null)
    setFormatId('')
    setPlaylist(null)
    resetPlaylistItemState()
  }

  // Reset the whole bar after a download / playlist is enqueued: clear the URL +
  // probe and drop the one-shot overrides (trim / schedule / advanced / incognito /
  // per-download options) so the next download starts from the global defaults.
  // Shared by download() and addPlaylist() so a playlist add resets the same state
  // a single download does (L132 — addPlaylist previously left trim/schedule open).
  function resetForm(): void {
    setUrl('')
    setTrim('')
    setShowTrim(false)
    setScheduleAt('')
    setShowSchedule(false)
    setShowAdvanced(false)
    setCommandPreview(null)
    setIncognito(false)
    setDownloadOptions(DEFAULT_DOWNLOAD_OPTIONS)
    clearProbe()
  }

  // Fetch (or toggle off) the exact yt-dlp command line for the current form
  // state. The options mirror what download() sends so the preview matches the
  // command that will actually run -- including a probe-selected format and any
  // trim spec (M5).
  async function toggleCommandPreview(): Promise<void> {
    if (commandPreview) {
      setCommandPreview(null)
      return
    }
    const trimmed = url.trim()
    if (!trimmed) return
    setPreviewLoading(true)
    try {
      const result = await window.api.previewCommand({
        id: 'preview',
        url: trimmed,
        kind: usingFormats ? 'video' : kind,
        quality: usingFormats && selectedFormat ? selectedFormat.label : quality,
        formatId: usingFormats ? selectedFormat?.id : undefined,
        formatHasAudio: usingFormats ? selectedFormat?.hasAudio : undefined,
        options: downloadOptions,
        trim: trim.trim() || undefined
      })
      setCommandPreview(result)
    } catch (e) {
      setCommandPreview({ ok: false, error: (e as Error).message })
    } finally {
      setPreviewLoading(false)
    }
  }

  function onUrlChange(next: string): void {
    setUrl(next)
    // Any probed info -- or a duplicate warning -- is stale once the URL changes.
    if (info || probeError || playlist) clearProbe()
    if (dup) setDup(null)
    setCommandPreview(null)
    confirmDup.current = false
    // A new URL gets a fresh chance to nudge toward the Library.
    setChannelHintDismissed(false)
  }

  function onKindChange(next: MediaKind): void {
    setKind(next)
    setQuality(QUALITY_OPTIONS[next][0])
    // The command changes with kind/quality, so any shown preview is now stale.
    setCommandPreview(null)
  }

  // Any change to quality / format / trim / options invalidates a shown preview.
  function onQualityChange(v: string): void {
    setQuality(v)
    setCommandPreview(null)
  }
  function onFormatIdChange(v: string): void {
    setFormatId(v)
    setCommandPreview(null)
  }
  function onTrimChange(v: string): void {
    setTrim(v)
    setCommandPreview(null)
  }
  function onDownloadOptionsChange(next: DownloadOptions): void {
    setDownloadOptions(next)
    setCommandPreview(null)
  }

  async function fetchFormats(): Promise<void> {
    const trimmed = url.trim()
    if (!trimmed || probing) return
    setProbing(true)
    setProbeError(null)
    setInfo(null)
    setPlaylist(null)
    resetPlaylistItemState()
    // Probing may switch the URL to the format-picker path, changing the command.
    setCommandPreview(null)
    try {
      const res = await window.api.probe(trimmed)
      if (res.ok && res.kind === 'playlist' && res.playlist) {
        setPlaylist(res.playlist)
        // Pre-select every entry; the user can trim the list.
        setSelected(new Set(res.playlist.entries.map((e) => e.index)))
      } else if (res.ok && res.info) {
        setInfo(res.info)
        setFormatId(res.info.formats[0]?.id ?? '')
      } else {
        setProbeError(res.error ?? 'Could not fetch video info.')
      }
    } catch (e) {
      setProbeError(String(e))
    } finally {
      setProbing(false)
    }
  }

  async function paste(): Promise<void> {
    try {
      const text = (await window.api?.readClipboard?.()) ?? ''
      if (text) onUrlChange(text.trim())
    } catch {
      /* ignore in preview */
    }
  }

  // Hand the current channel/playlist URL to the Library tab, which pre-fills its
  // add field with it, and clear the bar (the URL now lives over there) (UX3).
  function openInLibrary(): void {
    const trimmed = url.trim()
    if (!trimmed) return
    openLibraryWith(trimmed)
    setUrl('')
    // Clear the URL-specific command preview too, so no stale command lingers over
    // the now-empty bar (the same invariant onUrlChange keeps).
    setCommandPreview(null)
    clearProbe()
  }
  function dismissChannelHint(): void {
    setChannelHintDismissed(true)
  }

  // Selection helpers for the playlist panel.
  const allSelected = playlist !== null && selected.size === playlist.entries.length
  function toggleEntry(index: number, on: boolean): void {
    setSelected((prev) => {
      const next = new Set(prev)
      if (on) next.add(index)
      else next.delete(index)
      return next
    })
  }
  function toggleAll(): void {
    if (!playlist) return
    setSelected(allSelected ? new Set() : new Set(playlist.entries.map((e) => e.index)))
  }

  // Per-entry kind: the override if set, else the bar's global kind.
  function effKind(index: number): MediaKind {
    return itemKinds[index] ?? kind
  }
  function toggleItemKind(index: number): void {
    const next = effKind(index) === 'audio' ? 'video' : 'audio'
    setItemKinds((m) => ({ ...m, [index]: next }))
    // An exact format is a video-only choice; drop it (and the picker) when the
    // row switches to audio so the audio download uses the quality preset.
    if (next === 'audio') dropItemFormat(index)
  }
  function setAllKinds(k: MediaKind): void {
    if (!playlist) return
    const all: Record<number, MediaKind> = {}
    for (const e of playlist.entries) all[e.index] = k
    setItemKinds(all)
    // Same reason as toggleItemKind: an "All audio" flip clears every per-item format.
    if (k === 'audio') {
      setItemFormats({})
      setFormatExpanded(new Set())
    }
  }

  // Per-entry exact-format helpers.
  function itemFormat(index: number): ChosenFormat | undefined {
    return itemFormats[index]
  }
  function dropItemFormat(index: number): void {
    setItemFormats((m) => {
      const { [index]: _drop, ...rest } = m
      return rest
    })
    setFormatExpanded((s) => {
      const next = new Set(s)
      next.delete(index)
      return next
    })
  }

  // Expand/collapse a row's format picker. On first expand, lazily probe just that
  // entry's URL — the existing single-video probe returns its full format list — and
  // cache it so re-expanding is instant.
  async function toggleItemFormat(index: number): Promise<void> {
    if (formatExpanded.has(index)) {
      setFormatExpanded((s) => {
        const next = new Set(s)
        next.delete(index)
        return next
      })
      return
    }
    setFormatExpanded((s) => new Set(s).add(index))
    if (itemFormatOptions[index] || formatProbing.has(index)) return // already have (or fetching) formats
    const entry = playlist?.entries.find((e) => e.index === index)
    if (!entry) return
    setFormatProbing((s) => new Set(s).add(index))
    setFormatProbeError((m) => {
      const { [index]: _drop, ...rest } = m
      return rest
    })
    try {
      const res = await window.api.probe(entry.url)
      if (res.ok && res.kind === 'video' && res.info) {
        setItemFormatOptions((m) => ({ ...m, [index]: res.info!.formats }))
      } else {
        setFormatProbeError((m) => ({ ...m, [index]: res.error ?? 'Could not fetch formats.' }))
      }
    } catch (e) {
      setFormatProbeError((m) => ({ ...m, [index]: String(e) }))
    } finally {
      setFormatProbing((s) => {
        const next = new Set(s)
        next.delete(index)
        return next
      })
    }
  }

  // Pick an exact format for a row (or clear back to the preset for "Best available").
  function setItemFormat(index: number, formatId: string): void {
    const opt = itemFormatOptions[index]?.find((o) => o.id === formatId)
    if (!opt || opt.id === BEST_FORMAT_ID) {
      // "Best available" is the auto/preset path — carry no exact format.
      setItemFormats((m) => {
        const { [index]: _drop, ...rest } = m
        return rest
      })
      return
    }
    setItemFormats((m) => ({
      ...m,
      [index]: { id: opt.id, hasAudio: opt.hasAudio, label: opt.label }
    }))
    // Choosing a video format implies this row is a video download.
    if (effKind(index) === 'audio') setItemKinds((m) => ({ ...m, [index]: 'video' }))
  }

  function download(): void {
    const trimmed = url.trim()
    if (!trimmed) return

    // Duplicate guard: if this URL is already in the queue (and the user hasn't
    // confirmed via "Download anyway"), warn instead of silently enqueuing a copy.
    // Canceled/failed items don't count -- re-adding those is a legitimate retry.
    if (!confirmDup.current) {
      const existing = useDownloads
        .getState()
        .items.find(
          (i) => i.status !== 'canceled' && i.status !== 'error' && sameVideo(i.url, trimmed)
        )
      if (existing) {
        // Use the URL as fallback if the title is still the placeholder generated
        // by titleFromUrl() before metadata loads (L76).
        const isPlaceholder =
          /video \(\w+\)$/.test(existing.title) ||
          existing.title.endsWith(' download') ||
          existing.title === 'New download'
        setDup(isPlaceholder ? existing.url : existing.title)
        setDupInHistory(false)
        return
      }
      // Not in the queue, but maybe already downloaded earlier (L144): warn so a
      // re-download is a deliberate choice, not an accidental duplicate.
      const downloaded = useHistory.getState().entries.find((h) => sameVideo(h.url, trimmed))
      if (downloaded) {
        setDup(downloaded.title || downloaded.url)
        setDupInHistory(true)
        return
      }
    }
    confirmDup.current = false
    setDup(null)

    const meta = info
      ? {
          title: info.title,
          channel: info.channel,
          durationLabel: info.durationLabel,
          thumbnail: info.thumbnail
        }
      : {}

    const trimSpec = trim.trim() || undefined
    const scheduledFor = scheduleAt ? new Date(scheduleAt).getTime() : undefined

    if (usingFormats && selectedFormat) {
      addFromUrl(trimmed, 'video', selectedFormat.label, {
        ...meta,
        trim: trimSpec,
        scheduledFor,
        options: downloadOptions,
        incognito,
        format: {
          id: selectedFormat.id,
          hasAudio: selectedFormat.hasAudio,
          label: selectedFormat.label
        }
      })
    } else {
      addFromUrl(trimmed, kind, quality, {
        ...meta,
        trim: trimSpec,
        scheduledFor,
        options: downloadOptions,
        incognito
      })
    }

    resetForm()
  }

  function downloadAnyway(): void {
    confirmDup.current = true
    download()
  }

  function addPlaylist(): void {
    if (!playlist) return
    const chosen = playlist.entries.filter((e) => selected.has(e.index))
    // Each entry uses its own kind override and, for a video row the user gave an
    // exact probed format, that format; otherwise quality falls back to the bar's
    // current preset (when the kind matches) or that kind's default (see the pure
    // buildPlaylistAddEntries for the mapping).
    addMany(
      buildPlaylistAddEntries(chosen, { barKind: kind, barQuality: quality, effKind, itemFormats })
    )
    resetForm()
  }

  return {
    // form state
    url,
    kind,
    quality,
    // trim / schedule
    showTrim,
    setShowTrim,
    trim,
    showSchedule,
    setShowSchedule,
    scheduleAt,
    setScheduleAt,
    // drag / dup
    dupInHistory,
    dragActive,
    setDragActive,
    dup,
    setDup,
    // advanced / preview
    showAdvanced,
    setShowAdvanced,
    downloadOptions,
    incognito,
    setIncognito,
    commandPreview,
    previewLoading,
    // probe (single video)
    info,
    thumbFailed,
    setThumbFailed,
    probing,
    probeError,
    usingFormats,
    selectedFormat,
    // playlist
    playlist,
    selected,
    allSelected,
    // per-item format picker
    formatExpanded,
    itemFormatOptions,
    itemFormat,
    formatProbing,
    formatProbeError,
    // suggestion banner
    suggestion,
    suggestionSource,
    dismissSuggestion,
    // channel/playlist nudge → Library (UX3)
    channelHint,
    openInLibrary,
    dismissChannelHint,
    // handlers
    onDragEnter,
    onDragOver,
    onDragLeave,
    onDrop,
    onUrlChange,
    onKindChange,
    onQualityChange,
    onFormatIdChange,
    onTrimChange,
    onDownloadOptionsChange,
    toggleCommandPreview,
    fetchFormats,
    paste,
    acceptSuggestion,
    download,
    downloadAnyway,
    addPlaylist,
    toggleEntry,
    toggleAll,
    effKind,
    toggleItemKind,
    setAllKinds,
    toggleItemFormat,
    setItemFormat
  }
}
