import './style.css';
import {
  handleRedirectCallback,
  isLoggedIn,
  logout,
  startLogin,
} from './auth.js';
import { downloadTextFile } from './download.js';
import {
  buildExportFilename,
  buildLibraryCsv,
  buildLibraryHtml,
  buildLibraryJson,
  buildPlaylistCsv,
  buildPlaylistHtml,
  buildPlaylistJson,
  computePlaylistStats,
  sortItems,
} from './exportFormats.js';
import { buildLibraryMarkdown, buildPlaylistMarkdown } from './markdown.js';
import { extractPlaylistId } from './playlistUrl.js';
import {
  AuthExpiredError,
  getAllPlaylistItems,
  getEntireLibrary,
  getLikedSongs,
  getMyPlaylists,
  getPlaylistMeta,
  InsufficientScopeError,
  isExportablePlaylistSummary,
  PlaylistAccessError,
  RateLimitedError,
} from './spotifyApi.js';
import type {
  LibraryFetchProgress,
  LibrarySource,
  MyPlaylistSummary,
  PlaylistItemRow,
  PlaylistMeta,
} from './spotifyApi.js';

interface AppElements {
  app: HTMLElement;
  connectButton: HTMLButtonElement;
  connectionStatus: HTMLElement;
  sourcePicker: HTMLElement;
  sourcePickerStatus: HTMLElement;
  sourcePickerError: HTMLElement;
  sourceList: HTMLElement;
  exportForm: HTMLFormElement;
  exportFormat: HTMLSelectElement;
  playlistInput: HTMLInputElement;
  exportLibraryButton: HTMLButtonElement;
  exportButton: HTMLButtonElement;
  copyButton: HTMLButtonElement;
  recentPlaylistsContainer: HTMLElement;
  recentPlaylistsList: HTMLElement;
  loadPlaylistsButton: HTMLButtonElement;
  playlistError: HTMLElement;
  exportProgress: HTMLElement;
  status: HTMLElement;
}

type ExportSource =
  | {
      kind: 'liked-songs';
      label: string;
      origin: 'picker';
    }
  | {
      kind: 'playlist';
      id: string;
      label: string;
      origin: 'picker' | 'paste';
    };

type SourceFetchState = 'idle' | 'loading' | 'loaded' | 'failed';

interface ExportPayload {
  metadata: PlaylistMeta;
  items: PlaylistItemRow[];
}

type ExportFormat = 'markdown' | 'csv' | 'json' | 'html';

interface BuiltExportFile {
  filename: string;
  content: string;
  type: string;
}

const INVALID_PLAYLIST_MESSAGE = 'Enter a valid Spotify playlist URL, URI, or ID.';
const SESSION_EXPIRED_MESSAGE = 'Session expired, please reconnect';
const RATE_LIMITED_MESSAGE = 'Spotify is rate-limiting requests, try again in a moment.';
const GENERIC_EXPORT_ERROR_MESSAGE = 'Could not export playlist. Check the console for details.';
const SOURCE_LOADING_MESSAGE = 'Loading your Spotify sources...';
const SOURCE_FETCH_FALLBACK_MESSAGE = 'Could not load your Spotify sources. Paste a playlist URL below.';
const FOLLOWED_PLAYLIST_DISABLED_NOTE = "not accessible — you don't own or collaborate on this one";
const LIKED_SONGS_LABEL = 'Liked Songs';
const LIKED_SONGS_METADATA_OWNER = 'Spotify Library';
const LIKED_SONGS_METADATA_DESCRIPTION = 'Saved tracks from the connected Spotify account.';
const DEFAULT_EXPORT_SORT = 'addedAt';

let selectedSource: ExportSource | null = null;
let sourceFetchState: SourceFetchState = 'idle';
let sourceFetchToken = 0;
let exportInProgress = false;

