'use strict';

const os   = require('os');
const fs   = require('fs');
const path = require('path');

const { getDb, getSetting, setSetting } = require('../db/database');
const { BACKUP_DIR } = require('./backupService');

// ─── Helpers ─────────────────────────────────────────────────────────────────

function parseBool(key, defaultTrue = true) {
  const v = getSetting(key);
  if (v === null) return defaultTrue;
  return v !== 'false';
}

function parseIntSetting(key, fallback) {
  const v = parseInt(getSetting(key) || '', 10);
  return isNaN(v) ? fallback : v;
}

// ─── Individual cleanup tasks ─────────────────────────────────────────────────

/**
 * Delete rows from import_sessions where expires_at has passed.
 * These are created by the spreadsheet import preview endpoint (24h TTL).
 */
function pruneExpiredImportSessions() {
  const result = getDb()
    .prepare("DELETE FROM import_sessions WHERE expires_at <= datetime('now')")
    .run();
  return result.changes;
}

/**
 * Remove stale export temp files from the OS temp directory.
 *
 * SQLite exports (user-db): bill-tracker-user-{id}-{ts}.sqlite
 *   Written by the /export/user-db route, deleted in the download callback.
 *   Server crash can leave orphans → swept with maxAgeHours.
 *
 * Excel exports (user-excel): bill-tracker-user-{id}-{ts}.xlsx
 *   Currently streamed in-memory (no disk file), but guard exists so that
 *   if the code path ever changes, xlsx files are always removed within 24 h.
 */
function pruneStaleExportFiles(maxAgeHours) {
  const tmpDir       = os.tmpdir();
  const sqliteCutoff = maxAgeHours * 60 * 60 * 1000;
  const xlsxCutoff   = 24 * 60 * 60 * 1000; // xlsx files must not outlive 24 h
  const now          = Date.now();
  let removed        = 0;
  let errors         = 0;

  let entries;
  try {
    entries = fs.readdirSync(tmpDir);
  } catch (err) {
    console.warn('[cleanup] Cannot read tmpdir:', err.message);
    return { removed, errors: 1 };
  }

  for (const name of entries) {
    if (!name.startsWith('bill-tracker-user-')) continue;
    const isXlsx   = name.endsWith('.xlsx');
    const isSqlite = name.endsWith('.sqlite');
    if (!isXlsx && !isSqlite) continue;

    const cutoff = isXlsx ? xlsxCutoff : sqliteCutoff;
    const full   = path.join(tmpDir, name);
    try {
      const stat = fs.statSync(full);
      if (now - stat.mtimeMs > cutoff) {
        fs.unlinkSync(full);
        removed++;
      }
    } catch {
      errors++;
    }
  }

  return { removed, errors };
}

/**
 * Remove orphaned .partial and .upload backup files from the backup directory.
 * These are left behind only if the server crashes mid-backup or mid-import.
 * Uses a 2-hour cutoff so an in-progress operation is never interrupted.
 */
function pruneOrphanedBackupPartials(backupDir) {
  const cutoff = 2 * 60 * 60 * 1000;
  const now    = Date.now();
  let removed  = 0;
  let errors   = 0;

  if (!backupDir || !fs.existsSync(backupDir)) return { removed, errors };

  let entries;
  try {
    entries = fs.readdirSync(backupDir);
  } catch (err) {
    console.warn('[cleanup] Cannot read backup dir:', err.message);
    return { removed, errors: 1 };
  }

  for (const name of entries) {
    if (!name.endsWith('.partial') && !name.endsWith('.upload')) continue;
    const full = path.join(backupDir, name);
    try {
      const stat = fs.statSync(full);
      if (now - stat.mtimeMs > cutoff) {
        fs.unlinkSync(full);
        removed++;
      }
    } catch {
      errors++;
    }
  }

  return { removed, errors };
}

/**
 * Trim import_history rows older than maxAgeDays.
 * Rows are per-user audit records. This task is disabled by default.
 */
function pruneImportHistory(maxAgeDays) {
  if (!maxAgeDays || maxAgeDays <= 0) return 0;
  const result = getDb()
    .prepare("DELETE FROM import_history WHERE imported_at < datetime('now', ?)")
    .run(`-${maxAgeDays} days`);
  return result.changes;
}

/**
 * Permanently purge soft-deleted bills and categories after a 30-day recovery
 * window. Bill deletion cascades to bill-owned records via foreign keys.
 *
 * transactions.matched_bill_id is ON DELETE SET NULL, so purging a bill nulls the
 * pointer on any matched transaction but would leave match_status='matched' — a
 * limbo row excluded from spending (match_status != 'matched') yet attributed to no
 * bill. Release those matches back to 'unmatched' in the same transaction (and
 * self-heal any pre-existing orphans) so purged bills don't silently drop spend.
 */
