import "server-only";

import type { CheckTrigger } from "@/generated/prisma/client";
import { forEachLimited } from "../concurrency";
import { db } from "../db";
import { env } from "../env";
import { logger } from "../logger";
import { readMonitoringSettings } from "../settings/service";
import { runHealthCheck, type CheckResult } from "./checker";
import { recordHealthCheck } from "./recorder";

export class ProjectNotCheckableError extends Error {
  constructor(public readonly reason: "not_found" | "monitoring_disabled") {
    super(reason === "not_found" ? "Project not found" : "Monitoring is disabled for this project");
    this.name = "ProjectNotCheckableError";
  }
}

type Checkable = {
  id: string;
  projectCode: string;
  productionUrl: string;
  expectedHttpStatus: number;
  timeoutSeconds: number;
};

const CHECKABLE_FIELDS = {
  id: true,
  projectCode: true,
  productionUrl: true,
  expectedHttpStatus: true,
  timeoutSeconds: true,
} as const;

/** The one code path every health check takes, whatever triggered it. */
async function checkAndRecord(
  project: Checkable,
  trigger: CheckTrigger,
  slowThresholdSeconds: number,
  runId: string | null = null,
): Promise<CheckResult> {
  const result = await runHealthCheck(
    {
      url: project.productionUrl,
      expectedHttpStatus: project.expectedHttpStatus,
      timeoutSeconds: project.timeoutSeconds,
      slowThresholdSeconds,
    },
    env.HEALTH_CHECK_RETRY_DELAY_MS,
  );

  const recorded = await recordHealthCheck(project.id, result, trigger, runId);

  const fields = {
    projectId: project.id,
    projectCode: project.projectCode,
    trigger,
    status: result.status,
    httpStatus: result.httpStatusCode,
    responseTimeMs: result.responseTimeMs,
    attempts: result.attempts,
    errorType: result.errorType,
    errorMessage: result.errorMessage,
  };

  if (!recorded.recorded) logger.info("health.check.discarded", fields);
  else if (result.status === "WORKING") logger.info("health.check.working", { ...fields, transition: recorded.event });
  else if (result.status === "SLOW") logger.warn("health.check.slow", { ...fields, transition: recorded.event });
  else logger.warn("health.check.not_working", { ...fields, transition: recorded.event });

  return result;
}

/** Check one app now. Throws ProjectNotCheckableError for deleted or disabled apps. */
export async function checkProject(projectId: string, trigger: CheckTrigger): Promise<CheckResult> {
  const project = await db.project.findFirst({
    where: { id: projectId, deletedAt: null },
    select: { ...CHECKABLE_FIELDS, monitoringEnabled: true },
  });

  if (!project) throw new ProjectNotCheckableError("not_found");
  if (!project.monitoringEnabled) throw new ProjectNotCheckableError("monitoring_disabled");

  const { slowThresholdSeconds } = await readMonitoringSettings();
  return checkAndRecord(project, trigger, slowThresholdSeconds);
}

export type BulkCheckSummary = {
  found: number; // active apps, enabled or not
  disabled: number;
  checked: number;
  working: number;
  slow: number;
  notWorking: number;
  errored: number; // the check itself could not be completed or stored
};

/**
 * Check every active, enabled app with bounded concurrency. Each app is
 * isolated: a slow, failing or even throwing check never stops the others.
 * Every stored check is linked to `runId`, so the run's per-app results can be listed.
 */
export async function checkAllEnabledProjects(trigger: CheckTrigger, runId: string): Promise<BulkCheckSummary> {
  const [projects, disabled] = await Promise.all([
    db.project.findMany({
      where: { deletedAt: null, monitoringEnabled: true },
      select: CHECKABLE_FIELDS,
      orderBy: { projectCode: "asc" },
    }),
    db.project.count({ where: { deletedAt: null, monitoringEnabled: false } }),
  ]);

  const summary: BulkCheckSummary = {
    found: projects.length + disabled,
    disabled,
    checked: 0,
    working: 0,
    slow: 0,
    notWorking: 0,
    errored: 0,
  };

  // Read once per run so every app in the run is judged by the same threshold.
  const { slowThresholdSeconds } = await readMonitoringSettings();

  await forEachLimited(projects, env.HEALTH_CHECK_CONCURRENCY, async (project) => {
    try {
      const result = await checkAndRecord(project, trigger, slowThresholdSeconds, runId);
      summary.checked++;
      if (result.status === "WORKING") summary.working++;
      else if (result.status === "SLOW") summary.slow++;
      else summary.notWorking++;
    } catch (error) {
      summary.errored++;
      logger.error("health.check.errored", { projectId: project.id, projectCode: project.projectCode, error });
    }
  });

  return summary;
}

/** Fire-and-forget check that can never throw into the caller (used after create / re-enable). */
export async function checkProjectQuietly(projectId: string, trigger: CheckTrigger): Promise<void> {
  try {
    await checkProject(projectId, trigger);
  } catch (error) {
    if (error instanceof ProjectNotCheckableError) return;
    logger.error("health.check.errored", { projectId, error });
  }
}

/** Background checks for a batch of apps (e.g. after an import), with the same concurrency cap as a bulk run. */
export function checkProjectsQuietly(projectIds: string[], trigger: CheckTrigger): Promise<void> {
  return forEachLimited(projectIds, env.HEALTH_CHECK_CONCURRENCY, (id) => checkProjectQuietly(id, trigger));
}
