/* public/assets/settings.js */

const state = {
  settings: null,
  dirty: false,
  mediaRules: null,
  rulesList: [],
  templatesList: [],
  tasksList: [],
  toolsList: [],
  toolEditType: null,
  ruleEditId: null,
  ruleEditType: null,
  rootsList: [],
  rootEditId: null,
  taskEditId: null,
  templateEditId: null,
  logsView: {
    from: '',
    to: '',
  },
  auditItems: [],
  auditSelected: new Map(),
  auditItemsCache: new Map(),
  logSelected: new Map(),
  logItemsCache: new Map(),
  snapshots: [],
  ui: {
    table_mode: 'pagination',
    table_page_size: 50,
  },
  tables: {},
  settingsLoadPromise: null,
};

const AUDIT_EVENTS = [
  'register',
  'login_failed',
  'login_mfa_required',
  'login',
  'mfa_failed',
  'refresh',
  'logout',
  'mfa_setup',
  'mfa_enabled',
  'mfa_disabled',
  'password_reset_requested',
  'password_reset',
  'roles_changed',
  'user_disabled',
  'user_enabled',
  'mfa_reset',
  'forced_logout',
  'password_change',
  'password_change_failed',
  'email_change',
];

const i18n = {
  lang: 'ru',
  dict: {},
};

function t(key, fallback) {
  const v = i18n.dict && Object.prototype.hasOwnProperty.call(i18n.dict, key) ? i18n.dict[key] : null;
  return (typeof v === 'string' && v.length) ? v : (fallback ?? key);
}

function setStatus(text) {
  const el = UI.qs('#status');
  if (el) el.textContent = text;
}

function setDirty(d) {
  state.dirty = d;
  const btn = UI.qs('#btnSave');
  if (btn) btn.disabled = !d;
  const statusEl = UI.qs('#status');

  if (d) {
    setStatus(t('common.unsaved_changes', 'Unsaved changes'));
    if (statusEl) statusEl.classList.add('dirty');
    localStorage.setItem('scmedia_settings_dirty', '1');
  }
  if (!d) {
    if (statusEl) statusEl.classList.remove('dirty');
    localStorage.removeItem('scmedia_settings_dirty');
  }
}

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;
  return {
    items: items.slice(start, start + size),
    total,
    page: p,
    per_page: size,
  };
}

function sortItems(items, sortKey, dir, valueMap) {
  if (!sortKey || !valueMap?.[sortKey]) return items;
  const getter = valueMap[sortKey];
  const asc = dir !== 'desc';
  const out = [...items];
  out.sort((a, b) => {
    const av = getter(a);
    const bv = getter(b);
    if (av === bv) return 0;
    if (asc) return av < bv ? -1 : 1;
    return av > bv ? -1 : 1;
  });
  return out;
}

function buildSourceOptions(selected) {
  const out = [];
  const sourcesCfg = state.pluginConfig?.sources || {};
  if (sourcesCfg.transmission) {
    out.push({ value: 'transmission', label: t('settings.plugins.transmission_label', 'Transmission') });
  }
  if (selected && !out.some(o => o.value === selected)) {
    out.push({ value: selected, label: selected });
  }
  if (out.length === 0) {
    out.push({ value: 'transmission', label: t('settings.plugins.transmission_label', 'Transmission') });
  }
  return out;
}

function renderSourceOptions(selected) {
  return buildSourceOptions(selected)
    .map(o => `<option value="${escapeHtml(o.value)}">${escapeHtml(o.label)}</option>`)
    .join('');
}

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

async function api(url, method = 'GET', body = null) {
  const opts = { method };
  if (body !== null) {
    opts.body = body;
  }
  const res = await window.Api.request(url, opts);
  return res.data;
}

function commaToList(s) {
  const t = (s || '').split(',').map(x => x.trim()).filter(Boolean);
  return Array.from(new Set(t));
}

function listToComma(a) {
  if (!Array.isArray(a)) return '';
  return a.join(',');
}

function formatBytes(n) {
  const v = Number(n || 0);
  if (!isFinite(v) || v <= 0) return '';
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  let idx = 0;
  let cur = v;
  while (cur >= 1024 && idx < units.length - 1) {
    cur /= 1024;
    idx += 1;
  }
  return `${cur.toFixed(cur >= 10 ? 0 : 1)} ${units[idx]}`;
}

function initTabs() {
  UI.qsa('nav.tabs .tab').forEach(btn => {
    btn.addEventListener('click', () => {
      console.log('[debug] tab click', btn.dataset.tab);
      UI.qsa('nav.tabs .tab').forEach(x => x.classList.remove('active'));
      UI.qsa('.tabpane').forEach(x => x.classList.remove('active'));

      btn.classList.add('active');
      const pane = UI.qs(`#pane-${btn.dataset.tab}`);
      if (pane) pane.classList.add('active');
      if (btn.dataset.tab === 'debug') {
        console.log('[debug] tab debug: loadDebugStats');
        loadDebugStats().catch(() => {});
      }
    });
  });
}

function enforceAdminAccess() {
  const auth = window.Auth;
  if (!auth) return;
  const access = auth.getAccessToken?.();
  const refresh = auth.getRefreshToken?.();
  if (!access && refresh && auth.refreshTokens) {
    auth.refreshTokens().then((ok) => {
      if (!ok || (auth.isAdmin && !auth.isAdmin())) {
        window.location.href = '/';
      }
    });
    return;
  }
  if (auth.isAdmin && !auth.isAdmin()) {
    window.location.href = '/';
  }
}

function bindDirtyInputs() {
  // NOTE: language switch should NOT mark settings dirty
  UI.qsa('input,select').forEach(el => {
    if (el.dataset.local === 'true') return;
    if (el.closest('#ruleModal')) return;

    el.addEventListener('change', () => setDirty(true));
    el.addEventListener('input', () => setDirty(true));
  });
}

function setVersion() {
  const el = UI.qs('[data-version] .version');
  if (el && typeof APP_VERSION === 'string') {
    el.textContent = `v${APP_VERSION}`;
  }
}

function updateAboutVersions(meta) {
  const uiVersion = (typeof APP_VERSION === 'string' && APP_VERSION) ? APP_VERSION : '';
  const backendVersion = meta?.backend_version || '';
  const dbVersion = meta?.db_version || '';

  const pluginsTable = UI.qs('#aboutPluginsTable tbody');
  if (pluginsTable) {
    const plugins = [
      { name: 'UI', type: 'Core', author: 'SAFE-CAP', version: uiVersion || '-', update: '-' },
      { name: 'Backend', type: 'Core', author: 'SAFE-CAP', version: backendVersion || '-', update: '-' },
      { name: 'Database', type: 'Core', author: 'SAFE-CAP', version: dbVersion || '-', update: '-' },
      { name: 'OMDb', type: 'Metadata', author: 'OMDb API', version: '0.1.0', update: '-' },
      { name: 'TVDB', type: 'Metadata', author: 'TheTVDB', version: '0.1.0', update: '-' },
      { name: 'Kodi', type: 'Export', author: 'Kodi', version: '0.1.0', update: '-' },
      { name: 'Jellyfin', type: 'Export', author: 'Jellyfin', version: '0.1.0', update: '-' },
      { name: 'Transmission', type: 'Source', author: 'Transmission', version: '0.1.0', update: '-' },
    ];
    pluginsTable.innerHTML = '';
    for (const p of plugins) {
      const tr = document.createElement('tr');
      const tdName = document.createElement('td');
      tdName.textContent = p.name;
      const tdType = document.createElement('td');
      tdType.textContent = p.type;
      const tdAuthor = document.createElement('td');
      tdAuthor.textContent = p.author;
      const tdVersion = document.createElement('td');
      tdVersion.textContent = p.version;
      const tdUpdate = document.createElement('td');
      tdUpdate.textContent = p.update;
      tr.append(tdName, tdType, tdAuthor, tdVersion, tdUpdate);
      pluginsTable.appendChild(tr);
    }
  }
}

/* ---------------------------
  Settings UI mapping
--------------------------- */

function applySettingsToUI(all, userUi = null) {
  const data = all.data || all; // tolerate nesting
  state.settings = data;

  // Debug visibility
  const meta = data.meta || {};
  if (meta.debug_tools_enabled) {
    UI.qs('#tabDebug').style.display = 'block';
  }
  if (!meta.allow_db_reset) {
    UI.qs('#btnResetDb').disabled = true;
    const restoreBtn = UI.qs('#btnDbRestore');
    if (restoreBtn) restoreBtn.disabled = true;
  }
  updateAboutVersions(meta);
  const envEl = UI.qs('#serverEnv');
  if (envEl) envEl.value = meta.env || '';
  const appIdEl = UI.qs('#serverAppId');
  if (appIdEl) appIdEl.value = meta.app_id || '';
  const debugEl = UI.qs('#serverDebug');
  if (debugEl) debugEl.value = meta.debug_tools_enabled ? 'true' : 'false';
  const backendEl = UI.qs('#serverBackendVersion');
  if (backendEl) backendEl.value = meta.backend_version || '';
  const dbVerEl = UI.qs('#serverDbVersion');
  if (dbVerEl) dbVerEl.value = meta.db_version || '';

  // Paths (roots list)
  const paths = data.paths || {};
  state.rootsList = buildRootsList(paths);
  renderRootsList();

  // Layout (rules moved to Rules tab)
  const layout = data.layout || {};
  const cp = layout.collision_policy || 'stop';
  const collisionEl = UI.qs('#collisionPolicy');
  if (collisionEl) collisionEl.value = cp;

  // Tools list
  const tools = data.tools || {};
  state.toolsList = buildToolsList(tools);
  renderToolsList();

  // Logs
  const logs = data.logs || {};
  UI.qs('#logsRetention').value = String(logs.retention_days ?? 7);
  UI.qs('#logsLevel').value = logs.level || 'info';

  initLogsViewDates();

  // Rules list (new UI)
  const rulesList = Array.isArray(data.rules) ? data.rules : [];
  state.rulesList = rulesList.map(r => normalizeRule(r));
  renderRulesList();

  // Templates list
  const templatesList = Array.isArray(data.templates) ? data.templates : [];
  state.templatesList = templatesList.map(tpl => normalizeTemplate(tpl));
  renderTemplatesList();

  // Tasks list
  const tasks = Array.isArray(data.tasks) ? data.tasks : [];
  state.tasksList = tasks.map(t => normalizeTask(t));
  renderTasksList();

  // Plugins
  state.pluginConfig = {
    metadata: data.metadata || {},
    exports: data.exports || {},
    sources: data.sources || {},
  };
  normalizePluginConfig();
  renderPluginsList();

  // UI settings (per-user, stored in account prefs)
  const ui = userUi || {};
  const tableMode = ui.table_mode || 'pagination';
  const tableSize = ui.table_page_size || 50;
  state.ui = { table_mode: tableMode, table_page_size: tableSize };
  const serverUi = data.ui || {};
  UI.qs('#uiSseTickSeconds').value = String(serverUi.sse_tick_seconds ?? 10);

  // Background policies
  const bg = data.background || {};
  const bgMode = UI.qs('#bgMode');
  if (bgMode) bgMode.value = bg.mode || 'light';
  const bgParallel = UI.qs('#bgMaxParallel');
  if (bgParallel) bgParallel.value = String(bg.max_parallel_jobs ?? 1);
  const bgNetwork = UI.qs('#bgMaxNetwork');
  if (bgNetwork) bgNetwork.value = String(bg.max_network_jobs ?? 1);
  const bgIo = UI.qs('#bgMaxIo');
  if (bgIo) bgIo.value = String(bg.max_io_jobs ?? 1);
  const bgSleep = UI.qs('#bgBatchSleep');
  if (bgSleep) bgSleep.value = String(bg.batch_sleep_ms ?? 500);
  const bgWatchdog = UI.qs('#bgWatchdog');
  if (bgWatchdog) bgWatchdog.value = String(bg.watchdog_minutes ?? 10);
  const bgSseTtl = UI.qs('#bgSseTtl');
  if (bgSseTtl) bgSseTtl.value = String(bg.sse_session_ttl_seconds ?? 20);

  const safety = data.safety || {};
  const safetyDepth = UI.qs('#safetyMaxDepth');
  if (safetyDepth) safetyDepth.value = String(safety.max_depth ?? 10);
  const safetyFiles = UI.qs('#safetyMaxFiles');
  if (safetyFiles) safetyFiles.value = String(safety.max_files_per_item ?? 200000);
  const safetyItems = UI.qs('#safetyMaxItems');
  if (safetyItems) safetyItems.value = String(safety.max_items_per_scan ?? 1000000);

  setDirty(false);
}

function collectSettingsFromUI() {
  const meta = state.settings?.meta || {};
  const rev = meta.settings_revision ?? 1;

  const paths = buildPathsPayload();

  const layout = {
    collision_policy: UI.qs('#collisionPolicy')?.value || 'stop',
  };

  const tools = {
    mkvmerge_path: toolPathByType('mkvmerge'),
    mkvpropedit_path: toolPathByType('mkvpropedit'),
    ffmpeg_path: toolPathByType('ffmpeg'),
  };

  const logs = {
    retention_days: Number(UI.qs('#logsRetention').value || 7),
    level: UI.qs('#logsLevel').value || 'info',
  };

  const background = {
    mode: UI.qs('#bgMode')?.value || 'light',
    max_parallel_jobs: Number(UI.qs('#bgMaxParallel')?.value || 1),
    max_network_jobs: Number(UI.qs('#bgMaxNetwork')?.value || 1),
    max_io_jobs: Number(UI.qs('#bgMaxIo')?.value || 1),
    batch_sleep_ms: Number(UI.qs('#bgBatchSleep')?.value || 500),
    watchdog_minutes: Number(UI.qs('#bgWatchdog')?.value || 10),
    sse_session_ttl_seconds: Number(UI.qs('#bgSseTtl')?.value || 20),
    paused: !!state.settings?.background?.paused,
  };
  const ui = {
    sse_tick_seconds: Number(UI.qs('#uiSseTickSeconds')?.value || 10),
  };
  const safety = {
    max_depth: Number(UI.qs('#safetyMaxDepth')?.value || 10),
    max_files_per_item: Number(UI.qs('#safetyMaxFiles')?.value || 200000),
    max_items_per_scan: Number(UI.qs('#safetyMaxItems')?.value || 1000000),
  };

  const rules = state.rulesList || [];
  const templates = state.templatesList || [];
  const tasks = state.tasksList || [];

  const metadata = state.pluginConfig?.metadata || {};
  const exportsCfg = state.pluginConfig?.exports || {};
  const sources = state.pluginConfig?.sources || {};

  return {
    if_revision: rev,
    paths,
    tools,
    logs,
    layout,
    ui,
    safety,
    rules,
    templates,
    sources,
    metadata,
    exports: exportsCfg,
    background,
    tasks,
  };
}

function refreshStrategyVisibility() {
  // No-op (layout rules moved to Rules tab)
}

