const express = require('express');
const router = express.Router();
const requireAuth = require('../middleware/requireAuth');

const axios = require('axios');
const { mapTorrentToDownload } = require('../utils/qbittorrent');
const cache = require('../utils/cache');
const { pollAllServices, getLastPollTimings, onPollComplete, offPollComplete, POLLING_ENABLED } = require('../utils/poller');
const { getSonarrInstances, getRadarrInstances } = require('../utils/config');
const sanitizeError = require('../utils/sanitizeError');


// Helper function to extract poster/cover art URL from a movie or series object
function getCoverArt(item) {
  if (!item || !item.images) return null;
  const poster = item.images.find(img => img.coverType === 'poster');
  if (poster) return poster.remoteUrl || poster.url || null;
  // Fallback to fanart if no poster
  const fanart = item.images.find(img => img.coverType === 'fanart');
  return fanart ? (fanart.remoteUrl || fanart.url || null) : null;
}

// Return all resolved tag labels for a series/movie.
// For Radarr: tags is array of IDs, tagMap is id -> label mapping.
// For Sonarr: tags are objects with a label property.
function extractAllTags(tags, tagMap) {
  if (!tags || tags.length === 0) return [];
  if (tagMap) {
    return tags.map(id => tagMap.get(id)).filter(Boolean);
  }
  return tags.map(t => t && t.label).filter(Boolean);
}

// Return the tag label that matches the current username, or null.
function extractUserTag(tags, tagMap, username) {
  const allLabels = extractAllTags(tags, tagMap);
  if (!allLabels.length) return null;
  if (username) {
    const match = allLabels.find(label => tagMatchesUser(label, username));
    if (match) return match;
  }
  return null;
}

// Replicate Ombi's StringHelper.SanitizeTagLabel: lowercase, replace non-alphanumeric with hyphen, collapse, trim
function sanitizeTagLabel(input) {
  if (!input) return '';
  return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
}

// Check if a tag matches the username: exact match first, then sanitized match
function tagMatchesUser(tag, username) {
  if (!tag || !username) return false;
  const tagLower = tag.toLowerCase();
  // Exact match (handles users whose tags weren't mangled)
  if (tagLower === username) return true;
  // Sanitized match (handles Ombi-mangled tags for email-style usernames)
  if (tagLower === sanitizeTagLabel(username)) return true;
  return false;
}

// Extract import issues from a Sonarr/Radarr queue record
function getImportIssues(queueRecord) {
  if (!queueRecord) return null;
  const state = queueRecord.trackedDownloadState;
  const status = queueRecord.trackedDownloadStatus;
  if (state !== 'importPending' && status !== 'warning' && status !== 'error') return null;
  const messages = [];
  if (queueRecord.statusMessages && queueRecord.statusMessages.length > 0) {
    for (const sm of queueRecord.statusMessages) {
      if (sm.messages && sm.messages.length > 0) {
        messages.push(...sm.messages);
      } else if (sm.title) {
        messages.push(sm.title);
      }
    }
  }
  if (queueRecord.errorMessage) {
    messages.push(queueRecord.errorMessage);
  }
  if (messages.length === 0) return null;
  return messages;
}

// Helper to build Sonarr web UI link for a series
function getSonarrLink(series) {
  if (!series || !series._instanceUrl || !series.titleSlug) return null;
  return `${series._instanceUrl}/series/${series.titleSlug}`;
}

// Helper to build Radarr web UI link for a movie
function getRadarrLink(movie) {
  if (!movie || !movie._instanceUrl || !movie.titleSlug) return null;
  return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
}

// Fetch all Emby users and return a Map<lowerName -> displayName> (and sanitized variants).
// Result is cached for 60s to avoid hammering Emby on every dashboard poll.
async function getEmbyUsers() {
  const cached = cache.get('emby:users');
  if (cached) return cached;
  try {
    const response = await axios.get(`${process.env.EMBY_URL}/Users`, {
      headers: { 'X-MediaBrowser-Token': process.env.EMBY_API_KEY }
    });
    // Build map: both raw lowercase and sanitized form -> display name
    const map = new Map();
    for (const u of response.data) {
      const name = u.Name || '';
      map.set(name.toLowerCase(), name);
      map.set(sanitizeTagLabel(name), name);
    }
    cache.set('emby:users', map, 60000);
    return map;
  } catch (err) {
    console.error('[Dashboard] Failed to fetch Emby users:', err.message);
    return new Map();
  }
}

// Classify each tag label: matched to a known Emby user, or unmatched.
// Returns array of { label, matchedUser: string|null }
function buildTagBadges(allTags, embyUserMap) {
  return allTags.map(label => {
    const lower = label.toLowerCase();
    const sanitized = sanitizeTagLabel(label);
    const displayName = embyUserMap.get(lower) || embyUserMap.get(sanitized) || null;
    return { label, matchedUser: displayName };
  });
}

// Track active dashboard clients.
// SSE connections: registered on connect, removed on close — always accurate.
// Legacy HTTP poll clients: pruned after CLIENT_STALE_MS of inactivity.
const activeClients = new Map();
const CLIENT_STALE_MS = 30000;

