let currentUser = null;
let downloads = [];
let isAdmin = false;
let showAll = false;
let csrfToken = null; // double-submit CSRF token, sent as X-CSRF-Token on mutating requests
const SPLASH_MIN_MS = 1200; // minimum splash display time

// History section state
let historyDays = parseInt(localStorage.getItem('sofarr-history-days'), 10) || 7;
let historyRefreshHandle = null;
const HISTORY_REFRESH_MS = 5 * 60 * 1000; // auto-refresh history every 5 min

// SSE stream state
let sseSource = null;
let sseReconnectTimer = null;
const SSE_RECONNECT_MS = 3000; // browser already retries, but we add explicit backoff too

// Apply saved theme immediately (before DOMContentLoaded to avoid flash)
(function() {
  const saved = localStorage.getItem('sofarr-theme') || 'light';
  document.documentElement.setAttribute('data-theme', saved);
})();

// Check authentication on load
document.addEventListener('DOMContentLoaded', () => {
  checkAuthentication();
  initThemeSwitcher();
  initTabs();
  initHistoryControls();
  
  document.getElementById('login-form').addEventListener('submit', handleLogin);
  document.getElementById('logout-btn').addEventListener('click', handleLogout);
  document.getElementById('show-all-toggle').addEventListener('change', handleShowAllToggle);
  document.getElementById('status-btn').addEventListener('click', toggleStatusPanel);
});

function initThemeSwitcher() {
  const saved = localStorage.getItem('sofarr-theme') || 'light';
  document.querySelectorAll('.theme-btn').forEach(btn => {
    btn.classList.toggle('active', btn.dataset.theme === saved);
    btn.addEventListener('click', () => setTheme(btn.dataset.theme));
  });
}

function setTheme(theme) {
  document.documentElement.setAttribute('data-theme', theme);
  localStorage.setItem('sofarr-theme', theme);
  document.querySelectorAll('.theme-btn').forEach(btn => {
    btn.classList.toggle('active', btn.dataset.theme === theme);
  });
}

function initTabs() {
  const savedTab = localStorage.getItem('sofarr-active-tab') || 'downloads';
  activateTab(savedTab, false);

  document.querySelectorAll('.tab-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const tab = btn.dataset.tab;
      activateTab(tab, true);
    });
  });
}

function activateTab(tabName, save) {
  document.querySelectorAll('.tab-btn').forEach(btn => {
    btn.classList.toggle('active', btn.dataset.tab === tabName);
  });
  document.querySelectorAll('.tab-panel').forEach(panel => {
    panel.style.display = panel.id === `tab-${tabName}` ? '' : 'none';
  });
  if (save) localStorage.setItem('sofarr-active-tab', tabName);
  // Load history the first time the history tab is shown
  if (tabName === 'history') loadHistory();
}

// --- SSE connection management ---

function startSSE() {
  stopSSE();
  const params = showAll ? '?showAll=true' : '';
  const source = new EventSource('/api/dashboard/stream' + params);
  sseSource = source;

  let firstMessage = true;
  source.onmessage = (event) => {
    try {
      const data = JSON.parse(event.data);
      currentUser = data.user;
      isAdmin = !!data.isAdmin;
      downloads = data.downloads;
      document.getElementById('currentUser').textContent = currentUser || '-';
      renderDownloads();
      hideError();
      if (firstMessage) { firstMessage = false; hideLoading(); }
    } catch (err) {
      console.error('[SSE] Failed to parse message:', err);
    }
  };

  source.onerror = () => {
    // EventSource retries automatically; we just log and show a reconnecting indicator
    console.warn('[SSE] Connection lost, browser will retry...');
  };

  console.log('[SSE] Stream connected');
}

function stopSSE() {
  if (sseReconnectTimer) { clearTimeout(sseReconnectTimer); sseReconnectTimer = null; }
  if (sseSource) {
    sseSource.close();
    sseSource = null;
    console.log('[SSE] Stream closed');
  }
}

function handleShowAllToggle(e) {
  showAll = e.target.checked;
  // Re-open stream with updated showAll param
  startSSE();
  // Reload history with updated showAll param
  loadHistory();
}

function fadeOutLogin() {
  return new Promise(resolve => {
    const login = document.getElementById('login-container');
    login.classList.add('fade-out');
    login.addEventListener('transitionend', () => {
      login.style.display = 'none';
      login.classList.remove('fade-out');
      resolve();
    }, { once: true });
  });
}