export function setupApp(): void {
  const elements = getAppElements();

  if (!elements) {
    return;
  }

  initializeExportState(elements);

  elements.connectButton.addEventListener('click', () => {
    clearExportError(elements);
    clearSourcePickerError(elements);
    startLogin();
  });

  elements.playlistInput.addEventListener('input', () => {
    handlePlaylistInputChange(elements);
  });

  elements.exportForm.addEventListener('submit', (event) => {
    event.preventDefault();
    void exportSelectedSource(elements);
  });

  elements.exportLibraryButton.addEventListener('click', () => {
    void exportEntireLibrary(elements);
  });

  elements.copyButton.addEventListener('click', () => {
    void copySelectedSourceToClipboard(elements);
  });

  elements.loadPlaylistsButton.addEventListener('click', () => {
    void loadSourcePickerOnce(elements);
  });

  void handleRedirectCallback()
    .then(() => {
      const connected = isLoggedIn();
      updateConnectionState(elements, connected);

      if (connected) {
        const recents = getRecentPlaylists();
        renderRecentPlaylists(elements, recents);
        if (recents.length > 0) {
          elements.sourcePicker.dataset.sourceState = 'idle';
        } else {
          void loadSourcePickerOnce(elements);
        }
      }
    })
    .catch((error: unknown) => {
      console.error('Spotify auth callback failed', error);
      updateConnectionState(elements, false);
    });
}

async function loadSourcePickerOnce(elements: AppElements): Promise<void> {
  if (sourceFetchState !== 'idle') {
    return;
  }

  sourceFetchState = 'loading';
  const currentFetchToken = sourceFetchToken + 1;
  sourceFetchToken = currentFetchToken;
  elements.sourcePicker.dataset.sourceState = 'loading';
  elements.sourceList.setAttribute('aria-busy', 'true');
  showSourcePickerStatus(elements, SOURCE_LOADING_MESSAGE);
  clearSourcePickerError(elements);
  renderSourceOptions(elements, []);

  try {
    const playlists = await getMyPlaylists();

    if (currentFetchToken !== sourceFetchToken) {
      return;
    }

    sourceFetchState = 'loaded';
    elements.sourcePicker.dataset.sourceState = 'loaded';
    elements.sourceList.removeAttribute('aria-busy');
    hideSourcePickerStatus(elements);
    clearSourcePickerError(elements);
    renderSourceOptions(elements, playlists);
  } catch (error) {
    if (currentFetchToken !== sourceFetchToken) {
      return;
    }

    sourceFetchState = 'failed';
    elements.sourcePicker.dataset.sourceState = 'failed';
    elements.sourceList.removeAttribute('aria-busy');
    hideSourcePickerStatus(elements);
    renderSourceOptions(elements, []);
    showSourcePickerError(elements, sourcePickerErrorMessage(error));

    if (error instanceof AuthExpiredError) {
      logout();
      updateConnectionState(elements, false);
    }
  }
}

function renderSourceOptions(elements: AppElements, playlists: MyPlaylistSummary[]): void {
  const listItems: HTMLElement[] = [
    createSourceListItem(elements, {
      source: {
        kind: 'liked-songs',
        label: LIKED_SONGS_LABEL,
        origin: 'picker',
      },
      label: LIKED_SONGS_LABEL,
      disabled: false,
    }),
  ];

  playlists.forEach((playlist) => {
    const accessible = isExportablePlaylistSummary(playlist);
    const label = `${playlist.name} (${formatTrackCount(playlist.trackCount)})`;
    const visibleLabel = accessible
      ? label
      : `${label} — ${FOLLOWED_PLAYLIST_DISABLED_NOTE}`;

    listItems.push(createSourceListItem(elements, {
      source: {
        kind: 'playlist',
        id: playlist.id,
        label: playlist.name,
        origin: 'picker',
      },
      label: visibleLabel,
      disabled: !accessible,
    }));
  });

  elements.sourceList.replaceChildren(...listItems);
  syncSelectedSourceControls(elements);
}

function createSourceListItem(
  elements: AppElements,
  options: {
    source: ExportSource;
    label: string;
    disabled: boolean;
  },
): HTMLElement {
  const listItem = document.createElement('li');
  const button = document.createElement('button');

  listItem.className = 'source-list-item';
  button.type = 'button';
  button.className = options.disabled
    ? 'source-option source-option--disabled'
    : 'source-option';
  button.textContent = options.label;
  button.disabled = options.disabled || exportInProgress;
  button.dataset.sourceKey = sourceKey(options.source);
  button.dataset.sourceDisabled = options.disabled ? 'true' : 'false';
  button.setAttribute('aria-pressed', 'false');

  if (options.disabled) {
    button.setAttribute('aria-disabled', 'true');
  } else {
    button.addEventListener('click', () => {
      selectExportSource(elements, options.source, { clearPastedInput: true });
    });
  }

  listItem.appendChild(button);
  return listItem;
}

