// public/assets/app.js
/* English comments: minimal JS without external libs */

const state = {
  mediaItems: [],
  mediaTab: 'movies',

  i18n: {},
  lang: 'en',

  ui: {
    table_mode: 'pagination',
    table_page_size: 50,
  },
  background: {
    mode: 'light',
    max_parallel_jobs: 1,
    max_network_jobs: 1,
    max_io_jobs: 1,
    batch_sleep_ms: 500,
    watchdog_minutes: 10,
    paused: false,
  },
  tables: {},

  activeJobId: null,
  jobPollTimer: null,
  queuePollTimer: null,
  watchdogTimer: null,
  sourcesPollTimer: null,
  sourcesCache: null,
  sourceDetail: null,
  sourceDetailItem: null,
  sourceDetailData: null,
  sourceDetailLastFetch: 0,
  eventsSource: null,
  tasks: [],
  lastTasksFetch: 0,
  queueActiveList: [],
  queueRecentList: [],
  debugToolsEnabled: false,
  sse: {
    connected: false,
    lastEventAt: 0,
    lastEventType: '',
    reconnects: 0,
    retryDelayMs: 5000,
    leaseKey: 'scmedia_sse_lease',
    tabId: '',
  },
};

const LS_ACTIVE_JOB = 'scmedia_active_job_id';

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

function paginateItems(items, page, perPage) {
  const total = items.length;
  const size = Math.max(1, Number(perPage || 50));
  const p = Math.max(1, Number(page || 1));
  const start = (p - 1) * size;
  const slice = items.slice(start, start + size);
  return { items: slice, total, page: p, per_page: size };
}

function api(path, opts = {}) {
  return window.Api.request(path, opts);
}

/* ---------------------------
  i18n
--------------------------- */

function t(key, fallback = null) {
  const v = state.i18n?.[key];
  if (typeof v === 'string') return v;
  if (fallback !== null) return fallback;
  return key;
}

function updateThemeLabel() {
  const label = UI.qs('themeState');
  if (label) {
    const current = document.documentElement.getAttribute('data-theme') || 'dark';
    label.textContent = (current === 'light')
      ? t('theme.light', 'Light')
      : t('theme.dark', 'Dark');
  }
}

function initSourceDialog() {
  const closeBtn = UI.qs('btnSourceClose');
  const dlg = UI.qs('dlg-source');
  const approveBtn = UI.qs('btnSourceApprove');
  if (closeBtn && dlg) {
    closeBtn.addEventListener('click', () => dlg.close());
  }
  if (approveBtn) {
    approveBtn.addEventListener('click', () => {
      if (!state.sourceDetail) return;
      api('/api/sources/approve', {
        method: 'POST',
        body: JSON.stringify({ source: state.sourceDetail.source, id: state.sourceDetail.id }),
      }).catch(() => {});
    });
  }
  if (dlg) {
    dlg.addEventListener('close', () => {
      state.sourceDetail = null;
      state.sourceDetailItem = null;
      state.sourceDetailData = null;
    });
  }
}


function setVersion() {
  const el = document.querySelector('[data-version] .version');
  if (el && typeof APP_VERSION === 'string') {
    el.textContent = `v${APP_VERSION}`;
  }
}
/* ---------------------------
  Filters + Grid
--------------------------- */

async function loadUiSettings() {
  try {
    const res = await api('/api/settings');
    const data = res.data || res;
    state.debugToolsEnabled = !!(data.meta?.debug_tools_enabled);
    const prefs = window.UserPrefs ? await window.UserPrefs.load().catch(() => null) : null;
    const ui = prefs || {};
    state.ui.table_mode = ui.table_mode || 'pagination';
    state.ui.table_page_size = ui.table_page_size || 50;
    const bg = data.background || {};
    state.background = {
      mode: bg.mode || 'light',
      max_parallel_jobs: Number(bg.max_parallel_jobs || 1),
      max_network_jobs: Number(bg.max_network_jobs || 1),
      max_io_jobs: Number(bg.max_io_jobs || 1),
      batch_sleep_ms: Number(bg.batch_sleep_ms || 500),
      watchdog_minutes: Number(bg.watchdog_minutes || 10),
      paused: !!bg.paused,
    };
  } catch (e) {
    state.ui.table_mode = 'pagination';
    state.ui.table_page_size = 50;
  }
}

function setSseIndicator(stateClass) {
  if (window.UI?.updateSseIndicator) {
    window.UI.updateSseIndicator(stateClass.replace('sse-', ''));
    return;
  }
  const items = document.querySelectorAll('[data-sse-indicator]');
  items.forEach((el) => {
    el.classList.remove('sse-ok', 'sse-idle', 'sse-offline');
    el.classList.add(stateClass);
  });
}

function pulseSseIndicator() {
  if (window.UI?.blinkSseIndicator) {
    window.UI.blinkSseIndicator();
    return;
  }
  const items = document.querySelectorAll('[data-sse-indicator]');
  items.forEach((el) => {
    el.classList.add('sse-blink');
    setTimeout(() => el.classList.remove('sse-blink'), 250);
  });
}

function updateSseStats() {
  const wrap = UI.qs('queueSseStats');
  if (!wrap || !state.debugToolsEnabled) return;
  wrap.classList.remove('is-hidden');
  const el = wrap.querySelector('.queue-stats');
  if (!el) return;
  const sse = window.UI?.getSseStats?.() || null;
  const lastAt = state.sse.lastEventAt ? new Date(state.sse.lastEventAt).toLocaleTimeString() : '—';
  const reconnects = sse ? sse.reconnects : Number(state.sse.reconnects || 0);
  state.sse.reconnects = reconnects;
  el.textContent = `last: ${lastAt} | type: ${state.sse.lastEventType || '—'} | reconnects: ${reconnects}`;
}

function noteSseEvent(type) {
  state.sse.lastEventAt = Date.now();
  state.sse.lastEventType = type;
  localStorage.setItem('scmedia_sse_last', String(state.sse.lastEventAt));
  pulseSseIndicator();
  updateSseStats();
}

function setSseConnected(connected) {
  state.sse.connected = connected;
  localStorage.setItem('scmedia_sse_connected', connected ? '1' : '0');
  if (connected && state.sse.lastEventAt === 0) {
    state.sse.lastEventAt = Date.now();
    localStorage.setItem('scmedia_sse_last', String(state.sse.lastEventAt));
  }
  const now = Date.now();
  if (!connected) {
    setSseIndicator('sse-offline');
    return;
  }
  const recent = state.sse.lastEventAt > 0 && (now - state.sse.lastEventAt) <= 15000;
  setSseIndicator(recent ? 'sse-ok' : 'sse-idle');
}