/* ---------------------------
  Data loading
--------------------------- */

async function loadSettings() {
  setStatus(t('common.loading_settings', 'Loading settings…'));
  const data = await api('/api/settings', 'GET');
  console.log('[debug] loadSettings', data);
  const meta = data.data?.meta || data.meta || {};
  state.debugToolsEnabled = !!meta.debug_tools_enabled;
  const prefs = window.UserPrefs ? await window.UserPrefs.load().catch(() => null) : null;

  // Prefer per-user language, fall back to server default
  const lang = (prefs?.language || window.APP_LANG || data.data?.general?.language || data.general?.language || 'ru');

  // Apply settings to UI, then set status
  applySettingsToUI(data, prefs);
  if (meta.debug_tools_enabled) {
    loadDebugStats().catch(() => {});
  }

  setStatus(t('common.loaded', 'Loaded'));
}

async function loadDiagnostics() {
  const data = await api('/api/settings/diagnostics', 'GET');
  const diag = data.data || data || {};
  const phpEl = UI.qs('#serverPhpVersion');
  if (phpEl) phpEl.value = diag.php_version || '';
  const diskEl = UI.qs('#serverDisk');
  if (diskEl) {
    const free = formatBytes(diag.disk?.free_bytes);
    const total = formatBytes(diag.disk?.total_bytes);
    diskEl.value = (free && total) ? `${free} / ${total}` : '';
  }
  const binsEl = UI.qs('#serverBinaries');
  if (binsEl) {
    const bins = diag.binaries || {};
    const parts = Object.keys(bins).map(k => `${k}:${bins[k] ? 'ok' : 'missing'}`);
    binsEl.value = parts.join(' ');
  }
  const jobsEl = UI.qs('#serverJobs');
  if (jobsEl) {
    const jobs = diag.jobs || {};
    jobsEl.value = `running=${jobs.running || 0} queued=${jobs.queued || 0} error=${jobs.errors || 0}`;
  }
}

async function loadSnapshots() {
  const data = await api('/api/settings/snapshots', 'GET');
  const items = data.data?.items || data.items || [];
  state.snapshots = items;
  renderSnapshots();
}

function renderSnapshots() {
  const tbody = UI.qs('#snapshotsTable tbody');
  if (!tbody) return;
  tbody.innerHTML = '';
  (state.snapshots || []).forEach((s) => {
    const tr = document.createElement('tr');
    tr.dataset.id = String(s.id || '');
    const tdId = document.createElement('td');
    tdId.textContent = String(s.id || '');
    const tdLabel = document.createElement('td');
    tdLabel.textContent = s.label || '';
    const tdDate = document.createElement('td');
    tdDate.textContent = s.created_at || '';
    const tdActions = document.createElement('td');
    const btnRestore = document.createElement('button');
    btnRestore.className = 'btn';
    btnRestore.textContent = t('settings.server.snapshot_restore', 'Restore');
    btnRestore.addEventListener('click', () => restoreSnapshot(Number(s.id || 0)));
    const btnDelete = document.createElement('button');
    btnDelete.className = 'btn';
    btnDelete.textContent = t('common.delete', 'Delete');
    btnDelete.addEventListener('click', () => deleteSnapshot(Number(s.id || 0)));
    tdActions.appendChild(btnRestore);
    tdActions.appendChild(btnDelete);
    tr.appendChild(tdId);
    tr.appendChild(tdLabel);
    tr.appendChild(tdDate);
    tr.appendChild(tdActions);
    tbody.appendChild(tr);
  });
}

async function createSnapshot() {
  const hint = UI.qs('#snapshotsHint');
  if (hint) hint.textContent = t('common.saving', 'Saving…');
  try {
    const label = UI.qs('#snapshotLabel')?.value || '';
    await api('/api/settings/snapshots', 'POST', { label });
    if (UI.qs('#snapshotLabel')) UI.qs('#snapshotLabel').value = '';
    await loadSnapshots();
    if (hint) hint.textContent = t('common.saved', 'Saved');
  } catch (e) {
    if (hint) hint.textContent = `${t('common.error', 'Error')}: ${e.message}`;
  }
}

async function restoreSnapshot(id) {
  if (!id) return;
  const ok = confirm(t('settings.server.snapshot_restore_confirm', 'Restore this snapshot? Current settings will be replaced.'));
  if (!ok) return;
  const hint = UI.qs('#snapshotsHint');
  if (hint) hint.textContent = t('common.running', 'Running…');
  try {
    await api('/api/settings/snapshots/restore', 'POST', { id });
    await loadSettings();
    if (hint) hint.textContent = t('common.saved', 'Saved');
  } catch (e) {
    if (hint) hint.textContent = `${t('common.error', 'Error')}: ${e.message}`;
  }
}

async function deleteSnapshot(id) {
  if (!id) return;
  const ok = confirm(t('settings.server.snapshot_delete_confirm', 'Delete this snapshot?'));
  if (!ok) return;
  const hint = UI.qs('#snapshotsHint');
  if (hint) hint.textContent = t('common.running', 'Running…');
  try {
    await api(`/api/settings/snapshots/${id}`, 'DELETE');
    await loadSnapshots();
    if (hint) hint.textContent = t('common.saved', 'Saved');
  } catch (e) {
    if (hint) hint.textContent = `${t('common.error', 'Error')}: ${e.message}`;
  }
}

/* ---------------------------
  Rules UI (new)
--------------------------- */

function normalizeRule(r) {
  const id = r?.id || `r_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  const cfg = r?.config || {};
  const conditions = Array.isArray(r?.conditions)
    ? r.conditions
    : (Array.isArray(cfg.conditions) ? cfg.conditions : []);
  const tags = Array.isArray(r?.tags)
    ? r.tags
    : (Array.isArray(cfg.tags) ? cfg.tags : []);
  return {
    id,
    name: r?.name || '',
    enabled: (r?.enabled !== false),
    logic: r?.logic || cfg.logic || 'and',
    conditions,
    tags,
  };
}

function normalizeTask(t) {
  const id = t?.id || `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  return {
    id,
    name: t?.name || '',
    enabled: t?.enabled !== false,
    sources: Array.isArray(t?.sources) ? t.sources : [],
    rule_ids: Array.isArray(t?.rule_ids) ? t.rule_ids : [],
    template_id: t?.template_id || '',
  };
}