function handlePlaylistInputChange(elements: AppElements): void {
  if (exportInProgress) {
    return;
  }

  const rawValue = elements.playlistInput.value;

  if (!rawValue.trim()) {
    if (selectedSource?.origin === 'paste') {
      selectedSource = null;
      syncSelectedSourceControls(elements);
      setStatus(elements, isLoggedIn() ? 'Connected to Spotify.' : 'Not connected to Spotify.');
    }

    clearExportError(elements);
    return;
  }

  const playlistId = extractPlaylistId(rawValue);

  if (playlistId) {
    selectExportSource(elements, {
      kind: 'playlist',
      id: playlistId,
      label: 'pasted playlist',
      origin: 'paste',
    }, { clearPastedInput: false });
    return;
  }

  selectedSource = null;
  syncSelectedSourceControls(elements);
  clearExportError(elements);
}

async function exportSelectedSource(elements: AppElements): Promise<void> {
  clearExportError(elements);

  const source = resolveSelectedSourceFromForm(elements);

  if (!source) {
    showExportError(elements, INVALID_PLAYLIST_MESSAGE);
    return;
  }

  beginExport(elements);

  try {
    showProgress(elements, 0);
    const payload = await fetchExportPayload(source, elements);
    completeExport(elements, payload, selectedExportFormat(elements));

    // Save recent playlist on success!
    const playlistId = source.kind === 'liked-songs' ? 'liked-songs' : source.id;
    saveRecentPlaylist(playlistId, payload.metadata.name);
    renderRecentPlaylists(elements, getRecentPlaylists());
  } catch (error) {
    handleExportError(elements, error);
  } finally {
    endExport(elements);
  }
}

async function exportEntireLibrary(elements: AppElements): Promise<void> {
  clearExportError(elements);
  beginExport(elements);

  try {
    const sources = await getEntireLibrary({
      onProgress(progress: LibraryFetchProgress): void {
        showLibraryProgress(elements, progress);
      },
    });
    const sortedSources = sortLibrarySources(sources);
    const exportFile = buildLibraryExportFile(selectedExportFormat(elements), sortedSources);
    const trackCount = totalLibraryTrackCount(sortedSources);

    downloadTextFile(exportFile.filename, exportFile.content, exportFile.type);
    setExportState(elements, 'empty');
    setStatus(
      elements,
      `Exported entire library: ${sortedSources.length} ${sortedSources.length === 1 ? 'source' : 'sources'}, ${trackCount} ${trackCount === 1 ? 'track' : 'tracks'}.`,
    );
  } catch (error) {
    handleExportError(elements, error);
  } finally {
    endExport(elements);
  }
}

function resolveSelectedSourceFromForm(elements: AppElements): ExportSource | null {
  if (selectedSource) {
    return selectedSource;
  }

  const playlistId = extractPlaylistId(elements.playlistInput.value);

  if (!playlistId) {
    return null;
  }

  const source: ExportSource = {
    kind: 'playlist',
    id: playlistId,
    label: 'pasted playlist',
    origin: 'paste',
  };

  selectExportSource(elements, source, { clearPastedInput: false });
  return source;
}

async function fetchExportPayload(source: ExportSource, elements: AppElements): Promise<ExportPayload> {
  if (source.kind === 'liked-songs') {
    const items = await getLikedSongs({
      onProgress(fetchedCount: number): void {
        showProgress(elements, fetchedCount);
      },
    });

    return {
      metadata: {
        name: LIKED_SONGS_LABEL,
        owner: LIKED_SONGS_METADATA_OWNER,
        description: LIKED_SONGS_METADATA_DESCRIPTION,
        total: items.length,
      },
      items,
    };
  }

  const metadata = await getPlaylistMeta(source.id);
  const items = await getAllPlaylistItems(source.id, {
    onProgress(fetchedCount: number): void {
      showProgress(elements, fetchedCount);
    },
  });

  return { metadata, items };
}

