/**
 * Detailed health check controller for SentryAgent.ai AgentIdP.
 *
 * Implements `GET /health/detailed` — checks each dependency with latency
 * measurement and classifies the result as:
 *   healthy    — responded within 1000 ms
 *   degraded   — responded but latency exceeded 1000 ms
 *   unreachable — timed out or threw an error
 *
 * HTTP response codes (task 2.6):
 *   200 — all services healthy
 *   207 — at least one service degraded (but none unreachable)
 *   503 — at least one service unreachable
 */

import { Request, Response } from 'express';

/** Timeout applied to each individual health-check probe (ms). */
const PROBE_TIMEOUT_MS = 3000;

/** Latency threshold above which a service is considered degraded (ms). */
const DEGRADED_THRESHOLD_MS = 1000;

/** Classification of a single dependency's health. */
export type ServiceStatus = 'healthy' | 'degraded' | 'unreachable';

/** Per-service result returned in the response body. */
export interface ServiceHealthResult {
  status: ServiceStatus;
  latencyMs: number;
}

/** Full response body shape for GET /health/detailed. */
export interface DetailedHealthResponse {
  status: 'healthy' | 'degraded' | 'unreachable';
  version: string;
  uptime: number;
  services: Record<string, ServiceHealthResult>;
}

/** Minimal interface for a PostgreSQL liveness probe. */
export interface DbProbe {
  /** Checks DB liveness. Resolves when connected, rejects on failure. */
  checkLiveness(): Promise<void>;
}

/**
 * Dependencies injected into the controller.
 * All fields are optional — services are only probed when their client is provided.
 */
export interface HealthDetailedDeps {
  dbProbe: DbProbe;
  /** Optional Vault URL — when provided, the controller probes Vault's /v1/sys/health. */
  vaultAddr?: string;
  /** Optional OPA URL — when provided, the controller probes OPA's /health. */
  opaUrl?: string;
  /** Optional ioredis-compatible client for Redis probe. */
  redisClient?: { ping(): Promise<string> } | null;
}

/**
 * Wraps a probe promise with a hard timeout. Resolves to `'unreachable'`
 * classification when the timeout fires.
 *
 * @param probe - Async function that performs the health check and returns latencyMs.
 * @returns ServiceHealthResult with status and latency.
 */
async function runProbe(
  probe: () => Promise<number>,
): Promise<ServiceHealthResult> {
  const timeoutPromise = new Promise<ServiceHealthResult>((resolve) => {
    setTimeout(() => {
      resolve({ status: 'unreachable', latencyMs: PROBE_TIMEOUT_MS });
    }, PROBE_TIMEOUT_MS);
  });

  const probePromise = (async (): Promise<ServiceHealthResult> => {
    try {
      const latencyMs = await probe();
      const status: ServiceStatus =
        latencyMs > DEGRADED_THRESHOLD_MS ? 'degraded' : 'healthy';
      return { status, latencyMs };
    } catch {
      return { status: 'unreachable', latencyMs: PROBE_TIMEOUT_MS };
    }
  })();

  return Promise.race([probePromise, timeoutPromise]);
}

/**
 * Controller implementing GET /health/detailed.
 *
 * Constructed with the required infrastructure dependencies and optional
 * optional-service clients. The `handle` method is an Express route handler.
 */
export class HealthDetailedController {
  private readonly dbProbe: DbProbe;
  private readonly vaultAddr: string | undefined;
  private readonly opaUrl: string | undefined;
  private readonly redisClient: { ping(): Promise<string> } | null;

  constructor(deps: HealthDetailedDeps) {
    this.dbProbe = deps.dbProbe;
    this.vaultAddr = deps.vaultAddr;
    this.opaUrl = deps.opaUrl;
    this.redisClient = deps.redisClient ?? null;
  }

  /**
   * Express route handler for GET /health/detailed.
   *
   * @param _req - Express request (unused).
   * @param res - Express response.
   */
  handle = (_req: Request, res: Response): void => {
    void this.runChecks(res);
  };

  private async runChecks(res: Response): Promise<void> {
    const services: Record<string, ServiceHealthResult> = {};

    // ── PostgreSQL probe ────────────────────────────────────────────────────
    services['postgres'] = await runProbe(async () => {
      const start = Date.now();
      await this.dbProbe.checkLiveness();
      return Date.now() - start;
    });

    // ── Redis probe (optional — only when client supplied) ──────────────────
    if (this.redisClient !== null) {
      services['redis'] = await runProbe(async () => {
        const start = Date.now();
        await this.redisClient!.ping();
        return Date.now() - start;
      });
    }

    // ── Vault probe (optional — only when VAULT_ADDR is configured) ─────────
    if (this.vaultAddr) {
      services['vault'] = await runProbe(async () => {
        const url = `${this.vaultAddr}/v1/sys/health`;
        const start = Date.now();
        const fetchResponse = await fetch(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
        const latencyMs = Date.now() - start;
        // Vault returns 200 (initialised, unsealed, active), 429 (standby), 472/473 (DR).
        // All mean Vault is reachable — only network errors mean unreachable.
        if (fetchResponse.status >= 500) {
          throw new Error(`Vault health endpoint returned ${fetchResponse.status}`);
        }
        return latencyMs;
      });
    }

    // ── OPA probe (optional — only when OPA_URL is configured) ─────────────
    if (this.opaUrl) {
      services['opa'] = await runProbe(async () => {
        const url = `${this.opaUrl}/health`;
        const start = Date.now();
        const fetchResponse = await fetch(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
        const latencyMs = Date.now() - start;
        if (!fetchResponse.ok) {
          throw new Error(`OPA health endpoint returned ${fetchResponse.status}`);
        }
        return latencyMs;
      });
    }

    // ── Compute overall status (task 2.6) ───────────────────────────────────
    const statuses = Object.values(services).map((s) => s.status);
    const hasUnreachable = statuses.includes('unreachable');
    const hasDegraded = statuses.includes('degraded');

    let overallStatus: 'healthy' | 'degraded' | 'unreachable';
    let httpStatus: 200 | 207 | 503;

    if (hasUnreachable) {
      overallStatus = 'unreachable';
      httpStatus = 503;
    } else if (hasDegraded) {
      overallStatus = 'degraded';
      httpStatus = 207;
    } else {
      overallStatus = 'healthy';
      httpStatus = 200;
    }

    const body: DetailedHealthResponse = {
      status: overallStatus,
      version: process.env['npm_package_version'] ?? '1.0.0',
      uptime: Math.floor(process.uptime()),
      services,
    };

    res.status(httpStatus).json(body);
  }
}
