import type { Context, Middleware } from "@wrnexus/core";
import { MetricsRegistry, type MetricPoint } from "./metrics.ts";

export interface MetricsMiddlewareOptions {
  registry?: MetricsRegistry;
  routeLabel?: (ctx: Context) => string;
  includePath?: boolean;
}

export function metricsMiddleware(options: MetricsMiddlewareOptions = {}): Middleware {
  const registry = options.registry ?? defaultMetrics;
  let active = 0;
  return async (ctx, next) => {
    const started = performance.now();
    active++;
    registry.gauge("http.server.active_requests", active);
    try {
      const response = await next();
      const route =
        options.routeLabel?.(ctx) ?? (options.includePath ? ctx.url.pathname : "unknown");
      const labels = { method: ctx.req.method, status: response.status, route };
      registry.increment("http.server.requests", 1, labels);
      registry.observe("http.server.duration_ms", performance.now() - started, labels);
      return response;
    } catch (error) {
      registry.increment("http.server.errors", 1, { method: ctx.req.method });
      throw error;
    } finally {
      active--;
      registry.gauge("http.server.active_requests", active);
    }
  };
}

export interface WebVitalRecord {
  name: "LCP" | "INP" | "CLS" | "FCP" | "TTFB";
  value: number;
  rating?: "good" | "needs-improvement" | "poor";
  route?: string;
  navigationType?: string;
}

export interface WebVitalsHandlerOptions {
  registry?: MetricsRegistry;
  onRecord?: (record: WebVitalRecord, request: Request) => void | Promise<void>;
  maxBodyBytes?: number;
}

const VITAL_NAMES = new Set(["LCP", "INP", "CLS", "FCP", "TTFB"]);

export function createWebVitalsHandler(options: WebVitalsHandlerOptions = {}) {
  const registry = options.registry ?? defaultMetrics;
  const maxBodyBytes = options.maxBodyBytes ?? 8 * 1024;
  return async (request: Request): Promise<Response> => {
    if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
    if (request.headers.get("sec-fetch-site") === "cross-site") {
      return new Response("Forbidden", { status: 403 });
    }
    const origin = request.headers.get("origin");
    if (origin) {
      try {
        if (new URL(origin).origin !== new URL(request.url).origin) {
          return new Response("Forbidden", { status: 403 });
        }
      } catch {
        return new Response("Forbidden", { status: 403 });
      }
    }
    const length = Number(request.headers.get("content-length") ?? "0");
    if (length > maxBodyBytes) return new Response("Payload Too Large", { status: 413 });
    let body: unknown;
    try {
      const text = await request.text();
      if (new TextEncoder().encode(text).byteLength > maxBodyBytes) {
        return new Response("Payload Too Large", { status: 413 });
      }
      body = JSON.parse(text);
    } catch {
      return new Response("Invalid JSON", { status: 400 });
    }
    if (!body || typeof body !== "object") return new Response("Invalid metric", { status: 400 });
    const raw = body as Record<string, unknown>;
    if (
      !VITAL_NAMES.has(String(raw.name)) ||
      typeof raw.value !== "number" ||
      !Number.isFinite(raw.value)
    ) {
      return new Response("Invalid metric", { status: 400 });
    }
    const record: WebVitalRecord = {
      name: raw.name as WebVitalRecord["name"],
      value: raw.value,
      ...(typeof raw.rating === "string" ? { rating: raw.rating as WebVitalRecord["rating"] } : {}),
      ...(typeof raw.route === "string" ? { route: raw.route.slice(0, 256) } : {}),
      ...(typeof raw.navigationType === "string"
        ? { navigationType: raw.navigationType.slice(0, 64) }
        : {}),
    };
    registry.observe(`web.vitals.${record.name.toLowerCase()}`, record.value, {
      rating: record.rating ?? "unknown",
      route: record.route ?? "unknown",
    });
    await options.onRecord?.(record, request);
    return new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
  };
}

export interface MetricExporter {
  export(points: readonly MetricPoint[]): Promise<void> | void;
}

export function createHttpMetricExporter(
  endpoint: string,
  options: { headers?: HeadersInit; fetch?: typeof fetch } = {},
): MetricExporter {
  const send = options.fetch ?? fetch;
  return {
    async export(points) {
      const response = await send(endpoint, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          ...Object.fromEntries(new Headers(options.headers)),
        },
        body: JSON.stringify({ resourceMetrics: points }),
      });
      if (!response.ok) throw new Error(`Metric export failed with ${response.status}.`);
    },
  };
}

export function createOtlpMetricExporter(
  endpoint: string,
  options: { headers?: HeadersInit; fetch?: typeof fetch; serviceName?: string } = {},
): MetricExporter {
  const send = options.fetch ?? fetch;
  return {
    async export(points) {
      if (!points.length) return;
      const metrics = points.map((point) => {
        const attributes = Object.entries(point.labels).map(([key, value]) => ({
          key,
          value:
            typeof value === "boolean"
              ? { boolValue: value }
              : typeof value === "number"
                ? { doubleValue: value }
                : { stringValue: value },
        }));
        const timeUnixNano = String(BigInt(Math.trunc(point.timestamp)) * 1_000_000n);
        if (point.type === "histogram") {
          return {
            name: point.name,
            histogram: {
              aggregationTemporality: 2,
              dataPoints: [
                {
                  attributes,
                  timeUnixNano,
                  count: String(point.count ?? 0),
                  sum: point.sum ?? 0,
                  min: point.min,
                  max: point.max,
                  bucketCounts: [String(point.count ?? 0)],
                  explicitBounds: [],
                },
              ],
            },
          };
        }
        const kind = point.type === "counter" ? "sum" : "gauge";
        return {
          name: point.name,
          [kind]: {
            ...(kind === "sum" ? { aggregationTemporality: 2, isMonotonic: true } : {}),
            dataPoints: [{ attributes, timeUnixNano, asDouble: point.value }],
          },
        };
      });
      const response = await send(endpoint, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          ...Object.fromEntries(new Headers(options.headers)),
        },
        body: JSON.stringify({
          resourceMetrics: [
            {
              resource: {
                attributes: [
                  { key: "service.name", value: { stringValue: options.serviceName ?? "wrnexus" } },
                ],
              },
              scopeMetrics: [
                {
                  scope: { name: "@wrnexus/observability", version: "0.8.0" },
                  metrics,
                },
              ],
            },
          ],
        }),
      });
      if (!response.ok) throw new Error(`OTLP metric export failed with ${response.status}.`);
    },
  };
}

export const defaultMetrics = new MetricsRegistry();
