import type {
  DashboardDocument,
  DashboardModule,
  MetricValue,
  ServiceEntry,
  ServiceGroup,
  Severity,
  StatusItem,
  StatusStrip,
  TelemetryCard,
} from "@dimensionlab/dashboard-model";

export interface DatasourceResolutionOptions {
  fetch?: DatasourceFetch;
  prometheusBaseUrl?: string;
  prometheusRangeSeconds?: number;
  prometheusStepSeconds?: number;
  requestTimeoutMs?: number;
}

export async function resolveDashboardDatasources(
  document: DashboardDocument,
  options: DatasourceResolutionOptions = {},
): Promise<DashboardDocument> {
  const context = datasourceContext(options);
  const telemetry = await Promise.all(
    document.telemetry.map((card) => resolveTelemetryCard(card, context)),
  );
  const serviceGroups = await Promise.all(
    document.serviceGroups.map((group) => resolveServiceGroup(group, context)),
  );
  const modules = await resolveModules(document.modules || [], serviceGroups, context);
  const statusStrips = await resolveStatusStrips(
    document.statusStrips,
    document.metadata.refreshIntervalSeconds,
    serviceGroups,
    context,
  );

  return {
    ...structuredClone(document),
    telemetry,
    serviceGroups,
    modules,
    statusStrips,
  };
}

interface DatasourceContext {
  fetch: DatasourceFetch;
  prometheusBaseUrl: string;
  prometheusRangeSeconds: number;
  prometheusStepSeconds: number;
  requestTimeoutMs: number;
}

type DatasourceFetch = (input: string, init?: RequestInit) => Promise<Response>;

interface PrometheusVectorResult {
  metric?: Record<string, string>;
  value?: [number, string];
}

interface PrometheusMatrixResult {
  metric?: Record<string, string>;
  values?: Array<[number, string]>;
}

function datasourceContext(options: DatasourceResolutionOptions): DatasourceContext {
  return {
    fetch: options.fetch || globalThis.fetch,
    prometheusBaseUrl:
      options.prometheusBaseUrl ||
      process.env.PROMETHEUS_BASE_URL ||
      "https://prometheus.dimensionlab.net",
    prometheusRangeSeconds: options.prometheusRangeSeconds || 60 * 60,
    prometheusStepSeconds: options.prometheusStepSeconds || 120,
    requestTimeoutMs: options.requestTimeoutMs || 2_500,
  };
}

async function resolveTelemetryCard(
  card: TelemetryCard,
  context: DatasourceContext,
): Promise<TelemetryCard> {
  if (card.datasource?.type !== "external" || card.datasource.adapter !== "prometheus") {
    return structuredClone(card);
  }

  try {
    const [instant, range] = await Promise.all([
      prometheusQuery(card.datasource.reference, context),
      prometheusRangeQuery(card.datasource.reference, context),
    ]);
    const current = pickMaxVectorResult(instant);
    const currentValue = Number(current?.value?.[1]);
    const sparkline = matrixPoints(range);

    if (!Number.isFinite(currentValue)) return markStale(card, "no telemetry data");

    return {
      ...structuredClone(card),
      value: metricValueWithLiveNumber(card.value, currentValue),
      severity: severityForValue(currentValue, card.thresholds),
      detail: prometheusMetricDetail(current?.metric || {}, card.detail),
      sparkline: sparkline.length ? sparkline : card.sparkline,
    };
  } catch {
    return markStale(card, card.detail || "telemetry unavailable");
  }
}

async function resolveServiceGroup(
  group: ServiceGroup,
  context: DatasourceContext,
): Promise<ServiceGroup> {
  return {
    ...structuredClone(group),
    services: await Promise.all(
      group.services.map((service) => resolveService(service, context)),
    ),
  };
}