function showSplash() {
  const splash = document.getElementById('splash-screen');
  splash.style.display = 'flex';
  splash.style.opacity = '1';
  splash.classList.remove('fade-out');
}

function dismissSplash(startTime) {
  return new Promise(resolve => {
    const elapsed = Date.now() - (startTime || 0);
    const remaining = Math.max(0, SPLASH_MIN_MS - elapsed);
    setTimeout(() => {
      const splash = document.getElementById('splash-screen');
      splash.classList.add('fade-out');
      // Fallback: resolve after transition duration + buffer in case
      // transitionend never fires (e.g. display was toggled in same frame)
      const TRANSITION_MS = 400;
      const fallback = setTimeout(() => {
        splash.style.display = 'none';
        resolve();
      }, TRANSITION_MS + 100);
      splash.addEventListener('transitionend', () => {
        clearTimeout(fallback);
        splash.style.display = 'none';
        resolve();
      }, { once: true });
    }, remaining);
  });
}

async function checkAuthentication() {
  const splashStart = Date.now();
  try {
    // Fetch both auth state and a fresh CSRF token in parallel
    const [meRes, csrfRes] = await Promise.all([
      fetch('/api/auth/me'),
      fetch('/api/auth/csrf')
    ]);
    const data = await meRes.json();
    const csrfData = await csrfRes.json();
    if (csrfData.csrfToken) csrfToken = csrfData.csrfToken;

    if (data.authenticated) {
      currentUser = data.user;
      isAdmin = !!data.user.isAdmin;
      showDashboard();
      showLoading();
      startSSE();
      await dismissSplash(splashStart);
    } else {
      await dismissSplash(splashStart);
      showLogin();
    }
  } catch (err) {
    console.error('Authentication check failed:', err);
    await dismissSplash(splashStart);
    showLogin();
  }
}

async function handleLogin(e) {
  e.preventDefault();
  
  const username = document.getElementById('username').value;
  const password = document.getElementById('password').value;
  const rememberMe = document.getElementById('remember-me').checked;
  
  try {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ username, password, rememberMe })
    });
    
    const data = await response.json();
    
    if (data.success) {
      currentUser = data.user;
      isAdmin = !!data.user.isAdmin;
      // Store CSRF token returned by login for use in subsequent requests
      if (data.csrfToken) csrfToken = data.csrfToken;
      // Fade out login, then show splash while opening SSE stream.
      // requestAnimationFrame ensures the browser paints the splash at
      // opacity:1 before dismissSplash adds fade-out, so the CSS
      // transition fires and transitionend is guaranteed.
      await fadeOutLogin();
      showSplash();
      await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
      showDashboard();
      showLoading();
      const splashStart = Date.now();
      startSSE();
      await dismissSplash(splashStart);
    } else {
      showLoginError(data.error || 'Login failed');
    }
  } catch (err) {
    showLoginError('Login failed. Please try again.');
    console.error(err);
  }
}

async function handleLogout() {
  try {
    stopSSE();
    stopHistoryRefresh();
    if (statusRefreshHandle) { clearInterval(statusRefreshHandle); statusRefreshHandle = null; }
    await fetch('/api/auth/logout', {
      method: 'POST',
      headers: csrfToken ? { 'X-CSRF-Token': csrfToken } : {}
    });
    currentUser = null;
    csrfToken = null;
    downloads = [];
    clearHistory();
    showLogin();
  } catch (err) {
    console.error('Logout failed:', err);
  }
}

function showLogin() {
  document.getElementById('login-container').style.display = 'flex';
  document.getElementById('dashboard-container').style.display = 'none';
  hideLoginError();
}

function showDashboard() {
  document.getElementById('login-container').style.display = 'none';
  document.getElementById('dashboard-container').style.display = 'block';
  document.getElementById('currentUser').textContent = currentUser.name || '-';
  // Always start with status panel hidden (guards against stale display value on re-login)
  const sp = document.getElementById('status-panel');
  sp.style.display = 'none';
  sp.innerHTML = '';
  document.getElementById('admin-controls').style.display = isAdmin ? 'flex' : 'none';
  // Initialise days input from saved value
  const daysInput = document.getElementById('history-days');
  if (daysInput) daysInput.value = historyDays;
  startHistoryRefresh();
}

