/**
 * Health check handler
 * Health check handler for Beignet server adapters.
 */

import type { AnyPorts } from "../ports/index.js";
import type { HttpRequestLike, HttpResponseLike } from "./types.js";

/**
 * Per-dependency health detail returned by readiness checks.
 */
export interface HealthCheckDetail {
  /**
   * Whether this dependency is healthy.
   */
  ok: boolean;
  /**
   * Optional human-readable status. Avoid secrets and raw provider credentials.
   */
  message?: string;
  /**
   * Milliseconds spent running this dependency check.
   */
  durationMs?: number;
  /**
   * Optional safe metadata for operators.
   */
  metadata?: Record<string, unknown>;
}

/**
 * Health check result returned by health handlers.
 */
export interface HealthCheckResult {
  /**
   * Whether the app is healthy.
   */
  ok: boolean;
  /**
   * Optional per-dependency health details.
   */
  details?: Record<string, HealthCheckDetail>;
}

/**
 * A named dependency check run by {@link runHealthChecks}.
 */
export type HealthCheck<Ports> = (
  ports: Ports,
) =>
  | Promise<HealthCheckDetail | boolean | undefined>
  | HealthCheckDetail
  | boolean
  | undefined;

/**
 * Named dependency checks for an app-owned readiness endpoint.
 */
export type HealthChecks<Ports> = Record<string, HealthCheck<Ports>>;

/**
 * Options for running named dependency checks.
 */
export interface RunHealthChecksOptions {
  /**
   * Maximum time to wait for each dependency check.
   *
   * Defaults to 2000ms.
   */
  timeoutMs?: number;
  /**
   * Include thrown error messages in dependency details.
   *
   * Defaults to true. Set false in production responses when provider errors
   * may include sensitive details.
   */
  includeErrorDetails?: boolean;
}

/**
 * Health check configuration.
 */
export interface HealthConfig<Ports> {
  /** Enable health endpoint (default: false) */
  enabled?: boolean;
  /**
   * Suggested path for the health endpoint (e.g., "/api/health").
   * NOTE: This field is for documentation/metadata only and does not control routing.
   * You must manually wire the healthHandler to your desired route.
   */
  suggestedPath?: string;
  /** Custom health check function */
  check?: (ports: Ports) => Promise<HealthCheckResult>;
  /** Named dependency checks for an app-owned readiness endpoint */
  checks?: HealthChecks<Ports>;
  /** Per-check timeout for named dependency checks */
  timeoutMs?: number;
}

/**
 * Application environment.
 */
export type AppEnvironment = "development" | "production" | "test";

const DEFAULT_HEALTH_TIMEOUT_MS = 2000;

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

function assertTimeout(timeoutMs: number): void {
  if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
    throw new Error("Health check timeoutMs must be a positive integer.");
  }
}

function normalizeHealthDetail(
  result: HealthCheckDetail | boolean | undefined,
): HealthCheckDetail {
  if (typeof result === "boolean") return { ok: result };
  return result ?? { ok: true };
}

function redactHealthDetail(
  detail: HealthCheckDetail,
  includeErrorDetails: boolean,
): HealthCheckDetail {
  if (includeErrorDetails || detail.ok || detail.message === undefined) {
    return detail;
  }

  return {
    ...detail,
    message: "Health check failed",
  };
}

function withTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number,
  name: string,
): Promise<T> {
  let timeout: ReturnType<typeof setTimeout> | undefined;

  const timeoutPromise = new Promise<never>((_, reject) => {
    timeout = setTimeout(() => {
      reject(
        new Error(
          `Health check "${name}" did not complete within ${timeoutMs}ms.`,
        ),
      );
    }, timeoutMs);
  });

  return Promise.race([promise, timeoutPromise]).finally(() => {
    if (timeout) clearTimeout(timeout);
  });
}

/**
 * Run named dependency health checks in parallel and aggregate the result.
 *
 * This is intended for app-owned readiness endpoints. Checks should be cheap,
 * bounded, non-mutating probes such as `SELECT 1`, Redis `PING`, or provider
 * health endpoints. Do not start workers, drains, migrations, or polling loops
 * from readiness checks.
 */
export async function runHealthChecks<Ports extends AnyPorts>(
  ports: Ports,
  checks: HealthChecks<Ports>,
  options: RunHealthChecksOptions = {},
): Promise<HealthCheckResult> {
  const timeoutMs = options.timeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
  assertTimeout(timeoutMs);

  const includeErrorDetails = options.includeErrorDetails ?? true;
  const entries = await Promise.all(
    Object.entries(checks).map(async ([name, check]) => {
      const startedAt = Date.now();

      try {
        const detail = redactHealthDetail(
          normalizeHealthDetail(
            await withTimeout(Promise.resolve(check(ports)), timeoutMs, name),
          ),
          includeErrorDetails,
        );

        return [
          name,
          {
            ...detail,
            durationMs: Date.now() - startedAt,
          },
        ] as const;
      } catch (error) {
        return [
          name,
          {
            ok: false,
            message: includeErrorDetails
              ? errorMessage(error)
              : "Health check failed",
            durationMs: Date.now() - startedAt,
          },
        ] as const;
      }
    }),
  );

  const details = Object.fromEntries(entries);

  return {
    ok: Object.values(details).every((detail) => detail.ok),
    details,
  };
}

/**
 * Create a framework-neutral health check handler.
 *
 * The returned handler reports 200 when healthy and 503 when unhealthy. Thrown
 * health check errors include details in development/test and use a generic
 * message in production.
 */
export function createHealthHandler<Ports extends AnyPorts>(
  ports: Ports,
  healthConfig: HealthConfig<Ports> | undefined,
  env: AppEnvironment,
): (req: HttpRequestLike) => Promise<HttpResponseLike> {
  return async (_req: HttpRequestLike): Promise<HttpResponseLike> => {
    let result: HealthCheckResult;
    if (healthConfig?.check) {
      try {
        result = await healthConfig.check(ports);
      } catch (error) {
        // Health check function threw - treat as unhealthy
        // Only include error details in development/test to avoid leaking sensitive info
        const includeErrorDetails = env === "development" || env === "test";
        result = {
          ok: false,
          details: {
            error: {
              ok: false,
              message: includeErrorDetails
                ? error instanceof Error
                  ? error.message
                  : String(error)
                : "Health check failed",
            },
          },
        };
      }
    } else if (healthConfig?.checks) {
      result = await runHealthChecks(ports, healthConfig.checks, {
        timeoutMs: healthConfig.timeoutMs,
        includeErrorDetails: env !== "production",
      });
    } else {
      result = { ok: true };
    }

    const status = result.ok ? 200 : 503;

    return {
      status,
      body: result,
      headers: { "Content-Type": "application/json" },
    };
  };
}