async function resolveService(
  service: ServiceEntry,
  context: DatasourceContext,
): Promise<ServiceEntry> {
  const datasource = service.datasource;
  if (datasource?.type !== "external") return structuredClone(service);

  if (datasource.adapter === "http-status") {
    return resolveHttpStatusService(service, datasource.reference, context);
  }

  if (datasource.adapter === "prometheus") {
    return resolvePrometheusService(service, datasource.reference, context);
  }

  return structuredClone(service);
}

async function resolveHttpStatusService(
  service: ServiceEntry,
  reference: string,
  context: DatasourceContext,
): Promise<ServiceEntry> {
  const request = parseHttpStatusReference(reference);
  if (!request) return structuredClone(service);

  try {
    const startedAt = Date.now();
    const response = await fetchWithTimeout(
      context.fetch,
      request.url,
      {
        cache: "no-store",
        method: request.method,
      },
      context.requestTimeoutMs,
    );
    const elapsedMs = Math.max(0, Math.round(Date.now() - startedAt));
    const badge = await uptimeBadge(response);
    if (badge) {
      const ping = badge.ping;
      const status = badge.status || "UNKNOWN";
      return {
        ...structuredClone(service),
        severity: uptimeBadgeSeverity(status),
        detail: Number.isFinite(ping)
          ? `${Math.round(ping as number)} ms`
          : status.toLowerCase(),
      };
    }

    return {
      ...structuredClone(service),
      severity: response.ok ? "ok" : response.status >= 500 ? "danger" : "warning",
      detail: response.ok ? `${elapsedMs} ms` : `HTTP ${response.status}`,
    };
  } catch {
    return {
      ...structuredClone(service),
      severity: "unavailable",
      detail: "unavailable",
    };
  }
}

async function resolvePrometheusService(
  service: ServiceEntry,
  reference: string,
  context: DatasourceContext,
): Promise<ServiceEntry> {
  try {
    const result = pickMaxVectorResult(await prometheusQuery(reference, context));
    const value = Number(result?.value?.[1]);
    const ok = Number.isFinite(value) && value > 0;

    return {
      ...structuredClone(service),
      severity: ok ? "ok" : "unavailable",
      detail: ok ? "up" : "down",
    };
  } catch {
    return {
      ...structuredClone(service),
      severity: "unavailable",
      detail: "unavailable",
    };
  }
}

async function resolveModules(
  modules: DashboardModule[],
  serviceGroups: ServiceGroup[],
  context: DatasourceContext,
): Promise<DashboardModule[]> {
  const resolved = await Promise.all(
    modules.map((module) => resolveModule(module, context)),
  );
  return resolved.map((module) =>
    module.id === "runtime-health-summary"
      ? runtimeHealthSummary(module, serviceGroups)
      : module,
  );
}

async function resolveModule(
  module: DashboardModule,
  context: DatasourceContext,
): Promise<DashboardModule> {
  if (module.datasource?.type !== "external" || module.datasource.adapter !== "weather") {
    return structuredClone(module);
  }

  try {
    const weather = await requestJson<OpenMeteoResponse>(
      context.fetch,
      openMeteoUrl(module.datasource.reference),
      context.requestTimeoutMs,
    );
    const current = weather.current;
    const temperature = Number(current?.temperature_2m);
    if (!Number.isFinite(temperature)) {
      return {
        ...structuredClone(module),
        severity: "stale",
        detail: "weather unavailable",
      };
    }

    const apparent = Number(current?.apparent_temperature);
    const wind = Number(current?.wind_speed_10m);
    const condition = weatherCondition(Number(current?.weather_code));

    return {
      ...structuredClone(module),
      value: `${temperature.toFixed(1)} C`,
      detail: [
        condition,
        Number.isFinite(apparent) ? `feels ${apparent.toFixed(1)} C` : "",
        Number.isFinite(wind) ? `wind ${Math.round(wind)} km/h` : "",
      ].filter(Boolean).join(" - "),
      severity: "ok",
    };
  } catch {
    return {
      ...structuredClone(module),
      severity: "stale",
      detail: "weather unavailable",
    };
  }
}