function initTables() {
  if (!window.TableController) return;
  const mode = state.ui.table_mode || 'pagination';
  const pageSize = state.ui.table_page_size || 50;
  const grid = UI.qs('grid');
  const sourcesGrid = UI.qs('sources-grid');
  const getPrefs = (table) => {
    const id = table?.dataset?.tableId || table?.id || '';
    const prefs = window.UserPrefs?.getTable?.(id) || {};
    return { id, prefs };
  };

  if (grid) {
    const { id, prefs } = getPrefs(grid);
    state.tables.media = new TableController({
      table: grid,
      mode,
      pageSize,
      sort: prefs.sort || '',
      dir: prefs.dir || 'asc',
      onSortChange: (sort, dir) => window.UserPrefs?.setTable?.(id, { sort, dir }),
      fetchPage: async ({ page, per_page, sort, dir, filters, params }) => {
        const res = await api('/api/media/list', {
          method: 'POST',
          body: {
            page,
            per_page,
            sort,
            dir,
            filters,
            params: {
              tab: params?.tab || state.mediaTab || 'movies',
            },
          },
        });
        const data = res.data || {};
        return {
          items: data.items || [],
          total: data.total || 0,
          page: data.page || page,
          per_page: data.per_page || per_page,
        };
      },
      renderRow: renderMediaRow,
    });
  }

  if (sourcesGrid) {
    const { id, prefs } = getPrefs(sourcesGrid);
    state.tables.sources = new TableController({
      table: sourcesGrid,
      mode,
      pageSize,
      sort: prefs.sort || '',
      dir: prefs.dir || 'asc',
      onSortChange: (sort, dir) => window.UserPrefs?.setTable?.(id, { sort, dir }),
      fetchPage: async ({ page, per_page, sort, dir, filters }) => {
        if (Array.isArray(state.sourcesCache)) {
          const filtered = applySourceFilters(state.sourcesCache, filters);
          const sorted = sortSources(filtered, sort, dir);
          return paginateItems(sorted, page, per_page);
        }
        const res = await api('/api/sources/list', {
          method: 'POST',
          body: {
            page,
            per_page,
            sort,
            dir,
            filters,
          },
        });
        const data = res.data || {};
        return {
          items: data.items || [],
          total: data.total || 0,
          page: data.page || page,
          per_page: data.per_page || per_page,
        };
      },
      renderRow: renderSourceRow,
    });
  }
}

function sortSources(items, sort, dir) {
  if (!sort) return [...items];
  const desc = dir === 'desc';
  const list = [...items];
  const get = (it) => {
    if (sort === 'name') return (it.name || '').toLowerCase();
    if (sort === 'size') return Number(it.size_bytes || 0);
    if (sort === 'status') return (it.status || '').toLowerCase();
    if (sort === 'source') return (it.source || '').toLowerCase();
    if (sort === 'progress') return Number(it.percent_done || 0);
    return (it[sort] ?? '').toString().toLowerCase();
  };
  list.sort((a, b) => {
    const av = get(a);
    const bv = get(b);
    if (av === bv) return 0;
    if (desc) return av < bv ? 1 : -1;
    return av > bv ? 1 : -1;
  });
  return list;
}

function matchFilterValue(current, op, value) {
  const opKey = (op || 'eq').toLowerCase();
  if (opKey === 'empty') {
    if (Array.isArray(current)) return current.length === 0;
    return current === null || current === '';
  }
  if (opKey === 'like') {
    const needle = String(value || '').toLowerCase();
    const hay = String(current ?? '').toLowerCase();
    return needle === '' ? true : hay.includes(needle);
  }
  if (opKey === 'between' && Array.isArray(value) && value.length >= 2) {
    const left = value[0];
    const right = value[1];
    const cur = Number(current);
    const l = Number(left);
    const r = Number(right);
    if (!Number.isNaN(cur) && !Number.isNaN(l) && !Number.isNaN(r)) {
      return cur >= l && cur <= r;
    }
    return String(current ?? '') >= String(left ?? '') && String(current ?? '') <= String(right ?? '');
  }
  if (opKey === 'gt' || opKey === 'lt') {
    const cur = Number(current);
    const val = Number(value);
    if (!Number.isNaN(cur) && !Number.isNaN(val)) {
      return opKey === 'gt' ? cur > val : cur < val;
    }
    return opKey === 'gt'
      ? String(current ?? '') > String(value ?? '')
      : String(current ?? '') < String(value ?? '');
  }
  return String(current ?? '').toLowerCase() === String(value ?? '').toLowerCase();
}

function applySourceFilters(items, filters) {
  if (!Array.isArray(filters) || filters.length === 0) return items;
  const map = {
    source: it => it.source,
    type: it => it.content_type,
    name: it => it.name,
    size: it => it.size_bytes,
    status: it => it.status,
    progress: it => Number(it.percent_done || 0) * 100,
  };
  return items.filter((it) => {
    return filters.every((f) => {
      if (!f || !f.key) return true;
      const getter = map[f.key];
      const current = getter ? getter(it) : it[f.key];
      return matchFilterValue(current, f.op, f.value);
    });
  });
}

function renderMediaRow(body, it) {
  if (state.mediaTab === 'series') {
    renderSeriesRow(body, it);
  } else {
    renderFileRow(body, it);
  }
}

function renderSourceRow(body, it) {
  const tr = document.createElement('tr');
  tr.appendChild(cellText(it.source ?? '', '', 'source'));
  tr.appendChild(cellText(sourceTypeLabel(it.content_type), '', 'type'));
  tr.appendChild(cellText(it.name ?? '', '', 'name'));
  tr.appendChild(cellText(it.size_bytes ? formatSize(it.size_bytes) : '', 'align-right', 'size'));
  tr.appendChild(cellText(it.status ?? '', 'align-center', 'status'));
  const percent = Number(it.percent_done ?? 0);
  const pctLabel = Number.isFinite(percent) ? Math.round(percent * 100) : 0;
  tr.appendChild(cellText(`${pctLabel}%`, 'align-right', 'progress'));
  tr.addEventListener('click', () => openSourceDetail(it));
  body.appendChild(tr);
}

function sourceTypeLabel(type) {
  if (type === 'folder') return t('sources.type.folder', 'Folder');
  if (type === 'file') return t('sources.type.file', 'File');
  return '';
}

function cellText(txt, className = '', key = '') {
  const td = document.createElement('td');
  td.textContent = txt ?? '';
  if (className) td.className = className;
  if (key) td.dataset.key = key;
  return td;
}

function createYearCell(year) {
  const td = document.createElement('td');
  td.className = 'col-year';
  td.textContent = (year !== null && year !== undefined && year !== '') ? String(year) : '';
  return td;
}

function createTitleCell(display, original, fallback) {
  const td = document.createElement('td');
  td.className = 'col-title';
  const span = document.createElement('span');
  const displayText = display || fallback || '';
  const originalText = original || displayText;
  span.textContent = displayText;
  span.dataset.display = displayText;
  span.dataset.original = originalText;
  attachTitleToggle(span);
  td.appendChild(span);
  return td;
}

function attachTitleToggle(el) {
  const showOriginal = () => {
    if (el.dataset.original) el.textContent = el.dataset.original;
  };
  const showDisplay = () => {
    if (el.dataset.display) el.textContent = el.dataset.display;
  };
  el.addEventListener('mousedown', (e) => {
    if (e.button !== 0) return;
    showOriginal();
  });
  el.addEventListener('mouseup', showDisplay);
  el.addEventListener('mouseleave', showDisplay);
  el.addEventListener('touchstart', showOriginal, { passive: true });
  el.addEventListener('touchend', showDisplay);
}

function typeLabel(v) {
  if (v === 'auto') return t('types.auto', 'Auto');
  if (v === 'movie') return t('types.movie', 'Movie');
  if (v === 'series') return t('types.series', 'Series');
  return v;
}

function cellType(it) {
  const td = document.createElement('td');
  td.textContent = typeLabel(it.kind ?? 'auto');
  return td;
}

function issueBadges(issues) {
  if (!Array.isArray(issues) || issues.length === 0) return '';
  return issues.map(i => {
    if (i === 'rename') return '📝';
    if (i === 'delete') return '🗑';
    if (i === 'unknown_type') return '❓';
    return '⚠';
  }).join(' ');
}

