'use strict';

const { SUBJECT_TYPES, COLLECTION_TYPES, SUBJECT_LABEL, STATUS } = require('./constants');
const { sleep, fetchMe, fetchCollectionAll, fetchCollectionRecent, fetchSubjectDetail, setApiBase } = require('./bgm');
const { nowISO, normalizeCollection, applySubjectDetail, makeSnapshot, collectionFieldDiff } = require('./normalize');
const { cacheCoversForState } = require('./covers');

let syncStatus = {
  running: false, mode: null, trigger: null, phase: 'idle', percent: 0,
  processed: 0, total: 0, startedAt: null, finishedAt: null, message: '', lastResult: null, error: null
};
let lock = false;

function getStatus() { return syncStatus; }

function emptyState() { return { schema: 9, settings: {}, collections: {}, history: [], runs: [] }; }

// Previous cron fire time before `from` (scans backward minute by minute, capped). Used by the
// "interval" auto-sync range mode: the lookback window is one scheduling period.
function cronPrevRun(expr, from) {
  const d = new Date(from.getTime());
  d.setSeconds(0, 0);
  d.setMinutes(d.getMinutes() - 1);
  const limit = 45 * 24 * 60; // ~45 days covers weekly/monthly schedules
  for (let i = 0; i < limit; i++) {
    if (cronMatches(expr, d)) return new Date(d.getTime());
    d.setMinutes(d.getMinutes() - 1);
  }
  return null;
}

// Auto-sync "fixed" range cutoff = now − (autoSyncRangeValue × autoSyncRangeUnit).
function fixedCutoff(state) {
  const unit = state.settings.autoSyncRangeUnit || 'month';
  const val = Math.max(1, Number(state.settings.autoSyncRangeValue != null ? state.settings.autoSyncRangeValue : (state.settings.recentMonths || 3)) || 1);
  const d = new Date();
  if (unit === 'minute') d.setMinutes(d.getMinutes() - val);
  else if (unit === 'hour') d.setHours(d.getHours() - val);
  else if (unit === 'day') d.setDate(d.getDate() - val);
  else d.setMonth(d.getMonth() - val);
  return d;
}

function computeCutoff(state, mode, trigger, recentMonthsOverride) {
  if (mode !== 'recent') return null;
  const months = recentMonthsOverride != null ? recentMonthsOverride : Number(state.settings.recentMonths || 3);
  const recentMonths = Math.max(1, Math.min(120, months));
  const monthsCutoff = () => { const d = new Date(); d.setMonth(d.getMonth() - recentMonths); return d; };
  if (trigger === 'auto') {
    if (state.settings.autoSyncRangeMode === 'interval') {
      const cron = state.settings.autoSyncCron || legacyToCron(state.settings) || '0 2 * * *';
      const prev = cronPrevRun(cron, new Date()); // ≈ now − one scheduling interval
      if (prev) return prev;
      // fall through to the fixed range if no previous fire was found
    }
    return fixedCutoff(state); // value + unit (minute/hour/day/month)
  }
  return monthsCutoff(); // manual sync still uses months (from the dialog / recentMonths)
}