function showLoginError(message) {
  const errorDiv = document.getElementById('login-error');
  errorDiv.textContent = message;
  errorDiv.style.display = 'block';
}

function hideLoginError() {
  const errorDiv = document.getElementById('login-error');
  errorDiv.style.display = 'none';
}

// fetchUserDownloads is kept for the showAll toggle re-connection case
// but the primary data path is now via SSE (startSSE / EventSource).

function renderDownloads() {
  const downloadsList = document.getElementById('downloads-list');
  const noDownloads = document.getElementById('no-downloads');
  
  if (downloads.length === 0) {
    noDownloads.style.display = 'block';
    downloadsList.innerHTML = '';
    return;
  }
  
  noDownloads.style.display = 'none';
  
  // Get existing cards
  const existingCards = new Map();
  downloadsList.querySelectorAll('.download-card').forEach(card => {
    existingCards.set(card.dataset.id, card);
  });
  
  // Track which downloads we've processed
  const processedIds = new Set();
  
  downloads.forEach(download => {
    const id = download.title;
    processedIds.add(id);
    
    const existingCard = existingCards.get(id);
    if (existingCard) {
      // Update existing card
      updateDownloadCard(existingCard, download);
    } else {
      // Create new card
      const card = createDownloadCard(download);
      downloadsList.appendChild(card);
    }
  });
  
  // Remove cards for downloads that no longer exist
  existingCards.forEach((card, id) => {
    if (!processedIds.has(id)) {
      card.remove();
    }
  });
}

function updateDownloadCard(card, download) {
  // Update status
  const statusEl = card.querySelector('.download-status');
  if (statusEl && statusEl.textContent !== download.status) {
    statusEl.textContent = download.status;
    statusEl.className = `download-status ${download.status}`;
  }
  
  // Update progress bar and missing pieces
  const progressContainer = card.querySelector('.progress-container');
  if (progressContainer && download.progress !== undefined) {
    const progressBar = progressContainer.querySelector('.progress-bar');
    const progressText = progressContainer.querySelector('.progress-text');
    const missingText = progressContainer.querySelector('.missing-text');
    
    if (progressBar) {
      const downloaded = progressBar.querySelector('.downloaded');
      if (downloaded) {
        downloaded.style.width = download.progress + '%';
      }
    }
    
    if (progressText) {
      progressText.textContent = download.progress + '%';
    }
    
    if (missingText) {
      const totalMb = parseFloat(download.mb) || parseFloat(download.size);
      const missingMb = parseFloat(download.mbmissing) || 0;
      if (missingMb > 0 && totalMb > 0) {
        missingText.textContent = `(missing ${missingMb.toFixed(1)} of ${totalMb.toFixed(1)} MB)`;
      } else {
        missingText.textContent = '';
      }
    }
  }
  
  // Update speed
  const speedEl = card.querySelector('.detail-item[data-label="Speed"] .detail-value');
  if (speedEl && download.speed !== undefined) {
    speedEl.textContent = download.speed;
  }
  
  // Update ETA
  const etaEl = card.querySelector('.detail-item[data-label="ETA"] .detail-value');
  if (etaEl && download.eta !== undefined) {
    etaEl.textContent = download.eta;
  }
  
  // Update qBittorrent-specific fields
  if (download.qbittorrent) {
    const seedsEl = card.querySelector('.detail-item[data-label="Seeds"] .detail-value');
    if (seedsEl && download.seeds !== undefined) {
      seedsEl.textContent = download.seeds;
    }
    
    const peersEl = card.querySelector('.detail-item[data-label="Peers"] .detail-value');
    if (peersEl && download.peers !== undefined) {
      peersEl.textContent = download.peers;
    }
    
    const availabilityItem = card.querySelector('.detail-item[data-label="Availability"]');
    if (availabilityItem && download.availability !== undefined) {
      availabilityItem.querySelector('.detail-value').textContent = `${download.availability}%`;
      availabilityItem.classList.toggle('availability-warning', parseFloat(download.availability) < 100);
    }
  }
}