function normalizeTemplate(tpl) {
  const id = tpl?.id || `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  return {
    id,
    name: tpl?.name || '',
    action: tpl?.action || '',
    enabled: tpl?.enabled !== false,
  };
}

function renderRuleRow(tbody, r) {
  const tr = document.createElement('tr');
  tr.dataset.id = r.id;

  const tdName = document.createElement('td');
  tdName.textContent = r.name || t('rules.unnamed', 'Untitled');

  const tdTags = document.createElement('td');
  tdTags.textContent = (r.tags || []).join(', ');

  const tdSummary = document.createElement('td');
  tdSummary.textContent = ruleSummary(r);

  const tdStatus = document.createElement('td');
  tdStatus.textContent = r.enabled ? t('rules.status.on', 'On') : t('rules.status.off', 'Off');

  const tdActions = document.createElement('td');
  const btnEdit = document.createElement('button');
  btnEdit.className = 'btn';
  btnEdit.textContent = t('common.edit', 'Edit');
  btnEdit.addEventListener('click', () => openRuleModal(r.id));

  const btnToggle = document.createElement('button');
  btnToggle.className = 'btn';
  btnToggle.textContent = r.enabled ? t('rules.disable', 'Disable') : t('rules.enable', 'Enable');
  btnToggle.addEventListener('click', () => {
    r.enabled = !r.enabled;
    renderRulesList();
    setDirty(true);
  });

  const btnDel = document.createElement('button');
  btnDel.className = 'btn';
  btnDel.textContent = t('common.delete', 'Delete');
  btnDel.addEventListener('click', () => {
    if (!confirm(t('rules.confirm_delete', 'Delete rule?'))) return;
    state.rulesList = state.rulesList.filter(x => x.id !== r.id);
    renderRulesList();
    setDirty(true);
  });

  tdActions.appendChild(btnEdit);
  tdActions.appendChild(btnToggle);
  tdActions.appendChild(btnDel);

  tr.appendChild(tdName);
  tr.appendChild(tdTags);
  tr.appendChild(tdSummary);
  tr.appendChild(tdStatus);
  tr.appendChild(tdActions);

  tbody.appendChild(tr);
}

function renderRulesList() {
  const table = state.tables.rules;
  if (table) {
    table.reload();
    return;
  }

  const tbody = UI.qs('#rulesTable tbody');
  if (!tbody) return;
  tbody.innerHTML = '';
  state.rulesList.forEach(r => renderRuleRow(tbody, r));
}

function renderTaskRow(tbody, task) {
  const tr = document.createElement('tr');
  tr.dataset.id = task.id;
  const tdName = document.createElement('td');
  tdName.textContent = task.name || t('rules.unnamed', 'Untitled');

  const tdSources = document.createElement('td');
  tdSources.textContent = (task.sources || []).map(taskSourceLabel).join(', ');

  const tdRules = document.createElement('td');
  tdRules.textContent = (task.rule_ids || []).map(ruleNameById).filter(Boolean).join(', ');

  const tdTemplate = document.createElement('td');
  tdTemplate.textContent = templateNameById(task.template_id);

  const tdStatus = document.createElement('td');
  tdStatus.textContent = task.enabled ? t('settings.tasks.status.on', 'On') : t('settings.tasks.status.off', 'Off');

  const tdButtons = document.createElement('td');
  const btnEdit = document.createElement('button');
  btnEdit.className = 'btn';
  btnEdit.textContent = t('common.edit', 'Edit');
  btnEdit.addEventListener('click', () => openTaskModal(task.id));
  const btnToggle = document.createElement('button');
  btnToggle.className = 'btn';
  btnToggle.textContent = task.enabled ? t('rules.disable', 'Disable') : t('rules.enable', 'Enable');
  btnToggle.addEventListener('click', () => {
    task.enabled = !task.enabled;
    renderTasksList();
    setDirty(true);
  });
  const btnDel = document.createElement('button');
  btnDel.className = 'btn';
  btnDel.textContent = t('common.delete', 'Delete');
  btnDel.addEventListener('click', () => {
    if (!confirm(t('settings.tasks.confirm_delete', 'Delete task?'))) return;
    state.tasksList = state.tasksList.filter(x => x.id !== task.id);
    renderTasksList();
    setDirty(true);
  });
  tdButtons.appendChild(btnEdit);
  tdButtons.appendChild(btnToggle);
  tdButtons.appendChild(btnDel);

  tr.appendChild(tdName);
  tr.appendChild(tdSources);
  tr.appendChild(tdRules);
  tr.appendChild(tdTemplate);
  tr.appendChild(tdStatus);
  tr.appendChild(tdButtons);
  tbody.appendChild(tr);
}

function renderTasksList() {
  const table = state.tables.tasks;
  if (table) {
    table.reload();
    return;
  }
  const tbody = UI.qs('#tasksTable tbody');
  if (!tbody) return;
  tbody.innerHTML = '';
  state.tasksList.forEach(task => renderTaskRow(tbody, task));
}

function taskSourceLabel(src) {
  if (src === 'movie') return t('settings.tasks.source.movie', 'Movies');
  if (src === 'series') return t('settings.tasks.source.series', 'Series');
  if (src === 'staging') return t('settings.tasks.source.staging', 'Staging');
  if (src === 'transmission') return t('settings.tasks.source.transmission', 'Transmission');
  return src || '';
}

function ruleNameById(id) {
  if (!id) return '';
  const rule = state.rulesList.find(r => r.id === id);
  return rule?.name || '';
}

function templateNameById(id) {
  if (!id) return '';
  const tpl = state.templatesList.find(tpl => tpl.id === id);
  return tpl?.name || '';
}

function sourceStatusOptions(source) {
  if (source === 'transmission') {
    return [
      { value: 'completed', label: t('sources.status.completed', 'completed') },
      { value: 'downloading', label: t('sources.status.downloading', 'downloading') },
      { value: 'seeding', label: t('sources.status.seeding', 'seeding') },
      { value: 'stopped', label: t('sources.status.stopped', 'stopped') },
      { value: 'unknown', label: t('sources.status.unknown', 'unknown') },
    ];
  }
  return [];
}

function renderStatusOptions(source) {
  const options = sourceStatusOptions(source);
  if (options.length === 0) {
    return `<option value="">${t('rules.statuses.none', 'No statuses')}</option>`;
  }
  return options.map(opt => `<option value="${escapeHtml(opt.value)}">${escapeHtml(opt.label)}</option>`).join('');
}

function conditionFieldOptions() {
  return [
    { value: 'status', label: t('rules.cond.status', 'Status') },
    { value: 'label', label: t('rules.cond.label', 'Label') },
    { value: 'name_regex', label: t('rules.cond.name_regex', 'Name regex') },
    { value: 'path_regex', label: t('rules.cond.path_regex', 'Path regex') },
    { value: 'min_size', label: t('rules.cond.min_size', 'Min size') },
  ];
}

function conditionOperatorOptions(field) {
  if (field === 'status') {
    return [
      { value: 'any', label: t('rules.op.any', 'any') },
      { value: 'in', label: '=' },
      { value: 'not_in', label: '!=' },
    ];
  }
  if (field === 'min_size') {
    return [
      { value: '>=', label: '>=' },
      { value: '>', label: '>' },
      { value: '<=', label: '<=' },
      { value: '<', label: '<' },
      { value: '=', label: '=' },
      { value: '!=', label: '!=' },
    ];
  }
  if (field === 'name_regex' || field === 'path_regex') {
    return [
      { value: 'regex', label: 'regex' },
      { value: 'not_regex', label: '!regex' },
    ];
  }
  if (field === 'label') {
    return [
      { value: 'contains', label: t('rules.op.contains', 'contains') },
      { value: 'not_contains', label: t('rules.op.not_contains', 'not contains') },
      { value: '=', label: '=' },
      { value: '!=', label: '!=' },
    ];
  }
  return [
    { value: '=', label: '=' },
  ];
}

function createConditionValueControl(field, source, op, value) {
  if (field === 'status') {
    const sel = document.createElement('select');
    sel.multiple = true;
    sel.dataset.field = 'cond_value';
    sel.innerHTML = renderStatusOptions(source);
    const values = Array.isArray(value) ? value : [];
    Array.from(sel.options).forEach(opt => {
      if (values.includes(opt.value)) opt.selected = true;
    });
    if (op === 'any') {
      sel.disabled = true;
    }
    return sel;
  }
  if (field === 'min_size') {
    const input = document.createElement('input');
    input.type = 'number';
    input.dataset.field = 'cond_value';
    input.value = value ?? '';
    return input;
  }
  const input = document.createElement('input');
  input.type = 'text';
  input.dataset.field = 'cond_value';
  input.value = value ?? '';
  return input;
}

function formatConditionValue(field, source, value, op) {
  if (field === 'status') {
    if (op === 'any') return t('rules.op.any', 'any');
    const values = Array.isArray(value) ? value : [];
    const labels = sourceStatusOptions(source).reduce((acc, s) => {
      acc[s.value] = s.label;
      return acc;
    }, {});
    const parts = values.map(v => labels[v] || v).filter(Boolean);
    const orLabel = t('rules.logic.or', 'OR');
    return parts.length ? parts.join(` ${orLabel} `) : t('rules.statuses.none', 'No statuses');
  }
  if (field === 'min_size') {
    return String(value ?? '');
  }
  return String(value ?? '');
}

function formatConditionField(field) {
  const opt = conditionFieldOptions().find(o => o.value === field);
  return opt ? opt.label : field;
}

function formatConditionOp(field, op) {
  if (field === 'status' && op === 'any') return '=';
  if (op) return op;
  const opts = conditionOperatorOptions(field);
  return opts.length ? opts[0].value : '=';
}

function ruleSummary(r) {
  const cfg = r.config || {};
  if (r.type === 'name_map') {
    return `${cfg.mode || 'exact'}: ${cfg.pattern || '*'} -> ${cfg.canonical || '*'}`;
  }
  if (r.type === 'delete_track') {
    return `type=${cfg.track_type || '*'} lang=${cfg.lang || '*'} audio=${cfg.audio_type || '*'} contains=${cfg.name_contains || '*'}`;
  }
  if (r.type === 'priorities') {
    const langs = Array.isArray(cfg.languages) ? cfg.languages.join(',') : '';
    const aud = Array.isArray(cfg.audio_types) ? cfg.audio_types.join(',') : '';
    return `langs=${langs || '*'} audio=${aud || '*'}`;
  }
  if (r.type === 'lang_fix') {
    return `${cfg.from_lang || '*'} -> ${cfg.to_lang || '*'} (${cfg.mode || 'exact'})`;
  }
  if (r.type === 'source_filter') {
    const conds = Array.isArray(cfg.conditions) ? cfg.conditions : [];
    const condText = conds
      .filter(c => c?.enabled !== false)
      .map(c => `${c.field || '?'}${c.op || '='}${Array.isArray(c.value) ? c.value.join(',') : (c.value ?? '')}`)
      .join(' ');
    return `src=${cfg.source || '*'} ${condText || '*'}`;
  }
  return '';
}

function initRulesUi() {
  const addBtn = UI.qs('#btnAddRule');
  const menu = UI.qs('#ruleTypeMenu');
  const modalClose = UI.qs('#ruleModalClose');
  const modalCancel = UI.qs('#ruleModalCancel');
  const modalSave = UI.qs('#ruleModalSave');

  if (addBtn && menu) {
    addBtn.addEventListener('click', (e) => {
      e.preventDefault();
      addBtn.parentElement?.classList.toggle('open');
    });
    menu.querySelectorAll('button[data-rule-type]').forEach(btn => {
      btn.addEventListener('click', () => {
        const type = btn.dataset.ruleType || 'name_map';
        addBtn.parentElement?.classList.remove('open');
        openRuleModal(type, null);
      });
    });
    document.addEventListener('click', (e) => {
      if (!addBtn.parentElement?.contains(e.target)) {
        addBtn.parentElement?.classList.remove('open');
      }
    });
  }

  if (modalClose) modalClose.addEventListener('click', closeRuleModal);
  if (modalCancel) modalCancel.addEventListener('click', closeRuleModal);
  if (modalSave) {
    modalSave.addEventListener('click', () => {
      if (state.taskEditId !== null || state.templateEditId !== null) return;
      saveRuleModal();
    });
  }
}

function initTaskModalRouting() {
  const modalSave = UI.qs('#ruleModalSave');
  const modalCancel = UI.qs('#ruleModalCancel');
  const modalClose = UI.qs('#ruleModalClose');
  if (modalSave) {
    modalSave.addEventListener('click', () => {
      const body = UI.qs('#ruleModalBody');
      if (!body) return;
      if (state.taskEditId !== null) {
        saveTaskModal(body);
        return;
      }
      if (state.templateEditId !== null) {
        saveTemplateModal(body);
        return;
      }
    });
  }
  if (modalCancel) modalCancel.addEventListener('click', closeRuleModal);
  if (modalClose) modalClose.addEventListener('click', closeRuleModal);
}

function openRuleModal(type, id = null) {
  const modal = UI.qs('#ruleModal');
  const body = UI.qs('#ruleModalBody');
  const title = UI.qs('#ruleModalTitle');
  if (!modal || !body || !title) return;

  const rule = id ? state.rulesList.find(r => r.id === id) : null;
  const data = rule ? { ...rule } : normalizeRule({ type });
  data.type = type || data.type;

  state.ruleEditId = data.id;
  state.ruleEditType = data.type;
  state.toolEditType = null;
  state.taskEditId = null;

  title.textContent = `${t('rules.modal_title', 'Rule')}: ${ruleTypeLabel(data.type)}`;
  body.innerHTML = renderRuleForm(data);
  applyRuleFormValues(body, data);
  if (data.type === 'source_filter') {
    const sourceSel = body.querySelector('[data-field="source"]');
    if (sourceSel) {
      sourceSel.addEventListener('change', () => {
        renderConditionsTable(body, sourceSel.value, readConditionsTable(body));
      });
    }
  }
  modal.style.display = 'flex';
}

function closeRuleModal() {
  const modal = UI.qs('#ruleModal');
  if (modal) modal.style.display = 'none';
  state.ruleEditId = null;
  state.ruleEditType = null;
  state.toolEditType = null;
  state.rootEditId = null;
  state.pluginEditType = null;
  state.taskEditId = null;
  state.templateEditId = null;
}

function renderRuleForm(rule) {
  return `
    <div class="grid">
      <label>
        <div class="lbl">${t('rules.field.name','Name')}</div>
        <input data-field="name" type="text" value="${escapeHtml(rule.name || '')}">
      </label>
      <label>
        <div class="lbl">${t('rules.field.enabled','Enabled')}</div>
        <select data-field="enabled">
          <option value="1">${t('common.yes','Yes')}</option>
          <option value="0">${t('common.no','No')}</option>
        </select>
      </label>
      <label>
        <div class="lbl">${t('rules.field.logic','Logic')}</div>
        <select data-field="logic">
          <option value="and">${t('rules.logic.and','AND')}</option>
          <option value="or">${t('rules.logic.or','OR')}</option>
        </select>
      </label>
      <label>
        <div class="lbl">${t('rules.field.tags','Tags (comma)')}</div>
        <input data-field="tags" type="text" value="${escapeHtml((rule.tags || []).join(','))}">
      </label>
      <label class="full">
        <div class="lbl">${t('rules.field.conditions','Conditions')}</div>
        <div class="conditions" data-field="rule_conditions">
          <div class="conditions-list"></div>
          <button type="button" class="btn" data-action="add_rule_condition">${t('rules.cond.add','Add condition')}</button>
        </div>
      </label>
    </div>
  `;
}

function applyRuleFormValues(body, rule) {
  const setVal = (field, value) => {
    const el = body.querySelector(`[data-field="${field}"]`);
    if (el) el.value = value ?? '';
  };
  setVal('name', rule.name || '');
  setVal('enabled', rule.enabled ? '1' : '0');
  setVal('logic', rule.logic || 'and');
  setVal('tags', (rule.tags || []).join(','));
  renderRuleConditions(body, Array.isArray(rule.conditions) ? rule.conditions : []);
}

function ruleFieldOptions() {
  const roots = state.rootsList || [];
  const options = [];
  if (roots.some(r => r.type === 'movie')) options.push({ value: 'movie', label: t('settings.tasks.source.movie', 'Movies') });
  if (roots.some(r => r.type === 'series')) options.push({ value: 'series', label: t('settings.tasks.source.series', 'Series') });
  if (roots.some(r => r.type === 'staging')) options.push({ value: 'staging', label: t('settings.tasks.source.staging', 'Staging') });
  if (state.pluginConfig?.sources?.transmission) options.push({ value: 'transmission', label: t('settings.tasks.source.transmission', 'Transmission') });
  return {
    source: { label: t('rules.cond.source','Source'), type: 'select', options },
    status: { label: t('rules.cond.status','Status'), type: 'select', options: sourceStatusOptions('transmission') },
    name_regex: { label: t('rules.cond.name_regex','Name regex'), type: 'regex' },
    path_regex: { label: t('rules.cond.path_regex','Path regex'), type: 'regex' },
    size_mb: { label: t('rules.cond.size','Size (MB)'), type: 'number' },
    container: { label: t('rules.cond.container','Container'), type: 'select', options: [
      { value: 'mkv', label: 'mkv' },
      { value: 'mp4', label: 'mp4' },
      { value: 'avi', label: 'avi' },
    ]},
    lang: { label: t('rules.cond.lang','Language'), type: 'text' },
  };
}

function ruleOperatorOptions(field) {
  const meta = ruleFieldOptions()[field];
  const type = meta?.type || 'text';
  if (type === 'number') {
    return [
      { value: 'eq', label: '=' },
      { value: 'gt', label: '>' },
      { value: 'lt', label: '<' },
      { value: 'between', label: t('rules.op.between','between') },
    ];
  }
  if (type === 'regex') {
    return [{ value: 'regex', label: t('rules.op.regex','regex') }];
  }
  return [
    { value: 'eq', label: '=' },
    { value: 'contains', label: t('rules.op.contains','contains') },
    { value: 'in', label: t('rules.op.in','in') },
    { value: 'not_in', label: t('rules.op.not_in','not in') },
  ];
}

function renderRuleConditions(body, conditions) {
  const wrap = body.querySelector('[data-field="rule_conditions"] .conditions-list');
  const addBtn = body.querySelector('[data-action="add_rule_condition"]');
  if (!wrap || !addBtn) return;
  wrap.innerHTML = '';
  (conditions || []).forEach(cond => wrap.appendChild(createRuleConditionRow(cond)));
  addBtn.addEventListener('click', () => {
    wrap.appendChild(createRuleConditionRow({}));
  });
}

function createRuleConditionRow(cond) {
  const meta = ruleFieldOptions();
  const row = document.createElement('div');
  row.className = 'conditions-builder';

  const fieldSel = document.createElement('select');
  Object.keys(meta).forEach(key => {
    const opt = document.createElement('option');
    opt.value = key;
    opt.textContent = meta[key].label;
    fieldSel.appendChild(opt);
  });
  fieldSel.value = cond.field || 'source';

  const opSel = document.createElement('select');
  const valueWrap = document.createElement('div');
  valueWrap.className = 'cond-value';

  const removeBtn = document.createElement('button');
  removeBtn.type = 'button';
  removeBtn.className = 'btn';
  removeBtn.textContent = 'X';
  removeBtn.addEventListener('click', () => row.remove());

  const updateOps = () => {
    opSel.innerHTML = '';
    ruleOperatorOptions(fieldSel.value).forEach(opt => {
      const o = document.createElement('option');
      o.value = opt.value;
      o.textContent = opt.label;
      opSel.appendChild(o);
    });
  };

  const updateValue = () => {
    valueWrap.innerHTML = '';
    const field = fieldSel.value;
    const op = opSel.value;
    const fieldMeta = meta[field];
    const type = fieldMeta?.type || 'text';
    const value = cond.value;

    if (type === 'select') {
      const select = document.createElement('select');
      if (op === 'in' || op === 'not_in') select.multiple = true;
      (fieldMeta.options || []).forEach(opt => {
        const o = document.createElement('option');
        o.value = opt.value;
        o.textContent = opt.label;
        const list = Array.isArray(value) ? value : [value];
        if (list.includes(opt.value)) o.selected = true;
        select.appendChild(o);
      });
      valueWrap.appendChild(select);
      return;
    }

    if (type === 'number' && op === 'between') {
      const min = document.createElement('input');
      min.type = 'number';
      min.placeholder = 'min';
      min.value = Array.isArray(value) ? (value[0] ?? '') : '';
      min.dataset.bound = 'min';
      const max = document.createElement('input');
      max.type = 'number';
      max.placeholder = 'max';
      max.value = Array.isArray(value) ? (value[1] ?? '') : '';
      max.dataset.bound = 'max';
      valueWrap.appendChild(min);
      valueWrap.appendChild(max);
      return;
    }

    const input = document.createElement('input');
    input.type = (type === 'number') ? 'number' : 'text';
    input.value = (value ?? '');
    if (op === 'in' || op === 'not_in') {
      input.placeholder = 'a,b,c';
    }
    valueWrap.appendChild(input);
  };

  updateOps();
  opSel.value = cond.op || opSel.value || 'eq';
  updateValue();

  fieldSel.addEventListener('change', () => {
    updateOps();
    opSel.value = opSel.value || 'eq';
    updateValue();
  });
  opSel.addEventListener('change', updateValue);

  row.appendChild(fieldSel);
  row.appendChild(opSel);
  row.appendChild(valueWrap);
  row.appendChild(removeBtn);
  return row;
}

function readRuleConditions(body) {
  const rows = Array.from(body.querySelectorAll('[data-field="rule_conditions"] .conditions-list .conditions-builder'));
  const meta = ruleFieldOptions();
  return rows.map(row => {
    const selects = row.querySelectorAll('select');
    const field = selects[0]?.value || '';
    const op = selects[1]?.value || 'eq';
    const fieldMeta = meta[field];
    const type = fieldMeta?.type || 'text';
    let value = '';
    const valueWrap = row.querySelector('.cond-value');
    if (!valueWrap) return null;
    if (type === 'select') {
      const select = valueWrap.querySelector('select');
      if (select && select.multiple) {
        value = Array.from(select.selectedOptions).map(o => o.value);
      } else {
        value = select?.value || '';
      }
    } else if (type === 'number' && op === 'between') {
      const min = valueWrap.querySelector('[data-bound="min"]')?.value || '';
      const max = valueWrap.querySelector('[data-bound="max"]')?.value || '';
      value = [Number(min || 0), Number(max || 0)];
    } else {
      const input = valueWrap.querySelector('input');
      value = input?.value ?? '';
      if (op === 'in' || op === 'not_in') {
        value = String(value).split(',').map(v => v.trim()).filter(Boolean);
      } else if (type === 'number') {
        value = Number(value || 0);
      }
    }
    return { field, op, value };
  }).filter(Boolean);
}

function saveRuleModal() {
  const modal = UI.qs('#ruleModal');
  const body = UI.qs('#ruleModalBody');
  if (!modal || !body) return;
  if (state.pluginEditType) {
    savePluginModal(body);
    setDirty(true);
    closeRuleModal();
    return;
  }
  if (state.rootEditId !== null) {
    const type = body.querySelector('[data-field="root_type"]')?.value || 'movie';
    const path = body.querySelector('[data-field="root_path"]')?.value?.trim() || '';
    const enabled = body.querySelector('[data-field="root_enabled"]')?.value === '1';
    const current = state.rootsList.find(r => r.id === state.rootEditId);
    if (current) {
      current.type = type;
      current.path = path;
      current.enabled = enabled;
    } else {
      state.rootsList.push({
        id: state.rootEditId,
        type,
        path,
        enabled,
      });
    }
    renderRootsList();
    setDirty(true);
    closeRuleModal();
    return;
  }
  if (state.toolEditType) {
    const path = body.querySelector('[data-field="tool_path"]')?.value?.trim() || '';
    updateToolPath(state.toolEditType, path);
    setDirty(true);
    closeRuleModal();
    return;
  }
  const id = state.ruleEditId;
  const type = state.ruleEditType;
  const current = state.rulesList.find(r => r.id === id) || normalizeRule({ type });

  const getVal = (field) => body.querySelector(`[data-field="${field}"]`)?.value ?? '';
  const name = getVal('name').trim();
  const enabled = getVal('enabled') === '1';
  const config = {};

  if (type === 'name_map') {
    config.pattern = getVal('pattern').trim();
    config.canonical = getVal('canonical').trim();
    config.mode = getVal('mode') || 'exact';
  } else if (type === 'delete_track') {
    config.track_type = getVal('track_type').trim();
    config.lang = getVal('lang').trim();
    config.audio_type = getVal('audio_type').trim();
    config.name_contains = getVal('name_contains').trim();
    config.except_default = getVal('except_default') === '1';
    config.except_forced = getVal('except_forced') === '1';
  } else if (type === 'priorities') {
    config.languages = commaToList(getVal('languages'));
    config.audio_types = commaToList(getVal('audio_types'));
  } else if (type === 'lang_fix') {
    config.from_lang = getVal('from_lang').trim();
    config.to_lang = getVal('to_lang').trim();
    config.mode = getVal('mode') || 'exact';
  } else if (type === 'source_filter') {
    config.source = getVal('source').trim();
    config.conditions = readConditionsTable(body);
  }

  const updated = {
    id: current.id,
    type,
    name,
    enabled,
    config,
  };

  const idx = state.rulesList.findIndex(r => r.id === current.id);
  if (idx >= 0) {
    state.rulesList[idx] = updated;
  } else {
    state.rulesList.push(updated);
  }
  renderRulesList();
  setDirty(true);
  closeRuleModal();
}

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

function cellText(value) {
  const td = document.createElement('td');
  td.textContent = value ?? '';
  return td;
}

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

  const rootsTable = UI.qs('#rootsTable');
  if (rootsTable) {
    const { id, prefs } = getPrefs(rootsTable);
    state.tables.roots = new TableController({
      table: rootsTable,
      mode,
      pageSize,
      sort: prefs.sort || '',
      dir: prefs.dir || 'asc',
      onSortChange: (sort, dir) => window.UserPrefs?.setTable?.(id, { sort, dir }),
      fetchPage: ({ page, per_page, sort, dir }) => {
        const sorted = sortItems(state.rootsList, sort, dir, {
          path: r => (r.path || '').toLowerCase(),
        });
        return Promise.resolve(paginateItems(sorted, page, per_page));
      },
      renderRow: renderRootRow,
    });
  }

  const toolsTable = UI.qs('#toolsTable');
  if (toolsTable) {
    const { id, prefs } = getPrefs(toolsTable);
    state.tables.tools = new TableController({
      table: toolsTable,
      mode,
      pageSize,
      sort: prefs.sort || '',
      dir: prefs.dir || 'asc',
      onSortChange: (sort, dir) => window.UserPrefs?.setTable?.(id, { sort, dir }),
      fetchPage: ({ page, per_page, sort, dir }) => {
        const sorted = sortItems(state.toolsList, sort, dir, {
          name: t => (t.label || '').toLowerCase(),
        });
        return Promise.resolve(paginateItems(sorted, page, per_page));
      },
      renderRow: renderToolRow,
    });
  }

  const rulesTable = UI.qs('#rulesTable');
  if (rulesTable) {
    const { id, prefs } = getPrefs(rulesTable);
    state.tables.rules = new TableController({
      table: rulesTable,
      mode,
      pageSize,
      sort: prefs.sort || '',
      dir: prefs.dir || 'asc',
      onSortChange: (sort, dir) => window.UserPrefs?.setTable?.(id, { sort, dir }),
      fetchPage: ({ page, per_page, sort, dir }) => {
        const sortKey = sort || 'name';
        const sortDir = dir || 'asc';
        const sorted = sortItems(state.rulesList, sortKey, sortDir, {
          name: r => (r.name || '').toLowerCase(),
          type: r => (r.type || '').toLowerCase(),
        });
        return Promise.resolve(paginateItems(sorted, page, per_page));
      },
      renderRow: renderRuleRow,
    });
  }

  const pluginsTable = UI.qs('#pluginsTable');
  if (pluginsTable) {
    const { id, prefs } = getPrefs(pluginsTable);
    state.tables.plugins = new TableController({
      table: pluginsTable,
      mode,
      pageSize,
      sort: prefs.sort || '',
      dir: prefs.dir || 'asc',
      onSortChange: (sort, dir) => window.UserPrefs?.setTable?.(id, { sort, dir }),
      fetchPage: ({ page, per_page, sort, dir }) => {
        const list = buildPluginsList();
        const sorted = sortItems(list, sort, dir, {
          name: p => (p.label || '').toLowerCase(),
        });
        return Promise.resolve(paginateItems(sorted, page, per_page));
      },
      renderRow: renderPluginRow,
    });
  }

  const templatesTable = UI.qs('#templatesTable');
  if (templatesTable) {
    const { id, prefs } = getPrefs(templatesTable);
    state.tables.templates = new TableController({
      table: templatesTable,
      mode,
      pageSize,
      sort: prefs.sort || '',
      dir: prefs.dir || 'asc',
      onSortChange: (sort, dir) => window.UserPrefs?.setTable?.(id, { sort, dir }),
      fetchPage: ({ page, per_page, sort, dir }) => {
        const sorted = sortItems(state.templatesList, sort, dir, {
          name: t => (t.name || '').toLowerCase(),
        });
        return Promise.resolve(paginateItems(sorted, page, per_page));
      },
      renderRow: renderTemplateRow,
    });
  }

  const tasksTable = UI.qs('#tasksTable');
  if (tasksTable) {
    const { id, prefs } = getPrefs(tasksTable);
    state.tables.tasks = new TableController({
      table: tasksTable,
      mode,
      pageSize,
      sort: prefs.sort || '',
      dir: prefs.dir || 'asc',
      onSortChange: (sort, dir) => window.UserPrefs?.setTable?.(id, { sort, dir }),
      fetchPage: ({ page, per_page, sort, dir }) => {
        const sorted = sortItems(state.tasksList, sort, dir, {
          name: t => (t.name || '').toLowerCase(),
        });
        return Promise.resolve(paginateItems(sorted, page, per_page));
      },
      renderRow: renderTaskRow,
    });
  }
}

/* ---------------------------
  Actions
--------------------------- */

async function doSave() {
  const prevRetention = Number(state.settings?.logs?.retention_days ?? 7);
  const payload = collectSettingsFromUI();
  const newRetention = Number(payload.logs?.retention_days ?? 7);
  if (prevRetention > 0 && newRetention > 0 && newRetention < prevRetention) {
    if (!confirm(t('settings.logs.retention_warn', 'Older logs will be deleted. Continue?'))) {
      return;
    }
  }
  setStatus(t('common.saving', 'Saving…'));
  await api('/api/settings', 'POST', payload);
  setDirty(false);
  await loadSettings();
  setStatus(t('common.saved', 'Saved'));
}

async function generatePreview() {
  const box = UI.qs('#previewBox');
  if (!box) return;

  box.textContent = t('common.generating_preview', 'Generating preview…');

  try {
    const movies = await api('/api/layout/preview', 'POST', { kind: 'movies', mode: 'samples', limit: 10 });
    const series = await api('/api/layout/preview', 'POST', { kind: 'series', mode: 'samples', limit: 10 });

    let txt = '';
    txt += `${t('settings.preview.movies','MOVIES')}\n`;
    for (const ex of movies.examples || []) {
      txt += `- ${ex.input.title} (${ex.input.year ?? t('common.na','n/a')}) -> ${ex.output_abs}\n`;
    }
    txt += `\n${t('settings.preview.series','SERIES')}\n`;
    for (const ex of series.examples || []) {
      txt += `- ${ex.input.title} (${ex.input.year ?? t('common.na','n/a')}) -> ${ex.output_abs}\n`;
    }

    box.textContent = txt;
  } catch (e) {
    box.textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}

async function loadDebugStats() {
  const filesEl = UI.qs('#dbgContentFiles');
  if (!filesEl) return;
  console.log('[debug] loadDebugStats: start');
  try {
    const data = await api('/api/debug/stats', 'GET');
    console.log('[debug] loadDebugStats: response', data);
    const content = data?.content || {};
    const db = data?.db || {};
    const info = data?.db_info || {};
    UI.qs('#dbgContentFiles').textContent = String(content.files ?? 0);
    UI.qs('#dbgContentMeta').textContent = String(content.meta ?? 0);
    UI.qs('#dbgContentItems').textContent = String(content.items ?? 0);
    UI.qs('#dbgDbTables').textContent = String(db.tables ?? 0);
    UI.qs('#dbgDbSize').textContent = UI.formatBytes(db.size_bytes ?? 0);
    UI.qs('#dbgDbName').textContent = String(info.db_name || '-');
    UI.qs('#dbgDbUser').textContent = String(info.current_user || info.user_name || '-');
    console.log('[debug] loadDebugStats: db_info', info);
    loadDebugTables();
  } catch (e) {
    console.log('[debug] loadDebugStats: error', e);
    UI.qs('#dbgContentFiles').textContent = '-';
    UI.qs('#dbgContentMeta').textContent = '-';
    UI.qs('#dbgContentItems').textContent = '-';
    UI.qs('#dbgDbTables').textContent = '-';
    UI.qs('#dbgDbSize').textContent = '-';
    UI.qs('#dbgDbName').textContent = '-';
    UI.qs('#dbgDbUser').textContent = '-';
    const body = UI.qs('#dbgDbTablesList tbody');
    if (body) body.innerHTML = '';
    const preview = UI.qs('#dbgDbTablePreview');
    if (preview) preview.textContent = '';
  }
}

async function loadDebugTables() {
  const table = UI.qs('#dbgDbTablesList');
  if (!table) return;
  const body = table.querySelector('tbody');
  const status = UI.qs('#dbgDbTablesStatus');
  if (body) body.innerHTML = '';
  if (status) status.textContent = t('common.loading', 'Loading…');
  console.log('[debug] loadDebugTables: start');
  try {
    const data = await api('/api/debug/db-tables', 'GET');
    const items = Array.isArray(data?.items) ? data.items : [];
    if (status) status.textContent = `items: ${items.length}`;
    console.log('[debug] loadDebugTables: items', items.length, items.slice(0, 3));
    if (body) {
      if (items.length === 0) {
        const row = document.createElement('tr');
        const cell = document.createElement('td');
        cell.colSpan = 2;
        cell.textContent = t('common.empty', 'Empty');
        row.appendChild(cell);
        body.appendChild(row);
      } else {
        items.forEach((item) => {
          const row = document.createElement('tr');
          row.className = 'db-table-row';
          row.dataset.tableName = item.name || '';
          const name = document.createElement('td');
          name.textContent = item.name || '';
          const count = document.createElement('td');
          count.textContent = String(item.rows ?? 0);
          row.appendChild(name);
          row.appendChild(count);
          body.appendChild(row);
        });
      }
    }
  } catch (e) {
    if (body) {
      body.innerHTML = `<tr><td colspan="2">${escapeHtml(e.message)}</td></tr>`;
    }
    if (status) status.textContent = `error: ${e.message}`;
  }
}

async function loadDebugTablePreview(tableName) {
  const preview = UI.qs('#dbgDbTablePreview');
  if (!preview) return;
  if (!tableName) {
    preview.textContent = '';
    return;
  }
  preview.textContent = t('common.loading', 'Loading…');
  try {
    const data = await api('/api/debug/db-table?name=' + encodeURIComponent(tableName) + '&limit=50', 'GET');
    const rows = Array.isArray(data?.rows) ? data.rows : [];
    preview.textContent = rows.length === 0
      ? t('common.empty', 'Empty')
      : JSON.stringify(rows, null, 2);
  } catch (e) {
    preview.textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}

async function debugClearContent() {
  UI.qs('#dbgContentOut').textContent = t('common.running', 'Running…');
  try {
    await api('/api/debug/clear-content', 'POST', { confirm: UI.qs('#dbgContentConfirm').value.trim() });
    UI.qs('#dbgContentOut').textContent = 'OK';
    window.location.href = '/';
  } catch (e) {
    UI.qs('#dbgContentOut').textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}

async function debugResetDb() {
  UI.qs('#dbgResetOut').textContent = t('common.running', 'Running…');
  try {
    const data = await api('/api/debug/reset-db', 'POST', { confirm: UI.qs('#dbgResetConfirm').value.trim() });
    const log = data.log || [];
    UI.qs('#dbgResetOut').textContent = log.join('\n');
    window.location.href = '/';
  } catch (e) {
    UI.qs('#dbgResetOut').textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}

async function debugDump() {
  const out = UI.qs('#dbgDumpOut');
  out.textContent = t('common.running', 'Running…');
  try {
    const res = await fetch('/api/debug/db-dump');
    if (!res.ok) {
      throw new Error(`Request failed: ${res.status}`);
    }
    const blob = await res.blob();
    const url = URL.createObjectURL(blob);
    const ts = new Date().toISOString().replace(/[:.]/g, '-');
    const a = document.createElement('a');
    a.href = url;
    a.download = `scmedia_dump_${ts}.sql`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
    out.textContent = 'OK';
  } catch (e) {
    out.textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}

async function debugRestore() {
  const out = UI.qs('#dbgDumpOut');
  const file = UI.qs('#dbgRestoreFile')?.files?.[0];
  if (!file) {
    out.textContent = `${t('common.error','Error')}: ${t('settings.debug.dump.restore', 'Restore dump')}`;
    return;
  }
  const ok = confirm(t('settings.debug.dump.restore_confirm', 'Restore database from dump? This will overwrite current data.'));
  if (!ok) return;
  out.textContent = t('common.running', 'Running…');
  try {
    const sql = await file.text();
    await api('/api/debug/db-restore', 'POST', { confirm: 'RESTORE DATABASE', sql });
    out.textContent = 'OK';
    window.location.href = '/';
  } catch (e) {
    out.textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}

function init() {
  if (window.Auth?.requireAuth) {
    window.Auth.requireAuth();
  }
  enforceAdminAccess();
  window.UI?.initHeader?.();
  if (window.UserPrefs?.load) {
    window.UserPrefs.load().then((prefs) => {
      if (prefs?.theme && window.UI?.setTheme) {
        window.UI.setTheme(prefs.theme);
      }
    }).catch(() => {});
  }
  i18n.dict = window.I18N || {};
  i18n.lang = window.APP_LANG || 'en';
  initTabs();
  setVersion();
  const logsType = UI.qs('#logsType');
  if (logsType) {
    logsType.addEventListener('change', updateLogsTypeUI);
    updateLogsTypeUI();
  }
  updateAuditEventOptions([]);
  const logsEvent = UI.qs('#logsFilterEvent');
  if (logsEvent) {
    logsEvent.value = loadLogsEventFilter();
    logsEvent.addEventListener('change', async () => {
      saveLogsEventFilter(logsEvent.value || 'all');
      if (!state.tables.audit) return;
      const hint = UI.qs('#logsHint');
      if (hint) hint.textContent = t('common.loading', 'Loading…');
      try {
        const from = UI.qs('#logsDateFrom').value || '';
        const to = UI.qs('#logsDateTo').value || '';
        state.auditItemsCache = new Map();
        clearAuditSelections();
        const filters = [];
        if (from && to) {
          filters.push({ key: 'created_at', op: 'between', value: [from, to] });
        }
        const eventValue = logsEvent.value || 'all';
        if (eventValue !== 'all') {
          filters.push({ key: 'action', op: 'eq', value: eventValue });
        }
        state.tables.audit.setFilters(filters);
        await state.tables.audit.load(1, false);
        const total = Number(state.tables.audit.total || 0);
        if (hint) hint.textContent = `${t('common.loaded', 'Loaded')} (${total})`;
      } catch (e) {
        if (hint) hint.textContent = `${t('common.error','Error')}: ${e.message}`;
      }
    });
  }
  UI.qs('#btnSave')?.addEventListener('click', doSave);
  UI.qs('#btnExport')?.addEventListener('click', () => window.open('/api/settings', '_blank'));

  UI.qs('#btnPreview')?.addEventListener('click', generatePreview);
  UI.qs('#btnAddRoot')?.addEventListener('click', () => openRootModal(null));

  UI.qs('#btnClearContent')?.addEventListener('click', debugClearContent);
  UI.qs('#btnResetDb')?.addEventListener('click', debugResetDb);
  UI.qs('#btnDbDump')?.addEventListener('click', debugDump);
  UI.qs('#btnDbRestore')?.addEventListener('click', debugRestore);
  const dbTable = UI.qs('#dbgDbTablesList');
  if (dbTable) {
    dbTable.addEventListener('dblclick', (e) => {
      const row = e.target.closest('tr[data-table-name]');
      if (!row) return;
      loadDebugTablePreview(row.dataset.tableName || '');
    });
  }
  UI.qs('#btnDetectTools')?.addEventListener('click', detectTools);
  UI.qs('#btnCreateSnapshot')?.addEventListener('click', createSnapshot);
  UI.qs('#btnLoadLogs')?.addEventListener('click', loadLogs);
  UI.qs('#btnCleanupLogs')?.addEventListener('click', cleanupLogs);
  UI.qs('#btnResetLogsDate')?.addEventListener('click', resetLogsDates);
  UI.qs('#btnLogsCopy')?.addEventListener('click', () => copySelectedLogs().catch(() => {}));
  UI.qs('#btnLogsSelectAll')?.addEventListener('click', () => selectAllLogsOnPage());
  initLogsTabs();
  initToolsUi();
  initPluginsUi();
  initTasksUi();
  initTemplatesUi();
  initTaskModalRouting();

  initRulesUi();

  bindDirtyInputs();
  UI.initThemeToggle();
  UI.bindThemePreference?.(() => applyI18n());
  initUnsavedGuard();

  state.settingsLoadPromise = loadSettings();
  Promise.all([state.settingsLoadPromise, loadDiagnostics(), loadSnapshots()])
    .then(() => {
      initTableControllers();
      initLogsTable();
      renderRootsList();
      renderToolsList();
      renderRulesList();
      renderTemplatesList();
      renderPluginsList();
      renderTasksList();
    })
    .catch(e => setStatus(`${t('common.error','Error')}: ${e.message}`));
}

init();

function initUnsavedGuard() {
  window.addEventListener('beforeunload', (e) => {
    if (!state.dirty) return;
    e.preventDefault();
    e.returnValue = '';
  });

  document.querySelectorAll('a[href]').forEach(a => {
    a.addEventListener('click', (e) => {
      if (!state.dirty) return;
      const ok = confirm(t('settings.unsaved_confirm', 'Unsaved changes. Leave settings?'));
      if (!ok) {
        e.preventDefault();
      }
    });
  });
}

async function loadLogs() {
  const type = UI.qs('#logsType')?.value || 'system';
  if (type === 'audit') {
    await loadAuditLogs();
    return;
  }
  const from = UI.qs('#logsDateFrom').value;
  const to = UI.qs('#logsDateTo').value;
  const level = UI.qs('#logsFilterLevel').value || 'all';
  const hint = UI.qs('#logsHint');
  if (!from || !to) {
    if (hint) hint.textContent = t('settings.logs.date_required', 'Select a date range');
    return;
  }
  if (hint) hint.textContent = t('common.loading', 'Loading…');
  try {
    initLogsTable();
    if (!state.tables.logs) return;
    state.logItemsCache = new Map();
    clearLogSelections();
    const filters = [];
    if (from && to) {
      filters.push({ key: 'ts', op: 'between', value: [from, to] });
    }
    if (level && level !== 'all') {
      filters.push({ key: 'level', op: 'eq', value: level });
    }
    state.tables.logs.setFilters(filters);
    await state.tables.logs.load(1, false);
    const total = Number(state.tables.logs.total || 0);
    if (hint) hint.textContent = `${t('common.loaded', 'Loaded')} (${total})`;
    saveLogsViewDates(from, to);
  } catch (e) {
    if (hint) hint.textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}

async function loadAuditLogs() {
  const hint = UI.qs('#logsHint');
  if (hint) hint.textContent = t('common.loading', 'Loading…');
  try {
    initAuditTable();
    if (!state.tables.audit) return;
    state.auditItemsCache = new Map();
    clearAuditSelections();
    const from = UI.qs('#logsDateFrom').value || '';
    const to = UI.qs('#logsDateTo').value || '';
    const eventFilter = UI.qs('#logsFilterEvent').value || 'all';
    const filters = [];
    if (from && to) {
      filters.push({ key: 'created_at', op: 'between', value: [from, to] });
    }
    if (eventFilter && eventFilter !== 'all') {
      filters.push({ key: 'action', op: 'eq', value: eventFilter });
    }
    state.tables.audit.setFilters(filters);
    await state.tables.audit.load(1, false);
    const total = Number(state.tables.audit.total || 0);
    if (hint) hint.textContent = `${t('common.loaded', 'Loaded')} (${total})`;
  } catch (e) {
    if (hint) hint.textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}

function updateAuditEventOptions(items) {
  const sel = UI.qs('#logsFilterEvent');
  if (!sel) return;
  const current = sel.value || 'all';
  const saved = loadLogsEventFilter();
  const fromItems = Array.from(new Set(items.map(x => x.action).filter(Boolean)));
  const combined = [...AUDIT_EVENTS, ...fromItems.filter(x => !AUDIT_EVENTS.includes(x))];
  const options = ['all', ...combined];
  sel.innerHTML = options.map(v => {
    if (v === 'all') {
      return `<option value="all">${t('filters.all', 'All')}</option>`;
    }
    return `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`;
  }).join('');
  const preferred = current !== 'all' ? current : saved;
  sel.value = options.includes(preferred) ? preferred : 'all';
}

function renderAuditLogs() {}

function normalizeAuditDate(value) {
  if (!value) return '';
  const s = String(value);
  if (/^\d{2}\.\d{2}\.\d{4}/.test(s)) {
    const parts = s.slice(0, 10).split('.');
    return `${parts[2]}-${parts[1]}-${parts[0]}`;
  }
  if (/^\d{4}-\d{2}-\d{2}/.test(s)) {
    return s.slice(0, 10);
  }
  const d = new Date(s);
  if (Number.isNaN(d.getTime())) return '';
  return d.toISOString().slice(0, 10);
}

function formatLogContext(value) {
  if (!value || value === 'null') {
    return '';
  }
  try {
    return ' ' + JSON.stringify(JSON.parse(value));
  } catch (e) {
    return ' ' + value;
  }
}

function logRowId(row) {
  if (row?.id !== undefined && row?.id !== null) return String(row.id);
  return `${row?.ts || ''}|${row?.level || ''}|${row?.message || ''}`;
}

function updateLogSelectionCount() {
  const el = UI.qs('#logsSelectionCount');
  if (!el) return;
  const count = state.logSelected.size;
  el.textContent = count ? `${t('settings.logs.selected', 'Selected')}: ${count}` : '';
}

function clearLogSelections() {
  state.logSelected = new Map();
  updateLogSelectionCount();
}

function updateAuditSelectionCount() {
  const el = UI.qs('#logsSelectionCount');
  if (!el) return;
  const count = state.auditSelected.size;
  el.textContent = count ? `${t('settings.logs.selected', 'Selected')}: ${count}` : '';
}

function clearAuditSelections() {
  state.auditSelected = new Map();
  updateAuditSelectionCount();
}

function renderLogRow(body, row) {
  const tr = document.createElement('tr');
  const id = logRowId(row);
  state.logItemsCache.set(id, row);

  const selTd = document.createElement('td');
  const checkbox = document.createElement('input');
  checkbox.type = 'checkbox';
  checkbox.dataset.logSelect = '1';
  checkbox.dataset.id = id;
  checkbox.checked = state.logSelected.has(id);
  selTd.appendChild(checkbox);
  tr.appendChild(selTd);

  const ctx = (formatLogContext(row.context_json) || '').trim();
  tr.appendChild(cellText(row.ts || ''));
  tr.appendChild(cellText(row.level || ''));
  tr.appendChild(cellText(row.message || ''));
  tr.appendChild(cellText(ctx));

  body.appendChild(tr);
}

function renderAuditRow(body, row) {
  const tr = document.createElement('tr');
  const id = logRowId(row);
  state.auditItemsCache.set(id, row);

  const selTd = document.createElement('td');
  const checkbox = document.createElement('input');
  checkbox.type = 'checkbox';
  checkbox.dataset.auditSelect = '1';
  checkbox.dataset.id = id;
  checkbox.checked = state.auditSelected.has(id);
  selTd.appendChild(checkbox);
  tr.appendChild(selTd);

  tr.appendChild(cellText(row.created_at || ''));
  tr.appendChild(cellText(row.actor_email || row.actor_user_id || ''));
  tr.appendChild(cellText(row.action || ''));
  tr.appendChild(cellText(`${row.target_type || ''} ${row.target_id || ''}`.trim()));
  tr.appendChild(cellText(row.meta_json || ''));

  body.appendChild(tr);
}

function initLogsTable() {
  const table = UI.qs('#logsTable');
  if (!table || !window.TableController) return;
  const mode = state.ui.table_mode || 'pagination';
  const pageSize = state.ui.table_page_size || 50;
  const id = table?.dataset?.tableId || table?.id || '';
  const prefs = window.UserPrefs?.getTable?.(id) || {};

  state.tables.logs = new TableController({
    table,
    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 }) => {
      const data = await api('/api/logs', 'POST', {
        page,
        per_page,
        sort,
        dir,
        filters,
      });
      return {
        items: data.items || [],
        total: data.total || 0,
        page: data.page || page,
        per_page: data.per_page || per_page,
      };
    },
    renderRow: renderLogRow,
  });
  state.tables.logs.total = 0;
  state.tables.logs.page = 1;
  state.tables.logs.updateFooter();

  const body = table.querySelector('tbody');
  if (body && !body.dataset.bindLogs) {
    body.dataset.bindLogs = '1';
    body.addEventListener('change', (e) => {
      const input = e.target;
      if (!input || input.dataset.logSelect !== '1') return;
      const id = input.dataset.id || '';
      if (!id) return;
      if (input.checked) {
        const row = state.logItemsCache.get(id);
        if (row) state.logSelected.set(id, row);
      } else {
        state.logSelected.delete(id);
      }
      updateLogSelectionCount();
    });
  }
}

function initAuditTable() {
  const table = UI.qs('#adminAuditTable');
  if (!table || !window.TableController) return;
  const mode = state.ui.table_mode || 'pagination';
  const pageSize = state.ui.table_page_size || 50;
  const id = table?.dataset?.tableId || table?.id || '';
  const prefs = window.UserPrefs?.getTable?.(id) || {};

  state.tables.audit = new TableController({
    table,
    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 }) => {
      const data = await api('/api/admin/audit', 'POST', {
        page,
        per_page,
        sort,
        dir,
        filters,
      });
      const items = data.items || [];
      updateAuditEventOptions(items);
      return {
        items,
        total: data.total || 0,
        page: data.page || page,
        per_page: data.per_page || per_page,
      };
    },
    renderRow: renderAuditRow,
  });
  state.tables.audit.total = 0;
  state.tables.audit.page = 1;
  state.tables.audit.updateFooter();

  const body = table.querySelector('tbody');
  if (body && !body.dataset.bindAudit) {
    body.dataset.bindAudit = '1';
    body.addEventListener('change', (e) => {
      const input = e.target;
      if (!input || input.dataset.auditSelect !== '1') return;
      const id = input.dataset.id || '';
      if (!id) return;
      if (input.checked) {
        const row = state.auditItemsCache.get(id);
        if (row) state.auditSelected.set(id, row);
      } else {
        state.auditSelected.delete(id);
      }
      updateAuditSelectionCount();
    });
  }
}

async function copySelectedLogs() {
  const type = UI.qs('#logsType')?.value || 'system';
  const items = type === 'audit'
    ? Array.from(state.auditSelected.values())
    : Array.from(state.logSelected.values());
  if (!items.length) return;
  const lines = items.map((r) => {
    if (type === 'audit') {
      return `${r.created_at || ''} | ${r.actor_email || r.actor_user_id || ''} | ${r.action || ''} | ${r.target_type || ''} ${r.target_id || ''} | ${r.meta_json || ''}`;
    }
    const ctx = formatLogContext(r.context_json);
    return `${r.ts || ''} [${r.level || ''}] ${r.message || ''}${ctx}`;
  });
  const text = lines.join('\n');
  try {
    await navigator.clipboard.writeText(text);
  } catch (e) {
    const ta = document.createElement('textarea');
    ta.value = text;
    document.body.appendChild(ta);
    ta.select();
    document.execCommand('copy');
    ta.remove();
  }
}

function selectAllLogsOnPage() {
  const type = UI.qs('#logsType')?.value || 'system';
  const body = type === 'audit' ? UI.qs('#adminAudit') : UI.qs('#logsTableBody');
  if (!body) return;
  const inputs = Array.from(body.querySelectorAll(type === 'audit' ? 'input[data-audit-select="1"]' : 'input[data-log-select="1"]'));
  if (!inputs.length) return;
  const allSelected = inputs.every(input => input.checked);
  inputs.forEach((input) => {
    if (input.checked === !allSelected) return;
    input.checked = !allSelected;
    input.dispatchEvent(new Event('change', { bubbles: true }));
  });
}

async function cleanupLogs() {
  const hint = UI.qs('#logsSettingsHint');
  if (!confirm(t('settings.logs.delete_all_warn', 'Delete all logs?'))) {
    return;
  }
  const keepDays = 0;
  if (hint) hint.textContent = t('common.running', 'Running…');
  try {
    const data = await api('/api/logs/cleanup', 'POST', { keep_days: keepDays });
    if (hint) hint.textContent = t('settings.logs.cleaned', 'Cleaned') + ` (${data.deleted || 0})`;
    if (state.tables.logs?.tbody) {
      state.tables.logs.tbody.innerHTML = '';
    }
    if (state.tables.logs) {
      state.tables.logs.total = 0;
      state.tables.logs.page = 1;
      state.tables.logs.updateFooter();
    }
    clearLogSelections();
  } catch (e) {
    if (hint) hint.textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}

function initLogsTabs() {
  const tabs = document.querySelectorAll('.logs-tabs .tab');
  const panes = document.querySelectorAll('.logs-pane');
  tabs.forEach(btn => {
    btn.addEventListener('click', () => {
      tabs.forEach(x => x.classList.remove('active'));
      panes.forEach(x => x.classList.remove('active'));
      btn.classList.add('active');
      const target = btn.dataset.logsTab || 'view';
      UI.qs(`#logs-pane-${target}`).classList.add('active');
    });
  });
}