async function runSync({ store, mode, trigger, opts = {}, onProgress }) {
  const report = (p) => { try { onProgress && onProgress(p); } catch {} };
  // Per-run options (manual sync passes these; auto sync falls back to settings).
  const refreshSubjects = opts.refreshSubjects !== false;

  const state = (await store.getState()) || emptyState();
  state.settings = state.settings || {};
  state.collections = state.collections || {};
  state.history = state.history || [];
  state.runs = state.runs || [];

  setApiBase(state.settings.bgmApiBase); // custom API base / mirror (empty = default api.bgm.tv)

  const token = state.settings.token;
  if (!token) throw new Error('未设置 Access Token，无法同步');

  let username = state.settings.username;
  if (!username) {
    report({ phase: 'auth', percent: 2, message: '读取用户名…' });
    const me = await fetchMe(token);
    username = me.username || me.name;
    if (!username) throw new Error('无法自动获取用户名，请在设置中手动填写 Bangumi 用户名');
    state.settings.username = username;
  }

  const effectiveMonths = mode === 'recent'
    ? Math.max(1, Math.min(120, opts.recentMonths != null ? opts.recentMonths : Number(state.settings.recentMonths || 3)))
    : null;
  const cacheCovers = opts.cacheCovers !== undefined ? opts.cacheCovers : state.settings.autoCacheCovers;
  const cutoff = computeCutoff(state, mode, trigger, effectiveMonths);
  const getAll = () => Object.values(state.collections);

  // --- phase 1: fetch collections ---
  report({ phase: 'fetching', percent: 4, message: `开始${mode === 'recent' ? '增量' : '完整'}同步 @${username}` });
  const remote = [];
  const totalTasks = SUBJECT_TYPES.length * COLLECTION_TYPES.length;
  let taskDone = 0;
  for (const st of SUBJECT_TYPES) {
    for (const ct of COLLECTION_TYPES) {
      const onPage = (page, count) => report({
        phase: 'fetching', percent: 4 + Math.round(taskDone / totalTasks * 36),
        processed: taskDone, total: totalTasks,
        message: `${SUBJECT_LABEL[st] || st} / ${STATUS[ct] || ct}：第 ${page} 页，累计 ${count}`
      });
      const got = mode === 'recent'
        ? await fetchCollectionRecent(username, st, ct, token, cutoff, onPage)
        : await fetchCollectionAll(username, st, ct, token, onPage);
      remote.push(...got);
      taskDone++;
      report({ phase: 'fetching', percent: 4 + Math.round(taskDone / totalTasks * 36), processed: taskDone, total: totalTasks });
      await sleep(mode === 'recent' ? 80 : 160);
    }
  }

  // --- phase 2: subject details (skipped when refreshSubjects is off) ---
  const subjectDetails = new Map();
  if (refreshSubjects) {
    report({ phase: 'details', percent: 40, processed: 0, total: remote.length, message: `远端 ${remote.length} 条，获取条目详情…` });
    for (let i = 0; i < remote.length; i++) {
      const sid = remote[i].subject_id || remote[i].subject?.id;
      if (!sid) continue;
      const detail = await fetchSubjectDetail(sid, token);
      if (detail) subjectDetails.set(String(sid), detail);
      if ((i + 1) % 10 === 0 || i === remote.length - 1) {
        report({ phase: 'details', percent: 40 + Math.round((i + 1) / Math.max(1, remote.length) * 45), processed: i + 1, total: remote.length, message: `条目详情 ${i + 1}/${remote.length}` });
      }
      await sleep(110);
    }
  } else {
    report({ phase: 'details', percent: 85, message: '已跳过条目详情刷新' });
  }

  // --- phase 3: normalize + compare ---
  report({ phase: 'merging', percent: 86, message: '比对本地…' });
  const seen = new Map();
  for (const item of remote) {
    let n = normalizeCollection(item, item.__fallback);
    const sid = String(n.subject_id);
    if (subjectDetails.has(sid)) n = applySubjectDetail(n, subjectDetails.get(sid));
    const snap = makeSnapshot(n);
    if (snap.subject_id) seen.set(sid, snap);
  }

  // safety threshold (full only)
  if (mode === 'full') {
    const activeLocal = getAll().filter(c => c.status !== 'confirmed_deleted' && c.status !== 'protected_local');
    const missingPreview = activeLocal.filter(c => !seen.has(String(c.subject_id)));
    if (activeLocal.length && missingPreview.length > Math.max(10, Math.floor(activeLocal.length * 0.03))) {
      const run = { id: Date.now().toString(), mode, trigger, started_at: nowISO(), finished_at: nowISO(), success: false, audit_only: true, fetched_total: seen.size, missing_preview: missingPreview.length, error: '远端缺失数量超过安全阈值，本次不写入本地' };
      state.runs.push(run);
      await store.persistSyncResult({ collections: state.collections, history: state.history, runs: state.runs, lastRunDate: new Date().toLocaleDateString('sv') });
      return { aborted: true, reason: 'safety', missing_preview: missingPreview.length, active: activeLocal.length };
    }
  }

  let added = 0, modified = 0, restored = 0, missing = 0;
  const runId = Date.now().toString(); const started_at = nowISO();
  for (const [id, n] of seen) {
    const old = state.collections[id];
    if (!old) {
      const commentTime = String(n.comment || '').trim() ? (n.bangumi_updated_at || n.collection_created_at || started_at) : '';
      state.collections[id] = { ...n, status: 'active', marked_at: n.collection_created_at || '', mark_changed_at: n.bangumi_updated_at || n.collection_created_at || started_at, first_seen_at: started_at, last_seen_at: started_at, last_synced_at: started_at, comment_updated_at: commentTime, missing_count: 0, last_sync_run_id: runId };
      added++;
      state.history.push({ subject_id: n.subject_id, change_type: 'added', old: null, new: structuredClone(state.collections[id]), created_at: started_at, sync_run_id: runId, note: mode === 'recent' ? '最近同步首次备份' : '首次备份' });
    } else {
      if (old.status === 'missing_candidate' || old.status === 'protected_local' || old.status === 'confirmed_deleted') {
        restored++;
        state.history.push({ subject_id: n.subject_id, change_type: 'restored', old: structuredClone(old), new: structuredClone(n), changes: collectionFieldDiff(old, n), created_at: started_at, sync_run_id: runId, note: '远端重新出现' });
      }
      if (old.content_hash !== n.content_hash) {
        const changes = collectionFieldDiff(old, n); modified++;
        state.history.push({ subject_id: n.subject_id, change_type: 'modified', old: structuredClone(old), new: structuredClone(n), changes, created_at: started_at, sync_run_id: runId, note: '收藏字段、吐槽、标签、评分、进度或条目资料变化' });
      }
      const statusChanged = old.collection_type !== n.collection_type;
      const markChangedAt = statusChanged ? (n.bangumi_updated_at || started_at) : (old.mark_changed_at || n.bangumi_updated_at || old.collection_created_at || '');
      state.collections[id] = { ...old, ...n, status: 'active', mark_changed_at: markChangedAt, first_seen_at: old.first_seen_at || started_at, last_seen_at: started_at, last_synced_at: started_at, missing_count: 0, last_sync_run_id: runId };
    }
  }

  if (mode === 'full') {
    for (const c of getAll()) {
      const id = String(c.subject_id); if (seen.has(id)) continue;
      if (c.status === 'confirmed_deleted' || c.status === 'protected_local') continue;
      if (c.status !== 'missing_candidate') {
        state.history.push({ subject_id: c.subject_id, change_type: 'missing', old: structuredClone(c), new: null, created_at: started_at, sync_run_id: runId, note: '远端完整同步后未出现；未删除，仅进入安全箱' });
      }
      c.status = 'missing_candidate'; c.missing_count = (c.missing_count || 0) + 1; c.last_sync_run_id = runId; missing++;
    }
  }

  state.runs.push({ id: runId, mode, trigger, recent_months: effectiveMonths, started_at, finished_at: nowISO(), success: true, fetched_total: seen.size, added, modified, restored, missing });

  // --- phase 4: covers ---
  // Only cache covers for items fetched in THIS sync (the `seen` set), not the whole library.
  // So a recent sync caches just the recently-updated items; a full sync caches everything fetched.
  if (cacheCovers) {
    report({ phase: 'covers', percent: 95, message: '缓存封面…' });
    const seenColl = {};
    for (const id of seen.keys()) { if (state.collections[id]) seenColl[id] = state.collections[id]; }
    await cacheCoversForState(seenColl, (done, total) => {
      report({ phase: 'covers', percent: 95 + Math.round(done / Math.max(1, total) * 4), processed: done, total, message: `封面 ${done}/${total}` });
    }, state.settings.imageProxy);
  }

  report({ phase: 'saving', percent: 99, message: '写入数据库…' });
  await store.persistSyncResult({ collections: state.collections, history: state.history, runs: state.runs, lastRunDate: new Date().toLocaleDateString('sv') });

  return { added, modified, restored, missing, fetched_total: seen.size };
}