function createDownloadCard(download) {
  const card = document.createElement('div');
  card.className = `download-card ${download.type}`;
  card.dataset.id = download.title;
  
  // Cover art
  if (download.coverArt) {
    const coverDiv = document.createElement('div');
    coverDiv.className = 'download-cover';
    const coverImg = document.createElement('img');
    // Proxy cover art through the server so the CSP img-src 'self' rule
    // is satisfied (external poster URLs would be blocked otherwise).
    coverImg.src = download.coverArt
      ? '/api/dashboard/cover-art?url=' + encodeURIComponent(download.coverArt)
      : '';
    coverImg.alt = download.movieName || download.seriesName || download.title;
    coverImg.loading = 'lazy';
    coverDiv.appendChild(coverImg);
    card.appendChild(coverDiv);
  }
  
  // Info wrapper
  const infoDiv = document.createElement('div');
  infoDiv.className = 'download-info';
  
  const header = document.createElement('div');
  header.className = 'download-header';
  
  const type = document.createElement('span');
  type.className = `download-type ${download.type}`;
  if (download.type === 'series') {
    type.textContent = '📺 Series';
  } else if (download.type === 'movie') {
    type.textContent = '🎬 Movie';
  } else if (download.type === 'torrent') {
    const instName = download.instanceName ? ` (${download.instanceName})` : '';
    type.textContent = `📥 Torrent${instName}`;
  } else {
    type.textContent = download.type;
  }
  
  const status = document.createElement('span');
  status.className = `download-status ${download.status}`;
  status.textContent = download.status;
  
  header.appendChild(type);
  header.appendChild(status);

  if (download.importIssues && download.importIssues.length > 0) {
    const issueBadge = document.createElement('span');
    issueBadge.className = 'import-issue-badge';
    issueBadge.textContent = 'Import Pending';
    issueBadge.setAttribute('data-tooltip', download.importIssues.join('\n'));
    header.appendChild(issueBadge);
  }
  
  const title = document.createElement('h3');
  title.className = 'download-title';
  title.textContent = download.title;
  
  infoDiv.appendChild(header);
  infoDiv.appendChild(title);
  
  if (download.seriesName) {
    const series = document.createElement('p');
    series.className = 'download-series';
    if (isAdmin && download.arrLink) {
      series.innerHTML = 'Series: <a href="' + escapeHtml(download.arrLink) + '" target="_blank" class="arr-link">' + escapeHtml(download.seriesName) + '</a>';
    } else {
      series.textContent = `Series: ${download.seriesName}`;
    }
    infoDiv.appendChild(series);
  }
  
  if (download.movieName) {
    const movie = document.createElement('p');
    movie.className = 'download-movie';
    if (isAdmin && download.arrLink) {
      movie.innerHTML = 'Movie: <a href="' + escapeHtml(download.arrLink) + '" target="_blank" class="arr-link">' + escapeHtml(download.movieName) + '</a>';
    } else {
      movie.textContent = `Movie: ${download.movieName}`;
    }
    infoDiv.appendChild(movie);
  }

  if (showAll && download.tagBadges && download.tagBadges.length > 0) {
    // In showAll mode: render all tags classified by whether they match an Emby user.
    // Unmatched (no known Emby user) → amber, leftmost.
    // Matched → show Emby display name in accent colour, rightmost.
    const unmatched = download.tagBadges.filter(b => !b.matchedUser);
    const matched = download.tagBadges.filter(b => b.matchedUser);
    for (const b of unmatched) {
      const badge = document.createElement('span');
      badge.className = 'download-user-badge unmatched';
      badge.textContent = b.label;
      header.appendChild(badge);
    }
    for (const b of matched) {
      const badge = document.createElement('span');
      badge.className = 'download-user-badge';
      badge.textContent = b.matchedUser;
      header.appendChild(badge);
    }
  } else if (download.matchedUserTag) {
    // Normal (non-showAll) view: show only the current user's matched tag
    const matchedBadge = document.createElement('span');
    matchedBadge.className = 'download-user-badge';
    matchedBadge.textContent = download.matchedUserTag;
    header.appendChild(matchedBadge);
  }
  
  const details = document.createElement('div');
  details.className = 'download-details';
  
  const size = createDetailItem('Size', formatSize(download.size));
  details.appendChild(size);
  
  if (download.progress !== undefined) {
    const progressItem = document.createElement('div');
    progressItem.className = 'detail-item progress-item';
    progressItem.dataset.label = 'Progress';
    
    const labelSpan = document.createElement('span');
    labelSpan.className = 'detail-label';
    labelSpan.textContent = 'Progress';
    
    const valueDiv = document.createElement('div');
    valueDiv.className = 'progress-container';
    
    // Progress bar with segments
    const totalMb = parseFloat(download.mb) || parseFloat(download.size);
    const missingMb = parseFloat(download.mbmissing) || 0;
    const downloadedMb = totalMb - missingMb;
    const progressPercent = parseFloat(download.progress) || 0;
    const missingPercent = totalMb > 0 ? (missingMb / totalMb) * 100 : 0;
    
    const progressBar = document.createElement('div');
    progressBar.className = 'progress-bar';
    
    // Downloaded portion (green)
    if (progressPercent > 0) {
      const downloaded = document.createElement('div');
      downloaded.className = 'progress-segment downloaded';
      downloaded.style.width = progressPercent + '%';
      progressBar.appendChild(downloaded);
    }
    
    valueDiv.appendChild(progressBar);
    
    // Text showing percentage
    const progressText = document.createElement('span');
    progressText.className = 'progress-text';
    progressText.textContent = download.progress + '%';
    valueDiv.appendChild(progressText);
    
    // Missing pieces text
    if (missingMb > 0 && totalMb > 0) {
      const missingText = document.createElement('span');
      missingText.className = 'missing-text';
      missingText.textContent = `(missing ${missingMb.toFixed(1)} of ${totalMb.toFixed(1)} MB)`;
      valueDiv.appendChild(missingText);
    }
    
    progressItem.appendChild(labelSpan);
    progressItem.appendChild(valueDiv);
    details.appendChild(progressItem);
  }
  
  if (download.speed) {
    const speed = createDetailItem('Speed', download.speed);
    details.appendChild(speed);
  }
  
  if (download.eta) {
    const eta = createDetailItem('ETA', download.eta);
    details.appendChild(eta);
  }
  
  // qBittorrent-specific fields
  if (download.qbittorrent) {
    if (download.seeds !== undefined) {
      const seeds = createDetailItem('Seeds', download.seeds);
      details.appendChild(seeds);
    }
    
    if (download.peers !== undefined) {
      const peers = createDetailItem('Peers', download.peers);
      details.appendChild(peers);
    }
    
    if (download.availability !== undefined) {
      const availability = createDetailItem('Availability', `${download.availability}%`);
      if (parseFloat(download.availability) < 100) availability.classList.add('availability-warning');
      details.appendChild(availability);
    }
  }
  
  if (download.completedAt) {
    const completed = createDetailItem('Completed', formatDate(download.completedAt));
    details.appendChild(completed);
  }

  if (isAdmin && (download.downloadPath || download.targetPath)) {
    const pathsDiv = document.createElement('div');
    pathsDiv.className = 'download-paths';
    if (download.downloadPath) {
      const dlPath = document.createElement('div');
      dlPath.className = 'path-item';
      dlPath.innerHTML = '<span class="path-label">Download:</span> <span class="path-value">' + escapeHtml(download.downloadPath) + '</span>';
      pathsDiv.appendChild(dlPath);
    }
    if (download.targetPath) {
      const tgtPath = document.createElement('div');
      tgtPath.className = 'path-item';
      tgtPath.innerHTML = '<span class="path-label">Target:</span> <span class="path-value">' + escapeHtml(download.targetPath) + '</span>';
      pathsDiv.appendChild(tgtPath);
    }
    details.appendChild(pathsDiv);
  }
  
  infoDiv.appendChild(details);
  card.appendChild(infoDiv);
  
  return card;
}