function initLogsViewDates() {
  const saved = loadLogsViewDates();
  if (saved) {
    UI.qs('#logsDateFrom').value = saved.from;
    UI.qs('#logsDateTo').value = saved.to;
    return;
  }
  resetLogsDates();
}

function resetLogsDates() {
  const today = new Date();
  const yyyy = String(today.getFullYear());
  const mm = String(today.getMonth() + 1).padStart(2, '0');
  const dd = String(today.getDate()).padStart(2, '0');
  const d = `${yyyy}-${mm}-${dd}`;
  UI.qs('#logsDateFrom').value = d;
  UI.qs('#logsDateTo').value = d;
  saveLogsViewDates(d, d);
  if (state.tables.logs?.tbody) {
    state.tables.logs.tbody.innerHTML = '';
  }
  if (state.tables.logs) {
    state.tables.logs.total = 0;
    state.tables.logs.page = 1;
    state.tables.logs.updateFooter();
  }
  clearLogSelections();
  clearAuditSelections();
  const hint = UI.qs('#logsHint');
  if (hint) hint.textContent = '';
}

function updateLogsTypeUI() {
  const sel = UI.qs('#logsType');
  const type = sel ? sel.value : 'system';
  const levelControls = UI.qs('#logsControlsLevel');
  const eventControls = UI.qs('#logsControlsEvent');
  const systemActions = UI.qs('#logsControlsSystemActions');
  const output = UI.qs('#logsOutputWrap');
  const auditWrap = UI.qs('#logsAuditWrap');
  const auditBody = UI.qs('#adminAudit');
  const hint = UI.qs('#logsHint');

  const isAudit = type === 'audit';
  if (levelControls) levelControls.classList.toggle('is-hidden', isAudit);
  if (eventControls) eventControls.classList.toggle('is-hidden', !isAudit);
  if (systemActions) systemActions.classList.toggle('is-hidden', false);
  if (output) output.classList.toggle('is-hidden', isAudit);
  if (auditWrap) auditWrap.classList.toggle('is-hidden', !isAudit);
  if (isAudit) {
    state.auditItems = [];
    clearLogSelections();
    if (auditBody) auditBody.innerHTML = '';
    if (hint) hint.textContent = '';
    updateAuditSelectionCount();
  } else {
    updateLogSelectionCount();
  }
}

