import "server-only";

import type { CheckTrigger } from "@/generated/prisma/client";
import { dueSlot } from "@/lib/schedule";
import { forEachLimited } from "../concurrency";
import { db } from "../db";
import { logger } from "../logger";
import { readMonitoringSettings } from "../settings/service";
import { DOMAIN_LOOKUP_CONCURRENCY, checkDomain, domainsInUse } from "./service";

// DomainRun rows are the execution log *and* the lock (see prisma/schema.prisma),
// exactly like scheduler_runs, but separate so domain lookups never hold up health checks.

const HEARTBEAT_INTERVAL_MS = 30_000;
const STALE_AFTER_MS = 5 * 60_000;

function isUniqueViolation(error: unknown): boolean {
  return typeof error === "object" && error !== null && (error as { code?: string }).code === "P2002";
}

async function claimRun(options: { trigger: CheckTrigger; slotKey?: string; startedBy?: string }) {
  const { count } = await db.domainRun.updateMany({
    where: { status: "RUNNING", heartbeatAt: { lt: new Date(Date.now() - STALE_AFTER_MS) } },
    data: { status: "FAILED", finishedAt: new Date(), errorMessage: "Run did not finish (server stopped or crashed)" },
  });
  if (count > 0) logger.warn("domain.run.stale_cleared", { count });

  try {
    return await db.domainRun.create({
      data: { trigger: options.trigger, slotKey: options.slotKey ?? null, startedBy: options.startedBy ?? null },
    });
  } catch (error) {
    if (isUniqueViolation(error)) return null;
    throw error;
  }
}

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

/** Look every domain in use up once. Never throws: a failure is recorded on the run. */
async function executeRun(run: ClaimedRun): Promise<void> {
  const startedAt = Date.now();
  logger.info("domain.run.started", { runId: run.id, trigger: run.trigger, slot: run.slotKey });

  const heartbeat = setInterval(() => {
    db.domainRun
      .update({ where: { id: run.id }, data: { heartbeatAt: new Date() } })
      .catch((error) => logger.error("domain.run.heartbeat_failed", { runId: run.id, error }));
  }, HEARTBEAT_INTERVAL_MS);

  try {
    const names = await domainsInUse();
    let checked = 0;
    let failed = 0;
    await forEachLimited(names, DOMAIN_LOOKUP_CONCURRENCY, async (name) => {
      try {
        const result = await checkDomain(name);
        if (result === "FOUND" || result === "NOT_FOUND") checked++;
        else failed++;
      } catch (error) {
        failed++;
        logger.error("domain.lookup.errored", { runId: run.id, domain: name, error });
      }
    });

    await db.domainRun.update({
      where: { id: run.id },
      data: { status: "COMPLETED", finishedAt: new Date(), domainsFound: names.length, domainsChecked: checked, failed },
    });
    logger.info("domain.run.completed", {
      runId: run.id,
      trigger: run.trigger,
      slot: run.slotKey,
      domainsFound: names.length,
      domainsChecked: checked,
      failed,
      durationMs: Date.now() - startedAt,
    });
  } catch (error) {
    logger.error("domain.run.failed", { runId: run.id, trigger: run.trigger, slot: run.slotKey, error });
    const message = error instanceof Error ? error.message : String(error);
    await db.domainRun
      .update({ where: { id: run.id }, data: { status: "FAILED", finishedAt: new Date(), errorMessage: message.slice(0, 500) } })
      .catch((failError) => logger.error("domain.run.fail_record_failed", { runId: run.id, error: failError }));
  } finally {
    clearInterval(heartbeat);
  }
}

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

/**
 * Daily domain check, at the check time from Monitoring Settings. Called from
 * the same ticker (and cron endpoint) as the health scheduler; safe to call as
 * often as you like.
 */
export async function runDomainTick(now: Date = new Date()): Promise<DomainTickResult> {
  const settings = await readMonitoringSettings();

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

  const slotKey = slot.toISOString();
  if ((await db.domainRun.count({ where: { slotKey } })) > 0) return { ran: false, reason: "already_run" };

  const run = await claimRun({ trigger: "SCHEDULED", slotKey });
  if (!run) return { ran: false, reason: "busy" };

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

/** "Check Domains Now": claim a manual run and return at once; `execute` runs it to completion. */
export async function startManualDomainRun(startedBy: string): Promise<{ execute: () => Promise<void> } | null> {
  const run = await claimRun({ trigger: "MANUAL", startedBy });
  if (!run) return null;
  return { execute: () => executeRun(run) };
}

export function getActiveDomainRun() {
  return db.domainRun.findFirst({ where: { status: "RUNNING" }, select: { id: true, startedAt: true, startedBy: true } });
}

export function getLastDomainRun() {
  return db.domainRun.findFirst({ where: { status: { not: "RUNNING" } }, orderBy: { startedAt: "desc" } });
}