function createDetailItem(label, value) {
  const item = document.createElement('div');
  item.className = 'detail-item';
  item.dataset.label = label;
  
  const labelSpan = document.createElement('span');
  labelSpan.className = 'detail-label';
  labelSpan.textContent = label;
  
  const valueSpan = document.createElement('span');
  valueSpan.className = 'detail-value';
  valueSpan.textContent = value;
  
  item.appendChild(labelSpan);
  item.appendChild(valueSpan);
  
  return item;
}

function escapeHtml(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

let statusRefreshHandle = null;
const STATUS_REFRESH_MS = 5000;

async function toggleStatusPanel() {
  const panel = document.getElementById('status-panel');
  if (panel.style.display !== 'none') {
    panel.style.display = 'none';
    if (statusRefreshHandle) { clearInterval(statusRefreshHandle); statusRefreshHandle = null; }
    return;
  }
  panel.style.display = 'block';
  await refreshStatusPanel();
  if (statusRefreshHandle) clearInterval(statusRefreshHandle);
  statusRefreshHandle = setInterval(refreshStatusPanel, STATUS_REFRESH_MS);
}

function closeStatusPanel() {
  document.getElementById('status-panel').style.display = 'none';
  if (statusRefreshHandle) { clearInterval(statusRefreshHandle); statusRefreshHandle = null; }
}

async function refreshStatusPanel() {
  const panel = document.getElementById('status-panel');
  if (!panel || panel.style.display === 'none') return;
  try {
    const res = await fetch('/api/dashboard/status');
    if (!res.ok) throw new Error('Failed to fetch status');
    const data = await res.json();
    renderStatusPanel(data, panel);
  } catch (err) {
    // Don't overwrite panel on transient error during auto-refresh
    if (!panel.innerHTML || panel.innerHTML.includes('status-loading')) {
      panel.innerHTML = '<p class="status-error">Failed to load status.</p>';
    }
  }
}

function renderStatusPanel(data, panel) {
  const s = data.server;
  const hrs = Math.floor(s.uptimeSeconds / 3600);
  const mins = Math.floor((s.uptimeSeconds % 3600) / 60);
  const secs = s.uptimeSeconds % 60;
  const uptime = `${hrs}h ${mins}m ${secs}s`;

  const totalKB = (data.cache.totalSizeBytes / 1024).toFixed(1);

  let html = `
    <div class="status-header">
      <h3>Server Status</h3>
      <button class="status-close" onclick="closeStatusPanel()">&times;</button>
    </div>
    <div class="status-grid">
      <div class="status-card">
        <div class="status-card-title">Server</div>
        <div class="status-row"><span>Uptime</span><span>${uptime}</span></div>
        <div class="status-row"><span>Node</span><span>${escapeHtml(s.nodeVersion)}</span></div>
        <div class="status-row"><span>Memory (RSS)</span><span>${s.memoryUsageMB} MB</span></div>
        <div class="status-row"><span>Heap</span><span>${s.heapUsedMB} / ${s.heapTotalMB} MB</span></div>
      </div>
      <div class="status-card">
        <div class="status-card-title">Data Refresh</div>`;

  const pollIntervalMs = data.polling.intervalMs;
  const clients = data.clients || [];
  const sseClients = clients.filter(c => c.type === 'sse');

  if (data.polling.enabled) {
    html += `<div class="status-row"><span>Background poll</span><span>${pollIntervalMs / 1000}s</span></div>`;
  } else {
    html += `<div class="status-row"><span>Background poll</span><span>Disabled</span></div>`;
  }

  const mode = sseClients.length > 0
    ? `<span class="status-fg-badge">SSE push</span>`
    : (data.polling.enabled ? 'Background' : 'On-demand (idle)');
  html += `<div class="status-row"><span>Delivery mode</span><span>${mode}</span></div>`;

  html += `<div class="status-row"><span>SSE clients</span><span>${sseClients.length}</span></div>`;
  for (const c of sseClients) {
    const age = Math.round((Date.now() - c.connectedAt) / 1000);
    html += `<div class="status-row status-row-sub"><span>${escapeHtml(c.user)}</span><span>connected ${age}s ago</span></div>`;
  }

  html += `</div>`;

  // Poll timings card
  const lp = data.polling.lastPoll;
  if (lp) {
    const pollAge = Math.round((Date.now() - new Date(lp.timestamp).getTime()) / 1000);
    html += `
      <div class="status-card status-card-wide">
        <div class="status-card-title">Last Poll (${lp.totalMs}ms total, ${pollAge}s ago)</div>
        <div class="status-timings">`;
    const maxTaskMs = lp.tasks.reduce((max, t) => Math.max(max, t.ms), 1);
    for (const t of lp.tasks) {
      const barWidth = Math.max(2, (t.ms / maxTaskMs) * 100);
      html += `
          <div class="timing-row">
            <span class="timing-label">${escapeHtml(t.label)}</span>
            <div class="timing-bar-bg"><div class="timing-bar" data-w="${barWidth.toFixed(1)}"></div></div>
            <span class="timing-value">${t.ms}ms</span>
          </div>`;
    }
    html += `</div></div>`;
  }

  // Cache table
  html += `
      <div class="status-card status-card-wide">
        <div class="status-card-title">Cache (${data.cache.entryCount} entries, ${totalKB} KB)</div>
        <table class="status-table">
          <thead><tr><th>Key</th><th>Items</th><th>Size</th><th>TTL</th></tr></thead>
          <tbody>`;

  for (const e of data.cache.entries) {
    const sizeStr = e.sizeBytes > 1024 ? (e.sizeBytes / 1024).toFixed(1) + ' KB' : e.sizeBytes + ' B';
    const ttlStr = e.expired ? '<span class="status-expired">expired</span>' : (e.ttlRemainingMs / 1000).toFixed(0) + 's';
    const items = e.itemCount !== null ? e.itemCount : '—';
    html += `<tr><td><code>${escapeHtml(e.key)}</code></td><td>${items}</td><td>${sizeStr}</td><td>${ttlStr}</td></tr>`;
  }

  html += `</tbody></table></div></div>`;
  panel.innerHTML = html;
  // Set bar widths via JS DOM assignment — immune to CSP style-src restrictions
  panel.querySelectorAll('.timing-bar[data-w]').forEach(el => {
    el.style.width = el.dataset.w + '%';
  });
}

function formatSize(size) {
  if (!size) return 'N/A';
  // If already a formatted string (e.g., "21.5 GB"), return as-is
  if (typeof size === 'string') {
    return size;
  }
  // If it's a number (bytes), format it
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(size) / Math.log(1024));
  return Math.round(size / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}

function formatDate(dateString) {
  if (!dateString) return 'N/A';
  return new Date(dateString).toLocaleString();
}

function showError(message) {
  const errorDiv = document.getElementById('error-message');
  errorDiv.textContent = message;
  errorDiv.style.display = 'block';
}

function hideError() {
  const errorDiv = document.getElementById('error-message');
  errorDiv.style.display = 'none';
}

function showLoading() {
  const loading = document.getElementById('loading');
  loading.style.display = 'block';
}

function hideLoading() {
  const loading = document.getElementById('loading');
  loading.style.display = 'none';
}

// =============================================================================
// History section
// =============================================================================

function initHistoryControls() {
  const daysInput = document.getElementById('history-days');
  const refreshBtn = document.getElementById('history-refresh-btn');
  if (daysInput) {
    daysInput.addEventListener('change', () => {
      const v = parseInt(daysInput.value, 10);
      if (v > 0 && v <= 90) {
        historyDays = v;
        localStorage.setItem('sofarr-history-days', v);
        loadHistory();
      }
    });
  }
  if (refreshBtn) {
    refreshBtn.addEventListener('click', () => loadHistory(true));
  }
}

function startHistoryRefresh() {
  stopHistoryRefresh();
  historyRefreshHandle = setInterval(() => loadHistory(), HISTORY_REFRESH_MS);
}

function stopHistoryRefresh() {
  if (historyRefreshHandle) {
    clearInterval(historyRefreshHandle);
    historyRefreshHandle = null;
  }
}

function clearHistory() {
  document.getElementById('history-list').innerHTML = '';
  document.getElementById('no-history').style.display = 'none';
  document.getElementById('history-error').style.display = 'none';
}

async function loadHistory(forceRefresh = false) {
  const listEl = document.getElementById('history-list');
  const loadingEl = document.getElementById('history-loading');
  const errorEl = document.getElementById('history-error');
  const noHistoryEl = document.getElementById('no-history');

  loadingEl.style.display = 'block';
  errorEl.style.display = 'none';
  noHistoryEl.style.display = 'none';

  try {
    const params = new URLSearchParams({ days: historyDays });
    if (showAll) params.set('showAll', 'true');
    if (forceRefresh) params.set('_t', Date.now());
    const res = await fetch(`/api/history/recent?${params}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    loadingEl.style.display = 'none';
    renderHistory(data.history || []);
  } catch (err) {
    loadingEl.style.display = 'none';
    errorEl.textContent = 'Failed to load history.';
    errorEl.style.display = 'block';
    console.error('[History] Load error:', err);
  }
}

function renderHistory(items) {
  const listEl = document.getElementById('history-list');
  const noHistoryEl = document.getElementById('no-history');
  listEl.innerHTML = '';
  if (!items.length) {
    noHistoryEl.style.display = 'block';
    return;
  }
  noHistoryEl.style.display = 'none';
  items.forEach(item => listEl.appendChild(createHistoryCard(item)));
}

function createHistoryCard(item) {
  const card = document.createElement('div');
  card.className = `history-card ${item.type} ${item.outcome}`;

  if (item.coverArt) {
    const coverDiv = document.createElement('div');
    coverDiv.className = 'history-cover';
    const img = document.createElement('img');
    img.src = '/api/dashboard/cover-art?url=' + encodeURIComponent(item.coverArt);
    img.alt = item.movieName || item.seriesName || item.title;
    img.loading = 'lazy';
    coverDiv.appendChild(img);
    card.appendChild(coverDiv);
  }

  const info = document.createElement('div');
  info.className = 'history-info';

  // Header row: type badge + outcome badge
  const header = document.createElement('div');
  header.className = 'history-card-header';

  const typeBadge = document.createElement('span');
  typeBadge.className = `history-type-badge ${item.type}`;
  typeBadge.textContent = item.type === 'series' ? '📺 Series' : '🎬 Movie';
  header.appendChild(typeBadge);

  const outcomeBadge = document.createElement('span');
  outcomeBadge.className = `history-outcome-badge ${item.outcome}`;
  outcomeBadge.textContent = item.outcome === 'imported' ? '✓ Imported' : '✗ Failed';
  header.appendChild(outcomeBadge);

  if (item.instanceName) {
    const instBadge = document.createElement('span');
    instBadge.className = 'history-instance-badge';
    instBadge.textContent = item.instanceName;
    header.appendChild(instBadge);
  }

  if (showAll && item.tagBadges && item.tagBadges.length > 0) {
    const unmatched = item.tagBadges.filter(b => !b.matchedUser);
    const matched = item.tagBadges.filter(b => b.matchedUser);
    for (const b of unmatched) {
      const badge = document.createElement('span');
      badge.className = 'download-user-badge unmatched';
      badge.textContent = b.label;
      header.appendChild(badge);
    }
    for (const b of matched) {
      const badge = document.createElement('span');
      badge.className = 'download-user-badge';
      badge.textContent = b.matchedUser;
      header.appendChild(badge);
    }
  } else if (item.matchedUserTag) {
    const badge = document.createElement('span');
    badge.className = 'download-user-badge';
    badge.textContent = item.matchedUserTag;
    header.appendChild(badge);
  }

  info.appendChild(header);

  // Title
  const title = document.createElement('h3');
  title.className = 'history-title';
  title.textContent = item.title;
  info.appendChild(title);

  // Series/movie name with optional arr link
  if (item.seriesName) {
    const p = document.createElement('p');
    p.className = 'history-media-name';
    if (isAdmin && item.arrLink) {
      p.innerHTML = 'Series: <a href="' + escapeHtml(item.arrLink) + '" target="_blank" class="arr-link">' + escapeHtml(item.seriesName) + '</a>';
    } else {
      p.textContent = 'Series: ' + item.seriesName;
    }
    info.appendChild(p);
  }
  if (item.movieName) {
    const p = document.createElement('p');
    p.className = 'history-media-name';
    if (isAdmin && item.arrLink) {
      p.innerHTML = 'Movie: <a href="' + escapeHtml(item.arrLink) + '" target="_blank" class="arr-link">' + escapeHtml(item.movieName) + '</a>';
    } else {
      p.textContent = 'Movie: ' + item.movieName;
    }
    info.appendChild(p);
  }

  // Detail pills
  const details = document.createElement('div');
  details.className = 'history-details';

  if (item.completedAt) {
    details.appendChild(createDetailItem('Completed', formatDate(item.completedAt)));
  }
  if (item.quality) {
    details.appendChild(createDetailItem('Quality', item.quality));
  }

  // Failed imports: show failure message
  if (item.outcome === 'failed' && item.failureMessage) {
    const failItem = document.createElement('div');
    failItem.className = 'history-failure-message';
    failItem.textContent = item.failureMessage;
    details.appendChild(failItem);
  }

  info.appendChild(details);
  card.appendChild(info);
  return card;
}