function startSync(store, mode, trigger, opts = {}) {
  if (lock) return false;
  lock = true;
  syncStatus = {
    running: true, mode, trigger, phase: 'starting', percent: 0, processed: 0, total: 0,
    startedAt: new Date().toISOString(), finishedAt: null, message: '准备同步…', lastResult: null, error: null
  };
  runSync({ store, mode, trigger, opts, onProgress: p => Object.assign(syncStatus, p) })
    .then(r => { syncStatus.lastResult = r; syncStatus.phase = r?.aborted ? 'aborted' : 'done'; syncStatus.percent = 100; syncStatus.message = r?.aborted ? '安全保护触发，本次未覆盖本地' : '同步完成'; })
    .catch(e => { syncStatus.error = String(e?.message || e); syncStatus.phase = 'error'; syncStatus.message = '同步失败：' + syncStatus.error; })
    .finally(() => { syncStatus.running = false; syncStatus.finishedAt = new Date().toISOString(); lock = false; });
  return true;
}

// --- cron scheduling (no external deps) ---

// Parse one cron field into a Set of allowed values. Supports '*', '*/n', 'a', 'a-b', 'a-b/n', and comma lists.
function parseCronField(field, min, max) {
  const out = new Set();
  for (const part of String(field).split(',')) {
    const m = part.trim().match(/^(\*|\d+)(?:-(\d+))?(?:\/(\d+))?$/);
    if (!m) continue;
    const step = m[3] ? Number(m[3]) : 1;
    if (step < 1) continue;
    let lo, hi;
    if (m[1] === '*') { lo = min; hi = max; }
    else { lo = Number(m[1]); hi = m[2] !== undefined ? Number(m[2]) : (m[3] ? max : lo); }
    for (let v = lo; v <= hi; v += step) { if (v >= min && v <= max) out.add(v); }
  }
  return out;
}