function getActiveClients() {
  const now = Date.now();
  for (const [key, client] of activeClients.entries()) {
    if (client.type !== 'sse' && now - client.lastSeen > CLIENT_STALE_MS) {
      activeClients.delete(key);
    }
  }
  return Array.from(activeClients.values());
}

// Get user downloads for authenticated user
router.get('/user-downloads', requireAuth, async (req, res) => {
  try {
    const user = req.user;
    const username = user.name.toLowerCase();
    const usernameSanitized = sanitizeTagLabel(user.name);
    const isAdmin = !!user.isAdmin;
    const showAll = isAdmin && req.query.showAll === 'true';
    console.log(`[Dashboard] Serving downloads for user: ${user.name} (${username}), isAdmin: ${isAdmin}, showAll: ${showAll}`);

    // Track this client's refresh rate
    const clientRefreshRate = parseInt(req.query.refreshRate, 10);
    if (clientRefreshRate > 0) {
      activeClients.set(username, { user: user.name, refreshRateMs: clientRefreshRate, lastSeen: Date.now() });
    } else {
      // Client has refresh off or didn't send — still mark as seen but with no rate
      activeClients.set(username, { user: user.name, refreshRateMs: 0, lastSeen: Date.now() });
    }

    // When polling is disabled, fetch on-demand if cache has expired
    // The fetched data is cached (30s TTL) so subsequent requests from any user reuse it
    if (!POLLING_ENABLED && !cache.get('poll:sab-queue')) {
      console.log(`[Dashboard] Cache expired and polling disabled, fetching on-demand...`);
      await pollAllServices();
    }

    // Read all data from cache
    const sabQueueData = cache.get('poll:sab-queue') || { slots: [] };
    const sabHistoryData = cache.get('poll:sab-history') || { slots: [] };
    const sonarrTagsResults = cache.get('poll:sonarr-tags') || [];
    const sonarrQueueData = cache.get('poll:sonarr-queue') || { records: [] };
    const sonarrHistoryData = cache.get('poll:sonarr-history') || { records: [] };
    const radarrQueueData = cache.get('poll:radarr-queue') || { records: [] };
    const radarrHistoryData = cache.get('poll:radarr-history') || { records: [] };
    const radarrTagsData = cache.get('poll:radarr-tags') || [];
    const qbittorrentTorrents = cache.get('poll:qbittorrent') || [];

    // Wrap in the structure the rest of the code expects
    const sabnzbdQueue = { data: { queue: sabQueueData } };
    const sabnzbdHistory = { data: { history: sabHistoryData } };
    const sonarrQueue = { data: sonarrQueueData };
    const sonarrHistory = { data: sonarrHistoryData };
    const radarrQueue = { data: radarrQueueData };
    const radarrHistory = { data: radarrHistoryData };
    const radarrTags = { data: radarrTagsData };

    // Build series/movie maps from embedded objects in queue records
    // (history is fetched without includeSeries/includeMovie for speed;
    //  history matches fall back to the queue-built map via seriesId/movieId)
    const seriesMap = new Map();
    for (const r of sonarrQueue.data.records) {
      if (r.series && r.seriesId) seriesMap.set(r.seriesId, r.series);
    }
    for (const r of sonarrHistory.data.records) {
      if (r.series && r.seriesId && !seriesMap.has(r.seriesId)) seriesMap.set(r.seriesId, r.series);
    }
    const moviesMap = new Map();
    for (const r of radarrQueue.data.records) {
      if (r.movie && r.movieId) moviesMap.set(r.movieId, r.movie);
    }
    for (const r of radarrHistory.data.records) {
      if (r.movie && r.movieId && !moviesMap.has(r.movieId)) moviesMap.set(r.movieId, r.movie);
    }

    // Create tag maps (id -> label)
    const sonarrTagMap = new Map(sonarrTagsResults.flatMap(t => t.data || []).map(t => [t.id, t.label]));
    const radarrTagMap = new Map(radarrTags.data.map(t => [t.id, t.label]));

    // When showing all downloads, fetch full Emby user list to classify tags
    const embyUserMap = showAll ? await getEmbyUsers() : new Map();

    console.log(`[Dashboard] Cache data - Series: ${seriesMap.size}, Movies: ${moviesMap.size}, qBit: ${qbittorrentTorrents.length}`);

    // Match SABnzbd downloads to Sonarr/Radarr activity
    const userDownloads = [];

    // Process SABnzbd queue
    const queueStatus = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.status : null;
    const queueSpeed = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.speed : null;
    const queueKbpersec = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.kbpersec : null;
    console.log(`[Dashboard] Queue status: ${queueStatus}, speed: ${queueSpeed}, kbpersec: ${queueKbpersec}`);
    
    // Helper to determine status and speed
    function getSlotStatusAndSpeed(slot) {
      // If whole queue is paused, everything is paused with 0 speed
      if (queueStatus === 'Paused') {
        return { status: 'Paused', speed: '0' };
      }
      // Use slot's actual status and queue speed
      return { 
        status: slot.status || 'Unknown', 
        speed: queueSpeed || queueKbpersec || '0'
      };
    }
    
    if (sabnzbdQueue.data.queue && sabnzbdQueue.data.queue.slots) {
      for (const slot of sabnzbdQueue.data.queue.slots) {
        try {
          const nzbName = slot.filename || slot.nzbname;
          if (!nzbName) {
            console.log(`[Dashboard] Skipping slot with no filename/nzbname`);
            continue;
          }
          const slotState = getSlotStatusAndSpeed(slot);
          console.log(`[Dashboard] Slot ${nzbName}: status=${slotState.status}, speed=${slotState.speed}`);
          const nzbNameLower = nzbName.toLowerCase();
          
          // Try to match with Sonarr
          const sonarrMatch = sonarrQueue.data.records.find(r => {
            const rTitle = (r.title || r.sourceTitle || '').toLowerCase();
            return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle));
          });
          
          if (sonarrMatch && sonarrMatch.seriesId) {
            const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
            if (series) {
              const allTags = extractAllTags(series.tags, sonarrTagMap);
              const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
              const hasAnyTag = allTags.length > 0;
              if (showAll ? hasAnyTag : !!matchedUserTag) {
                const dlObj = {
                  type: 'series',
                  title: nzbName,
                  coverArt: getCoverArt(series),
                  status: slotState.status,
                  progress: slot.percentage,
                  mb: slot.mb,
                  mbmissing: slot.mbmissing,
                  size: slot.size,
                  speed: slotState.speed,
                  eta: slot.timeleft,
                  seriesName: series.title,
                  episodeInfo: sonarrMatch,
                  allTags,
                  matchedUserTag: matchedUserTag || null,
                  tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
                };
                const issues = getImportIssues(sonarrMatch);
                if (issues) dlObj.importIssues = issues;
                if (isAdmin) {
                  dlObj.downloadPath = slot.storage || null;
                  dlObj.targetPath = series.path || null;
                  dlObj.arrLink = getSonarrLink(series);
                }
                userDownloads.push(dlObj);
              }
            }
          }

          // Try to match with Radarr
          const radarrMatch = radarrQueue.data.records.find(r => {
            const rTitle = (r.title || r.sourceTitle || '').toLowerCase();
            return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle));
          });
          
          if (radarrMatch && radarrMatch.movieId) {
            const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
            if (movie) {
              const allTags = extractAllTags(movie.tags, radarrTagMap);
              const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username);
              const hasAnyTag = allTags.length > 0;
              if (showAll ? hasAnyTag : !!matchedUserTag) {
                const dlObj = {
                  type: 'movie',
                  title: nzbName,
                  coverArt: getCoverArt(movie),
                  status: slotState.status,
                  progress: slot.percentage,
                  mb: slot.mb,
                  mbmissing: slot.mbmissing,
                  size: slot.size,
                  speed: slotState.speed,
                  eta: slot.timeleft,
                  movieName: movie.title,
                  movieInfo: radarrMatch,
                  allTags,
                  matchedUserTag: matchedUserTag || null,
                  tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
                };
                const issues = getImportIssues(radarrMatch);
                if (issues) dlObj.importIssues = issues;
                if (isAdmin) {
                  dlObj.downloadPath = slot.storage || null;
                  dlObj.targetPath = movie.path || null;
                  dlObj.arrLink = getRadarrLink(movie);
                }
                userDownloads.push(dlObj);
              }
            }
          }
        } catch (err) {
          console.error(`[Dashboard] Error processing slot:`, err.message);
          console.error(`[Dashboard] Slot data:`, JSON.stringify(slot));
        }
      }
    }

    // Process SABnzbd history
    if (sabnzbdHistory.data.history && sabnzbdHistory.data.history.slots) {
      for (const slot of sabnzbdHistory.data.history.slots) {
        try {
          const nzbName = slot.name || slot.nzb_name || slot.nzbname;
          if (!nzbName) {
            console.log(`[Dashboard] Skipping history slot with no name/nzb_name/nzbname`);
            continue;
          }
          const nzbNameLower = nzbName.toLowerCase();
          
          // Try to match with Sonarr history
          const sonarrMatch = sonarrHistory.data.records.find(r => {
            const rTitle = (r.sourceTitle || r.title || '').toLowerCase();
            return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle));
          });
          
          if (sonarrMatch && sonarrMatch.seriesId) {
            const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
            if (series) {
              const allTags = extractAllTags(series.tags, sonarrTagMap);
              const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
              const hasAnyTag = allTags.length > 0;
              if (showAll ? hasAnyTag : !!matchedUserTag) {
                const dlObj = {
                  type: 'series',
                  title: nzbName,
                  coverArt: getCoverArt(series),
                  status: slot.status,
                  size: slot.size,
                  completedAt: slot.completed_time,
                  seriesName: series.title,
                  episodeInfo: sonarrMatch,
                  allTags,
                  matchedUserTag: matchedUserTag || null,
                  tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
                };
                if (isAdmin) {
                  dlObj.downloadPath = slot.storage || null;
                  dlObj.targetPath = series.path || null;
                  dlObj.arrLink = getSonarrLink(series);
                }
                userDownloads.push(dlObj);
              }
            }
          }

          // Try to match with Radarr history
          const radarrMatch = radarrHistory.data.records.find(r => {
            const rTitle = (r.sourceTitle || r.title || '').toLowerCase();
            return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle));
          });
          
          if (radarrMatch && radarrMatch.movieId) {
            const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
            if (movie) {
              const allTags = extractAllTags(movie.tags, radarrTagMap);
              const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username);
              const hasAnyTag = allTags.length > 0;
              if (showAll ? hasAnyTag : !!matchedUserTag) {
                const dlObj = {
                  type: 'movie',
                  title: nzbName,
                  coverArt: getCoverArt(movie),
                  status: slot.status,
                  size: slot.size,
                  completedAt: slot.completed_time,
                  movieName: movie.title,
                  movieInfo: radarrMatch,
                  allTags,
                  matchedUserTag: matchedUserTag || null,
                  tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
                };
                if (isAdmin) {
                  dlObj.downloadPath = slot.storage || null;
                  dlObj.targetPath = movie.path || null;
                  dlObj.arrLink = getRadarrLink(movie);
                }
                userDownloads.push(dlObj);
              }
            }
          }
        } catch (err) {
          console.error(`[Dashboard] Error processing history slot:`, err.message);
          console.error(`[Dashboard] History slot data:`, JSON.stringify(slot));
        }
      }
    }

    // Debug: show what queue records look like and which movies/series are tagged for this user
    console.log(`[Dashboard] Sonarr queue titles:`, sonarrQueue.data.records.map(r => ({ title: r.title, seriesId: r.seriesId })));
    console.log(`[Dashboard] Radarr queue titles:`, radarrQueue.data.records.map(r => ({ title: r.title, movieId: r.movieId })));
    console.log(`[Dashboard] Sonarr history titles:`, sonarrHistory.data.records.slice(0, 10).map(r => ({ title: r.title, seriesId: r.seriesId })));
    console.log(`[Dashboard] Radarr history titles:`, radarrHistory.data.records.slice(0, 10).map(r => ({ title: r.title, movieId: r.movieId })));
    console.log(`[Dashboard] Sonarr tag map:`, Array.from(sonarrTagMap.entries()));
    console.log(`[Dashboard] Radarr tag map:`, Array.from(radarrTagMap.entries()));
    
    // Show movies/series tagged for this user (from embedded objects in queue/history)
    const userMovies = Array.from(moviesMap.values()).filter(m => {
      return !!extractUserTag(m.tags, radarrTagMap, username);
    });
    const userSeries = Array.from(seriesMap.values()).filter(s => {
      return !!extractUserTag(s.tags, sonarrTagMap, username);
    });
    console.log(`[Dashboard] Movies tagged for ${username}:`, userMovies.map(m => m.title));
    console.log(`[Dashboard] Series tagged for ${username}:`, userSeries.map(s => s.title));

    // Process qBittorrent torrents - match to user-tagged Sonarr/Radarr activity
    console.log(`[Dashboard] Processing ${qbittorrentTorrents.length} qBittorrent torrents for user ${username}`);
    for (const torrent of qbittorrentTorrents) {
      try {
        const torrentName = torrent.name || '';
        const torrentNameLower = torrentName.toLowerCase();
        if (!torrentName) continue;
        
        console.log(`[Dashboard] Checking torrent "${torrentName}"`);
        
        // Try to match with Sonarr queue (user-tagged series)
        const sonarrMatch = sonarrQueue.data.records.find(r => {
          const rTitle = (r.title || r.sourceTitle || '').toLowerCase();
          return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle));
        });
        
        if (sonarrMatch && sonarrMatch.seriesId) {
          const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
          if (series) {
            const allTags = extractAllTags(series.tags, sonarrTagMap);
            const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
            const hasAnyTag = allTags.length > 0;
            if (showAll ? hasAnyTag : !!matchedUserTag) {
              console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr series "${series.title}"`);
              const download = mapTorrentToDownload(torrent);
              download.type = 'series';
              download.coverArt = getCoverArt(series);
              download.seriesName = series.title;
              download.episodeInfo = sonarrMatch;
              download.allTags = allTags;
              download.matchedUserTag = matchedUserTag || null;
              download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
              const sonarrIssues = getImportIssues(sonarrMatch);
              if (sonarrIssues) download.importIssues = sonarrIssues;
              if (isAdmin) {
                download.downloadPath = download.savePath || null;
                download.targetPath = series.path || null;
                download.arrLink = getSonarrLink(series);
              }
              userDownloads.push(download);
              continue; // Skip to next torrent
            }
          }
        }
        
        // Try to match with Radarr queue (user-tagged movies)
        const radarrMatch = radarrQueue.data.records.find(r => {
          const rTitle = (r.title || r.sourceTitle || '').toLowerCase();
          return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle));
        });
        
        if (radarrMatch && radarrMatch.movieId) {
          const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
          if (movie) {
            const allTags = extractAllTags(movie.tags, radarrTagMap);
            const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username);
            const hasAnyTag = allTags.length > 0;
            if (showAll ? hasAnyTag : !!matchedUserTag) {
              console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr movie "${movie.title}"`);
              const download = mapTorrentToDownload(torrent);
              download.type = 'movie';
              download.coverArt = getCoverArt(movie);
              download.movieName = movie.title;
              download.movieInfo = radarrMatch;
              download.allTags = allTags;
              download.matchedUserTag = matchedUserTag || null;
              download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
              const radarrIssues = getImportIssues(radarrMatch);
              if (radarrIssues) download.importIssues = radarrIssues;
              if (isAdmin) {
                download.downloadPath = download.savePath || null;
                download.targetPath = movie.path || null;
                download.arrLink = getRadarrLink(movie);
              }
              userDownloads.push(download);
              continue; // Skip to next torrent
            }
          }
        }
        
        // Try to match with Sonarr history
        const sonarrHistoryMatch = sonarrHistory.data.records.find(r => {
          const rTitle = (r.sourceTitle || r.title || '').toLowerCase();
          return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle));
        });
        
        if (sonarrHistoryMatch && sonarrHistoryMatch.seriesId) {
          const series = seriesMap.get(sonarrHistoryMatch.seriesId) || sonarrHistoryMatch.series;
          if (series) {
            const allTags = extractAllTags(series.tags, sonarrTagMap);
            const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
            const hasAnyTag = allTags.length > 0;
            if (showAll ? hasAnyTag : !!matchedUserTag) {
              console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr history "${series.title}"`);
              const download = mapTorrentToDownload(torrent);
              download.type = 'series';
              download.coverArt = getCoverArt(series);
              download.seriesName = series.title;
              download.episodeInfo = sonarrHistoryMatch;
              download.allTags = allTags;
              download.matchedUserTag = matchedUserTag || null;
              download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
              if (isAdmin) {
                download.downloadPath = download.savePath || null;
                download.targetPath = series.path || null;
                download.arrLink = getSonarrLink(series);
              }
              userDownloads.push(download);
              continue;
            }
          }
        }
        
        // Try to match with Radarr history
        const radarrHistoryMatch = radarrHistory.data.records.find(r => {
          const rTitle = (r.sourceTitle || r.title || '').toLowerCase();
          return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle));
        });
        
        if (radarrHistoryMatch && radarrHistoryMatch.movieId) {
          const movie = moviesMap.get(radarrHistoryMatch.movieId) || radarrHistoryMatch.movie;
          if (movie) {
            const allTags = extractAllTags(movie.tags, radarrTagMap);
            const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username);
            const hasAnyTag = allTags.length > 0;
            if (showAll ? hasAnyTag : !!matchedUserTag) {
              console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr history "${movie.title}"`);
              const download = mapTorrentToDownload(torrent);
              download.type = 'movie';
              download.coverArt = getCoverArt(movie);
              download.movieName = movie.title;
              download.movieInfo = radarrHistoryMatch;
              download.allTags = allTags;
              download.matchedUserTag = matchedUserTag || null;
              download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
              if (isAdmin) {
                download.downloadPath = download.savePath || null;
                download.targetPath = movie.path || null;
                download.arrLink = getRadarrLink(movie);
              }
              userDownloads.push(download);
              continue;
            }
          }
        }
      } catch (err) {
        console.error(`[Dashboard] Error processing torrent:`, err.message);
        console.error(`[Dashboard] Torrent data:`, JSON.stringify(torrent));
      }
    }

    console.log(`[Dashboard] Found ${userDownloads.length} downloads for user ${username}`);
    console.log(`[Dashboard] Sending ${userDownloads.length} downloads`);
    if (userDownloads.length > 0) {
      console.log(`[Dashboard] First download:`, JSON.stringify(userDownloads[0]));
    }
    
    res.json({
      user: user.name,
      isAdmin: isAdmin,
      downloads: userDownloads
    });
  } catch (error) {
    console.error(`[Dashboard] Error fetching user downloads:`, error.message);
    console.error(`[Dashboard] Full error:`, error);
    res.status(500).json({ error: 'Failed to fetch user downloads', details: sanitizeError(error) });
  }
});

