// Tiny in-process metrics. Thread-safe enough for a 5-user MVP — counters
// and sums are only ever incremented under a single-threaded Node event
// loop, no atomic ops required.

export type RouteMetric = {
  route: string;
  method: string;
  status: number;
  durationMs: number;
  timestamp: number;
};

type Aggregate = {
  count: number;
  statusBuckets: Map<number, number>;
  sumMs: number;
  maxMs: number;
  p95Slots: number[]; // simple streaming reservoir for a coarse p95
};

const RESERVOIR_SIZE = 200;

const createAggregate = (): Aggregate => ({
  count: 0,
  statusBuckets: new Map(),
  sumMs: 0,
  maxMs: 0,
  p95Slots: [],
});

const metricsState = {
  startedAt: Date.now(),
  aggregates: new Map<string, Aggregate>(),
  // Last N events for the /metrics JSON inspector. Bounded to avoid leaks.
  recent: [] as RouteMetric[],
  recentLimit: 50,
  errorCounts: new Map<string, number>(),
};

const keyFor = (route: string, method: string) => `${method.toUpperCase()} ${route}`;

export const observeHttp = (metric: RouteMetric) => {
  const key = keyFor(metric.route, metric.method);
  let agg = metricsState.aggregates.get(key);
  if (!agg) {
    agg = createAggregate();
    metricsState.aggregates.set(key, agg);
  }
  agg.count += 1;
  agg.sumMs += metric.durationMs;
  if (metric.durationMs > agg.maxMs) agg.maxMs = metric.durationMs;
  const statusBucket = Math.floor(metric.status / 100) * 100;
  agg.statusBuckets.set(statusBucket, (agg.statusBuckets.get(statusBucket) ?? 0) + 1);

  if (agg.p95Slots.length < RESERVOIR_SIZE) {
    agg.p95Slots.push(metric.durationMs);
  } else {
    // Cheap replacement: evict the current max so the reservoir tracks the slowest N.
    let maxIdx = 0;
    for (let i = 1; i < agg.p95Slots.length; i++) {
      if (agg.p95Slots[i] > agg.p95Slots[maxIdx]) maxIdx = i;
    }
    if (metric.durationMs < agg.p95Slots[maxIdx]) {
      agg.p95Slots[maxIdx] = metric.durationMs;
    }
  }

  if (metric.status >= 500) {
    metricsState.errorCounts.set(key, (metricsState.errorCounts.get(key) ?? 0) + 1);
  }

  metricsState.recent.push(metric);
  if (metricsState.recent.length > metricsState.recentLimit) {
    metricsState.recent.splice(0, metricsState.recent.length - metricsState.recentLimit);
  }
};

const percentile = (sorted: number[], p: number): number => {
  if (sorted.length === 0) return 0;
  const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));
  return sorted[idx];
};

export const snapshotMetrics = () => {
  const routes: Array<{
    route: string;
    method: string;
    count: number;
    avg_ms: number;
    p95_ms: number;
    max_ms: number;
    status_buckets: Record<string, number>;
  }> = [];

  for (const [key, agg] of metricsState.aggregates.entries()) {
    const [method, ...rest] = key.split(" ");
    const route = rest.join(" ");
    const sorted = [...agg.p95Slots].sort((a, b) => a - b);
    routes.push({
      route,
      method,
      count: agg.count,
      avg_ms: agg.count === 0 ? 0 : Math.round(agg.sumMs / agg.count),
      p95_ms: Math.round(percentile(sorted, 95)),
      max_ms: agg.maxMs,
      status_buckets: Object.fromEntries(
        [...agg.statusBuckets.entries()].map(([k, v]) => [`${k}_${k + 99}`, v]),
      ),
    });
  }

  return {
    started_at: new Date(metricsState.startedAt).toISOString(),
    uptime_seconds: Math.round((Date.now() - metricsState.startedAt) / 1000),
    totals: {
      requests: [...metricsState.aggregates.values()].reduce((sum, a) => sum + a.count, 0),
      errors_5xx: [...metricsState.errorCounts.values()].reduce((sum, n) => sum + n, 0),
    },
    routes: routes.sort((a, b) => b.count - a.count),
    recent: [...metricsState.recent].reverse(),
  };
};

// Tiny Prometheus-style exposition. Stable enough for a scraper.
export const renderPrometheusText = (): string => {
  const lines: string[] = [];
  lines.push("# HELP sic_uptime_seconds Seconds since the API process started");
  lines.push("# TYPE sic_uptime_seconds gauge");
  lines.push(`sic_uptime_seconds ${Math.round((Date.now() - metricsState.startedAt) / 1000)}`);
  lines.push("");

  lines.push("# HELP sic_http_requests_total Total HTTP requests, labelled by route, method, status");
  lines.push("# TYPE sic_http_requests_total counter");
  for (const [key, agg] of metricsState.aggregates.entries()) {
    const [method, ...rest] = key.split(" ");
    const route = rest.join(" ");
    for (const [bucket, count] of agg.statusBuckets.entries()) {
      const statusClass = `${bucket}_${bucket + 99}`;
      lines.push(
        `sic_http_requests_total{route="${route}",method="${method}",status_class="${statusClass}"} ${count}`,
      );
    }
  }
  lines.push("");

  lines.push("# HELP sic_http_request_duration_ms Request duration in ms");
  lines.push("# TYPE sic_http_request_duration_ms summary");
  for (const [key, agg] of metricsState.aggregates.entries()) {
    const [method, ...rest] = key.split(" ");
    const route = rest.join(" ");
    const sorted = [...agg.p95Slots].sort((a, b) => a - b);
    const avg = agg.count === 0 ? 0 : Math.round(agg.sumMs / agg.count);
    lines.push(
      `sic_http_request_duration_ms{route="${route}",method="${method}",quantile="0.95"} ${percentile(sorted, 95)}`,
    );
    lines.push(
      `sic_http_request_duration_ms_sum{route="${route}",method="${method}"} ${agg.sumMs}`,
    );
    lines.push(
      `sic_http_request_duration_ms_count{route="${route}",method="${method}"} ${agg.count}`,
    );
    lines.push(
      `sic_http_request_duration_ms_max{route="${route}",method="${method}"} ${agg.maxMs}`,
    );
    lines.push(
      `sic_http_request_duration_ms_avg{route="${route}",method="${method}"} ${avg}`,
    );
  }

  return `${lines.join("\n")}\n`;
};

export const __resetMetricsForTests = () => {
  metricsState.startedAt = Date.now();
  metricsState.aggregates.clear();
  metricsState.recent.length = 0;
  metricsState.errorCounts.clear();
};