import type { MonitoringView, ServiceHealthView, ServiceStatus } from "@batch-cooking/shared";
import { env } from "../../config/env.js";
import { prisma } from "../../db/prisma.js";

/** How long each outbound probe (Postgres query, intent-service HTTP) is allowed to take before it counts as `down`. */
const PROBE_TIMEOUT_MS = 2000;

/**
 * Heartbeat-age thresholds for the LLM worker. Its default cron is weekly
 * (`TECH_STEP_WORKER_CRON`, `0 3 * * 0`), and it also pings on boot/tick —
 * so no ping for **8 days** means it likely missed its last scheduled fire
 * (`degraded`), and none for **3 weeks** means it's almost certainly not
 * running at all (`down`).
 */
const WORKER_STALE_AFTER_MS = 8 * 24 * 60 * 60 * 1000;
const WORKER_DOWN_AFTER_MS = 21 * 24 * 60 * 60 * 1000;

const WORKER_KEY = "tech-step-llm-worker";

function nowIso(): string {
  return new Date().toISOString();
}

function roundMs(value: number): number {
  return Math.round(value * 10) / 10;
}

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

/** `12500` → `"il y a 12 s"`, `90000` → `"il y a 1 min"`, `172800000` → `"il y a 2 j"`. */
function formatAgo(ms: number): string {
  const s = Math.round(ms / 1000);
  if (s < 60) return `il y a ${s} s`;
  const m = Math.round(s / 60);
  if (m < 60) return `il y a ${m} min`;
  const h = Math.round(m / 60);
  if (h < 48) return `il y a ${h} h`;
  return `il y a ${Math.round(h / 24)} j`;
}

/** `process.uptime()` seconds → `"3 h 12 min"` / `"5 min"` / `"42 s"`. */
function formatUptime(seconds: number): string {
  const s = Math.floor(seconds);
  if (s < 60) return `${s} s`;
  const m = Math.floor(s / 60);
  if (m < 60) return `${m} min`;
  const h = Math.floor(m / 60);
  return `${h} h ${m % 60} min`;
}

/** `prisma.$queryRaw\`SELECT 1\`` with a bounded timeout — the DB connectivity probe. */
async function probePostgres(): Promise<ServiceHealthView> {
  const start = performance.now();
  try {
    // `$queryRaw` doesn't take an AbortSignal — bound it with a race instead.
    await Promise.race([
      prisma.$queryRaw`SELECT 1`,
      new Promise((_resolve, reject) =>
        setTimeout(() => reject(new Error("timeout")), PROBE_TIMEOUT_MS),
      ),
    ]);
    return {
      key: "postgres",
      status: "up",
      latencyMs: roundMs(performance.now() - start),
      detail: null,
      checkedAt: nowIso(),
    };
  } catch (err) {
    return {
      key: "postgres",
      status: "down",
      latencyMs: null,
      detail: errMessage(err),
      checkedAt: nowIso(),
    };
  }
}

/** The API itself — trivially "up" (it's answering), reported with its process uptime/memory. */
function probeApi(): ServiceHealthView {
  const mem = process.memoryUsage();
  return {
    key: "api",
    status: "up",
    latencyMs: 0,
    detail: `uptime ${formatUptime(process.uptime())} · RSS ${Math.round(mem.rss / 1_000_000)} Mo`,
    checkedAt: nowIso(),
  };
}

/** `GET {INTENT_SERVICE_BASE_URL}/health` — no secret needed on that route (see the service's `routes/health.py`). */
async function probeIntentService(): Promise<ServiceHealthView> {
  const start = performance.now();
  try {
    const res = await fetch(`${env.INTENT_SERVICE_BASE_URL}/health`, {
      signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
    });
    const latencyMs = roundMs(performance.now() - start);
    return {
      key: "intent-service",
      status: res.ok ? "up" : "degraded",
      latencyMs,
      detail: `HTTP ${res.status}`,
      checkedAt: nowIso(),
    };
  } catch (err) {
    return {
      key: "intent-service",
      status: "down",
      latencyMs: null,
      detail: errMessage(err),
      checkedAt: nowIso(),
    };
  }
}

/** Reads the LLM worker's stored `WorkerHeartbeat` (it has no HTTP surface to probe directly) and grades it by age + last job outcome. */
async function probeWorker(): Promise<ServiceHealthView> {
  const heartbeat = await prisma.workerHeartbeat.findUnique({ where: { workerKey: WORKER_KEY } });
  if (!heartbeat) {
    return {
      key: WORKER_KEY,
      status: "unknown",
      latencyMs: null,
      detail: "aucun battement reçu",
      checkedAt: nowIso(),
      lastRunAt: null,
      lastResult: null,
    };
  }

  const ageMs = Date.now() - heartbeat.lastSeenAt.getTime();
  const lastResult = (heartbeat.lastResult ?? null) as ServiceHealthView["lastResult"];

  let status: ServiceStatus = "up";
  if (ageMs > WORKER_DOWN_AFTER_MS) status = "down";
  else if (ageMs > WORKER_STALE_AFTER_MS || lastResult?.ok === false) status = "degraded";

  return {
    key: WORKER_KEY,
    status,
    latencyMs: null,
    detail: `dernier battement ${formatAgo(ageMs)}`,
    checkedAt: nowIso(),
    lastRunAt: heartbeat.lastRunAt?.toISOString() ?? null,
    lastResult,
  };
}

/**
 * Actively probes every dependency the admin monitoring board watches —
 * Postgres, the API itself, `tech-step-intent-service` (`/health`), and the
 * LLM worker (via its stored heartbeat). Each probe is independent and
 * bounded ({@link PROBE_TIMEOUT_MS}); one being `down` never fails the
 * others or the endpoint.
 */
export async function getMonitoring(): Promise<MonitoringView> {
  try {
    const [postgres, intentService, worker] = await Promise.all([
      probePostgres(),
      probeIntentService(),
      probeWorker(),
    ]);
    return {
      generatedAt: nowIso(),
      services: [postgres, probeApi(), intentService, worker],
    };
  } catch (err) {
    throw err; // see recipe.service.ts's equivalent catch comment
  }
}