// Get all users with their download counts
router.get('/user-summary', requireAuth, async (req, res) => {
  try {
    const sonarrInstances = getSonarrInstances();
    const radarrInstances = getRadarrInstances();

    // Get all Emby users
    const usersResponse = await axios.get(`${process.env.EMBY_URL}/Users`, {
      headers: { 'X-MediaBrowser-Token': process.env.EMBY_API_KEY }
    });

    // Get all series, movies, and tags from all instances
    const [sonarrSeriesResults, sonarrTagsResults, radarrMoviesResults, radarrTagsResults] = await Promise.all([
      Promise.all(sonarrInstances.map(inst =>
        axios.get(`${inst.url}/api/v3/series`, {
          headers: { 'X-Api-Key': inst.apiKey }
        }).then(r => r.data).catch(() => [])
      )),
      Promise.all(sonarrInstances.map(inst =>
        axios.get(`${inst.url}/api/v3/tag`, {
          headers: { 'X-Api-Key': inst.apiKey }
        }).then(r => r.data).catch(() => [])
      )),
      Promise.all(radarrInstances.map(inst =>
        axios.get(`${inst.url}/api/v3/movie`, {
          headers: { 'X-Api-Key': inst.apiKey }
        }).then(r => r.data).catch(() => [])
      )),
      Promise.all(radarrInstances.map(inst =>
        axios.get(`${inst.url}/api/v3/tag`, {
          headers: { 'X-Api-Key': inst.apiKey }
        }).then(r => r.data).catch(() => [])
      ))
    ]);

    const allSeries = sonarrSeriesResults.flat();
    const sonarrTagMap = new Map(sonarrTagsResults.flat().map(t => [t.id, t.label]));
    const allMovies = radarrMoviesResults.flat();
    const radarrTagMap = new Map(radarrTagsResults.flat().map(t => [t.id, t.label]));

    // Count downloads per user
    const userDownloads = {};
    usersResponse.data.forEach(user => {
      userDownloads[user.Name.toLowerCase()] = {
        username: user.Name,
        seriesCount: 0,
        movieCount: 0
      };
    });

    // Process series tags
    allSeries.forEach(series => {
      const tags = extractAllTags(series.tags, sonarrTagMap);
      tags.forEach(userTag => {
        const uname = userTag.toLowerCase();
        if (userDownloads[uname]) userDownloads[uname].seriesCount++;
      });
    });

    // Process movie tags
    allMovies.forEach(movie => {
      const tags = extractAllTags(movie.tags, radarrTagMap);
      tags.forEach(userTag => {
        const uname = userTag.toLowerCase();
        if (userDownloads[uname]) userDownloads[uname].movieCount++;
      });
    });

    res.json(Object.values(userDownloads));
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch user summary', details: sanitizeError(error) });
  }
});