function completeExport(elements: AppElements, payload: ExportPayload, format: ExportFormat): void {
  const sortedItems = sortItems(payload.items, DEFAULT_EXPORT_SORT);
  const exportFile = buildExportFile(format, payload.metadata, sortedItems);

  downloadTextFile(exportFile.filename, exportFile.content, exportFile.type);
  setExportState(elements, 'empty');
  setStatus(
    elements,
    `Exported ${payload.items.length} ${payload.items.length === 1 ? 'track' : 'tracks'}.`,
  );
}

function buildExportFile(format: ExportFormat, metadata: PlaylistMeta, items: PlaylistItemRow[]): BuiltExportFile {
  if (format === 'csv') {
    return {
      filename: buildExportFilename(metadata.name, 'csv'),
      content: buildPlaylistCsv(metadata, items),
      type: 'text/csv;charset=utf-8',
    };
  }

  if (format === 'json') {
    return {
      filename: buildExportFilename(metadata.name, 'json'),
      content: buildPlaylistJson(metadata, items),
      type: 'application/json;charset=utf-8',
    };
  }

  if (format === 'html') {
    return {
      filename: buildExportFilename(metadata.name, 'html'),
      content: buildPlaylistHtml(metadata, items),
      type: 'text/html;charset=utf-8',
    };
  }

  const markdown = buildPlaylistMarkdown(metadata, items);
  return {
    filename: markdown.filename,
    content: markdown.content,
    type: 'text/markdown;charset=utf-8',
  };
}

function buildLibraryExportFile(format: ExportFormat, sources: LibrarySource[]): BuiltExportFile {
  if (format === 'csv') {
    return {
      filename: buildExportFilename('Spotify Library', 'csv'),
      content: buildLibraryCsv(sources),
      type: 'text/csv;charset=utf-8',
    };
  }

  if (format === 'json') {
    return {
      filename: buildExportFilename('Spotify Library', 'json'),
      content: buildLibraryJson(sources),
      type: 'application/json;charset=utf-8',
    };
  }

  if (format === 'html') {
    return {
      filename: buildExportFilename('Spotify Library', 'html'),
      content: buildLibraryHtml(sources),
      type: 'text/html;charset=utf-8',
    };
  }

  const markdown = buildLibraryMarkdown(sources);
  return {
    filename: markdown.filename,
    content: markdown.content,
    type: 'text/markdown;charset=utf-8',
  };
}

function sortLibrarySources(sources: LibrarySource[]): LibrarySource[] {
  return sources.map((source) => ({
    ...source,
    items: sortItems(source.items, DEFAULT_EXPORT_SORT),
  }));
}

function totalLibraryTrackCount(sources: LibrarySource[]): number {
  return sources.reduce((total, source) => total + computePlaylistStats(source.items).trackCount, 0);
}

function selectedExportFormat(elements: AppElements): ExportFormat {
  const value = elements.exportFormat.value;

  if (value === 'csv' || value === 'json' || value === 'html') {
    return value;
  }

  return 'markdown';
}

function selectExportSource(
  elements: AppElements,
  source: ExportSource,
  options: { clearPastedInput: boolean },
): void {
  if (exportInProgress) {
    return;
  }

  selectedSource = source;

  if (options.clearPastedInput) {
    elements.playlistInput.value = '';
  }

  clearExportError(elements);
  clearProgress(elements);
  setExportState(elements, 'empty');
  syncSelectedSourceControls(elements);
  setStatus(elements, `Selected ${source.label}. Ready to export.`);
}

function handleExportError(elements: AppElements, error: unknown): void {
  if (error instanceof AuthExpiredError) {
    logout();
    updateConnectionState(elements, false);
    showExportError(elements, SESSION_EXPIRED_MESSAGE);
    return;
  }

  if (error instanceof PlaylistAccessError || error instanceof InsufficientScopeError) {
    showExportError(elements, error.message);
    return;
  }

  if (error instanceof RateLimitedError) {
    showExportError(elements, RATE_LIMITED_MESSAGE);
    return;
  }

  console.error('Playlist export failed', error);
  showExportError(elements, GENERIC_EXPORT_ERROR_MESSAGE);
}

