import "server-only";

import type { CheckTrigger } from "@/generated/prisma/client";
import { dueSlot } from "@/lib/schedule";
import { checkAllEnabledProjects } from "../health/runner";
import { logger } from "../logger";
import { readMonitoringSettings } from "../settings/service";
import { HEARTBEAT_INTERVAL_MS, claimRun, completeRun, failRun, slotAlreadyRun, touchRun } from "./runs";

type ClaimedRun = { id: string; trigger: CheckTrigger; slotKey: string | null };

/** Execute a claimed run to the end. Never throws: a failure is recorded on the run. */
async function executeRun(run: ClaimedRun): Promise<void> {
  const startedAt = Date.now();
  logger.info("scheduler.run.started", { runId: run.id, trigger: run.trigger, slot: run.slotKey });

  const heartbeat = setInterval(() => {
    touchRun(run.id).catch((error) => logger.error("scheduler.run.heartbeat_failed", { runId: run.id, error }));
  }, HEARTBEAT_INTERVAL_MS);

  try {
    const summary = await checkAllEnabledProjects(run.trigger, run.id);
    await completeRun(run.id, summary);
    logger.info("scheduler.run.completed", {
      runId: run.id,
      trigger: run.trigger,
      slot: run.slotKey,
      appsFound: summary.found,
      appsChecked: summary.checked,
      working: summary.working,
      slow: summary.slow,
      failed: summary.notWorking,
      disabled: summary.disabled,
      errored: summary.errored,
      durationMs: Date.now() - startedAt,
    });
  } catch (error) {
    logger.error("scheduler.run.failed", { runId: run.id, trigger: run.trigger, slot: run.slotKey, error });
    await failRun(run.id, error).catch((failError) =>
      logger.error("scheduler.run.fail_record_failed", { runId: run.id, error: failError }),
    );
  } finally {
    clearInterval(heartbeat);
  }
}

export type TickResult =
  | { ran: true; slot: string }
  | { ran: false; reason: "not_due" | "already_run" | "busy" };

/**
 * One scheduler heartbeat. Safe to call as often as you like, from anywhere
 * (in-process ticker, external cron, several instances at once): it only ever
 * starts a run when a slot is due and the database lets it claim that slot.
 */
export async function runSchedulerTick(now: Date = new Date()): Promise<TickResult> {
  const settings = await readMonitoringSettings();

  const slot = dueSlot(settings, now, settings.updatedAt);
  if (!slot) return { ran: false, reason: "not_due" };

  const slotKey = slot.toISOString();
  if (await slotAlreadyRun(slotKey)) return { ran: false, reason: "already_run" };

  // Lost the race for this slot, or a manual "Check All" is running. In the
  // second case the slot stays unclaimed and the next tick tries again.
  const run = await claimRun({ trigger: "SCHEDULED", slotKey });
  if (!run) return { ran: false, reason: "busy" };

  await executeRun(run);
  return { ran: true, slot: slotKey };
}

/**
 * "Check All": claim a manual run and return at once; `execute` runs it to
 * completion (the caller decides where — e.g. inside next/server `after`).
 */
export async function startManualRun(startedBy: string): Promise<{ execute: () => Promise<void> } | null> {
  const run = await claimRun({ trigger: "MANUAL", startedBy });
  if (!run) return null;
  return { execute: () => executeRun(run) };
}