function renderFileRow(body, it) {
  const tr = document.createElement('tr');
  tr.dataset.path = it.abs_path || '';

  const tdExpand = document.createElement('td');
  const btn = document.createElement('button');
  btn.className = 'expand-btn';
  btn.textContent = '+';
  btn.addEventListener('click', () => toggleFileRow(tr, it, btn));
  tdExpand.appendChild(btn);
  tr.appendChild(tdExpand);

  tr.appendChild(cellType(it));
  const displayTitle = it.title_display || it.name || '';
  const originalTitle = it.title_original || displayTitle;
  tr.appendChild(createTitleCell(displayTitle, originalTitle, it.name ?? ''));
  tr.appendChild(createYearCell(it.year ?? ''));
  tr.appendChild(cellText(it.needs_attention ? t('status.needs', 'Needs') : t('status.ok', 'OK')));
  tr.appendChild(cellText(issueBadges(it.issues)));

  body.appendChild(tr);
}

function renderSeriesRow(body, it) {
  const tr = document.createElement('tr');
  tr.dataset.series = it.series_key || '';

  const tdExpand = document.createElement('td');
  const btn = document.createElement('button');
  btn.className = 'expand-btn';
  btn.textContent = '+';
  btn.addEventListener('click', () => toggleSeriesRow(tr, it, btn));
  tdExpand.appendChild(btn);
  tr.appendChild(tdExpand);

  tr.appendChild(cellText(t('types.series', 'Series')));
  const displayTitle = it.title_display || it.series_key || '';
  const originalTitle = it.title_original || displayTitle;
  tr.appendChild(createTitleCell(displayTitle, originalTitle, it.series_key ?? ''));
  tr.appendChild(createYearCell(it.year ?? ''));
  const status = (it.needs_attention > 0) ? t('status.needs', 'Needs') : t('status.ok', 'OK');
  tr.appendChild(cellText(status));
  tr.appendChild(cellText(issueBadges(it.issues)));

  body.appendChild(tr);
}

async function loadMediaList() {
  const mediaTable = state.tables.media;
  const sourcesTable = state.tables.sources;
  if (state.mediaTab === 'sources') {
    if (sourcesTable) {
      sourcesTable.setParams({});
      await sourcesTable.load(1, false);
    }
    return;
  }
  if (mediaTable) {
    mediaTable.setParams({ tab: state.mediaTab });
    await mediaTable.load(1, false);
  }
}

async function loadSources() {
  const sourcesTable = state.tables.sources;
  if (sourcesTable) {
    await sourcesTable.load(1, false);
  }
}

async function toggleFileRow(tr, it, btn) {
  const next = tr.nextElementSibling;
  if (next && next.classList.contains('detail-row')) {
    next.remove();
    if (btn) btn.textContent = '+';
    return;
  }
  if (btn) btn.textContent = '-';

  const detail = document.createElement('tr');
  detail.className = 'detail-row';
  const td = document.createElement('td');
  td.colSpan = 6;
  td.textContent = t('common.loading', 'Loading…');
  detail.appendChild(td);
  tr.after(detail);

  const res = await api('/api/media/file?path=' + encodeURIComponent(it.abs_path || ''));
  if (!res.ok || !res.data) {
    td.textContent = t('common.error', 'Error');
    return;
  }
  td.innerHTML = renderDetail(res.data);
  bindMetadataHandlers(td);
}

async function toggleSeriesRow(tr, it, btn) {
  const next = tr.nextElementSibling;
  if (next && next.classList.contains('detail-row')) {
    next.remove();
    if (btn) btn.textContent = '+';
    return;
  }
  if (btn) btn.textContent = '-';

  const detail = document.createElement('tr');
  detail.className = 'detail-row';
  const td = document.createElement('td');
  td.colSpan = 6;
  td.textContent = t('common.loading', 'Loading…');
  detail.appendChild(td);
  tr.after(detail);

  const res = await api('/api/media/series?key=' + encodeURIComponent(it.series_key || ''));
  if (!res.ok || !res.data) {
    td.textContent = t('common.error', 'Error');
    return;
  }
  td.innerHTML = renderSeriesDetail(res.data || {});
  bindSeriesDetailHandlers(td);
  bindMetadataHandlers(td);
}

function renderDetail(data) {
  const file = data.file || {};
  const tracks = data.tracks || [];
  const meta = data.meta || {};

  let html = '';
  html += `<div class="detail-meta">`;
  html += `<div>${escapeHtml(file.abs_path || '')}</div>`;
  html += `<div class="small">` +
    `${t('media.container','Container')}: ${escapeHtml(file.container || t('common.na','n/a'))} ` +
    `${t('media.size','Size')}: ${formatSize(file.size_bytes || 0)} ` +
    `${t('media.duration','Duration')}: ${formatDuration(file.duration_ms || 0)}` +
    `</div>`;
  html += `</div>`;
  html += renderMetaBox(meta, file.name || '', file.kind || 'movie');

  html += `<table class="detail-table"><thead><tr>` +
    `<th>${t('media.track.type','Type')}</th>` +
    `<th>${t('media.track.lang','Lang')}</th>` +
    `<th>${t('media.track.name','Name')}</th>` +
    `<th>${t('media.track.codec','Codec')}</th>` +
    `<th>${t('media.track.channels','Channels')}</th>` +
    `<th>${t('media.track.flags','Flags')}</th>` +
    `<th>${t('media.track.audio_type','Audio type')}</th>` +
    `</tr></thead><tbody>`;

  for (const tr of tracks) {
    const flags = [];
    if (tr.default) flags.push('default');
    if (tr.forced) flags.push('forced');
    html += `<tr>` +
      `<td>${escapeHtml(tr.type || '')}</td>` +
      `<td>${escapeHtml(tr.lang || '')}</td>` +
      `<td>${escapeHtml(tr.name_norm || tr.name || '')}</td>` +
      `<td>${escapeHtml(tr.codec || '')}</td>` +
      `<td>${escapeHtml(tr.channels || '')}</td>` +
      `<td>${escapeHtml(flags.join(','))}</td>` +
      `<td>${escapeHtml(tr.audio_type || '')}</td>` +
      `</tr>`;
  }

  html += `</tbody></table>`;
  return html;
}

function renderSeriesDetail(payload) {
  const files = Array.isArray(payload.files) ? payload.files : [];
  const meta = payload.meta || {};
  let html = renderMetaBox(meta, meta.title_display || '', 'series');
  html += `<table class="detail-table"><thead><tr>` +
    `<th></th>` +
    `<th>${t('grid.name','Name')}</th>` +
    `<th>${t('grid.path','Path')}</th>` +
    `<th>${t('grid.status','Status')}</th>` +
    `<th>${t('grid.issues','Issues')}</th>` +
    `</tr></thead><tbody>`;
  for (const f of files) {
    html += `<tr data-path="${escapeHtml(f.abs_path || '')}">` +
      `<td><button class="expand-btn" data-action="file">+</button></td>` +
      `<td>${escapeHtml(f.name || '')}</td>` +
      `<td>${escapeHtml(f.rel_path || f.abs_path || '')}</td>` +
      `<td>${escapeHtml(f.needs_attention ? t('status.needs','Needs') : t('status.ok','OK'))}</td>` +
      `<td>${escapeHtml(issueBadges(f.issues))}</td>` +
      `</tr>`;
  }
  html += `</tbody></table>`;
  return html;
}