function getAppElements(): AppElements | null {
  const app = document.getElementById('app');
  const connectButton = document.getElementById('connect-spotify');
  const connectionStatus = document.getElementById('connection-status');
  const sourcePicker = document.getElementById('source-picker');
  const sourcePickerStatus = document.getElementById('source-picker-status');
  const sourcePickerError = document.getElementById('source-picker-error');
  const sourceList = document.getElementById('source-list');
  const exportForm = document.getElementById('export-form');
  const exportFormat = document.getElementById('export-format');
  const playlistInput = document.getElementById('playlist-input');
  const exportLibraryButton = document.getElementById('export-library-button');
  const exportButton = document.getElementById('export-button');
  const copyButton = document.getElementById('copy-button');
  const recentPlaylistsContainer = document.getElementById('recent-playlists-container');
  const recentPlaylistsList = document.getElementById('recent-playlists-list');
  const loadPlaylistsButton = document.getElementById('load-all-playlists');
  const playlistError = document.getElementById('playlist-error');
  const exportProgress = document.getElementById('export-progress');
  const status = document.getElementById('status');

  if (
    !app ||
    !connectButton ||
    !connectionStatus ||
    !sourcePicker ||
    !sourcePickerStatus ||
    !sourcePickerError ||
    !sourceList ||
    !exportForm ||
    !exportFormat ||
    !playlistInput ||
    !exportLibraryButton ||
    !exportButton ||
    !copyButton ||
    !recentPlaylistsContainer ||
    !recentPlaylistsList ||
    !loadPlaylistsButton ||
    !playlistError ||
    !exportProgress ||
    !status
  ) {
    console.error('Spotify playlist exporter markup is missing required elements.');
    return null;
  }

  return {
    app,
    connectButton: connectButton as HTMLButtonElement,
    connectionStatus,
    sourcePicker,
    sourcePickerStatus,
    sourcePickerError,
    sourceList,
    exportForm: exportForm as HTMLFormElement,
    exportFormat: exportFormat as HTMLSelectElement,
    playlistInput: playlistInput as HTMLInputElement,
    exportLibraryButton: exportLibraryButton as HTMLButtonElement,
    exportButton: exportButton as HTMLButtonElement,
    copyButton: copyButton as HTMLButtonElement,
    recentPlaylistsContainer,
    recentPlaylistsList,
    loadPlaylistsButton: loadPlaylistsButton as HTMLButtonElement,
    playlistError,
    exportProgress,
    status,
  };
}

function initializeExportState(elements: AppElements): void {
  selectedSource = null;
  exportInProgress = false;
  setExportState(elements, 'empty');
  clearExportError(elements);
  clearProgress(elements);
  resetSourcePicker(elements);
  elements.recentPlaylistsContainer.hidden = true;
  elements.recentPlaylistsContainer.style.display = 'none';
  elements.recentPlaylistsList.replaceChildren();
}

function updateConnectionState(elements: AppElements, connected: boolean): void {
  elements.app.dataset.authState = connected ? 'connected' : 'disconnected';
  elements.connectionStatus.textContent = connected
    ? 'Connected to Spotify.'
    : 'Not connected to Spotify.';
  elements.connectButton.textContent = connected ? 'Reconnect Spotify' : 'Connect Spotify';
  setStatus(elements, connected ? 'Connected to Spotify.' : 'Not connected to Spotify.');

  if (!connected) {
    resetSourcePicker(elements);
    elements.recentPlaylistsContainer.hidden = true;
    elements.recentPlaylistsContainer.style.display = 'none';
    elements.recentPlaylistsList.replaceChildren();
  } else {
    renderRecentPlaylists(elements, getRecentPlaylists());
  }
}

function resetSourcePicker(elements: AppElements): void {
  sourceFetchToken += 1;
  sourceFetchState = 'idle';
  selectedSource = null;
  elements.sourcePicker.dataset.sourceState = 'idle';
  elements.sourceList.removeAttribute('aria-busy');
  elements.sourceList.replaceChildren();
  hideSourcePickerStatus(elements);
  clearSourcePickerError(elements);
}

function beginExport(elements: AppElements): void {
  exportInProgress = true;
  elements.exportLibraryButton.disabled = true;
  elements.exportButton.disabled = true;
  elements.copyButton.disabled = true;
  elements.exportFormat.disabled = true;
  elements.playlistInput.disabled = true;
  setExportState(elements, 'loading');
  syncSelectedSourceControls(elements);
}

