import { STATUS_CODES } from "node:http";
import { isUp, type HealthStatus } from "@/lib/status";
import { isBlockedHost, parseHttpUrl } from "@/lib/validation/project";

// Pure HTTP probing: no database, no framework. One call = one GET request.
// probe() never throws — every failure mode comes back as an outcome.

export type { HealthStatus };
export type HealthErrorType = "TIMEOUT" | "DNS" | "CONNECTION" | "TLS" | "HTTP_STATUS" | "SLOW_RESPONSE" | "UNKNOWN";

export type CheckTarget = {
  url: string;
  expectedHttpStatus: number;
  timeoutSeconds: number;
  /** A successful response slower than this is SLOW rather than WORKING. */
  slowThresholdSeconds: number;
};

export type CheckOutcome = {
  status: HealthStatus;
  httpStatusCode: number | null;
  responseTimeMs: number | null;
  errorType: HealthErrorType | null;
  errorMessage: string | null;
};

const USER_AGENT = "BTS-Apps-Dashboard/1.0 (health-check)";
const MAX_ERROR_LENGTH = 500;

const DNS_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "EAI_FAIL", "EAI_NODATA", "EAI_NONAME"]);
const TIMEOUT_CODES = new Set(["ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"]);
const CONNECTION_CODES = new Set([
  "ECONNREFUSED",
  "ECONNRESET",
  "ECONNABORTED",
  "EHOSTUNREACH",
  "EHOSTDOWN",
  "ENETUNREACH",
  "ENETDOWN",
  "EADDRNOTAVAIL",
  "EPIPE",
  "UND_ERR_SOCKET",
  "UND_ERR_CLOSED",
]);
// OpenSSL / Node TLS codes, e.g. CERT_HAS_EXPIRED, DEPTH_ZERO_SELF_SIGNED_CERT, ERR_TLS_CERT_ALTNAME_INVALID.
const TLS_CODE_PATTERN = /^EPROTO$|TLS|SSL|CERT|SELF_SIGNED|UNABLE_TO_VERIFY|UNABLE_TO_GET_ISSUER/;

const CONNECTION_MESSAGES: Record<string, string> = {
  ECONNREFUSED: "Connection refused",
  ECONNRESET: "Connection reset by the server",
  EHOSTUNREACH: "Host unreachable",
  ENETUNREACH: "Network unreachable",
};

function failure(
  errorType: HealthErrorType,
  errorMessage: string,
  extra: Partial<Pick<CheckOutcome, "httpStatusCode" | "responseTimeMs">> = {},
): CheckOutcome {
  return {
    status: "NOT_WORKING",
    httpStatusCode: extra.httpStatusCode ?? null,
    responseTimeMs: extra.responseTimeMs ?? null,
    errorType,
    errorMessage: errorMessage.replace(/\s+/g, " ").trim().slice(0, MAX_ERROR_LENGTH),
  };
}

/** Node's fetch wraps the real error in `cause`, sometimes as an AggregateError (IPv4 + IPv6 attempts). */
function rootCause(error: unknown): { code?: string; message: string } {
  let current: unknown = error;
  // "fetch failed" on its own says nothing, so keep the deepest message seen.
  let message = error instanceof Error ? error.message : String(error);
  for (let depth = 0; depth < 5; depth++) {
    if (typeof current !== "object" || current === null) break;
    const node = current as { code?: unknown; message?: unknown; cause?: unknown; errors?: unknown };
    if (typeof node.message === "string" && node.message) message = node.message;
    if (typeof node.code === "string") return { code: node.code, message };
    if (Array.isArray(node.errors) && node.errors.length > 0) current = node.errors[0];
    else if (node.cause) current = node.cause;
    else break;
  }
  return { message };
}