// Admin-only status page with cache stats
router.get('/status', requireAuth, (req, res) => {
  try {
    const user = req.user;
    if (!user.isAdmin) {
      return res.status(403).json({ error: 'Admin access required' });
    }

    const cacheStats = cache.getStats();
    const uptime = process.uptime();

    res.json({
      server: {
        uptimeSeconds: Math.floor(uptime),
        nodeVersion: process.version,
        memoryUsageMB: Math.round(process.memoryUsage().rss / 1024 / 1024 * 10) / 10,
        heapUsedMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 10) / 10,
        heapTotalMB: Math.round(process.memoryUsage().heapTotal / 1024 / 1024 * 10) / 10
      },
      polling: {
        enabled: POLLING_ENABLED,
        intervalMs: POLLING_ENABLED ? require('../utils/poller').POLL_INTERVAL : 0,
        lastPoll: getLastPollTimings()
      },
      cache: cacheStats,
      clients: getActiveClients()
    });
  } catch (err) {
    res.status(500).json({ error: 'Failed to get status', details: err.message });
  }
});

// Cover art proxy — fetches external poster images server-side so the
// browser loads them from 'self' and the CSP img-src stays tight.
// Requires authentication. Only proxies http/https URLs.
router.get('/cover-art', requireAuth, async (req, res) => {
  const { url } = req.query;
  if (!url || typeof url !== 'string') {
    return res.status(400).json({ error: 'Missing url parameter' });
  }
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    return res.status(400).json({ error: 'Invalid url' });
  }
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
    return res.status(400).json({ error: 'Only http/https URLs are supported' });
  }
  try {
    const response = await axios.get(url, {
      responseType: 'stream',
      timeout: 8000,
      maxContentLength: 5 * 1024 * 1024 // 5 MB max
    });
    const contentType = response.headers['content-type'] || 'image/jpeg';
    // Only proxy image content types
    if (!contentType.startsWith('image/')) {
      return res.status(400).json({ error: 'Remote URL is not an image' });
    }
    res.setHeader('Content-Type', contentType);
    res.setHeader('Cache-Control', 'public, max-age=86400'); // 24h browser cache
    res.setHeader('X-Content-Type-Options', 'nosniff');
    response.data.pipe(res);
  } catch (err) {
    res.status(502).json({ error: 'Failed to fetch cover art' });
  }
});