function endExport(elements: AppElements): void {
  exportInProgress = false;
  elements.exportLibraryButton.disabled = false;
  elements.exportButton.disabled = false;
  elements.copyButton.disabled = false;
  elements.exportFormat.disabled = false;
  elements.playlistInput.disabled = false;
  clearProgress(elements);
  syncSelectedSourceControls(elements);

  if (elements.exportForm.dataset.exportState === 'loading') {
    setExportState(elements, 'empty');
  }
}

function setExportState(elements: AppElements, state: 'empty' | 'loading' | 'error'): void {
  elements.app.dataset.exportState = state;
  elements.exportForm.dataset.exportState = state;
}

function showExportError(elements: AppElements, message: string): void {
  setExportState(elements, 'error');
  elements.playlistError.textContent = message;
  elements.playlistError.hidden = false;
  elements.playlistError.setAttribute('role', 'alert');
  elements.playlistInput.setAttribute('aria-invalid', 'true');
}

function clearExportError(elements: AppElements): void {
  elements.playlistError.textContent = '';
  elements.playlistError.hidden = true;
  elements.playlistError.removeAttribute('role');
  elements.playlistInput.removeAttribute('aria-invalid');
}

function showSourcePickerStatus(elements: AppElements, message: string): void {
  elements.sourcePickerStatus.textContent = message;
  elements.sourcePickerStatus.hidden = false;
}

function hideSourcePickerStatus(elements: AppElements): void {
  elements.sourcePickerStatus.textContent = '';
  elements.sourcePickerStatus.hidden = true;
}

function showSourcePickerError(elements: AppElements, message: string): void {
  elements.sourcePickerError.textContent = message;
  elements.sourcePickerError.hidden = false;
  elements.sourcePickerError.setAttribute('role', 'alert');
}

function clearSourcePickerError(elements: AppElements): void {
  elements.sourcePickerError.textContent = '';
  elements.sourcePickerError.hidden = true;
  elements.sourcePickerError.removeAttribute('role');
}

function showProgress(elements: AppElements, fetchedCount: number): void {
  elements.exportProgress.textContent = `Fetching tracks... ${fetchedCount} so far`;
  elements.exportProgress.hidden = false;
}

function showLibraryProgress(elements: AppElements, progress: LibraryFetchProgress): void {
  const sourceKind = progress.kind === 'playlist' ? 'playlist' : 'source';
  elements.exportProgress.textContent = `Fetching ${sourceKind} ${progress.sourceIndex} of ${progress.sourceTotal} ('${progress.sourceName}')... ${formatTrackCount(progress.fetchedCount)} so far`;
  elements.exportProgress.hidden = false;
}

function clearProgress(elements: AppElements): void {
  elements.exportProgress.textContent = '';
  elements.exportProgress.hidden = true;
}

function setStatus(elements: AppElements, message: string): void {
  elements.status.textContent = message;
  elements.status.removeAttribute('role');
}

function syncSelectedSourceControls(elements: AppElements): void {
  const selectedKey = selectedSource ? sourceKey(selectedSource) : '';

  for (const listItem of Array.from(elements.sourceList.children)) {
    const sourceButton = listItem.children[0] as HTMLButtonElement | undefined;

    if (!sourceButton) {
      continue;
    }

    const isSelected = selectedKey !== '' && sourceButton.dataset.sourceKey === selectedKey;
    const sourceDisabled = sourceButton.dataset.sourceDisabled === 'true';

    sourceButton.setAttribute('aria-pressed', isSelected ? 'true' : 'false');
    sourceButton.disabled = sourceDisabled || exportInProgress;
  }

  // Sync recent playlist shortcut selection & disabled states
  for (const btn of Array.from(elements.recentPlaylistsList.children) as HTMLButtonElement[]) {
    const isSelected = selectedKey !== '' && btn.dataset.sourceKey === selectedKey;
    btn.setAttribute('aria-pressed', isSelected ? 'true' : 'false');
    btn.disabled = exportInProgress;
  }

  elements.loadPlaylistsButton.disabled = exportInProgress;
}

function sourceKey(source: ExportSource): string {
  return source.kind === 'liked-songs'
    ? 'liked-songs'
    : `playlist:${source.id}`;
}

function formatTrackCount(trackCount: number): string {
  return `${trackCount} ${trackCount === 1 ? 'track' : 'tracks'}`;
}

