'use strict';
  // Bangumi API caller — still used by the token test and public-tag refresh (read-only helpers).
  async function apiFetch(path, token){
    const headers = {'Accept':'application/json'}; if(token) headers.Authorization = 'Bearer ' + token.trim();
    const res = await fetch(bgmApiBase() + path, {headers, mode:'cors'});
    if(!res.ok){ const text=await res.text().catch(()=>res.statusText); throw new Error(`HTTP ${res.status}: ${text.slice(0,200)}`); }
    return res.json();
  }
  async function testLogin(){
    const token = $('tokenInput').value.trim() || runtimeToken || state.settings.token;
    if(!token){ toast('请先粘贴 Access Token'); return; }
    $('loginLog').textContent = ''; log('loginLog','读取 /v0/me ...');
    try{
      const me = await apiFetch('/v0/me', token); const username = me.username || me.name || $('usernameInput').value.trim();
      $('usernameInput').value = username || ''; log('loginLog',`登录成功：${username || JSON.stringify(me).slice(0,80)}`); toast('登录测试成功');
    }catch(e){ log('loginLog','失败：' + e.message); toast('登录失败：' + e.message); }
  }
  function readToken(){ return $('tokenInput').value.trim() || runtimeToken || state.settings.token || ''; }
  async function saveSettingsFromUI(){
    const btn=$('saveSettingsBtn');
    if(btn){ btn.disabled=true; btn.textContent='保存中…'; }
    try{
      state.settings.username = $('usernameInput').value.trim(); if(String(state.settings.autoSyncRangeMode) !== 'interval'){ state.settings.autoSyncRangeValue = Math.max(1, Math.min(999, Number($('recentMonthsInput')?.value || 3))); state.settings.autoSyncRangeUnit = $('autoSyncRangeUnit')?.value || 'month'; if(state.settings.autoSyncRangeUnit === 'month') state.settings.recentMonths = state.settings.autoSyncRangeValue; } state.settings.pageSize = clampNum($('pageSizeInput')?.value, 40, 360, 120); state.settings.commentLines = clampNum($('commentLinesInput')?.value, 0, 6, 3); state.settings.borderWidth = clampNum($('borderWidthInput')?.value, 0, 4, 1); state.settings.token = $('tokenInput').value.trim(); state.settings.autoCacheCovers = !!$('autoCacheCoversInput')?.checked; if($('autoSyncEnabledInput')) state.settings.autoSyncEnabled = !!$('autoSyncEnabledInput').checked; state.settings.bgmApiBase = ($('bgmApiBaseInput')?.value || '').trim(); state.settings.imageProxy = ($('imageProxyInput')?.value || '').trim(); state.settings.siteBase = ($('siteBaseInput')?.value || '').trim(); if(typeof refreshCronUI === 'function') refreshCronUI(); // writes state.settings.autoSyncCron from the editor
      runtimeToken = $('tokenInput').value.trim() || runtimeToken; applyAppearance(false); refreshSegmentedControls(); resetRenderLimit();
      if(typeof snapshotAppearance==='function') snapshotAppearance(); // saved values become the new baseline (close won't revert)
      renderStats(); applyAuthVisibility(); toast('设置已保存');
      // Persist asynchronously without blocking the UI (settings subtree only, never collections).
      saveSettings().catch(()=>{});
      if($('publicModeInput') && $('publicNsfwInput')){
        localPostJSON('/api/admin/public-mode',{enabled:!!$('publicModeInput').checked, showNsfw:!!$('publicNsfwInput').checked}).catch(()=>{});
      }
    }catch(e){ toast('保存失败：'+e.message); }
    finally{ if(btn){ btn.disabled=false; btn.textContent='保存设置'; } }
  }
  function fillSettingsUI(){ $('tokenInput').value = state.settings.token || runtimeToken; $('usernameInput').value = state.settings.username||''; if(state.settings.autoSyncRangeValue == null) state.settings.autoSyncRangeValue = Number(state.settings.recentMonths || 3); if(!state.settings.autoSyncRangeUnit) state.settings.autoSyncRangeUnit = 'month'; if($('recentMonthsInput')) $('recentMonthsInput').value = state.settings.autoSyncRangeValue || 3; if($('autoSyncRangeUnit')) $('autoSyncRangeUnit').value = state.settings.autoSyncRangeUnit || 'month'; if($('pageSizeInput')) $('pageSizeInput').value = pageSize(); if($('commentLinesInput')) $('commentLinesInput').value = clampNum(state.settings.commentLines,0,6,3); if($('borderWidthInput')) $('borderWidthInput').value = clampNum(state.settings.borderWidth,0,4,1); if($('autoCacheCoversInput')) $('autoCacheCoversInput').checked = !!state.settings.autoCacheCovers; if($('bgmApiBaseInput')) $('bgmApiBaseInput').value = state.settings.bgmApiBase || ''; if($('imageProxyInput')) $('imageProxyInput').value = state.settings.imageProxy || ''; if($('siteBaseInput')) $('siteBaseInput').value = state.settings.siteBase || ''; if($('autoSyncEnabledInput')) $('autoSyncEnabledInput').checked = (state.settings.autoSyncEnabled === true || state.settings.autoSyncEnabled === 'true'); if(!state.settings.autoSyncCron && (state.settings.autoSyncTime || Array.isArray(state.settings.autoSyncDays))){ const [h,m]=String(state.settings.autoSyncTime||'02:00').split(':').map(Number); const days=Array.isArray(state.settings.autoSyncDays)?state.settings.autoSyncDays:[]; const dow=(days.length===0||days.length>=7)?'*':[...days].sort((a,b)=>a-b).join(','); state.settings.autoSyncCron=`${m||0} ${h||0} * * ${dow}`; } if(typeof applyCronToControls==='function') applyCronToControls(state.settings.autoSyncCron || '0 2 * * *'); if(typeof refreshAutoSyncCollapse==='function') refreshAutoSyncCollapse(); if(typeof refreshAutoSyncRange==='function') refreshAutoSyncRange(); refreshSegmentedControls(); apiJSON('/api/session').then(s=>{ if($('publicModeInput')) $('publicModeInput').checked=!!s?.publicMode; if($('publicNsfwInput')) $('publicNsfwInput').checked=!!s?.publicShowNsfw; }).catch(()=>{}); }

  // --- Server-side sync: the container runs it; the browser only kicks it off and polls progress. ---
  // Open the pre-sync confirm dialog; the shared modal adapts to recent/full mode.
  function openSyncConfirm(mode){
    const isRecent = mode === 'recent';
    const modal = $('syncConfirmModal'); if(!modal) return;
    modal.dataset.mode = isRecent ? 'recent' : 'full';
    if($('syncConfirmTitle')) $('syncConfirmTitle').textContent = isRecent ? '同步最近' : '完整同步';
    if($('syncConfirmHelp')) $('syncConfirmHelp').textContent = isRecent
      ? '只扫描最近指定月份内更新过的收藏，不触发安全箱判断，适合日常同步。'
      : '完整分页核对全部收藏，并对远端缺失的条目做安全箱判断。';
    // Months row is recent-only; the subject-data toggle is full-only; cover toggle shows in both.
    const monthsRow = $('syncConfirmMonthsRow'); if(monthsRow) monthsRow.style.display = isRecent ? '' : 'none';
    const subjectsRow = $('syncConfirmSubjectsRow'); if(subjectsRow) subjectsRow.style.display = isRecent ? 'none' : '';
    if(isRecent){
      // Months default to the auto-sync range until the user overrides it once (then sticky).
      const autoMonths = Number(state.settings.recentMonths || 3);
      const shown = state.settings.manualRecentMonths != null ? state.settings.manualRecentMonths : autoMonths;
      if($('syncConfirmMonthsInput')) $('syncConfirmMonthsInput').value = shown;
      if($('syncConfirmCoversInput')) $('syncConfirmCoversInput').checked = !!state.settings.manualRecentCovers;
    }else{
      if($('syncConfirmCoversInput')) $('syncConfirmCoversInput').checked = !!state.settings.fullCovers;
      if($('syncConfirmSubjectsInput')) $('syncConfirmSubjectsInput').checked = state.settings.fullRefreshSubjects !== false;
    }
    openModal('syncConfirmModal');
  }
  // Read the dialog choices, persist the months range, then start the server sync.
  async function confirmStartSync(){
    const mode = $('syncConfirmModal')?.dataset.mode === 'full' ? 'full' : 'recent';
    let opts;
    if(mode === 'recent'){
      const autoMonths = Number(state.settings.recentMonths || 3);
      const m = Math.max(1, Math.min(120, Number($('syncConfirmMonthsInput')?.value || autoMonths)));
      const covers = !!$('syncConfirmCoversInput')?.checked;
      // Manual recent months are independent of the auto-sync range; once the user picks a value
      // different from the auto default, remember it (sticky) for next time.
      if(state.settings.manualRecentMonths != null || m !== autoMonths) state.settings.manualRecentMonths = m;
      state.settings.manualRecentCovers = covers;
      // Recent sync always refreshes subject data (few items, cheap).
      opts = { recentMonths: m, cacheCovers: covers, refreshSubjects: true };
    }else{
      const covers = !!$('syncConfirmCoversInput')?.checked;
      const subjects = !!$('syncConfirmSubjectsInput')?.checked;
      state.settings.fullCovers = covers;
      state.settings.fullRefreshSubjects = subjects;
      opts = { cacheCovers: covers, refreshSubjects: subjects };
    }
    saveSettings().catch(()=>{});
    closeModal('syncConfirmModal');
    return startServerSync(mode, opts);
  }
  let _syncPollTimer = null;
  let _lastSyncMsg = '';
  async function startServerSync(mode, opts = {}){
    try{
      const r = await apiJSON('/api/sync', {method:'POST', body:JSON.stringify({mode, ...opts})});
      if(!r?.started){ toast('同步已在进行中'); }
    }catch(e){ toast('无法启动同步：' + e.message); return; }
    openModal('syncModal'); $('syncLog').textContent=''; setProgress(0); _lastSyncMsg='';
    if($('syncTitle')) $('syncTitle').textContent = mode === 'recent' ? '同步最近' : '完整同步中';
    if($('syncHelp')) $('syncHelp').textContent = '同步在服务器后台进行。可关闭此窗口，进度会显示在左上角；完成后目录会自动刷新。';
    syncInProgress = true;
    pollSyncStatus();
  }
  async function pollSyncStatus(){
    if(_syncPollTimer){ clearTimeout(_syncPollTimer); _syncPollTimer=null; }
    let st = null;
    try{ st = await apiJSON('/api/sync/status'); }catch{}
    if(st){
      syncInProgress = !!st.running;
      if(typeof st.percent === 'number') setProgress(st.percent);
      if(st.message && st.message !== _lastSyncMsg){ _lastSyncMsg = st.message; log('syncLog', st.message); }
      if(!st.running){
        const r = st.lastResult;
        if(st.phase === 'error') toast('同步失败：' + (st.error || ''));
        else if(r && r.aborted) toast('安全保护触发：本次没有覆盖本地');
        else if(st.phase === 'done' && r) toast(`同步完成：新增 ${r.added||0}，修改 ${r.modified||0}`);
        try{ await loadState(); }catch{}
        resetRenderLimit(); render();
        const mb=$('syncMiniBar');
        if(mb && mb.classList.contains('show')){
          mb.classList.add('smi-done');
          if($('syncMiniLabel')) $('syncMiniLabel').textContent='同步完成';
          if($('syncMiniFill')) $('syncMiniFill').style.width='100%';
          setTimeout(()=>mb.classList.remove('show','smi-done'), 2400);
        }
        return;
      }
    }
    _syncPollTimer = setTimeout(pollSyncStatus, 1500);
  }
  // On page load, if the container is mid-sync, resume into the unobtrusive mini bar
  // (top-right) rather than popping the big modal. The user can click it to expand.
  async function resumeSyncIfRunning(){
    try{
      const st = await apiJSON('/api/sync/status');
      if(st && st.running){
        $('syncLog').textContent=''; setProgress(st.percent||0); _lastSyncMsg='';
        if($('syncTitle')) $('syncTitle').textContent = (st.mode==='full'?'完整同步中':'同步最近') + (st.trigger==='auto'?'（定时）':'');
        if($('syncHelp')) $('syncHelp').textContent = '服务器正在后台同步；完成后目录会自动刷新。';
        syncInProgress = true;
        showSyncMini();
        pollSyncStatus();
      }
    }catch{}
  }

  async function fetchSubjectPublicTags(subjectId, token){
    // Fetch public tags via the API only (no website scraping).
    // Note: API tags may differ slightly in count/order from the website's "tagged by" section.
    const js = await apiFetch(`/v0/subjects/${encodeURIComponent(subjectId)}`, token || '');
    return {
      tags: normalizePublicTags(js.tags || js.subject?.tags || []).sort((a,b)=>(Number(b.count)||0)-(Number(a.count)||0) || String(a.name).localeCompare(String(b.name),'zh-Hans-CN')),
      source: 'api'
    };
  }
  async function enrichPublicTagsCurrent(){
    const token = readToken();
    const arr = visibleCollections();
    if(!arr.length){ toast('当前筛选下没有条目'); return; }
    if(!confirm(`将为当前筛选下 ${arr.length} 个条目逐个刷新公共标签，并覆盖旧的公共标签缓存。条目较多时会比较慢，继续吗？`)) return;
    openModal('syncModal'); $('syncLog').textContent=''; setProgress(0);
    if($('syncTitle')) $('syncTitle').textContent = '刷新公共标签';
    if($('syncHelp')) $('syncHelp').textContent = '公共标签来自 Bangumi API，不抓取官网网页；可能与网页标签区的展示数量和顺序略有差异。';
    let ok=0, empty=0, fail=0; const started_at=nowISO();
    for(let i=0;i<arr.length;i++){
      const c = arr[i]; setProgress((i+1)/arr.length*100);
      try{
        const tagResult = await fetchSubjectPublicTags(c.subject_id, token);
        const tags = Array.isArray(tagResult) ? tagResult : (tagResult.tags || []);
        const row = state.collections[String(c.subject_id)];
        if(row){
          // History is server-authoritative now (the browser no longer holds it), so don't push
          // history entries here — saveState() posts an empty history and the server preserves its own.
          row.publicTags = tags;
          row.public_tags_source = Array.isArray(tagResult) ? 'api' : (tagResult.source || 'api');
          row.public_tags_fetched_at = nowISO();
          row.content_hash = hashString(stableStringify(comparable(row)));
        }
        if(tags.length) ok++; else empty++;
        if((i+1)%10===0){ log('syncLog',`已处理 ${i+1}/${arr.length}，有标签 ${ok}，空标签 ${empty}，失败 ${fail}`); await saveState(); await sleep(220); }
        else await sleep(120);
      }catch(e){ fail++; if(fail <= 8) log('syncLog',`失败 ${c.subject_id} ${c.title||''}：${e.message || e}`); await sleep(300); }
    }
    await saveState(); resetRenderLimit(); render(); log('syncLog',`完成：有标签 ${ok}，空标签 ${empty}，失败 ${fail}`); toast(`公共标签刷新完成：${ok} 个条目有标签`);
  }

