/**
 * `@platform-modules/util/health` — a uniform health-probe contract + aggregator (pure, zero-dep).
 *
 * A site-health screen reports DB / mail / jobs / cache / storage reachability without bespoke
 * wiring: each infra module exports a `HealthCheck` factory; the host composes them into one
 * `aggregateHealth([...])` call for the modules it actually installed.
 *
 * Availability floor: a probe failure is a `HealthResult{status:'down'}`, NEVER a thrown
 * exception — the screen must render even when a dependency is dead.
 */

export type HealthStatus = 'ok' | 'degraded' | 'down'

export interface HealthResult {
  name: string
  status: HealthStatus
  detail?: string
  latencyMs?: number
}

/** The per-module contract: a named probe that resolves to a typed result (never throws by contract). */
export interface HealthCheck {
  name: string
  check(): Promise<HealthResult>
}

/**
 * Run all checks (settled — a thrown/rejected check becomes `status:'down'`, never propagates),
 * roll up to the worst status: any 'down' => 'down'; else any 'degraded' => 'degraded'; else 'ok'.
 * An empty check set rolls up to 'ok' (nothing unhealthy).
 */
export async function aggregateHealth(
  checks: HealthCheck[],
): Promise<{ status: HealthStatus; checks: HealthResult[] }> {
  // NB: the `async` wrapper is load-bearing — it converts a probe that throws
  // SYNCHRONOUSLY (a conforming `check(): Promise<…>` may be a non-async fn whose
  // body throws before returning, e.g. `() => adapter.ping()` with a null adapter)
  // into a rejected promise that `allSettled` captures. Without it, a sync throw in
  // the `.map` callback escapes `allSettled` and rejects the aggregate — breaching
  // the availability floor (the screen must render even when a dependency is dead).
  const settled = await Promise.allSettled(checks.map(async (c) => c.check()))
  const results: HealthResult[] = settled.map((s, i) => {
    if (s.status === 'fulfilled') return s.value
    // a thrown check => down (availability floor — the aggregate never throws).
    // The raw rejection reason is DELIBERATELY NOT surfaced: a throwing check (esp. a
    // host-composed inline mail/jobs/uploads probe) may carry a provider error with a
    // DSN/credentials/host topology, and `detail` renders on an admin screen
    // (info-disclosure hard floor). A check wanting a curated detail returns its own
    // HealthResult{status:'down', detail} instead of throwing.
    return { name: checks[i]!.name, status: 'down', detail: 'check failed' }
  })
  return { status: rollup(results), checks: results }
}

function rollup(results: HealthResult[]): HealthStatus {
  if (results.some((r) => r.status === 'down')) return 'down'
  if (results.some((r) => r.status === 'degraded')) return 'degraded'
  return 'ok'
}