function sourcePickerErrorMessage(error: unknown): string {
  if (error instanceof AuthExpiredError) {
    return SESSION_EXPIRED_MESSAGE;
  }

  if (error instanceof Error && error.message.trim()) {
    return error.message;
  }

  return SOURCE_FETCH_FALLBACK_MESSAGE;
}

setupApp();

interface RecentPlaylist {
  id: string;
  name: string;
  lastExportedAt: string;
}

let copyConfirmationTimeoutId: number | null = null;

function showCopyConfirmation(elements: AppElements): void {
  if (copyConfirmationTimeoutId !== null) {
    clearTimeout(copyConfirmationTimeoutId);
  }
  elements.copyButton.textContent = 'Copied!';
  copyConfirmationTimeoutId = setTimeout(() => {
    elements.copyButton.textContent = 'Copy';
    copyConfirmationTimeoutId = null;
  }, 2000);
}

async function copySelectedSourceToClipboard(elements: AppElements): Promise<void> {
  clearExportError(elements);

  const source = resolveSelectedSourceFromForm(elements);

  if (!source) {
    showExportError(elements, INVALID_PLAYLIST_MESSAGE);
    return;
  }

  beginExport(elements);

  try {
    showProgress(elements, 0);
    const payload = await fetchExportPayload(source, elements);
    const sortedItems = sortItems(payload.items, DEFAULT_EXPORT_SORT);
    const format = selectedExportFormat(elements);
    const exportFile = buildExportFile(format, payload.metadata, sortedItems);

    await navigator.clipboard.writeText(exportFile.content);

    // Save recent playlist on success!
    const playlistId = source.kind === 'liked-songs' ? 'liked-songs' : source.id;
    saveRecentPlaylist(playlistId, payload.metadata.name);
    renderRecentPlaylists(elements, getRecentPlaylists());

    showCopyConfirmation(elements);
    setStatus(
      elements,
      `Copied ${payload.items.length} ${payload.items.length === 1 ? 'track' : 'tracks'} export to clipboard.`,
    );
    setExportState(elements, 'empty');
  } catch (error) {
    handleExportError(elements, error);
  } finally {
    endExport(elements);
  }
}

function getRecentPlaylists(): RecentPlaylist[] {
  try {
    const raw = localStorage.getItem('recentPlaylists');
    if (!raw) {
      return [];
    }
    const list = JSON.parse(raw);
    if (Array.isArray(list)) {
      return list.filter(
        (item): item is RecentPlaylist =>
          Boolean(item) &&
          typeof item === 'object' &&
          typeof item.id === 'string' &&
          typeof item.name === 'string'
      );
    }
  } catch (error) {
    // Fail silently
  }
  return [];
}

function saveRecentPlaylist(id: string, name: string): void {
  try {
    let list = getRecentPlaylists();
    list = list.filter((item) => item.id !== id);
    list.unshift({
      id,
      name,
      lastExportedAt: new Date().toISOString(),
    });
    if (list.length > 10) {
      list = list.slice(0, 10);
    }
    localStorage.setItem('recentPlaylists', JSON.stringify(list));
  } catch (error) {
    // Fail silently
  }
}

function renderRecentPlaylists(elements: AppElements, recents: RecentPlaylist[]): void {
  if (recents.length === 0) {
    elements.recentPlaylistsContainer.hidden = true;
    elements.recentPlaylistsContainer.style.display = 'none';
    elements.recentPlaylistsList.replaceChildren();
    return;
  }

  elements.recentPlaylistsContainer.hidden = false;
  elements.recentPlaylistsContainer.style.display = 'flex';

  const buttons = recents.map((item) => {
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'recent-playlist-btn';
    btn.textContent = item.name;
    btn.dataset.sourceKey = item.id === 'liked-songs' ? 'liked-songs' : `playlist:${item.id}`;
    btn.disabled = exportInProgress;

    btn.addEventListener('click', () => {
      if (item.id === 'liked-songs') {
        selectExportSource(elements, {
          kind: 'liked-songs',
          label: LIKED_SONGS_LABEL,
          origin: 'picker',
        }, { clearPastedInput: true });
      } else {
        selectExportSource(elements, {
          kind: 'playlist',
          id: item.id,
          label: item.name,
          origin: 'picker',
        }, { clearPastedInput: true });
      }
    });

    return btn;
  });

  elements.recentPlaylistsList.replaceChildren(...buttons);
  syncSelectedSourceControls(elements);
}