function saveLogsViewDates(from, to) {
  state.logsView.from = from;
  state.logsView.to = to;
  localStorage.setItem('scmedia_logs_from', from);
  localStorage.setItem('scmedia_logs_to', to);
}

function loadLogsViewDates() {
  const from = localStorage.getItem('scmedia_logs_from') || '';
  const to = localStorage.getItem('scmedia_logs_to') || '';
  if (!from || !to) return null;
  return { from, to };
}

function saveLogsEventFilter(value) {
  localStorage.setItem('scmedia_logs_event', value || 'all');
}

function loadLogsEventFilter() {
  return localStorage.getItem('scmedia_logs_event') || 'all';
}

async function detectTools() {
  if (state.settingsLoadPromise) {
    await state.settingsLoadPromise;
  }
  const hint = UI.qs('#toolsHint');
  if (hint) hint.textContent = t('common.running', 'Running…');
  try {
    const data = await api('/api/tools/detect-binaries', 'GET');
    state.toolsList = buildToolsList(data || {});
    renderToolsList();
    setDirty(true);
    if (hint) hint.textContent = t('settings.tools.detected', 'Detected');
  } catch (e) {
    if (hint) hint.textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}

function buildToolsList(tools) {
  const list = [];
  if (tools.mkvmerge_path) list.push({ type: 'mkvmerge', label: 'mkvmerge', path: tools.mkvmerge_path });
  if (tools.mkvpropedit_path) list.push({ type: 'mkvpropedit', label: 'mkvpropedit', path: tools.mkvpropedit_path });
  if (tools.ffmpeg_path) list.push({ type: 'ffmpeg', label: 'ffmpeg', path: tools.ffmpeg_path });
  return list;
}

function toolPathByType(type) {
  const t = state.toolsList.find(x => x.type === type);
  return t ? t.path : '';
}

function updateToolPath(type, path) {
  const existing = state.toolsList.find(x => x.type === type);
  if (path === '' && existing) {
    state.toolsList = state.toolsList.filter(x => x.type !== type);
  } else if (existing) {
    existing.path = path;
  } else if (path !== '') {
    state.toolsList.push({ type, label: type, path });
  }
  renderToolsList();
}

function renderToolRow(tbody, tl) {
  const tr = document.createElement('tr');
  const tdName = document.createElement('td');
  tdName.textContent = t(`settings.tools.${tl.type}`, tl.label);
  const tdPath = document.createElement('td');
  tdPath.textContent = tl.path || '';
  const tdActions = document.createElement('td');
  const btnEdit = document.createElement('button');
  btnEdit.className = 'btn';
  btnEdit.textContent = t('common.edit', 'Edit');
  btnEdit.addEventListener('click', () => openToolModal(tl.type));
  const btnRemove = document.createElement('button');
  btnRemove.className = 'btn';
  btnRemove.textContent = t('common.delete', 'Delete');
  btnRemove.addEventListener('click', () => {
    updateToolPath(tl.type, '');
    setDirty(true);
  });
  tdActions.appendChild(btnEdit);
  tdActions.appendChild(btnRemove);
  tr.appendChild(tdName);
  tr.appendChild(tdPath);
  tr.appendChild(tdActions);
  tbody.appendChild(tr);
}

function renderToolsList() {
  const table = state.tables.tools;
  if (table) {
    table.reload();
    return;
  }
  const tbody = UI.qs('#toolsTable tbody');
  if (!tbody) return;
  tbody.innerHTML = '';
  state.toolsList.forEach(tl => renderToolRow(tbody, tl));
}

function initToolsUi() {
  const btnAdd = UI.qs('#btnAddTool');
  const menu = UI.qs('#toolTypeMenu');
  if (btnAdd && menu) {
    btnAdd.addEventListener('click', (e) => {
      e.preventDefault();
      btnAdd.parentElement?.classList.toggle('open');
    });
    menu.querySelectorAll('button[data-tool-type]').forEach(btn => {
      btn.addEventListener('click', () => {
        const type = btn.dataset.toolType || 'mkvmerge';
        btnAdd.parentElement?.classList.remove('open');
        openToolModal(type);
      });
    });
    document.addEventListener('click', (e) => {
      if (!btnAdd.parentElement?.contains(e.target)) {
        btnAdd.parentElement?.classList.remove('open');
      }
    });
  }
}

function initTasksUi() {
  const btnAdd = UI.qs('#btnAddTask');
  if (!btnAdd) return;
  btnAdd.addEventListener('click', () => openTaskModal(null));
}

function initTemplatesUi() {
  const btnAdd = UI.qs('#btnAddTemplate');
  if (!btnAdd) return;
  btnAdd.addEventListener('click', () => openTemplateModal(null));
}

function openTaskModal(id = null) {
  const modal = UI.qs('#ruleModal');
  const body = UI.qs('#ruleModalBody');
  const title = UI.qs('#ruleModalTitle');
  if (!modal || !body || !title) return;

  state.ruleEditId = null;
  state.ruleEditType = null;
  state.toolEditType = null;
  state.taskEditId = (id !== null && id !== undefined) ? id : 'new';
  state.templateEditId = null;

  const task = id ? state.tasksList.find(t => t.id === id) : null;
  const current = task || normalizeTask({});

  title.textContent = t('settings.tasks.modal_title', 'Task');
  const checked = (list, value) => list.includes(value) ? 'checked' : '';
  const sourceOptions = buildTaskSourcesOptions(current.sources || []);
  const ruleOptions = buildRuleOptions(current.rule_ids || []);
  const templateOptions = buildTemplateOptions(current.template_id || '');

  body.innerHTML = `
    <div class="grid">
      <label>
        <div class="lbl" data-i18n="settings.tasks.field_name">${t('settings.tasks.field_name', 'Name')}</div>
        <input data-field="task_name" type="text" value="${escapeHtml(current.name || '')}">
      </label>
      <label>
        <div class="lbl" data-i18n="settings.tasks.field_enabled">${t('settings.tasks.field_enabled', 'Enabled')}</div>
        <select data-field="task_enabled">
          <option value="1">${t('rules.status.on', 'On')}</option>
          <option value="0">${t('rules.status.off', 'Off')}</option>
        </select>
      </label>
      <div class="full">
        <div class="lbl" data-i18n="settings.tasks.field_sources">${t('settings.tasks.field_sources', 'Sources')}</div>
        <div class="checks">${sourceOptions}</div>
      </div>
      <div class="full">
        <div class="lbl" data-i18n="settings.tasks.field_rules">${t('settings.tasks.field_rules', 'Rules')}</div>
        <div class="checks">${ruleOptions}</div>
      </div>
      <label class="full">
        <div class="lbl" data-i18n="settings.tasks.field_template">${t('settings.tasks.field_template', 'Template')}</div>
        <select data-field="task_template">
          ${templateOptions}
        </select>
      </label>
      <div class="hint" data-i18n="settings.tasks.hint">${t('settings.tasks.hint', 'Sources + rules choose what to process. Template defines the action pipeline.')}</div>
      </div>
    </div>
  `;
  const enabledSel = body.querySelector('[data-field="task_enabled"]');
  if (enabledSel) enabledSel.value = current.enabled ? '1' : '0';

  modal.style.display = 'flex';
}

function saveTaskModal(body) {
  const name = body.querySelector('[data-field="task_name"]')?.value?.trim() || '';
  const enabled = body.querySelector('[data-field="task_enabled"]')?.value === '1';
  const sources = Array.from(body.querySelectorAll('[data-field="task_source"]')).filter(x => x.checked).map(x => x.value);
  const ruleIds = Array.from(body.querySelectorAll('[data-field="task_rule"]')).filter(x => x.checked).map(x => x.value);
  const templateId = body.querySelector('[data-field="task_template"]')?.value || '';

  const updated = {
    id: state.taskEditId || normalizeTask({}).id,
    name,
    enabled,
    sources,
    rule_ids: ruleIds,
    template_id: templateId,
  };

  const idx = state.tasksList.findIndex(t => t.id === updated.id);
  if (idx >= 0) {
    state.tasksList[idx] = updated;
  } else {
    state.tasksList.push(updated);
  }
  renderTasksList();
  setDirty(true);
  closeRuleModal();
}

function buildTaskSourcesOptions(selected) {
  const list = Array.isArray(selected) ? selected : [];
  const roots = state.rootsList || [];
  const options = [];
  const hasMovie = roots.some(r => r.type === 'movie');
  const hasSeries = roots.some(r => r.type === 'series');
  const hasStaging = roots.some(r => r.type === 'staging');
  if (hasMovie) options.push({ value: 'movie', label: t('settings.tasks.source.movie', 'Movies') });
  if (hasSeries) options.push({ value: 'series', label: t('settings.tasks.source.series', 'Series') });
  if (hasStaging) options.push({ value: 'staging', label: t('settings.tasks.source.staging', 'Staging') });
  if (state.pluginConfig?.sources?.transmission) {
    options.push({ value: 'transmission', label: t('settings.tasks.source.transmission', 'Transmission') });
  }
  if (options.length === 0) {
    options.push({ value: 'staging', label: t('settings.tasks.source.staging', 'Staging') });
  }
  return options
    .map(opt => {
      const isChecked = list.includes(opt.value) ? 'checked' : '';
      return `<label class="row"><input type="checkbox" data-field="task_source" value="${escapeHtml(opt.value)}" ${isChecked}> ${escapeHtml(opt.label)}</label>`;
    })
    .join('');
}

function buildRuleOptions(selected) {
  const list = Array.isArray(selected) ? selected : [];
  const rules = state.rulesList || [];
  if (rules.length === 0) {
    return `<div class="hint">${t('settings.tasks.rules_empty', 'No rules yet')}</div>`;
  }
  return rules
    .map(r => {
      const isChecked = list.includes(r.id) ? 'checked' : '';
      return `<label class="row"><input type="checkbox" data-field="task_rule" value="${escapeHtml(r.id)}" ${isChecked}> ${escapeHtml(r.name || r.id)}</label>`;
    })
    .join('');
}

function buildTemplateOptions(selected) {
  const templates = state.templatesList || [];
  const options = templates
    .map(tpl => `<option value="${escapeHtml(tpl.id)}"${tpl.id === selected ? ' selected' : ''}>${escapeHtml(tpl.name || tpl.id)}</option>`);
  return [`<option value="">${t('settings.tasks.template_none', 'No template')}</option>`, ...options].join('');
}

function renderTemplateRow(tbody, tpl) {
  const tr = document.createElement('tr');
  tr.dataset.id = tpl.id;
  const tdName = document.createElement('td');
  tdName.textContent = tpl.name || t('settings.templates.unnamed', 'Untitled');
  const tdAction = document.createElement('td');
  tdAction.textContent = tpl.action || '';
  const tdStatus = document.createElement('td');
  tdStatus.textContent = tpl.enabled ? t('rules.status.on', 'On') : t('rules.status.off', 'Off');
  const tdActions = document.createElement('td');
  const btnEdit = document.createElement('button');
  btnEdit.className = 'btn';
  btnEdit.textContent = t('common.edit', 'Edit');
  btnEdit.addEventListener('click', () => openTemplateModal(tpl.id));
  const btnToggle = document.createElement('button');
  btnToggle.className = 'btn';
  btnToggle.textContent = tpl.enabled ? t('rules.disable', 'Disable') : t('rules.enable', 'Enable');
  btnToggle.addEventListener('click', () => {
    tpl.enabled = !tpl.enabled;
    renderTemplatesList();
    setDirty(true);
  });
  const btnDel = document.createElement('button');
  btnDel.className = 'btn';
  btnDel.textContent = t('common.delete', 'Delete');
  btnDel.addEventListener('click', () => {
    if (!confirm(t('settings.templates.confirm_delete', 'Delete template?'))) return;
    state.templatesList = state.templatesList.filter(x => x.id !== tpl.id);
    renderTemplatesList();
    setDirty(true);
  });
  tdActions.appendChild(btnEdit);
  tdActions.appendChild(btnToggle);
  tdActions.appendChild(btnDel);
  tr.appendChild(tdName);
  tr.appendChild(tdAction);
  tr.appendChild(tdStatus);
  tr.appendChild(tdActions);
  tbody.appendChild(tr);
}

function renderTemplatesList() {
  const table = state.tables.templates;
  if (table) {
    table.reload();
    return;
  }
  const tbody = UI.qs('#templatesTable tbody');
  if (!tbody) return;
  tbody.innerHTML = '';
  state.templatesList.forEach(tpl => renderTemplateRow(tbody, tpl));
}

function openTemplateModal(id = null) {
  const modal = UI.qs('#ruleModal');
  const body = UI.qs('#ruleModalBody');
  const title = UI.qs('#ruleModalTitle');
  if (!modal || !body || !title) return;

  state.ruleEditId = null;
  state.ruleEditType = null;
  state.toolEditType = null;
  state.taskEditId = null;
  state.rootEditId = null;
  state.templateEditId = (id !== null && id !== undefined) ? id : 'new';

  const tpl = id ? state.templatesList.find(t => t.id === id) : null;
  const current = tpl || normalizeTemplate({});

  title.textContent = t('settings.templates.modal_title', 'Template');
  body.innerHTML = `
    <div class="grid">
      <label>
        <div class="lbl">${t('settings.templates.field_name', 'Name')}</div>
        <input data-field="template_name" type="text" value="${escapeHtml(current.name || '')}">
      </label>
      <label>
        <div class="lbl">${t('settings.templates.field_action', 'Action')}</div>
        <input data-field="template_action" type="text" value="${escapeHtml(current.action || '')}">
      </label>
      <label>
        <div class="lbl">${t('settings.templates.field_enabled', 'Enabled')}</div>
        <select data-field="template_enabled">
          <option value="1">${t('rules.status.on', 'On')}</option>
          <option value="0">${t('rules.status.off', 'Off')}</option>
        </select>
      </label>
      <div class="hint">${t('settings.templates.hint', 'Describe the action pipeline (mapping, convert, normalize, export).')}</div>
    </div>
  `;
  const enabledSel = body.querySelector('[data-field="template_enabled"]');
  if (enabledSel) enabledSel.value = current.enabled ? '1' : '0';
  modal.style.display = 'flex';
}

function saveTemplateModal(body) {
  const name = body.querySelector('[data-field="template_name"]')?.value?.trim() || '';
  const action = body.querySelector('[data-field="template_action"]')?.value?.trim() || '';
  const enabled = body.querySelector('[data-field="template_enabled"]')?.value === '1';
  const updated = {
    id: state.templateEditId || normalizeTemplate({}).id,
    name,
    action,
    enabled,
  };
  const idx = state.templatesList.findIndex(t => t.id === updated.id);
  if (idx >= 0) {
    state.templatesList[idx] = updated;
  } else {
    state.templatesList.push(updated);
  }
  renderTemplatesList();
  setDirty(true);
  closeRuleModal();
}
function openToolModal(type) {
  const modal = UI.qs('#ruleModal');
  const body = UI.qs('#ruleModalBody');
  const title = UI.qs('#ruleModalTitle');
  if (!modal || !body || !title) return;
  state.ruleEditId = null;
  state.ruleEditType = null;
  state.toolEditType = type;
  title.textContent = t('settings.tools.modal_title', 'Tool');
  const current = toolPathByType(type);
  body.innerHTML = `
    <div class="grid">
      <label>
        <div class="lbl">${t('settings.tools.th_name','Tool')}</div>
        <input data-field="tool_type" type="text" value="${escapeHtml(type)}" disabled>
      </label>
      <label>
        <div class="lbl">${t('settings.tools.th_path','Path')}</div>
        <input data-field="tool_path" type="text" value="${escapeHtml(current)}" placeholder="/usr/bin/${escapeHtml(type)}">
      </label>
    </div>
  `;
  modal.style.display = 'flex';
}

function normalizePluginConfig() {
  if (!state.pluginConfig) state.pluginConfig = { metadata: {}, exports: {}, sources: {} };
  const meta = state.pluginConfig.metadata || {};
  const exportsCfg = state.pluginConfig.exports || {};
  const sourcesCfg = state.pluginConfig.sources || {};

  if (typeof meta.enabled !== 'boolean') meta.enabled = false;
  if (!Array.isArray(meta.languages)) meta.languages = ['de','ru','en'];
  if (!Array.isArray(meta.provider_priority)) meta.provider_priority = ['tvdb','omdb'];
  if (!meta.providers) meta.providers = {};
  if (!meta.providers.omdb) meta.providers.omdb = { enabled: false, api_key: '', base_url: 'https://www.omdbapi.com/' };
  if (!meta.providers.tvdb) meta.providers.tvdb = { enabled: false, api_key: '', pin: '' };

  if (!exportsCfg.kodi) exportsCfg.kodi = { enabled: false };
  if (!exportsCfg.jellyfin) exportsCfg.jellyfin = { enabled: false };

  if (!sourcesCfg.transmission) {
    sourcesCfg.transmission = { enabled: false, last_test_ok: false, last_test_at: null, protocol: 'http', host: '', port: 9091, path: '/transmission/rpc', username: '', password: '', display_fields: [] };
  }
  if (typeof sourcesCfg.transmission.last_test_ok !== 'boolean') {
    sourcesCfg.transmission.last_test_ok = false;
  }
  if (!Array.isArray(sourcesCfg.transmission.display_fields)) {
    sourcesCfg.transmission.display_fields = [];
  }

  state.pluginConfig.metadata = meta;
  state.pluginConfig.exports = exportsCfg;
  state.pluginConfig.sources = sourcesCfg;
}

function renderPluginRow(tbody, p) {
  const tr = document.createElement('tr');
  tr.className = `plugin-row${p.enabled ? '' : ' is-off'}`;
  const tdName = document.createElement('td');
  tdName.textContent = p.label;
  const tdType = document.createElement('td');
  tdType.textContent = p.kindLabel;
  const tdStatus = document.createElement('td');
  tdStatus.textContent = p.enabled === null ? t('rules.status.on', 'On') : (p.enabled ? t('rules.status.on', 'On') : t('rules.status.off', 'Off'));
  const tdActions = document.createElement('td');

  const btnEdit = document.createElement('button');
  btnEdit.className = 'btn';
  btnEdit.textContent = t('common.edit', 'Edit');
  btnEdit.addEventListener('click', () => openPluginModal(p.type));
  tdActions.appendChild(btnEdit);

  if (p.enabled !== null) {
    const btnToggle = document.createElement('button');
    btnToggle.className = 'btn';
    btnToggle.textContent = p.enabled ? t('rules.disable', 'Disable') : t('rules.enable', 'Enable');
    btnToggle.addEventListener('click', () => {
      setPluginEnabled(p.type, !p.enabled);
      renderPluginsList();
      setDirty(true);
    });
    tdActions.appendChild(btnToggle);
  }

  tr.appendChild(tdName);
  tr.appendChild(tdType);
  tr.appendChild(tdStatus);
  tr.appendChild(tdActions);
  tbody.appendChild(tr);
}

function renderPluginsList() {
  const table = state.tables.plugins;
  if (table) {
    table.reload();
    return;
  }
  const tbody = UI.qs('#pluginsTable tbody');
  if (!tbody) return;
  tbody.innerHTML = '';

  const items = buildPluginsList();
  items.forEach(p => renderPluginRow(tbody, p));
}

function buildPluginsList() {
  const meta = state.pluginConfig?.metadata || {};
  const providers = meta.providers || {};
  const exportsCfg = state.pluginConfig?.exports || {};
  const sourcesCfg = state.pluginConfig?.sources || {};

  return [
    { type: 'meta_settings', label: t('settings.plugins.meta_settings', 'Metadata settings'), kindLabel: t('settings.plugins.kind.meta', 'Metadata'), enabled: !!meta.enabled },
    { type: 'omdb', label: t('settings.plugins.omdb_label', 'IMDb (OMDb)'), kindLabel: t('settings.plugins.kind.meta', 'Metadata'), enabled: !!providers.omdb?.enabled && !!meta.enabled },
    { type: 'tvdb', label: t('settings.plugins.tvdb_label', 'TVDB'), kindLabel: t('settings.plugins.kind.meta', 'Metadata'), enabled: !!providers.tvdb?.enabled && !!meta.enabled },
    { type: 'kodi', label: t('settings.plugins.kodi_label', 'Kodi'), kindLabel: t('settings.plugins.kind.export', 'Export'), enabled: !!exportsCfg.kodi?.enabled },
    { type: 'jellyfin', label: t('settings.plugins.jellyfin_label', 'Jellyfin'), kindLabel: t('settings.plugins.kind.export', 'Export'), enabled: !!exportsCfg.jellyfin?.enabled },
    { type: 'transmission', label: t('settings.plugins.transmission_label', 'Transmission'), kindLabel: t('settings.plugins.kind.source', 'Source'), enabled: !!sourcesCfg.transmission?.enabled },
  ];
}

function ensureTransmissionRule() {
  const existing = state.rulesList.find(r => r.type === 'source_filter' && r.config?.source === 'transmission');
  if (existing) {
    if (!existing.enabled) existing.enabled = true;
    return;
  }
  const rule = normalizeRule({
    type: 'source_filter',
    name: 'Transmission (all)',
    enabled: true,
    config: {
      source: 'transmission',
      status: '',
      label: '',
      min_size: 0,
      name_regex: '',
      path_regex: '',
    },
  });
  state.rulesList.push(rule);
}

function setPluginEnabled(type, enabled) {
  normalizePluginConfig();
  if ((type === 'omdb' || type === 'tvdb') && !state.pluginConfig.metadata.enabled && enabled) {
    const hint = UI.qs('#pluginsHint');
    if (hint) hint.textContent = t('settings.plugins.requires_meta', 'Enable metadata settings first');
    return;
  }
  if (type === 'transmission' && enabled && !state.pluginConfig.sources.transmission.last_test_ok) {
    const hint = UI.qs('#pluginsHint');
    if (hint) hint.textContent = t('settings.plugins.requires_test', 'Test connection first');
    return;
  }
  if (type === 'omdb') state.pluginConfig.metadata.providers.omdb.enabled = enabled;
  if (type === 'tvdb') state.pluginConfig.metadata.providers.tvdb.enabled = enabled;
  if (type === 'meta_settings') state.pluginConfig.metadata.enabled = enabled;
  if (type === 'kodi') state.pluginConfig.exports.kodi.enabled = enabled;
  if (type === 'jellyfin') state.pluginConfig.exports.jellyfin.enabled = enabled;
  if (type === 'transmission') {
    state.pluginConfig.sources.transmission.enabled = enabled;
    if (enabled) {
      ensureTransmissionRule();
      renderRulesList();
      setDirty(true);
    }
  }
}

function initPluginsUi() {
  const btnAdd = UI.qs('#btnAddPlugin');
  const fileInput = UI.qs('#pluginFileInput');
  if (btnAdd && fileInput) {
    btnAdd.addEventListener('click', (e) => {
      e.preventDefault();
      fileInput.click();
    });
    fileInput.addEventListener('change', () => {
      // Placeholder only: no install yet
      fileInput.value = '';
      const hint = UI.qs('#pluginsHint');
      if (hint) hint.textContent = t('settings.plugins.install_placeholder', 'Installer coming soon');
    });
  }
}

function openPluginModal(type) {
  const modal = UI.qs('#ruleModal');
  const body = UI.qs('#ruleModalBody');
  const title = UI.qs('#ruleModalTitle');
  if (!modal || !body || !title) return;
  state.ruleEditId = null;
  state.ruleEditType = null;
  state.toolEditType = null;
  state.rootEditId = null;

  state.pluginEditType = type;
  title.textContent = t('settings.plugins.modal_title', 'Plugin');
  body.innerHTML = renderPluginForm(type);
  applyPluginFormValues(body, type);
  modal.style.display = 'flex';
}

function renderPluginForm(type) {
  const meta = state.pluginConfig?.metadata || {};
  const providers = meta.providers || {};
  const exportsCfg = state.pluginConfig?.exports || {};
  const sourcesCfg = state.pluginConfig?.sources || {};
  const tr = sourcesCfg.transmission || {};

  if (type === 'meta_settings') {
    return `
      <div class="grid">
        <label>
          <div class="lbl">${t('settings.plugins.enable','Enabled')}</div>
          <select data-field="enabled"><option value="1">${t('common.yes','Yes')}</option><option value="0">${t('common.no','No')}</option></select>
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.languages','Metadata languages (comma)')}</div>
          <input data-field="meta_languages" type="text" value="${escapeHtml((meta.languages || []).join(','))}">
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.provider_priority','Provider priority (comma)')}</div>
          <input data-field="meta_priority" type="text" value="${escapeHtml((meta.provider_priority || []).join(','))}">
        </label>
      </div>
    `;
  }

  if (type === 'omdb') {
    return `
      <div class="grid">
        <label>
          <div class="lbl">${t('settings.plugins.enable','Enabled')}</div>
          <select data-field="enabled"><option value="1">${t('common.yes','Yes')}</option><option value="0">${t('common.no','No')}</option></select>
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.omdb_key','OMDb API key')}</div>
          <input data-field="api_key" type="text" value="${escapeHtml(providers.omdb?.api_key || '')}">
        </label>
      </div>
    `;
  }

  if (type === 'tvdb') {
    return `
      <div class="grid">
        <label>
          <div class="lbl">${t('settings.plugins.enable','Enabled')}</div>
          <select data-field="enabled"><option value="1">${t('common.yes','Yes')}</option><option value="0">${t('common.no','No')}</option></select>
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.tvdb_key','TVDB API key')}</div>
          <input data-field="api_key" type="text" value="${escapeHtml(providers.tvdb?.api_key || '')}">
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.tvdb_pin','TVDB PIN (optional)')}</div>
          <input data-field="pin" type="text" value="${escapeHtml(providers.tvdb?.pin || '')}">
        </label>
      </div>
    `;
  }

  if (type === 'kodi' || type === 'jellyfin') {
    const enabled = (type === 'kodi') ? exportsCfg.kodi?.enabled : exportsCfg.jellyfin?.enabled;
    return `
      <div class="grid">
        <label>
          <div class="lbl">${t('settings.plugins.enable','Enabled')}</div>
          <select data-field="enabled"><option value="1">${t('common.yes','Yes')}</option><option value="0">${t('common.no','No')}</option></select>
        </label>
        <div class="hint">${type === 'kodi' ? t('settings.plugins.kodi_hint','Writes movie.nfo / tvshow.nfo near files') : t('settings.plugins.jellyfin_hint','Writes movie.nfo / tvshow.nfo near files')}</div>
      </div>
    `;
  }

  if (type === 'transmission') {
    return `
      <div class="grid">
        <label>
          <div class="lbl">${t('settings.plugins.enable','Enabled')}</div>
          <select data-field="enabled"><option value="1">${t('common.yes','Yes')}</option><option value="0">${t('common.no','No')}</option></select>
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.transmission_protocol','Protocol')}</div>
          <select data-field="protocol">
            <option value="http">http</option>
            <option value="https">https</option>
          </select>
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.transmission_host','Host')}</div>
          <input data-field="host" type="text" value="${escapeHtml(tr.host || '')}">
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.transmission_port','Port')}</div>
          <input data-field="port" type="number" value="${escapeHtml(String(tr.port ?? 9091))}">
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.transmission_path','RPC path')}</div>
          <input data-field="path" type="text" value="${escapeHtml(tr.path || '/transmission/rpc')}">
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.transmission_user','Username')}</div>
          <input data-field="username" type="text" value="${escapeHtml(tr.username || '')}">
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.transmission_pass','Password')}</div>
          <input data-field="password" type="password" value="${escapeHtml(tr.password || '')}">
        </label>
        <label>
          <div class="lbl">${t('settings.plugins.transmission_display_fields','Display fields (comma)')}</div>
          <input data-field="display_fields" type="text" value="${escapeHtml((tr.display_fields || []).join(','))}">
        </label>
      </div>
      <div class="row">
        <button class="btn" type="button" id="btnPluginTest">${t('settings.plugins.transmission_test','Test connection')}</button>
        <div class="hint" id="pluginTestHint"></div>
      </div>
    `;
  }

  return '';
}

function applyPluginFormValues(body, type) {
  normalizePluginConfig();
  const meta = state.pluginConfig.metadata;
  const providers = meta.providers || {};
  const exportsCfg = state.pluginConfig.exports;
  const sourcesCfg = state.pluginConfig.sources;

  const setVal = (field, value) => {
    const el = body.querySelector(`[data-field="${field}"]`);
    if (el) el.value = value ?? '';
  };

  if (type === 'meta_settings') {
    setVal('enabled', meta.enabled ? '1' : '0');
    setVal('meta_languages', (meta.languages || []).join(','));
    setVal('meta_priority', (meta.provider_priority || []).join(','));
  } else if (type === 'omdb') {
    setVal('enabled', providers.omdb?.enabled ? '1' : '0');
    setVal('api_key', providers.omdb?.api_key || '');
  } else if (type === 'tvdb') {
    setVal('enabled', providers.tvdb?.enabled ? '1' : '0');
    setVal('api_key', providers.tvdb?.api_key || '');
    setVal('pin', providers.tvdb?.pin || '');
  } else if (type === 'kodi') {
    setVal('enabled', exportsCfg.kodi?.enabled ? '1' : '0');
  } else if (type === 'jellyfin') {
    setVal('enabled', exportsCfg.jellyfin?.enabled ? '1' : '0');
  } else if (type === 'transmission') {
    const tr = sourcesCfg.transmission || {};
    setVal('enabled', tr.enabled ? '1' : '0');
    setVal('protocol', tr.protocol || 'http');
    setVal('host', tr.host || '');
    setVal('port', String(tr.port ?? 9091));
    setVal('path', tr.path || '/transmission/rpc');
    setVal('username', tr.username || '');
    setVal('password', tr.password || '');
    setVal('display_fields', (tr.display_fields || []).join(','));
    const btn = body.querySelector('#btnPluginTest');
    if (btn) {
      btn.addEventListener('click', async () => {
        const hint = body.querySelector('#pluginTestHint');
        if (hint) hint.textContent = t('common.testing', 'Testing…');
        clearFieldErrors(body, ['host','port','path']);
        const payload = { transmission: readTransmissionFields(body) };
        const missing = [];
        if (!payload.transmission.host) missing.push('host');
        if (!payload.transmission.port) missing.push('port');
        if (!payload.transmission.path) missing.push('path');
        if (missing.length > 0) {
          markFieldErrors(body, missing);
          if (hint) hint.textContent = t('settings.plugins.transmission_missing', 'Fill required fields');
          return;
        }
        try {
          await api('/api/sources/test', 'POST', payload);
          state.pluginConfig.sources.transmission.last_test_ok = true;
          state.pluginConfig.sources.transmission.last_test_at = new Date().toISOString();
          renderPluginsList();
          setDirty(true);
          if (hint) hint.textContent = t('settings.plugins.transmission_ok', 'OK');
        } catch (e) {
          state.pluginConfig.sources.transmission.last_test_ok = false;
          state.pluginConfig.sources.transmission.last_test_at = new Date().toISOString();
          renderPluginsList();
          setDirty(true);
          const msg = normalizeTransmissionErrorMessage(e.message);
          if (hint) hint.textContent = `${t('common.error','Error')}: ${msg}`;
        }
      });
    }
  }
}

function normalizeTransmissionErrorMessage(message) {
  if (!message) return t('settings.plugins.transmission_rpc_failed', 'RPC failed. Check address or auth.');
  if (message === 'unauthorized') {
    return t('settings.plugins.transmission_unauthorized', 'Unauthorized. Check RPC username/password.');
  }
  if (message === 'forbidden') {
    return t('settings.plugins.transmission_forbidden', 'Access denied. Check whitelist or credentials.');
  }
  if (message === 'rpc failed') {
    return t('settings.plugins.transmission_rpc_failed', 'RPC failed. Check address or auth.');
  }
  return message;
}

function isTaskModalOpen() {
  return state.taskEditId !== null;
}

function savePluginModal(body) {
  normalizePluginConfig();
  const type = state.pluginEditType;
  if (!type) return;
  const meta = state.pluginConfig.metadata;
  const providers = meta.providers || {};
  const exportsCfg = state.pluginConfig.exports;
  const sourcesCfg = state.pluginConfig.sources;

  const getVal = (field) => body.querySelector(`[data-field="${field}"]`)?.value ?? '';

  if (type === 'meta_settings') {
    meta.enabled = getVal('enabled') === '1';
    meta.languages = commaToList(getVal('meta_languages'));
    meta.provider_priority = commaToList(getVal('meta_priority'));
  } else if (type === 'omdb') {
    providers.omdb = providers.omdb || {};
    providers.omdb.enabled = getVal('enabled') === '1';
    providers.omdb.api_key = getVal('api_key').trim();
    providers.omdb.base_url = providers.omdb.base_url || 'https://www.omdbapi.com/';
  } else if (type === 'tvdb') {
    providers.tvdb = providers.tvdb || {};
    providers.tvdb.enabled = getVal('enabled') === '1';
    providers.tvdb.api_key = getVal('api_key').trim();
    providers.tvdb.pin = getVal('pin').trim();
  } else if (type === 'kodi') {
    exportsCfg.kodi.enabled = getVal('enabled') === '1';
  } else if (type === 'jellyfin') {
    exportsCfg.jellyfin.enabled = getVal('enabled') === '1';
  } else if (type === 'transmission') {
    sourcesCfg.transmission = readTransmissionFields(body);
    if (sourcesCfg.transmission.enabled && !sourcesCfg.transmission.last_test_ok) {
      sourcesCfg.transmission.enabled = false;
      const hint = UI.qs('#pluginsHint');
      if (hint) hint.textContent = t('settings.plugins.requires_test', 'Test connection first');
    }
  }

  state.pluginConfig.metadata = meta;
  state.pluginConfig.exports = exportsCfg;
  state.pluginConfig.sources = sourcesCfg;
  renderPluginsList();
}

function readTransmissionFields(body) {
  return {
    enabled: body.querySelector('[data-field="enabled"]')?.value === '1',
    last_test_ok: state.pluginConfig?.sources?.transmission?.last_test_ok ?? false,
    last_test_at: state.pluginConfig?.sources?.transmission?.last_test_at ?? null,
    protocol: body.querySelector('[data-field="protocol"]')?.value || 'http',
    host: body.querySelector('[data-field="host"]')?.value?.trim() || '',
    port: Number(body.querySelector('[data-field="port"]')?.value || 9091),
    path: body.querySelector('[data-field="path"]')?.value?.trim() || '/transmission/rpc',
    username: body.querySelector('[data-field="username"]')?.value?.trim() || '',
    password: body.querySelector('[data-field="password"]')?.value ?? '',
    display_fields: commaToList(body.querySelector('[data-field="display_fields"]')?.value ?? ''),
  };
}

function markFieldErrors(body, fields) {
  fields.forEach(f => {
    const el = body.querySelector(`[data-field="${f}"]`);
    if (el) el.classList.add('input-error');
  });
}

function clearFieldErrors(body, fields) {
  fields.forEach(f => {
    const el = body.querySelector(`[data-field="${f}"]`);
    if (el) el.classList.remove('input-error');
  });
}

function buildRootsList(paths) {
  const roots = Array.isArray(paths.roots) ? paths.roots : [];
  if (roots.length > 0) {
    return roots.map(r => normalizeRoot(r));
  }
  const out = [];
  if (paths.movies_root) out.push(normalizeRoot({ type: 'movie', path: paths.movies_root, enabled: true }));
  if (paths.series_root) out.push(normalizeRoot({ type: 'series', path: paths.series_root, enabled: true }));
  if (paths.staging_root) out.push(normalizeRoot({ type: 'staging', path: paths.staging_root, enabled: true }));
  return out;
}

function normalizeRoot(r) {
  return {
    id: r?.id || `root_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
    type: r?.type || 'movie',
    path: r?.path || '',
    enabled: r?.enabled !== false,
  };
}

function renderRootRow(tbody, r) {
  const tr = document.createElement('tr');
  const tdType = document.createElement('td');
  tdType.textContent = rootTypeLabel(r.type);
  const tdPath = document.createElement('td');
  tdPath.textContent = r.path || '';
  const tdStatus = document.createElement('td');
  tdStatus.textContent = r.enabled ? t('rules.status.on', 'On') : t('rules.status.off', 'Off');
  const tdActions = document.createElement('td');
  const btnEdit = document.createElement('button');
  btnEdit.className = 'btn';
  btnEdit.textContent = t('common.edit', 'Edit');
  btnEdit.addEventListener('click', () => openRootModal(r.id));
  const btnTest = document.createElement('button');
  btnTest.className = 'btn';
  btnTest.textContent = t('common.test', 'Test');
  btnTest.addEventListener('click', () => testRootPath(r));
  const btnDel = document.createElement('button');
  btnDel.className = 'btn';
  btnDel.textContent = t('common.delete', 'Delete');
  btnDel.addEventListener('click', () => {
    if (!confirm(t('settings.sources.confirm_delete', 'Delete source?'))) return;
    state.rootsList = state.rootsList.filter(x => x.id !== r.id);
    renderRootsList();
    setDirty(true);
  });
  tdActions.appendChild(btnEdit);
  tdActions.appendChild(btnTest);
  tdActions.appendChild(btnDel);

  tr.appendChild(tdType);
  tr.appendChild(tdPath);
  tr.appendChild(tdStatus);
  tr.appendChild(tdActions);
  tbody.appendChild(tr);
}

function renderRootsList() {
  const table = state.tables.roots;
  if (table) {
    table.reload();
    return;
  }
  const tbody = UI.qs('#rootsTable tbody');
  if (!tbody) return;
  tbody.innerHTML = '';
  state.rootsList.forEach(r => renderRootRow(tbody, r));
}

function rootTypeLabel(type) {
  if (type === 'movie') return t('root.type.movie', 'Movie');
  if (type === 'series') return t('root.type.series', 'Series');
  if (type === 'staging') return t('root.type.staging', 'Staging');
  return type || '';
}

function openRootModal(id = null) {
  const modal = UI.qs('#ruleModal');
  const body = UI.qs('#ruleModalBody');
  const title = UI.qs('#ruleModalTitle');
  if (!modal || !body || !title) return;
  state.ruleEditId = null;
  state.ruleEditType = null;
  state.toolEditType = null;

  const root = id ? state.rootsList.find(r => r.id === id) : null;
  const data = root ? { ...root } : normalizeRoot({});
  state.rootEditId = data.id;

  title.textContent = t('settings.sources.modal_title', 'Source');
  body.innerHTML = `
    <div class="root-row">
      <label>
        <div class="lbl">${t('settings.sources.th_type','Type')}</div>
        <select data-field="root_type">
          <option value="movie">${t('root.type.movie','Movie')}</option>
          <option value="series">${t('root.type.series','Series')}</option>
          <option value="staging">${t('root.type.staging','Staging')}</option>
        </select>
      </label>
      <label>
        <div class="lbl">${t('settings.sources.th_path','Path')}</div>
        <input data-field="root_path" type="text" value="${escapeHtml(data.path || '')}">
      </label>
      <label>
        <div class="lbl">${t('settings.sources.th_status','Status')}</div>
        <select data-field="root_enabled">
          <option value="1">${t('common.yes','Yes')}</option>
          <option value="0">${t('common.no','No')}</option>
        </select>
      </label>
    </div>
  `;
  const typeSel = body.querySelector('[data-field="root_type"]');
  const enabledSel = body.querySelector('[data-field="root_enabled"]');
  if (typeSel) typeSel.value = data.type;
  if (enabledSel) enabledSel.value = data.enabled ? '1' : '0';
  modal.style.display = 'flex';
}

function buildPathsPayload() {
  const roots = state.rootsList || [];
  const movie = roots.find(r => r.type === 'movie' && r.enabled);
  const series = roots.find(r => r.type === 'series' && r.enabled);
  const staging = roots.find(r => r.type === 'staging' && r.enabled);
  return {
    movies_root: movie?.path || '',
    series_root: series?.path || '',
    staging_root: staging?.path || '',
    roots,
  };
}

async function testRootPath(root) {
  const hint = UI.qs('#rootsHint');
  if (hint) hint.textContent = t('common.testing', 'Testing…');
  try {
    const data = await api('/api/tools/test-path', 'POST', { path: root.path, checks: ['exists','read','write','rename'] });
    const r = data.results || {};
    const txt = `${t('common.exists','exists')}=${r.exists} ${t('common.read','read')}=${r.read} ${t('common.write','write')}=${r.write} ${t('common.rename','rename')}=${r.rename} ${(data.notes || []).join('; ')}`;
    if (hint) hint.textContent = txt;
  } catch (e) {
    if (hint) hint.textContent = `${t('common.error','Error')}: ${e.message}`;
  }
}