function pruneSoftDeletedFinancialRecords(maxAgeDays = 30) {
  const db = getDb();
  const cutoff = `-${maxAgeDays} days`;
  const purge = db.transaction(() => {
    const releasedMatches = db.prepare(`
      UPDATE transactions
      SET match_status = 'unmatched', matched_bill_id = NULL, updated_at = datetime('now')
      WHERE match_status = 'matched'
        AND (matched_bill_id IS NULL
             OR matched_bill_id IN (
               SELECT id FROM bills WHERE deleted_at IS NOT NULL AND deleted_at < datetime('now', ?)
             ))
    `).run(cutoff).changes;
    const bills = db.prepare("DELETE FROM bills WHERE deleted_at IS NOT NULL AND deleted_at < datetime('now', ?)").run(cutoff).changes;
    const categories = db.prepare("DELETE FROM categories WHERE deleted_at IS NOT NULL AND deleted_at < datetime('now', ?)").run(cutoff).changes;
    return { bills, categories, releasedMatches };
  });
  return purge();
}

// ─── Settings ─────────────────────────────────────────────────────────────────

function readSettings() {
  return {
    import_sessions_enabled:       parseBool('cleanup_import_sessions_enabled', true),
    temp_exports_enabled:          parseBool('cleanup_temp_exports_enabled', true),
    temp_export_max_age_hours:     parseIntSetting('cleanup_temp_export_max_age_hours', 2),
    backup_partials_enabled:       parseBool('cleanup_backup_partials_enabled', true),
    import_history_enabled:        parseBool('cleanup_import_history_enabled', false),
    import_history_max_age_days:   parseIntSetting('cleanup_import_history_max_age_days', 365),
  };
}

function validateAndApplySettings(input = {}) {
  const toSave = {};

  const bool = (key, settingKey) => {
    if (input[key] !== undefined) toSave[settingKey] = input[key] ? 'true' : 'false';
  };
  const rangedInt = (key, settingKey, min, max, label) => {
    if (input[key] === undefined) return;
    const v = parseInt(input[key], 10);
    if (isNaN(v) || v < min || v > max) {
      const err = new Error(`${label} must be between ${min} and ${max}`);
      err.status = 400;
      throw err;
    }
    toSave[settingKey] = String(v);
  };

  bool('import_sessions_enabled',    'cleanup_import_sessions_enabled');
  bool('temp_exports_enabled',       'cleanup_temp_exports_enabled');
  bool('backup_partials_enabled',    'cleanup_backup_partials_enabled');
  bool('import_history_enabled',     'cleanup_import_history_enabled');

  rangedInt('temp_export_max_age_hours',   'cleanup_temp_export_max_age_hours',   1, 72,   'temp_export_max_age_hours');
  rangedInt('import_history_max_age_days', 'cleanup_import_history_max_age_days', 30, 3650, 'import_history_max_age_days');

  for (const [k, v] of Object.entries(toSave)) setSetting(k, v);
  return readSettings();
}

// ─── Main runner ──────────────────────────────────────────────────────────────

async function runAllCleanup() {
  const settings = readSettings();
  const tasks    = {};

  if (settings.import_sessions_enabled) {
    const pruned = pruneExpiredImportSessions();
    tasks.import_sessions = { pruned };
  }

  if (settings.temp_exports_enabled) {
    tasks.temp_export_files = pruneStaleExportFiles(settings.temp_export_max_age_hours);
  }

  if (settings.backup_partials_enabled) {
    tasks.backup_partials = pruneOrphanedBackupPartials(BACKUP_DIR);
  }

  if (settings.import_history_enabled) {
    const pruned = pruneImportHistory(settings.import_history_max_age_days);
    tasks.import_history = { pruned };
  }

  tasks.soft_deleted_records = pruneSoftDeletedFinancialRecords(30);

  // Prune match suggestion rejections older than 90 days
  try {
    const { changes } = getDb().prepare(
      "DELETE FROM match_suggestion_rejections WHERE rejected_at <= datetime('now', '-90 days')"
    ).run();
    tasks.suggestion_rejections = { pruned: changes };
  } catch { tasks.suggestion_rejections = { pruned: 0 }; }

  const ran_at = new Date().toISOString();
  setSetting('cleanup_last_run_at', ran_at);
  setSetting('cleanup_last_result', JSON.stringify(tasks));

  console.log(`[cleanup] Ran at ${ran_at}:`, JSON.stringify(tasks));
  return { ran_at, tasks };
}

// ─── Status ───────────────────────────────────────────────────────────────────

function getCleanupStatus() {
  const raw = getSetting('cleanup_last_result');
  let last_result = null;
  try { if (raw) last_result = JSON.parse(raw); } catch {}

  return {
    settings:    readSettings(),
    last_run_at: getSetting('cleanup_last_run_at') || null,
    last_result,
  };
}

module.exports = {
  runAllCleanup,
  getCleanupStatus,
  readSettings,
  validateAndApplySettings,
  // Exported individually so the daily worker can call them by name in logs
  pruneExpiredImportSessions,
  pruneStaleExportFiles,
  pruneOrphanedBackupPartials,
  pruneImportHistory,
  pruneSoftDeletedFinancialRecords,
};