function runtimeHealthSummary(
  module: DashboardModule,
  serviceGroups: ServiceGroup[],
): DashboardModule {
  const services = serviceGroups.flatMap((group) => group.services);
  const down = services.filter((service) =>
    service.severity === "danger" || service.severity === "unavailable"
  ).length;
  const warning = services.filter((service) => service.severity === "warning").length;
  const ok = services.filter((service) => service.severity === "ok").length;

  if (down > 0) {
    return {
      ...structuredClone(module),
      value: `${down} service${down === 1 ? "" : "s"} down`,
      detail: `${warning} warning${warning === 1 ? "" : "s"} - ${ok} service${ok === 1 ? "" : "s"} ok`,
      severity: "danger",
    };
  }

  if (warning > 0) {
    return {
      ...structuredClone(module),
      value: `${warning} service${warning === 1 ? "" : "s"} warning`,
      detail: `${ok} service${ok === 1 ? "" : "s"} ok`,
      severity: "warning",
    };
  }

  return {
    ...structuredClone(module),
    value: "all systems operational",
    detail: `${ok} service${ok === 1 ? "" : "s"} ok`,
    severity: "ok",
  };
}

async function resolveStatusStrips(
  strips: StatusStrip[],
  refreshIntervalSeconds: number | undefined,
  serviceGroups: ServiceGroup[],
  context: DatasourceContext,
): Promise<StatusStrip[]> {
  const [uptime, loadAverage] = await Promise.all([
    prometheusScalar(
      'time() - node_boot_time_seconds{job="node",host="linux-infra"}',
      context,
    ).catch(() => null),
    prometheusLoadAverage(context).catch(() => null),
  ]);
  const health = serviceHealthSummary(serviceGroups);

  return strips.map((strip) => ({
    ...structuredClone(strip),
    items: strip.items.map((item) =>
      resolveStatusItem(item, {
        health,
        loadAverage,
        refreshIntervalSeconds,
        uptime,
      }),
    ),
  }));
}

function resolveStatusItem(
  item: StatusItem,
  values: {
    health: { severity: Severity; value: string };
    loadAverage: string | null;
    refreshIntervalSeconds?: number;
    uptime: number | null;
  },
): StatusItem {
  if (item.id === "system-status") {
    return {
      ...structuredClone(item),
      value: values.health.value,
      severity: values.health.severity,
    };
  }

  if (item.id === "last-sync") {
    return {
      ...structuredClone(item),
      value: "just now",
      severity: "ok",
    };
  }

  if (item.id === "uptime" && values.uptime !== null) {
    return {
      ...structuredClone(item),
      value: formatDuration(values.uptime),
      severity: "ok",
    };
  }

  if (item.id === "load-avg" && values.loadAverage) {
    return {
      ...structuredClone(item),
      value: values.loadAverage,
      severity: "neutral",
    };
  }

  if (item.id === "auto-refresh" && values.refreshIntervalSeconds) {
    return {
      ...structuredClone(item),
      value: `${values.refreshIntervalSeconds}s`,
      severity: "neutral",
    };
  }

  return structuredClone(item);
}

function serviceHealthSummary(serviceGroups: ServiceGroup[]): {
  severity: Severity;
  value: string;
} {
  const services = serviceGroups.flatMap((group) => group.services);
  const down = services.filter((service) =>
    service.severity === "danger" || service.severity === "unavailable"
  ).length;
  const warning = services.filter((service) => service.severity === "warning").length;

  if (down > 0) {
    return {
      severity: "danger",
      value: `${down} service${down === 1 ? "" : "s"} down`,
    };
  }

  if (warning > 0) {
    return {
      severity: "warning",
      value: `${warning} service${warning === 1 ? "" : "s"} warning`,
    };
  }

  return {
    severity: "ok",
    value: "All systems operational",
  };
}