function renderMetaBox(meta, fallbackTitle, type) {
  const subjectKind = meta.subject_kind || type || 'movie';
  const subjectKey = meta.subject_key || '';
  const titleDisplay = meta.title_display || '';
  const titleOriginal = meta.title_original || '';
  const year = (meta.year !== null && meta.year !== undefined) ? String(meta.year) : '';
  const provider = meta.provider || '';
  const providerId = meta.provider_id || '';
  const source = meta.source || 'auto';
  const manualTitle = meta.manual_title || '';
  const manualYear = (meta.manual_year !== null && meta.manual_year !== undefined) ? String(meta.manual_year) : '';

  let html = '';
  html += `<div class="meta-box" data-subject-kind="${escapeHtml(subjectKind)}" data-subject-key="${escapeHtml(subjectKey)}" data-type="${escapeHtml(type || 'movie')}">`;
  html += `<div class="meta-current">`;
  html += `<div class="meta-line"><span class="meta-label">${t('meta.title','Title')}</span><span class="meta-value" data-role="title">${escapeHtml(titleDisplay || fallbackTitle)}</span></div>`;
  html += `<div class="meta-line"><span class="meta-label">${t('meta.original','Original')}</span><span class="meta-value" data-role="original">${escapeHtml(titleOriginal)}</span></div>`;
  html += `<div class="meta-line"><span class="meta-label">${t('meta.year','Year')}</span><span class="meta-value" data-role="year">${escapeHtml(year)}</span></div>`;
  html += `<div class="meta-line"><span class="meta-label">${t('meta.provider','Provider')}</span><span class="meta-value" data-role="provider">${escapeHtml(providerId ? (provider + ' / ' + providerId) : provider)}</span></div>`;
  html += `<div class="meta-line"><span class="meta-label">${t('meta.source','Source')}</span><span class="meta-value" data-role="source">${escapeHtml(source)}</span></div>`;
  html += `</div>`;

  html += `<div class="meta-actions">`;
  html += `<div class="meta-search">` +
    `<input type="text" data-role="query" placeholder="${escapeHtml(t('meta.search_placeholder','Search title'))}" value="${escapeHtml(titleDisplay || fallbackTitle)}">` +
    `<button class="btn" data-action="meta-search">${t('meta.search','Search')}</button>` +
    `</div>`;
  html += `<div class="meta-results" data-role="results"></div>`;

  html += `<div class="meta-manual">` +
    `<input type="text" data-role="manual-title" placeholder="${escapeHtml(t('meta.manual_title','Manual title'))}" value="${escapeHtml(manualTitle)}">` +
    `<input type="number" data-role="manual-year" placeholder="${escapeHtml(t('meta.manual_year','Year'))}" value="${escapeHtml(manualYear)}">` +
    `<button class="btn" data-action="meta-save">${t('meta.save','Save')}</button>` +
    `<button class="btn" data-action="meta-clear">${t('meta.clear','Clear')}</button>` +
    `</div>`;
  html += `</div>`;
  html += `</div>`;
  return html;
}

function bindSeriesDetailHandlers(root) {
  root.querySelectorAll('button[data-action="file"]').forEach(btn => {
    btn.addEventListener('click', async () => {
      const tr = btn.closest('tr');
      if (!tr) return;
      const path = tr.dataset.path || '';
      const next = tr.nextElementSibling;
      if (next && next.classList.contains('detail-row')) {
        next.remove();
        btn.textContent = '+';
        return;
      }
      btn.textContent = '-';
      const detail = document.createElement('tr');
      detail.className = 'detail-row';
      const td = document.createElement('td');
      td.colSpan = 5;
      td.textContent = t('common.loading', 'Loading…');
      detail.appendChild(td);
      tr.after(detail);
      const res = await api('/api/media/file?path=' + encodeURIComponent(path));
      if (!res.ok || !res.data) {
        td.textContent = t('common.error', 'Error');
        return;
      }
      td.innerHTML = renderDetail(res.data);
      bindMetadataHandlers(td);
    });
  });
}

function bindMetadataHandlers(root) {
  root.querySelectorAll('.meta-box').forEach(box => {
    const subjectKind = box.dataset.subjectKind || 'movie';
    const subjectKey = box.dataset.subjectKey || '';
    const type = box.dataset.type || 'movie';
    const resultsEl = box.querySelector('[data-role="results"]');
    const queryInput = box.querySelector('[data-role="query"]');
    const manualTitle = box.querySelector('[data-role="manual-title"]');
    const manualYear = box.querySelector('[data-role="manual-year"]');

    const setDisplay = (data) => {
      if (!data) return;
      const title = data.title_display || '';
      const original = data.title_original || '';
      const year = (data.year !== null && data.year !== undefined) ? String(data.year) : '';
      const provider = data.meta?.provider || data.provider || '';
      const providerId = data.meta?.provider_id || data.provider_id || '';
      const source = data.meta?.source || data.source || 'auto';

      const titleEl = box.querySelector('[data-role="title"]');
      const origEl = box.querySelector('[data-role="original"]');
      const yearEl = box.querySelector('[data-role="year"]');
      const provEl = box.querySelector('[data-role="provider"]');
      const srcEl = box.querySelector('[data-role="source"]');

      if (titleEl) titleEl.textContent = title;
      if (origEl) origEl.textContent = original;
      if (yearEl) yearEl.textContent = year;
      if (provEl) provEl.textContent = providerId ? `${provider} / ${providerId}` : provider;
      if (srcEl) srcEl.textContent = source;

      if (manualTitle) manualTitle.value = data.meta?.manual_title || '';
      if (manualYear) manualYear.value = data.meta?.manual_year ?? '';

      updateRowMeta(subjectKind, subjectKey, data);
    };

    const onSearch = async () => {
      const query = queryInput?.value?.trim() || '';
      if (!query || subjectKey === '') return;
      if (resultsEl) resultsEl.textContent = t('common.loading', 'Loading…');
      const res = await api('/api/metadata/search', {
        method: 'POST',
        body: JSON.stringify({ query, type }),
      });
      if (!res.ok) {
        if (resultsEl) resultsEl.textContent = t('common.error', 'Error');
        return;
      }
      renderMetaResults(resultsEl, res.data || [], async (selection) => {
        const save = await api('/api/metadata/select', {
          method: 'POST',
          body: JSON.stringify({
            subject_kind: subjectKind,
            subject_key: subjectKey,
            selection,
          }),
        });
        if (save.ok) {
          setDisplay(save.data || {});
        }
      });
    };

    const onSaveManual = async () => {
      const title = manualTitle?.value?.trim() || '';
      const year = manualYear?.value?.trim() || '';
      if (subjectKey === '') return;
      const save = await api('/api/metadata/manual', {
        method: 'POST',
        body: JSON.stringify({
          subject_kind: subjectKind,
          subject_key: subjectKey,
          title,
          year: year === '' ? null : Number(year),
        }),
      });
      if (save.ok) {
        setDisplay(save.data || {});
      }
    };

    const onClearManual = async () => {
      if (subjectKey === '') return;
      const save = await api('/api/metadata/manual/clear', {
        method: 'POST',
        body: JSON.stringify({
          subject_kind: subjectKind,
          subject_key: subjectKey,
        }),
      });
      if (save.ok) {
        setDisplay(save.data || {});
      }
    };

    const btnSearch = box.querySelector('[data-action="meta-search"]');
    const btnSave = box.querySelector('[data-action="meta-save"]');
    const btnClear = box.querySelector('[data-action="meta-clear"]');
    if (btnSearch) btnSearch.addEventListener('click', onSearch);
    if (btnSave) btnSave.addEventListener('click', onSaveManual);
    if (btnClear) btnClear.addEventListener('click', onClearManual);
  });
}