// SSE stream — pushes download data to the client on every poll cycle.
// Uses the browser's built-in EventSource API (no library required).
// Auth is enforced by requireAuth (emby_user cookie sent with the upgrade request).
// No CSRF token needed — SSE is a GET request (safe method, no state change).
router.get('/stream', requireAuth, async (req, res) => {
  const user = req.user;
  const username = user.name.toLowerCase();
  const showAll = !!user.isAdmin && req.query.showAll === 'true';

  // SSE headers — disable buffering at every layer
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache, no-transform');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no'); // nginx: disable proxy buffering
  res.flushHeaders();

  // Register as an active SSE client
  activeClients.set(username, { user: user.name, type: 'sse', connectedAt: Date.now(), lastSeen: Date.now() });
  console.log(`[SSE] Client connected: ${user.name}`);

  // Helper: build and send the downloads payload for this user
  async function sendDownloads() {
    try {
      // On-demand: trigger a fresh poll if cache is stale and polling is disabled
      if (!POLLING_ENABLED && !cache.get('poll:sab-queue')) {
        await pollAllServices();
      }

      const sabQueueData = cache.get('poll:sab-queue') || { slots: [] };
      const sabHistoryData = cache.get('poll:sab-history') || { slots: [] };
      const sonarrTagsResults = cache.get('poll:sonarr-tags') || [];
      const sonarrQueueData = cache.get('poll:sonarr-queue') || { records: [] };
      const sonarrHistoryData = cache.get('poll:sonarr-history') || { records: [] };
      const radarrQueueData = cache.get('poll:radarr-queue') || { records: [] };
      const radarrHistoryData = cache.get('poll:radarr-history') || { records: [] };
      const radarrTagsData = cache.get('poll:radarr-tags') || [];
      const qbittorrentTorrents = cache.get('poll:qbittorrent') || [];

      const sabnzbdQueue = { data: { queue: sabQueueData } };
      const sabnzbdHistory = { data: { history: sabHistoryData } };
      const sonarrQueue = { data: sonarrQueueData };
      const sonarrHistory = { data: sonarrHistoryData };
      const radarrQueue = { data: radarrQueueData };
      const radarrHistory = { data: radarrHistoryData };
      const radarrTags = { data: radarrTagsData };

      const seriesMap = new Map();
      for (const r of sonarrQueue.data.records) {
        if (r.series && r.seriesId) seriesMap.set(r.seriesId, r.series);
      }
      for (const r of sonarrHistory.data.records) {
        if (r.series && r.seriesId && !seriesMap.has(r.seriesId)) seriesMap.set(r.seriesId, r.series);
      }
      const moviesMap = new Map();
      for (const r of radarrQueue.data.records) {
        if (r.movie && r.movieId) moviesMap.set(r.movieId, r.movie);
      }
      for (const r of radarrHistory.data.records) {
        if (r.movie && r.movieId && !moviesMap.has(r.movieId)) moviesMap.set(r.movieId, r.movie);
      }

      const sonarrTagMap = new Map(sonarrTagsResults.flatMap(t => t.data || []).map(t => [t.id, t.label]));
      const radarrTagMap = new Map(radarrTags.data.map(t => [t.id, t.label]));
      const embyUserMap = showAll ? await getEmbyUsers() : new Map();

      // Inline the matching logic (same as /user-downloads)
      const userDownloads = [];
      const isAdmin = !!user.isAdmin;
      const usernameSanitized = sanitizeTagLabel(user.name);
      const queueStatus = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.status : null;
      const queueSpeed = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.speed : null;
      const queueKbpersec = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.kbpersec : null;

      function getSlotStatusAndSpeed(slot) {
        if (queueStatus === 'Paused') return { status: 'Paused', speed: '0' };
        return { status: slot.status || 'Unknown', speed: queueSpeed || queueKbpersec || '0' };
      }

      // SABnzbd queue
      if (sabnzbdQueue.data.queue && sabnzbdQueue.data.queue.slots) {
        for (const slot of sabnzbdQueue.data.queue.slots) {
          const nzbName = slot.filename || slot.nzbname;
          if (!nzbName) continue;
          const slotState = getSlotStatusAndSpeed(slot);
          const nzbNameLower = nzbName.toLowerCase();

          const sonarrMatch = sonarrQueue.data.records.find(r => {
            const rTitle = (r.title || r.sourceTitle || '').toLowerCase();
            return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle));
          });
          if (sonarrMatch && sonarrMatch.seriesId) {
            const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
            if (series) {
              const allTags = extractAllTags(series.tags, sonarrTagMap);
              const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
              if (showAll ? allTags.length > 0 : !!matchedUserTag) {
                const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slotState.status, progress: slot.percentage, mb: slot.mb, mbmissing: slot.mbmissing, size: slot.size, speed: slotState.speed, eta: slot.timeleft, seriesName: series.title, episodeInfo: sonarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined };
                const issues = getImportIssues(sonarrMatch);
                if (issues) dlObj.importIssues = issues;
                if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = getSonarrLink(series); }
                userDownloads.push(dlObj);
              }
            }
          }

          const radarrMatch = radarrQueue.data.records.find(r => {
            const rTitle = (r.title || r.sourceTitle || '').toLowerCase();
            return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle));
          });
          if (radarrMatch && radarrMatch.movieId) {
            const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
            if (movie) {
              const allTags = extractAllTags(movie.tags, radarrTagMap);
              const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username);
              if (showAll ? allTags.length > 0 : !!matchedUserTag) {
                const dlObj = { type: 'movie', title: nzbName, coverArt: getCoverArt(movie), status: slotState.status, progress: slot.percentage, mb: slot.mb, mbmissing: slot.mbmissing, size: slot.size, speed: slotState.speed, eta: slot.timeleft, movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined };
                const issues = getImportIssues(radarrMatch);
                if (issues) dlObj.importIssues = issues;
                if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; dlObj.arrLink = getRadarrLink(movie); }
                userDownloads.push(dlObj);
              }
            }
          }
        }
      }

      // SABnzbd history
      if (sabnzbdHistory.data.history && sabnzbdHistory.data.history.slots) {
        for (const slot of sabnzbdHistory.data.history.slots) {
          const nzbName = slot.name || slot.nzb_name || slot.nzbname;
          if (!nzbName) continue;
          const nzbNameLower = nzbName.toLowerCase();

          const sonarrMatch = sonarrHistory.data.records.find(r => {
            const rTitle = (r.sourceTitle || r.title || '').toLowerCase();
            return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle));
          });
          if (sonarrMatch && sonarrMatch.seriesId) {
            const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
            if (series) {
              const allTags = extractAllTags(series.tags, sonarrTagMap);
              const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
              if (showAll ? allTags.length > 0 : !!matchedUserTag) {
                const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slot.status, size: slot.size, completedAt: slot.completed_time, seriesName: series.title, episodeInfo: sonarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined };
                if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = getSonarrLink(series); }
                userDownloads.push(dlObj);
              }
            }
          }

          const radarrMatch = radarrHistory.data.records.find(r => {
            const rTitle = (r.sourceTitle || r.title || '').toLowerCase();
            return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle));
          });
          if (radarrMatch && radarrMatch.movieId) {
            const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
            if (movie) {
              const allTags = extractAllTags(movie.tags, radarrTagMap);
              const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username);
              if (showAll ? allTags.length > 0 : !!matchedUserTag) {
                const dlObj = { type: 'movie', title: nzbName, coverArt: getCoverArt(movie), status: slot.status, size: slot.size, completedAt: slot.completed_time, movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined };
                if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; dlObj.arrLink = getRadarrLink(movie); }
                userDownloads.push(dlObj);
              }
            }
          }
        }
      }

      // qBittorrent
      for (const torrent of qbittorrentTorrents) {
        const torrentName = torrent.name || '';
        if (!torrentName) continue;
        const torrentNameLower = torrentName.toLowerCase();

        const sonarrMatch = sonarrQueue.data.records.find(r => { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); });
        if (sonarrMatch && sonarrMatch.seriesId) {
          const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
          if (series) {
            const allTags = extractAllTags(series.tags, sonarrTagMap);
            const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
            if (showAll ? allTags.length > 0 : !!matchedUserTag) {
              const download = mapTorrentToDownload(torrent);
              Object.assign(download, { type: 'series', coverArt: getCoverArt(series), seriesName: series.title, episodeInfo: sonarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined });
              const issues = getImportIssues(sonarrMatch); if (issues) download.importIssues = issues;
              if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; download.arrLink = getSonarrLink(series); }
              userDownloads.push(download); continue;
            }
          }
        }

        const radarrMatch = radarrQueue.data.records.find(r => { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); });
        if (radarrMatch && radarrMatch.movieId) {
          const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
          if (movie) {
            const allTags = extractAllTags(movie.tags, radarrTagMap);
            const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username);
            if (showAll ? allTags.length > 0 : !!matchedUserTag) {
              const download = mapTorrentToDownload(torrent);
              Object.assign(download, { type: 'movie', coverArt: getCoverArt(movie), movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined });
              const issues = getImportIssues(radarrMatch); if (issues) download.importIssues = issues;
              if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; download.arrLink = getRadarrLink(movie); }
              userDownloads.push(download); continue;
            }
          }
        }

        const sonarrHistoryMatch = sonarrHistory.data.records.find(r => { const rTitle = (r.sourceTitle || r.title || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); });
        if (sonarrHistoryMatch && sonarrHistoryMatch.seriesId) {
          const series = seriesMap.get(sonarrHistoryMatch.seriesId) || sonarrHistoryMatch.series;
          if (series) {
            const allTags = extractAllTags(series.tags, sonarrTagMap);
            const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
            if (showAll ? allTags.length > 0 : !!matchedUserTag) {
              const download = mapTorrentToDownload(torrent);
              Object.assign(download, { type: 'series', coverArt: getCoverArt(series), seriesName: series.title, episodeInfo: sonarrHistoryMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined });
              if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; download.arrLink = getSonarrLink(series); }
              userDownloads.push(download); continue;
            }
          }
        }

        const radarrHistoryMatch = radarrHistory.data.records.find(r => { const rTitle = (r.sourceTitle || r.title || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); });
        if (radarrHistoryMatch && radarrHistoryMatch.movieId) {
          const movie = moviesMap.get(radarrHistoryMatch.movieId) || radarrHistoryMatch.movie;
          if (movie) {
            const allTags = extractAllTags(movie.tags, radarrTagMap);
            const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username);
            if (showAll ? allTags.length > 0 : !!matchedUserTag) {
              const download = mapTorrentToDownload(torrent);
              Object.assign(download, { type: 'movie', coverArt: getCoverArt(movie), movieName: movie.title, movieInfo: radarrHistoryMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined });
              if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; download.arrLink = getRadarrLink(movie); }
              userDownloads.push(download);
            }
          }
        }
      }

      // Write SSE event
      res.write(`data: ${JSON.stringify({ user: user.name, isAdmin, downloads: userDownloads })}\n\n`);
    } catch (err) {
      console.error('[SSE] Error building payload:', sanitizeError(err));
    }
  }

  // Send initial data immediately
  await sendDownloads();

  // Subscribe to poll-complete notifications
  onPollComplete(sendDownloads);

  // 25s heartbeat comment to keep the connection alive through proxies/load-balancers
  const heartbeat = setInterval(() => {
    try { res.write(': heartbeat\n\n'); } catch { /* ignore — cleanup below handles it */ }
  }, 25000);

  // Cleanup on client disconnect
  req.on('close', () => {
    clearInterval(heartbeat);
    offPollComplete(sendDownloads);
    activeClients.delete(username);
    console.log(`[SSE] Client disconnected: ${user.name}`);
  });
});

module.exports = router;