async function prometheusQuery(
  query: string,
  context: DatasourceContext,
): Promise<PrometheusVectorResult[]> {
  const url = prometheusApiUrl(context.prometheusBaseUrl, "/api/v1/query", {
    query,
  });
  const payload = await requestJson<PrometheusResponse<PrometheusVectorResult>>(
    context.fetch,
    url,
    context.requestTimeoutMs,
  );
  return payload.status === "success" ? payload.data.result || [] : [];
}

async function prometheusRangeQuery(
  query: string,
  context: DatasourceContext,
): Promise<PrometheusMatrixResult[]> {
  const end = Math.floor(Date.now() / 1000);
  const start = end - context.prometheusRangeSeconds;
  const url = prometheusApiUrl(context.prometheusBaseUrl, "/api/v1/query_range", {
    query,
    start: String(start),
    end: String(end),
    step: String(context.prometheusStepSeconds),
  });
  const payload = await requestJson<PrometheusResponse<PrometheusMatrixResult>>(
    context.fetch,
    url,
    context.requestTimeoutMs,
  );
  return payload.status === "success" ? payload.data.result || [] : [];
}

async function prometheusScalar(
  query: string,
  context: DatasourceContext,
): Promise<number | null> {
  const result = pickMaxVectorResult(await prometheusQuery(query, context));
  const value = Number(result?.value?.[1]);
  return Number.isFinite(value) ? value : null;
}

async function prometheusLoadAverage(context: DatasourceContext): Promise<string | null> {
  const [one, five, fifteen] = await Promise.all([
    prometheusScalar('node_load1{job="node",host="linux-infra"}', context),
    prometheusScalar('node_load5{job="node",host="linux-infra"}', context),
    prometheusScalar('node_load15{job="node",host="linux-infra"}', context),
  ]);

  if (one === null || five === null || fifteen === null) return null;
  return [one, five, fifteen].map((value) => value.toFixed(2)).join(" ");
}

async function requestJson<T>(
  fetch: DatasourceFetch,
  url: string,
  timeoutMs: number,
): Promise<T> {
  const response = await fetchWithTimeout(
    fetch,
    url,
    { cache: "no-store" },
    timeoutMs,
  );
  if (!response.ok) throw new Error(`request failed: ${response.status}`);
  return response.json() as Promise<T>;
}

async function fetchWithTimeout(
  fetch: DatasourceFetch,
  url: string,
  init: RequestInit,
  timeoutMs: number,
): Promise<Response> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetch(url, {
      ...init,
      signal: controller.signal,
    });
  } finally {
    clearTimeout(timeout);
  }
}

function prometheusApiUrl(
  baseUrl: string,
  pathname: string,
  params: Record<string, string>,
): string {
  const url = new URL(pathname, ensureTrailingSlash(baseUrl));
  Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
  return url.toString();
}

function ensureTrailingSlash(value: string): string {
  return value.endsWith("/") ? value : `${value}/`;
}

function pickMaxVectorResult(
  results: PrometheusVectorResult[],
): PrometheusVectorResult | null {
  return results.reduce<PrometheusVectorResult | null>((winner, item) => {
    if (!winner) return item;
    return Number(item.value?.[1]) > Number(winner.value?.[1]) ? item : winner;
  }, null);
}

function matrixPoints(results: PrometheusMatrixResult[]): number[] {
  const byTimestamp = new Map<number, number>();

  for (const series of results) {
    for (const [timestamp, value] of series.values || []) {
      const numeric = Number(value);
      if (!Number.isFinite(numeric)) continue;
      const previous = byTimestamp.get(timestamp);
      if (previous === undefined || numeric > previous) {
        byTimestamp.set(timestamp, numeric);
      }
    }
  }

  return [...byTimestamp.entries()]
    .sort(([left], [right]) => left - right)
    .map(([, value]) => value);
}