function renderMetaResults(root, results, onSelect) {
  if (!root) return;
  root.innerHTML = '';
  if (!Array.isArray(results) || results.length === 0) {
    root.textContent = t('meta.no_results', 'No results');
    return;
  }
  results.forEach(r => {
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'meta-result';

    const img = document.createElement('img');
    img.alt = '';
    img.loading = 'lazy';
    img.src = r.poster || '';
    img.className = 'meta-poster';
    if (!r.poster) img.style.display = 'none';

    const body = document.createElement('div');
    body.className = 'meta-result-body';

    const title = document.createElement('div');
    title.className = 'meta-result-title';
    const titleMap = r.title_map || {};
    const bestTitle = titleMap[state.lang] || r.original_title || titleMap[Object.keys(titleMap)[0]] || '';
    title.textContent = bestTitle;

    const meta = document.createElement('div');
    meta.className = 'meta-result-meta';
    const year = (r.year !== null && r.year !== undefined) ? String(r.year) : '';
    const providerLabel = r.provider_name || r.provider || '';
    meta.textContent = `${providerLabel}${r.provider_id ? ' / ' + r.provider_id : ''}${year ? ' • ' + year : ''}`;

    body.appendChild(title);
    body.appendChild(meta);

    btn.appendChild(img);
    btn.appendChild(body);
    btn.addEventListener('click', () => onSelect(r));
    root.appendChild(btn);
  });
}

function updateRowMeta(kind, key, data) {
  const rows = Array.from(document.querySelectorAll('#grid tbody tr'));
  const row = rows.find(r => (kind === 'series')
    ? r.dataset.series === key
    : r.dataset.path === key);
  if (!row) return;
  const titleCell = row.querySelector('.col-title span');
  const yearCell = row.querySelector('.col-year');
  const title = data.title_display || '';
  const original = data.title_original || title;
  const year = (data.year !== null && data.year !== undefined) ? String(data.year) : '';
  if (titleCell) {
    titleCell.textContent = title;
    titleCell.dataset.display = title;
    titleCell.dataset.original = original;
  }
  if (yearCell) yearCell.textContent = year;
}

function escapeHtml(s) {
  return String(s).replace(/[&<>"']/g, c => (
    c === '&' ? '&amp;' :
    c === '<' ? '&lt;' :
    c === '>' ? '&gt;' :
    c === '"' ? '&quot;' : '&#39;'
  ));
}

function formatSize(bytes) {
  const b = Number(bytes || 0);
  if (!b) return '0 B';
  const units = ['B','KB','MB','GB','TB'];
  let i = 0;
  let v = b;
  while (v >= 1024 && i < units.length - 1) {
    v /= 1024;
    i++;
  }
  return `${v.toFixed(1)} ${units[i]}`;
}

