// Backup / restore for data/avtoambor.db.
//
// - Scheduler ticks every 5 minutes; takes a backup only if the DB file's
//   mtime has advanced since the previous backup (so an idle shop doesn't
//   accumulate identical snapshots).
// - Backups land in ./backups/ at the repo root, named
//   zamena-masla-gp-YYYY-MM-DD_HH-MM-SS.db (sortable, human-readable).
//   Legacy avtoambor-*.db backups are still recognized for listing and
//   restore so existing snapshots remain accessible until they age out.
// - After each new backup, prune older snapshots: keep ALL backups from the
//   last 7 days; for anything older, keep only the most recent backup of
//   each calendar day.
// - Restore closes the live DB, removes WAL/SHM, copies the chosen backup
//   over the .db file, and lets the next getDb() reopen.

import { getDb, closeDb, DB_FILE } from './db.js';
import {
  mkdirSync, existsSync, readdirSync, statSync, unlinkSync, copyFileSync
} from 'node:fs';
import { resolve, dirname, join } from 'node:path';

const DATA_DIR   = dirname(DB_FILE);
export const BACKUP_DIR = resolve(DATA_DIR, '..', 'backups');
const PREFIX     = 'zamena-masla-gp-';
const EXT        = '.db';
// Read path accepts the new prefix and the legacy avtoambor- prefix so
// pre-rename snapshots stay listable and restorable.
const FILE_RE    = /^(?:zamena-masla-gp-|avtoambor-)(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.db$/;
const RETAIN_DAYS = 7;
const DAY_MS      = 24 * 60 * 60 * 1000;
const TICK_MS     = 5 * 60 * 1000;

function ensureDir() {
  if (!existsSync(BACKUP_DIR)) mkdirSync(BACKUP_DIR, { recursive: true });
}

function pad(n) { return String(n).padStart(2, '0'); }

function stamp(d = new Date()) {
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
         `_${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`;
}

function parseStamp(name) {
  const m = name.match(FILE_RE);
  if (!m) return null;
  const [, Y, Mo, D, H, Mi, S] = m;
  return new Date(+Y, +Mo - 1, +D, +H, +Mi, +S);
}

function dayKey(d) {
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}

export function safeName(name) {
  return typeof name === 'string'
    && !name.includes('/')
    && !name.includes('\\')
    && FILE_RE.test(name);
}

export function listBackups() {
  ensureDir();
  return readdirSync(BACKUP_DIR)
    .filter((n) => n.endsWith(EXT) && FILE_RE.test(n))
    .map((name) => {
      const p = join(BACKUP_DIR, name);
      const st = statSync(p);
      return {
        name,
        path: p,
        size: st.size,
        createdAt: parseStamp(name) ?? st.mtime
      };
    })
    .sort((a, b) => b.createdAt - a.createdAt);
}

// Latest mtime across the SQLite triplet (.db, -wal, -shm).
function dbSourceMtime() {
  let max = 0;
  for (const suffix of ['', '-wal', '-shm']) {
    const p = DB_FILE + suffix;
    if (existsSync(p)) max = Math.max(max, statSync(p).mtimeMs);
  }
  return max;
}

export async function takeBackup() {
  ensureDir();
  const name = `${PREFIX}${stamp()}${EXT}`;
  const dest = join(BACKUP_DIR, name);
  await getDb().backup(dest);
  const st = statSync(dest);
  pruneOldBackups();
  return { name, path: dest, createdAt: new Date(), size: st.size };
}

// Keep everything in the last RETAIN_DAYS; for older snapshots, keep only
// the newest one per calendar day. Deletions are silent on error.
export function pruneOldBackups(now = new Date()) {
  const cutoff = now.getTime() - RETAIN_DAYS * DAY_MS;
  const all = listBackups();          // newest first
  const seenDays = new Set();
  for (const b of all) {
    const t = b.createdAt.getTime();
    if (t >= cutoff) continue;        // inside retention window — keep
    const key = dayKey(b.createdAt);
    if (seenDays.has(key)) {
      try { unlinkSync(b.path); } catch { /* ignore */ }
    } else {
      seenDays.add(key);              // first (newest) of this day — keep
    }
  }
}

export async function restoreBackup(name) {
  if (!safeName(name)) throw new Error('Invalid backup name');
  const src = join(BACKUP_DIR, name);
  if (!existsSync(src)) throw new Error('Backup not found');

  // Snapshot the current DB first so a misclick is recoverable.
  try { await takeBackup(); } catch (e) { console.error('[backup] pre-restore snapshot failed', e); }

  // From here on: no awaits. Node is single-threaded so other requests
  // cannot interleave and reopen the DB on partial state.
  closeDb();
  for (const suffix of ['-wal', '-shm']) {
    const p = DB_FILE + suffix;
    if (existsSync(p)) {
      try { unlinkSync(p); } catch { /* ignore */ }
    }
  }
  copyFileSync(src, DB_FILE);
  // Next getDb() reopens against the restored file.
}

// Module-level scheduler state. globalThis-guarded so dev HMR doesn't stack
// multiple intervals on top of each other.
const STARTED_FLAG = '__avtoambor_backup_started';

export function startBackupScheduler() {
  if (globalThis[STARTED_FLAG]) return;
  globalThis[STARTED_FLAG] = true;

  ensureDir();

  // Baseline: pretend the most recent backup's timestamp is "what we've
  // already captured", so we don't immediately take a duplicate at boot.
  let lastSeenMtime = 0;
  const existing = listBackups();
  if (existing.length > 0) lastSeenMtime = existing[0].createdAt.getTime();

  const tick = async () => {
    try {
      const mtime = dbSourceMtime();
      if (mtime > lastSeenMtime) {
        await takeBackup();
        lastSeenMtime = mtime;
      }
    } catch (e) {
      console.error('[backup] scheduled tick failed', e);
    }
  };

  // Run once shortly after boot, then every TICK_MS.
  setTimeout(tick, 5_000);
  setInterval(tick, TICK_MS);
}