function metricValueWithLiveNumber(value: MetricValue, nextValue: number): MetricValue {
  if (value.kind === "text") return value;
  return {
    ...value,
    value: value.kind === "percent" ? clamp(nextValue, 0, 100) : Math.max(0, nextValue),
  };
}

function severityForValue(
  value: number,
  thresholds: TelemetryCard["thresholds"],
): Severity {
  if (thresholds?.danger !== undefined && value >= thresholds.danger) return "danger";
  if (thresholds?.warning !== undefined && value >= thresholds.warning) return "warning";
  return "ok";
}

function prometheusMetricDetail(
  metric: Record<string, string>,
  fallback = "",
): string {
  const host = metric.host || metric.instance || metric.job;
  const detail = metric.mountpoint || metric.name || metric.container || metric.id;
  const parts = [host, detail].filter(Boolean);
  if (parts.length) return parts.join(" ");
  return fallback.replace(/^fallback\s*-\s*/i, "") || "telemetry";
}

function markStale(card: TelemetryCard, detail: string): TelemetryCard {
  return {
    ...structuredClone(card),
    severity: "stale",
    detail,
  };
}

function parseHttpStatusReference(reference: string): { method: string; url: string } | null {
  const match = /^(GET|HEAD|POST)\s+(.+)$/i.exec(reference.trim());
  if (!match) return null;
  return { method: match[1].toUpperCase(), url: match[2] };
}

interface PrometheusResponse<T> {
  status: string;
  data: {
    result?: T[];
  };
}

interface OpenMeteoResponse {
  current?: {
    apparent_temperature?: number;
    temperature_2m?: number;
    weather_code?: number;
    wind_speed_10m?: number;
  };
}

interface UptimeBadgeResponse {
  status?: string;
  ping?: number;
}

async function uptimeBadge(response: Response): Promise<UptimeBadgeResponse | null> {
  const contentType = response.headers.get("content-type") || "";
  if (!contentType.includes("json")) return null;

  try {
    const payload = await response.clone().json() as UptimeBadgeResponse;
    return typeof payload.status === "string" ? payload : null;
  } catch {
    return null;
  }
}

function uptimeBadgeSeverity(status = ""): Severity {
  const normalized = status.toUpperCase();
  if (normalized === "UP") return "ok";
  if (normalized === "PENDING" || normalized === "MAINTENANCE") return "warning";
  return "unavailable";
}

function openMeteoUrl(reference: string): string {
  const params = new URLSearchParams();
  const serialized = reference.startsWith("open-meteo:")
    ? reference.slice("open-meteo:".length)
    : reference;

  new URLSearchParams(serialized).forEach((value, key) => {
    params.set(key, value);
  });
  params.set(
    "current",
    "temperature_2m,apparent_temperature,weather_code,wind_speed_10m",
  );
  params.set("timezone", "Europe/Amsterdam");

  return `https://api.open-meteo.com/v1/forecast?${params.toString()}`;
}

function weatherCondition(code: number): string {
  if (code === 0) return "Clear";
  if ([1, 2].includes(code)) return "Partly cloudy";
  if (code === 3) return "Cloudy";
  if ([45, 48].includes(code)) return "Fog";
  if (code >= 51 && code <= 67) return "Rain";
  if (code >= 71 && code <= 77) return "Snow";
  if (code >= 80 && code <= 82) return "Showers";
  if (code >= 95) return "Thunderstorm";
  return "Mixed";
}

function clamp(value: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, value));
}

function formatDuration(totalSeconds: number): string {
  const seconds = Math.max(0, Math.floor(totalSeconds));
  const days = Math.floor(seconds / 86_400);
  const hours = Math.floor((seconds % 86_400) / 3_600);
  const minutes = Math.floor((seconds % 3_600) / 60);
  return `${days}d ${hours}h ${minutes}m`;
}