function formatDuration(ms) {
  const m = Number(ms || 0);
  if (!m) return t('common.na','n/a');
  const total = Math.floor(m / 1000);
  const h = Math.floor(total / 3600);
  const min = Math.floor((total % 3600) / 60);
  const s = total % 60;
  return `${h}:${String(min).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
}

function formatSpeed(bytesPerSec) {
  return `${UI.formatBytes(bytesPerSec)}/s`;
}

function formatDate(ts) {
  const n = Number(ts || 0);
  if (!Number.isFinite(n) || n <= 0) return t('common.na', 'n/a');
  return new Date(n * 1000).toLocaleString();
}

function formatSeconds(sec) {
  const n = Number(sec || 0);
  if (!Number.isFinite(n) || n <= 0) return t('common.na', 'n/a');
  const h = Math.floor(n / 3600);
  const m = Math.floor((n % 3600) / 60);
  const s = n % 60;
  return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
}

function openSourceDetail(item) {
  const dlg = UI.qs('dlg-source');
  if (!dlg || !item?.id || !item?.source) return;
  state.sourceDetail = { id: item.id, source: item.source };
  state.sourceDetailItem = item;
  const title = UI.qs('sourceTitle');
  const meta = UI.qs('sourceMeta');
  const fieldsWrap = UI.qs('sourceFields');
  const raw = UI.qs('sourceRaw');
  if (title) title.textContent = t('sources.detail.title', 'Source detail');
  if (meta) meta.textContent = t('common.loading', 'Loading…');
  if (fieldsWrap) fieldsWrap.innerHTML = '';
  if (raw) raw.textContent = '';

  fetchSourceDetail(item.source, item.id);
  if (typeof dlg.showModal === 'function') dlg.showModal();
}

function refreshOpenSourceDetail() {
  if (!state.sourceDetail) return;
  const now = Date.now();
  if (now - state.sourceDetailLastFetch < 1500) return;
  fetchSourceDetail(state.sourceDetail.source, state.sourceDetail.id);
}

function fetchSourceDetail(source, id) {
  state.sourceDetailLastFetch = Date.now();
  const title = UI.qs('sourceTitle');
  const meta = UI.qs('sourceMeta');
  const fieldsWrap = UI.qs('sourceFields');
  const raw = UI.qs('sourceRaw');
  const fallback = state.sourceDetailItem || {};
  api('/api/sources/detail?source=' + encodeURIComponent(source) + '&id=' + encodeURIComponent(id))
    .then(res => {
      if (!res || res.ok !== true) {
        const msg = res?.error?.message || res?.error || 'Request failed';
        throw new Error(msg);
      }
      const data = res.data || {};
      state.sourceDetailData = data;
      const core = data.core || {};
      if (title) title.textContent = `${core.name || fallback.name || ''}`;
      if (meta) {
        const pct = Math.round((core.percent_done || 0) * 100);
        meta.innerHTML = `
          <div>${t('sources.source','Source')}: ${core.source || fallback.source || ''}</div>
          <div>${t('sources.status','Status')}: ${core.status || fallback.status || ''}</div>
          <div>${t('sources.size','Size')}: ${UI.formatBytes(core.size_bytes || 0)}</div>
          <div>${t('sources.progress','Progress')}: ${pct}%</div>
        `;
      }
      if (fieldsWrap) {
        const fields = Array.isArray(data.fields) ? data.fields : [];
        fieldsWrap.innerHTML = '';
        fields.forEach(f => {
          const val = f.value;
          let display = '';
          if (f.type === 'bytes') display = UI.formatBytes(val);
          else if (f.type === 'speed') display = formatSpeed(val);
          else if (f.type === 'date') display = formatDate(val);
          else if (f.type === 'seconds') display = formatSeconds(val);
          else if (f.type === 'list') display = Array.isArray(val) ? val.join(', ') : '';
          else display = (val ?? '').toString();
          const card = document.createElement('div');
          card.className = 'source-field';
          card.innerHTML = `<div class="lbl">${f.label || f.key || ''}</div><div>${display}</div>`;
          fieldsWrap.appendChild(card);
        });
        const files = Array.isArray(data.files) ? data.files : [];
        if (files.length > 0) {
          const tree = buildFileTree(files);
          const block = document.createElement('div');
          block.className = 'source-files';
          block.innerHTML = `<div class="lbl">${t('sources.files', 'Files')}</div>`;
          block.appendChild(renderFileTree(tree));
          fieldsWrap.appendChild(block);
        }
        const preview = data.preview || null;
        if (preview) {
          const block = document.createElement('div');
          block.className = 'source-preview';
          block.innerHTML = `
            <div class="lbl">${t('sources.preview.title','Preview')}</div>
            <div class="preview-grid">
              <div class="preview-col">
                <div class="preview-title">${t('sources.preview.current','Current')}</div>
                <div>${t('sources.preview.name','Name')}: ${preview.current?.name || ''}</div>
                <div>${t('sources.preview.kind','Type')}: ${preview.current?.kind || ''}</div>
                <div>${t('sources.preview.structure','Structure')}: ${preview.current?.structure || ''}</div>
              </div>
              <div class="preview-col">
                <div class="preview-title">${t('sources.preview.planned','Planned')}</div>
                <div>${t('sources.preview.name','Name')}: ${preview.planned?.name || ''}</div>
                <div>${t('sources.preview.kind','Type')}: ${preview.planned?.kind || ''}</div>
                <div>${t('sources.preview.structure','Structure')}: ${preview.planned?.structure || ''}</div>
                ${preview.planned?.note ? `<div class="hint">${preview.planned?.note}</div>` : ''}
              </div>
            </div>
          `;
          fieldsWrap.appendChild(block);
        }
      }
  if (raw) {
    raw.textContent = JSON.stringify(data.raw || {}, null, 2);
  }
    })
    .catch(e => {
      if (meta) meta.textContent = `${t('common.error','Error')}: ${e.message}`;
    });
}


function buildFileTree(files) {
  const root = { name: '', children: new Map(), files: [] };
  files.forEach(f => {
    const path = (f.path || '').split('/').filter(Boolean);
    let node = root;
    for (let i = 0; i < path.length; i++) {
      const part = path[i];
      if (i === path.length - 1) {
        node.files.push({ name: part, meta: f });
      } else {
        if (!node.children.has(part)) {
          node.children.set(part, { name: part, children: new Map(), files: [] });
        }
        node = node.children.get(part);
      }
    }
  });
  return root;
}

function renderFileTree(node) {
  const ul = document.createElement('ul');
  ul.className = 'file-tree';
  node.children.forEach(child => {
    const li = document.createElement('li');
    li.innerHTML = `<span class="file-folder">${child.name}</span>`;
    li.appendChild(renderFileTree(child));
    ul.appendChild(li);
  });
  node.files.forEach(f => {
    const li = document.createElement('li');
    const pct = Number.isFinite(f.meta?.percent_done) ? f.meta.percent_done : 0;
    const pctText = pct ? ` (${pct}%)` : '';
    li.innerHTML = `<span class="file-item">${f.name}</span><span class="file-meta">${pctText}</span>`;
    ul.appendChild(li);
  });
  return ul;
}
/* ---------------------------
  Jobs: global indicator + persistence
--------------------------- */

function setActiveJob(jobId) {
  state.activeJobId = jobId ? String(jobId) : null;
  if (state.activeJobId) {
    localStorage.setItem(LS_ACTIVE_JOB, state.activeJobId);
  } else {
    localStorage.removeItem(LS_ACTIVE_JOB);
  }
  updateJobIndicatorVisible(!!state.activeJobId);
}

function updateJobIndicatorVisible(show) {
  const el = UI.qs('job-indicator');
  if (!el) return;
  if (show) el.classList.remove('hidden');
  else el.classList.add('hidden');
}

function updateJobIndicator(job) {
  const p = UI.qs('job-indicator-progress');
  if (!p) return;

  const total = Number(job.progress_total || 0);
  const cur = Number(job.progress || 0);
  const pct = total > 0 ? Math.floor((cur / total) * 100) : 0;

  p.max = 100;
  p.value = pct;
}

function updateQueueSummary(data) {
  const summaryEl = UI.qs('queueSummary');
  if (!summaryEl) return;
  const activeEl = summaryEl.querySelector('[data-queue-active]');
  const errorsEl = summaryEl.querySelector('[data-queue-errors]');
  const dividerEl = summaryEl.querySelector('[data-queue-divider]');
  if (activeEl || errorsEl) {
    const activeLabel = activeEl?.dataset.queueActiveLabel || t('queue.active', 'Active');
    const errorsLabel = errorsEl?.dataset.queueErrorsLabel || t('queue.errors', 'Errors');
    const activeCount = Number(data.running || 0);
    const errorsCount = Number(data.errors || 0);
    if (activeEl) activeEl.textContent = `${activeLabel}: ${activeCount}`;
    if (errorsEl) {
      errorsEl.textContent = `${errorsLabel}: ${errorsCount}`;
      errorsEl.classList.toggle('is-hidden', errorsCount <= 0);
    }
    if (dividerEl) dividerEl.classList.toggle('is-hidden', errorsCount <= 0);
    return;
  }
  const textEl = summaryEl.querySelector('[data-queue-text]');
  const template = t('queue.summary', 'Active: {active} | Errors: {errors}');
  const text = template
    .replace('{active}', String(data.running || 0))
    .replace('{errors}', String(data.errors || 0));
  if (textEl) {
    textEl.textContent = text;
  } else {
    summaryEl.textContent = text;
  }
}

function isQueueMenuOpen() {
  const menu = UI.qs('queueMenu');
  return !!menu && !menu.classList.contains('is-hidden');
}

function toggleQueueMenu() {
  const menu = UI.qs('queueMenu');
  if (!menu) return;
  menu.classList.toggle('is-hidden');
  if (!menu.classList.contains('is-hidden')) {
    loadQueueStatus();
    loadQueueRecent();
  }
}

async function cancelJobById(id) {
  if (!id) return;
  if (!confirm(t('job.cancel_confirm', 'Cancel job?'))) return;
  try {
    await api('/api/jobs/cancel', {
      method: 'POST',
      body: JSON.stringify({ id }),
    });
  } catch (e) {
    alert(`${t('common.error','Error')}: ${e.message}`);
  }
}

async function loadQueueRecent() {
  try {
    const res = await api('/api/jobs/recent');
    if (!res.ok || !res.data) return;
    state.queueRecentList = res.data.items || [];
    if (isQueueMenuOpen()) renderQueueMenu();
  } catch (e) {
    // ignore
  }
}

function renderQueueMenu() {
  const activeWrap = UI.qs('queueMenuActive');
  const finishedWrap = UI.qs('queueMenuFinished');
  if (!activeWrap || !finishedWrap) return;

  activeWrap.innerHTML = '';
  finishedWrap.innerHTML = '';

  if (state.queueActiveList.length === 0) {
    const empty = document.createElement('div');
    empty.className = 'queue-item-status';
    empty.textContent = t('queue.none_active', 'No active tasks');
    activeWrap.appendChild(empty);
  } else {
    state.queueActiveList.forEach(j => {
      const row = document.createElement('div');
      row.className = 'queue-item';
      const title = document.createElement('div');
      const pct = j.progress_total > 0 ? Math.floor((j.progress / j.progress_total) * 100) : 0;
      title.textContent = `${j.title} (${pct}%)`;
      const actions = document.createElement('div');
      actions.className = 'queue-item-actions';
      const btn = document.createElement('button');
      btn.className = 'btn';
      btn.textContent = t('queue.cancel', 'Cancel');
      btn.addEventListener('click', () => cancelJobById(j.id));
      actions.appendChild(btn);
      row.appendChild(title);
      row.appendChild(actions);
      activeWrap.appendChild(row);
    });
  }

  if (state.queueRecentList.length === 0) {
    const empty = document.createElement('div');
    empty.className = 'queue-item-status';
    empty.textContent = t('queue.none_finished', 'No finished tasks');
    finishedWrap.appendChild(empty);
  } else {
    state.queueRecentList.forEach(j => {
      const row = document.createElement('div');
      row.className = 'queue-item';
      const title = document.createElement('div');
      title.textContent = j.title || '';
      const status = document.createElement('div');
      status.className = 'queue-item-status';
      status.textContent = j.status || '';
      row.appendChild(title);
      row.appendChild(status);
      finishedWrap.appendChild(row);
    });
  }
  updateSseStats();
}

async function loadQueueStatus() {
  try {
    const res = await api('/api/jobs/status');
    if (!res.ok || !res.data) return;
    const data = res.data;
    state.background.paused = !!data.paused;
    state.queueActiveList = Array.isArray(data.active_list) ? data.active_list : [];
    updateQueueSummary(data);
    if (data.active) updateJobIndicator(data.active);
    if (isQueueMenuOpen()) renderQueueMenu();
  } catch (e) {
    // ignore
  }
}

function initQueuePanel() {
  const summary = UI.qs('queueSummary');
  if (summary) {
    summary.addEventListener('click', toggleQueueMenu);
  }
  document.addEventListener('click', (e) => {
    const menu = UI.qs('queueMenu');
    if (!menu) return;
    if (summary && (summary.contains(e.target) || menu.contains(e.target))) return;
    menu.classList.add('is-hidden');
  });

  updateQueueSummary({ running: 0, errors: 0 });
  loadQueueStatus();
}

function initWatchdog() {
  const minutes = Number(state.background.watchdog_minutes || 0);
  if (minutes <= 0) return;
  const intervalMs = Math.max(1, Math.floor(minutes / 2)) * 60 * 1000;
  if (state.watchdogTimer) clearInterval(state.watchdogTimer);
  state.watchdogTimer = setInterval(() => {
    api('/api/jobs/watchdog', {
      method: 'POST',
      body: JSON.stringify({ minutes }),
    }).catch(() => {});
  }, intervalMs);
}

function initSourcesPolling() {
  if (state.sourcesPollTimer) clearInterval(state.sourcesPollTimer);
}

function claimSseLease() {
  if (!state.sse.tabId) {
    const existing = sessionStorage.getItem('scmedia_sse_tab_id');
    state.sse.tabId = existing || Math.random().toString(36).slice(2);
    sessionStorage.setItem('scmedia_sse_tab_id', state.sse.tabId);
  }
  const now = Date.now();
  const ttlMs = 15000;
  const raw = localStorage.getItem(state.sse.leaseKey) || '';
  let lease = null;
  try {
    lease = raw ? JSON.parse(raw) : null;
  } catch (e) {
    lease = null;
  }
  if (lease && lease.ts && (now - lease.ts) < ttlMs && lease.id !== state.sse.tabId) {
    return false;
  }
  localStorage.setItem(state.sse.leaseKey, JSON.stringify({ ts: now, id: state.sse.tabId }));
  return true;
}

function refreshSseLease() {
  localStorage.setItem(state.sse.leaseKey, JSON.stringify({ ts: Date.now(), id: state.sse.tabId }));
}

function releaseSseLease() {
  const raw = localStorage.getItem(state.sse.leaseKey) || '';
  try {
    const lease = raw ? JSON.parse(raw) : null;
    if (lease && lease.id && lease.id !== state.sse.tabId) {
      return;
    }
  } catch (e) {
    // ignore
  }
  localStorage.removeItem(state.sse.leaseKey);
}

function initEventsPipe() {
  if (!window.EventSource) return false;
  if (!claimSseLease()) {
    return false;
  }
  try {
    if (state.eventsSource) {
      state.eventsSource.close();
      state.eventsSource = null;
    }
    const open = async () => {
      if (!window.Auth?.getAccessToken?.() && window.Auth?.refreshTokens) {
        await window.Auth.refreshTokens();
      }
      const res = await window.Http.apiJson('/api/auth/sse-key', { method: 'POST' });
      const key = res?.data?.key || '';
      const ttl = Number(res?.data?.expires_in || 60);
      if (!res?.ok || !key) {
        initSourcesPolling();
        return;
      }
      document.cookie = `scmedia_sse_key=${encodeURIComponent(key)}; path=/; max-age=${Math.max(10, ttl)}`;
      await new Promise((r) => setTimeout(r, 50));
      const es = new EventSource('/api/events');
      state.eventsSource = es;
      es.addEventListener('open', () => {
        setSseConnected(true);
        state.sse.retryDelayMs = 5000;
        refreshSseLease();
        if (state.queuePollTimer) {
          clearInterval(state.queuePollTimer);
          state.queuePollTimer = null;
        }
        if (state.sourcesPollTimer) {
          clearInterval(state.sourcesPollTimer);
          state.sourcesPollTimer = null;
        }
        loadQueueStatus();
      });
      es.addEventListener('jobs', (e) => {
        try {
          const data = JSON.parse(e.data);
          noteSseEvent('jobs');
          refreshSseLease();
          state.queueActiveList = Array.isArray(data.active_list) ? data.active_list : [];
          updateQueueSummary(data);
          if (data.active) updateJobIndicator(data.active);
          if (isQueueMenuOpen()) renderQueueMenu();
        } catch (err) {
          // ignore
        }
      });
      es.addEventListener('sources', (e) => {
        try {
          const payload = JSON.parse(e.data);
          noteSseEvent('sources');
          refreshSseLease();
          const items = Array.isArray(payload.items) ? payload.items : [];
          const removed = Array.isArray(payload.removed) ? payload.removed : [];
          const isSnapshot = !!payload.snapshot;

          if (!Array.isArray(state.sourcesCache) || isSnapshot) {
            state.sourcesCache = items;
          } else {
            const map = new Map();
            state.sourcesCache.forEach(it => {
              const key = `${it.source || ''}:${it.id || ''}`;
              if (key !== ':') map.set(key, it);
            });
            items.forEach(it => {
              const key = `${it.source || ''}:${it.id || ''}`;
              if (key !== ':') map.set(key, it);
            });
            removed.forEach(key => map.delete(key));
            state.sourcesCache = Array.from(map.values());
          }

          if (state.mediaTab === 'sources') {
            const sourcesTable = state.tables.sources;
            if (sourcesTable) sourcesTable.load(sourcesTable.page || 1, false);
          }
          refreshOpenSourceDetail();
        } catch (err) {
          // ignore
        }
      });
      es.addEventListener('tick', () => {
        noteSseEvent('tick');
        refreshSseLease();
      });
      es.addEventListener('error', () => {
        if (state.eventsSource) state.eventsSource.close();
        state.eventsSource = null;
        state.sse.reconnects += 1;
        setSseConnected(false);
        const delay = state.sse.retryDelayMs;
        state.sse.retryDelayMs = Math.min(state.sse.retryDelayMs * 2, 60000);
        releaseSseLease();
        setTimeout(() => initEventsPipe(), delay);
      });
    };
    open().catch(() => {
      initSourcesPolling();
      releaseSseLease();
    });
    return true;
  } catch (e) {
    state.eventsSource = null;
    releaseSseLease();
    return false;
  }
}

async function restoreJobFromStorage() {
  const id = localStorage.getItem(LS_ACTIVE_JOB);
  if (!id) return;

  setActiveJob(id);

  // start polling silently
  if (state.jobPollTimer) clearInterval(state.jobPollTimer);
  state.jobPollTimer = setInterval(async () => {
    try {
      const res = await api('/api/jobs/get?id=' + encodeURIComponent(id));
      if (!res.ok || !res.job) return;

      updateJobIndicator(res.job);

      if (res.job.status === 'done' || res.job.status === 'error' || res.job.status === 'canceled') {
        clearInterval(state.jobPollTimer);
        state.jobPollTimer = null;
        setActiveJob(null);
      }
    } catch (e) {
      // ignore
    }
  }, 1200);
}

/* ---------------------------
  Scan
--------------------------- */

function openJobDialog() {
  const dlg = UI.qs('dlg-job');
  if (dlg && typeof dlg.showModal === 'function') dlg.showModal();
}

async function cancelActiveJob() {
  if (!state.activeJobId) return;
  if (!confirm(t('job.cancel_confirm', 'Cancel job?'))) return;
  try {
    await api('/api/jobs/cancel', {
      method: 'POST',
      body: JSON.stringify({ id: state.activeJobId }),
    });
  } catch (e) {
    alert(`${t('common.error','Error')}: ${e.message}`);
  }
}

async function runScan() {
  // Prefer job-based scan. If backend returns job_id, we track progress.
  const res = await api('/api/items/scan', { method: 'POST', body: '{}' });

  if (!res.ok) {
    alert(t('errors.scan_failed', 'Scan failed') + ': ' + (res.error || ''));
    return;
  }

  if (res.job_id) {
    openJobDialog();
    await watchJob(res.job_id, { autoClose: true });
    await loadMediaList();
    return;
  }

  // Fallback (legacy): scan completes immediately
  alert(t('messages.scan_finished', 'Scan finished'));
  await loadMediaList();
}

async function watchJob(jobId, opts = {}) {
  const statusEl = UI.qs('job-status');
  const prog = UI.qs('job-progress');
  const logEl = UI.qs('job-log');

  setActiveJob(jobId);

  for (;;) {
    const res = await api('/api/jobs/get?id=' + encodeURIComponent(jobId));
    if (!res.ok) {
      if (statusEl) statusEl.textContent = t('errors.job_fetch', 'Error fetching job');
      setActiveJob(null);
      return;
    }

    const job = res.job;
    updateJobIndicator(job);

    const total = Number(job.progress_total || 0);
    const cur = Number(job.progress || 0);
    const pct = total > 0 ? Math.floor((cur / total) * 100) : 0;

    if (statusEl) {
      statusEl.textContent =
        `${t('job.status', 'Status')}: ${job.status} | ${cur}/${total}`;
    }

    if (prog) {
      prog.max = 100;
      prog.value = pct;
    }

    if (logEl) {
      logEl.textContent = job.log_text || '';
    }

    if (job.status === 'done' || job.status === 'error' || job.status === 'canceled') {
      setActiveJob(null);

      if (opts.autoClose) {
        // do not force-close dialog; user can read log
      }
      return;
    }

    await sleep(1000);
  }
}

async function runDryRun() {
  const dlg = UI.qs('dlg-dry-run');
  const out = UI.qs('dry-run-log');
  if (out) out.textContent = t('common.loading', 'Loading…');
  if (dlg && typeof dlg.showModal === 'function') dlg.showModal();

  const q = new URLSearchParams();
  q.set('tab', state.mediaTab || 'movies');
  const res = await api('/api/media/dry-run?' + q.toString());
  if (!res.ok || !res.data) {
    if (out) out.textContent = t('common.error', 'Error');
    return;
  }

  const data = res.data;
  let text = '';
  text += `${t('media.dry_run.summary','Summary')}\n`;
  text += `${t('media.dry_run.files','Files')}: ${data.files}\n`;
  text += `${t('media.dry_run.rename','Rename')}: ${data.rename}\n`;
  text += `${t('media.dry_run.delete','Delete')}: ${data.delete}\n`;
  text += `${t('media.dry_run.unknown','Unknown type')}: ${data.unknown_type}\n`;
  text += `${t('media.dry_run.convert','Convert')}: ${data.convert}\n\n`;
  text += `${t('media.dry_run.details','Details')}\n`;
  for (const row of data.rows || []) {
    text += `- ${row.name} | ${row.actions.join(', ')}\n`;
  }
  if (out) out.textContent = text;
}

async function runApply() {
  const res = await api('/api/media/apply', {
    method: 'POST',
    body: JSON.stringify({ tab: state.mediaTab || 'movies' }),
  });

  if (!res.ok) {
    alert(t('errors.apply_failed', 'Apply failed') + ': ' + (res.error || ''));
    return;
  }

  if (res.job_id) {
    openJobDialog();
    await watchJob(res.job_id, { autoClose: true });
    await loadMediaList();
  }
}

/* ---------------------------
  Init
--------------------------- */

function init() {
  if (window.Auth?.requireAuth) {
    window.Auth.requireAuth();
  }
  window.UI?.initHeader?.();
  state.i18n = window.I18N || {};
  state.lang = window.APP_LANG || 'en';
  setVersion();
  if (window.UserPrefs?.load) {
    window.UserPrefs.load().then((prefs) => {
      if (prefs?.theme && window.UI?.setTheme) {
        window.UI.setTheme(prefs.theme);
      }
    }).catch(() => {});
  }
  UI.initThemeToggle();
  updateThemeLabel();
  UI.bindThemePreference?.(() => updateThemeLabel());
  initSourceDialog();
  if (UI.qs('btn-scan')) UI.qs('btn-scan').addEventListener('click', runScan);
  if (UI.qs('btnJobCancel')) UI.qs('btnJobCancel').addEventListener('click', cancelActiveJob);

  restoreJobFromStorage();
  initMediaTabs();
  updateTabVisibility();
  loadUiSettings()
    .then(() => {
      initTables();
      if (window.Sse?.on) {
        window.Sse.on('jobs', (data) => {
          if (!data) return;
          noteSseEvent('jobs');
          state.queueActiveList = Array.isArray(data.active_list) ? data.active_list : [];
          updateQueueSummary(data);
          if (data.active) updateJobIndicator(data.active);
          if (isQueueMenuOpen()) renderQueueMenu();
        });
        window.Sse.on('sources', (payload) => {
          if (!payload) return;
          noteSseEvent('sources');
          const items = Array.isArray(payload.items) ? payload.items : [];
          const removed = Array.isArray(payload.removed) ? payload.removed : [];
          const isSnapshot = !!payload.snapshot;
          if (!Array.isArray(state.sourcesCache) || isSnapshot) {
            state.sourcesCache = items;
          } else {
            const map = new Map();
            state.sourcesCache.forEach(it => {
              const key = `${it.source || ''}:${it.id || ''}`;
              if (key !== ':') map.set(key, it);
            });
            items.forEach(it => {
              const key = `${it.source || ''}:${it.id || ''}`;
              if (key !== ':') map.set(key, it);
            });
            removed.forEach(key => map.delete(key));
            state.sourcesCache = Array.from(map.values());
          }
          if (state.mediaTab === 'sources') {
            const sourcesTable = state.tables.sources;
            if (sourcesTable) sourcesTable.load(sourcesTable.page || 1, false);
          }
          refreshOpenSourceDetail();
        });
        window.Sse.on('tick', () => {
          noteSseEvent('tick');
        });
      }
      window.Sse?.start?.();
      initWatchdog();
      loadMediaList();
    })
    .catch(() => {
      initTables();
      window.Sse?.start?.();
      initWatchdog();
      loadMediaList();
    });
}

init();

window.addEventListener('pagehide', () => {
  window.Sse?.stop?.();
});

function initMediaTabs() {
  const tabs = document.querySelectorAll('#media-tabs .tab');
  tabs.forEach(btn => {
    btn.addEventListener('click', () => {
      tabs.forEach(x => x.classList.remove('active'));
      btn.classList.add('active');
      state.mediaTab = btn.dataset.tab || 'movies';
      updateTabVisibility();
      loadMediaList();
    });
  });
}

function updateTabVisibility() {
  const grid = UI.qs('grid');
  const sourcesGrid = UI.qs('sources-grid');
  const showSources = state.mediaTab === 'sources';
  const gridWrap = grid?.closest('.table-wrap');
  const sourcesWrap = sourcesGrid?.closest('.table-wrap');
  if (gridWrap) gridWrap.classList.toggle('is-hidden', showSources);
  if (sourcesWrap) sourcesWrap.classList.toggle('is-hidden', !showSources);
}
