import { hostname as osHostname } from "node:os";
import type { Adapter, FetchLike } from "../adapter";
import type { AdapterResult, Item, Panel } from "../schema";

export type FetchFn = FetchLike;

export interface PrometheusAdapterOptions {
  id?: string;
  interval?: number;
  baseUrl?: string;
  fetchFn?: FetchFn;
  hostname?: string;
  now?: () => number;
}

// Thresholds mirror netdata health.d (mem_trajectory.conf, disk_fill.conf) — do not invent new ones.
const MEM_RUNWAY_ALERT_SECONDS = 30 * 60;
const DISK_FREE_ALERT_PERCENT = 5;

const QUERIES = {
  cpuPsiSomePercent: "rate(node_pressure_cpu_waiting_seconds_total[1m]) * 100",
  memRunwayEtaSeconds: "node_mem_exhaustion_eta_seconds",
  memRunwayPercent: "node_mem_runway_percent",
  memPsiFullAvg10Percent: "node_mem_psi_full_avg10_percent",
  buildSliceJobCount: 'count(node_systemd_unit_state{name="build.slice",state="active"}) or vector(0)',
  packageThrottleRate: "rate(node_cpu_package_throttles_total[1m])",
  diskRootFreePercent:
    'node_filesystem_avail_bytes{fstype!="tmpfs",mountpoint="/"} / node_filesystem_size_bytes{fstype!="tmpfs",mountpoint="/"} * 100',
} as const;

type QueryKey = keyof typeof QUERIES;

interface PromVectorResult {
  status: string;
  data?: {
    resultType: string;
    result: Array<{ metric: Record<string, string>; value: [number, string] }>;
  };
}

async function runQuery(
  fetchFn: FetchFn,
  baseUrl: string,
  expr: string,
): Promise<number | undefined> {
  const url = `${baseUrl}/api/v1/query?query=${encodeURIComponent(expr)}`;
  const res = await fetchFn(url);
  if (!res.ok) {
    throw new Error(`prometheus query failed (${res.status}): ${expr}`);
  }
  const body = (await res.json()) as PromVectorResult;
  if (body.status !== "success" || !body.data) {
    throw new Error(`prometheus query returned non-success status: ${expr}`);
  }
  const sample = body.data.result[0];
  if (!sample) return undefined;
  return Number(sample.value[1]);
}

async function runQueries(
  fetchFn: FetchFn,
  baseUrl: string,
): Promise<Record<QueryKey, number | undefined>> {
  const keys = Object.keys(QUERIES) as QueryKey[];
  const values = await Promise.all(keys.map((key) => runQuery(fetchFn, baseUrl, QUERIES[key])));
  const out = {} as Record<QueryKey, number | undefined>;
  keys.forEach((key, i) => {
    out[key] = values[i];
  });
  return out;
}

/**
 * Host health from Prometheus/node_exporter. PROCHOT has no direct kernel-exposed
 * gauge on this hardware; the package-throttle counter's instantaneous rate is the
 * closest real signal (nonzero rate == throttling actively accruing right now).
 * build.slice job count queries the real systemd collector metric; it reads 0 when
 * the exporter's unit-include filter excludes slice units, which is a config gap
 * outside this adapter's scope, not an invented metric.
 */
export function createPrometheusAdapter(opts: PrometheusAdapterOptions = {}): Adapter {
  const id = opts.id ?? "prometheus";
  const interval = opts.interval ?? 30_000;
  const baseUrl = opts.baseUrl ?? "http://127.0.0.1:9090";
  const fetchFn = opts.fetchFn ?? fetch;
  const host = opts.hostname ?? osHostname();
  const now = opts.now ?? Date.now;

  async function poll(): Promise<AdapterResult> {
    const ts = new Date(now()).toISOString();
    const q = await runQueries(fetchFn, baseUrl);

    const items: Item[] = [];

    const prochot = (q.packageThrottleRate ?? 0) > 0;
    if (prochot) {
      items.push(
        alertItem(id, ts, "prochot", "PROCHOT active", "CPU package throttle events accruing now."),
      );
    }

    if (q.memRunwayEtaSeconds !== undefined && q.memRunwayEtaSeconds < MEM_RUNWAY_ALERT_SECONDS) {
      items.push(
        alertItem(
          id,
          ts,
          "mem-runway",
          "Memory runway low",
          `Effective memory+swap exhausts in ~${Math.round(q.memRunwayEtaSeconds)}s.`,
        ),
      );
    }

    if (q.diskRootFreePercent !== undefined && q.diskRootFreePercent < DISK_FREE_ALERT_PERCENT) {
      items.push(
        alertItem(
          id,
          ts,
          "disk-free",
          "Disk space low",
          `Root filesystem has ${q.diskRootFreePercent.toFixed(1)}% free.`,
        ),
      );
    }

    const panels: Panel[] = [
      {
        id: `host:${host}`,
        ts,
        data: {
          cpuPsiSomePercent: q.cpuPsiSomePercent ?? null,
          memRunwayEtaSeconds: q.memRunwayEtaSeconds ?? null,
          memRunwayPercent: q.memRunwayPercent ?? null,
          memPsiFullAvg10Percent: q.memPsiFullAvg10Percent ?? null,
          buildSliceJobCount: q.buildSliceJobCount ?? 0,
          prochot,
          diskRootFreePercent: q.diskRootFreePercent ?? null,
        },
      },
    ];

    return { items, panels };
  }

  return { id, interval, poll };
}

function alertItem(source: string, ts: string, key: string, title: string, detail: string): Item {
  return {
    // Stable per-condition id (no ts) so a persisting alert updates in place across
    // polls instead of spawning a new item each cycle — state is additive-by-id.
    id: `${source}:${key}`,
    source,
    severity: "act",
    kind: "alert",
    title,
    detail,
    ts,
    actions: [],
  };
}