// Match a 5-field cron expression (min hour day-of-month month day-of-week) against a Date (local time).
function cronMatches(expr, date) {
  const parts = String(expr).trim().split(/\s+/);
  if (parts.length !== 5) return false;
  const [mi, ho, dom, mon, dow] = parts;
  if (!parseCronField(mi, 0, 59).has(date.getMinutes())) return false;
  if (!parseCronField(ho, 0, 23).has(date.getHours())) return false;
  if (!parseCronField(mon, 1, 12).has(date.getMonth() + 1)) return false;
  const domRestricted = dom.trim() !== '*';
  const dowRestricted = dow.trim() !== '*';
  const domOk = parseCronField(dom, 1, 31).has(date.getDate());
  const dowOk = parseCronField(dow, 0, 6).has(date.getDay());
  // Standard cron: when both DOM and DOW are restricted, match if either matches.
  if (domRestricted && dowRestricted) return domOk || dowOk;
  if (domRestricted) return domOk;
  if (dowRestricted) return dowOk;
  return true;
}

// Fallback: derive a cron from the legacy autoSyncTime/autoSyncDays settings.
function legacyToCron(s) {
  if (!s.autoSyncTime && !Array.isArray(s.autoSyncDays)) return null;
  const [h, m] = String(s.autoSyncTime || '02:00').split(':').map(Number);
  const days = Array.isArray(s.autoSyncDays) ? s.autoSyncDays : [];
  const dow = (days.length === 0 || days.length >= 7) ? '*' : [...days].sort((a, b) => a - b).join(',');
  return `${m || 0} ${h || 0} * * ${dow}`;
}

let lastAutoRunKey = '';

async function checkSchedule(store) {
  if (lock) return;
  let state;
  try { state = await store.getState(); } catch { return; }
  const s = (state && state.settings) || {};
  if (s.autoSyncEnabled !== true && s.autoSyncEnabled !== 'true') return; // accept boolean or legacy string
  const cron = s.autoSyncCron || legacyToCron(s) || '0 2 * * *';
  const now = new Date();
  const key = now.toLocaleString('sv').slice(0, 16); // local 'YYYY-MM-DD HH:MM'
  if (lastAutoRunKey === key) return;                // in-memory dedup (this minute already handled)
  if (s.autoSyncLastRunKey === key) return;          // persisted dedup (survives container restart)
  if (!cronMatches(cron, now)) return;
  lastAutoRunKey = key;
  try { await store.setStateSettings({ autoSyncLastRunKey: key }); } catch {}
  startSync(store, s.autoSyncMode || 'recent', 'auto');
}

module.exports = { runSync, startSync, getStatus, checkSchedule, cronMatches, parseCronField };
