import "server-only";

import type { CheckTrigger } from "@/generated/prisma/client";
import { isUp } from "@/lib/status";
import { db } from "../db";
import type { CheckResult } from "./checker";
import { nextHealth, type HealthTransition } from "./transition";

type LockedProject = {
  failure_started_at: Date | null;
  last_recovered_at: Date | null;
  last_checked_at: Date | null;
};

export type RecordedCheck = { recorded: true; event: HealthTransition["event"] } | { recorded: false };

/**
 * Stores one health-check result and moves the project's current status, in a
 * single transaction so history and status can never disagree.
 *
 * The project row is locked first: a manual "Check Now" and a scheduled check
 * of the same app are serialised, so failure_started_at can't be raced.
 */
export function recordHealthCheck(
  projectId: string,
  result: CheckResult,
  trigger: CheckTrigger,
  runId: string | null = null,
): Promise<RecordedCheck> {
  return db.$transaction(async (tx) => {
    const [project] = await tx.$queryRaw<LockedProject[]>`
      SELECT failure_started_at, last_recovered_at, last_checked_at
      FROM projects
      WHERE id = ${projectId} AND deleted_at IS NULL AND monitoring_enabled = true
      FOR UPDATE`;

    // Deleted or disabled while the request was in flight: discard the result
    // rather than mark an app the user just switched off as failed.
    if (!project) return { recorded: false };

    await tx.healthCheck.create({
      data: {
        projectId,
        checkedAt: result.checkedAt,
        status: result.status,
        httpStatusCode: result.httpStatusCode,
        responseTimeMs: result.responseTimeMs,
        errorType: result.errorType,
        errorMessage: result.errorMessage,
        trigger,
        attempts: result.attempts,
        runId,
      },
    });

    // A result older than the one already shown stays in history but must not
    // move the current status backwards.
    if (project.last_checked_at && project.last_checked_at > result.checkedAt) {
      return { recorded: true, event: null };
    }

    const transition = nextHealth(
      { failureStartedAt: project.failure_started_at, lastRecoveredAt: project.last_recovered_at },
      result.status,
      result.checkedAt,
    );
    // Slow counts as a successful check: the API answered.
    const working = isUp(result.status);

    // Raw SQL on purpose: a Prisma update would bump updated_at, which must
    // only change when the configuration is edited.
    await tx.$executeRaw`
      UPDATE projects SET
        current_status           = ${result.status}::"HealthStatus",
        last_checked_at          = ${result.checkedAt},
        last_successful_check_at = CASE WHEN ${working} THEN ${result.checkedAt} ELSE last_successful_check_at END,
        last_failed_check_at     = CASE WHEN ${working} THEN last_failed_check_at ELSE ${result.checkedAt} END,
        failure_started_at       = ${transition.failureStartedAt},
        last_recovered_at        = ${transition.lastRecoveredAt},
        current_http_status      = ${result.httpStatusCode},
        current_response_time_ms = ${result.responseTimeMs},
        current_error_message    = ${result.errorMessage}
      WHERE id = ${projectId}`;

    return { recorded: true, event: transition.event };
  });
}