function classifyError(error: unknown, target: CheckTarget, elapsedMs: number): CheckOutcome {
  const name = error instanceof Error ? error.name : "";
  if (name === "TimeoutError" || name === "AbortError") {
    return failure("TIMEOUT", `Request Timeout (no response within ${target.timeoutSeconds}s)`, {
      responseTimeMs: elapsedMs,
    });
  }

  const { code, message } = rootCause(error);
  if (code && TIMEOUT_CODES.has(code)) {
    return failure("TIMEOUT", `Request Timeout (${code})`, { responseTimeMs: elapsedMs });
  }
  if (code && DNS_CODES.has(code)) return failure("DNS", `DNS lookup failed (${code})`);
  if (code && TLS_CODE_PATTERN.test(code)) {
    // Certificate errors read well ("certificate has expired"); raw OpenSSL traces don't.
    const detail = message.includes(":error:") ? "" : `: ${message}`;
    return failure("TLS", `SSL/TLS error${detail} (${code})`);
  }
  if (code && CONNECTION_CODES.has(code)) {
    return failure("CONNECTION", `${CONNECTION_MESSAGES[code] ?? "Connection failed"} (${code})`);
  }
  return failure("UNKNOWN", code ? `Network error: ${message} (${code})` : `Network error: ${message}`);
}

function formatSeconds(ms: number): string {
  return `${(ms / 1000).toFixed(1)}s`;
}

function describeStatus(code: number): string {
  const text = STATUS_CODES[code];
  return text ? `HTTP ${code} ${text}` : `HTTP ${code}`;
}

export async function probe(target: CheckTarget): Promise<CheckOutcome> {
  const url = parseHttpUrl(target.url);
  if (!url) return failure("UNKNOWN", "Invalid Health API URL");
  if (isBlockedHost(url.hostname)) return failure("UNKNOWN", "Health API host is not allowed");

  const startedAt = performance.now();
  const elapsed = () => Math.round(performance.now() - startedAt);

  try {
    const response = await fetch(url, {
      method: "GET",
      // A redirect (e.g. to a login page that answers 200) must not count as healthy.
      redirect: "manual",
      cache: "no-store",
      signal: AbortSignal.timeout(target.timeoutSeconds * 1000),
      headers: { "User-Agent": USER_AGENT, Accept: "*/*" },
    });
    // Time to response headers; the body is irrelevant and is discarded unread.
    const responseTimeMs = elapsed();
    void response.body?.cancel().catch(() => {});

    if (response.status === target.expectedHttpStatus) {
      if (responseTimeMs > target.slowThresholdSeconds * 1000) {
        return {
          status: "SLOW",
          httpStatusCode: response.status,
          responseTimeMs,
          errorType: "SLOW_RESPONSE",
          errorMessage: `Slow response: ${formatSeconds(responseTimeMs)} (threshold ${target.slowThresholdSeconds}s)`,
        };
      }
      return {
        status: "WORKING",
        httpStatusCode: response.status,
        responseTimeMs,
        errorType: null,
        errorMessage: null,
      };
    }

    const redirected = response.status >= 300 && response.status < 400 ? ", redirect not followed" : "";
    return failure(
      "HTTP_STATUS",
      `${describeStatus(response.status)} (expected ${target.expectedHttpStatus}${redirected})`,
      { httpStatusCode: response.status, responseTimeMs },
    );
  } catch (error) {
    return classifyError(error, target, elapsed());
  }
}

export type CheckResult = CheckOutcome & { checkedAt: Date; attempts: number };

/**
 * The health check used everywhere (scheduled and manual): one probe, and if
 * it fails, a single retry after a short pause so a momentary blip doesn't
 * mark an app Not Working until the next scheduled run. A slow answer is not
 * a failure, so it is reported as-is rather than retried.
 */
export async function runHealthCheck(target: CheckTarget, retryDelayMs: number): Promise<CheckResult> {
  const checkedAt = new Date();
  const first = await probe(target);
  if (isUp(first.status)) return { ...first, checkedAt, attempts: 1 };

  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  const second = await probe(target);
  return { ...second, checkedAt, attempts: 2 };
}
