import crypto from 'node:crypto';
import { readFile, rename, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { startBackupForVm } from './routes/backup.js';

const schedulesPath = path.resolve(process.cwd(), 'schedules.json');
const checkIntervalMs = 30 * 1000;
let schedules = [];
let timer = null;

export async function startScheduler() {
  await loadSchedules();
  if (timer) return;
  timer = setInterval(runDueSchedules, checkIntervalMs);
  timer.unref?.();
  runDueSchedules();
}

export function listSchedules() {
  return schedules
    .map((schedule) => ({ ...schedule }))
    .sort((a, b) => a.vmName.localeCompare(b.vmName));
}

export async function replaceSchedules(nextSchedules) {
  schedules = normalizeSchedules(nextSchedules);
  await saveSchedules();
  return listSchedules();
}

async function runDueSchedules() {
  const now = new Date();
  let changed = false;

  for (const schedule of schedules) {
    if (!schedule.enabled) continue;
    if (!schedule.nextRunAt) {
      schedule.nextRunAt = nextRunFrom(now, schedule.intervalHours, schedule.timeOfDay);
      changed = true;
      continue;
    }
    if (new Date(schedule.nextRunAt) > now) continue;

    try {
      await startBackupForVm(schedule.vmName);
      schedule.lastRunAt = now.toISOString();
      schedule.lastError = '';
      schedule.nextRunAt = nextRunFrom(now, schedule.intervalHours, schedule.timeOfDay);
    } catch (error) {
      schedule.lastError = String(error.message || error);
      schedule.nextRunAt = new Date(now.getTime() + 5 * 60 * 1000).toISOString();
    }
    changed = true;
  }

  if (changed) {
    await saveSchedules();
  }
}

async function loadSchedules() {
  try {
    const content = await readFile(schedulesPath, 'utf8');
    schedules = normalizeSchedules(JSON.parse(content));
  } catch (error) {
    if (error.code === 'ENOENT') {
      schedules = [];
      return;
    }
    throw error;
  }
}

async function saveSchedules() {
  const tempPath = `${schedulesPath}.tmp`;
  await writeFile(tempPath, `${JSON.stringify(schedules, null, 2)}\n`, { mode: 0o600 });
  await rename(tempPath, schedulesPath);
}

function normalizeSchedules(values) {
  const seen = new Set();
  return (Array.isArray(values) ? values : [])
    .map((value) => {
      const vmName = String(value.vmName || '').trim();
      const intervalHours = Math.max(1, Number(value.intervalHours) || 24);
      if (!vmName || seen.has(vmName)) return null;
      seen.add(vmName);
      return {
        id: value.id || `schedule_${crypto.randomBytes(8).toString('hex')}`,
        vmName,
        enabled: Boolean(value.enabled),
        intervalHours,
        timeOfDay: normalizeTimeOfDay(value.timeOfDay),
        nextRunAt: value.nextRunAt || nextRunFrom(new Date(), intervalHours, normalizeTimeOfDay(value.timeOfDay)),
        lastRunAt: value.lastRunAt || null,
        lastError: value.lastError || '',
      };
    })
    .filter(Boolean);
}

function nextRunFrom(date, intervalHours, timeOfDay) {
  const intervalMs = intervalHours * 60 * 60 * 1000;
  const candidate = withTimeOfDay(date, timeOfDay);

  while (candidate <= date) {
    candidate.setTime(candidate.getTime() + intervalMs);
  }

  return candidate.toISOString();
}

function withTimeOfDay(date, timeOfDay) {
  const [hours, minutes] = normalizeTimeOfDay(timeOfDay).split(':').map(Number);
  const next = new Date(date);
  next.setHours(hours, minutes, 0, 0);
  return next;
}

function normalizeTimeOfDay(value) {
  const text = String(value || '02:00').trim();
  if (/^\d{2}:\d{2}$/.test(text)) {
    const [hours, minutes] = text.split(':').map(Number);
    if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
      return text;
    }
  }
  return '02:00';
}
