// src/observability/api-metrics.ts
var MINUTE_MS = 6e4;
var MAX_MINUTES = 60;
var MAX_LATENCY_SAMPLES_PER_BUCKET = 500;
function newBucket() {
  return {
    count: 0,
    status_2xx: 0,
    status_3xx: 0,
    status_4xx: 0,
    status_5xx: 0,
    latencies: [],
    latency_sum: 0
  };
}
var store = /* @__PURE__ */ new Map();
function currentMinute() {
  return Math.floor(Date.now() / MINUTE_MS);
}
function pruneOldMinutes(now) {
  const cutoff = now - MAX_MINUTES;
  for (const key of store.keys()) {
    if (key < cutoff) store.delete(key);
  }
}
function normalizeRoute(url) {
  let path;
  try {
    path = new URL(url, "http://x").pathname;
  } catch {
    path = url.split("?")[0];
  }
  const parts = path.split("/");
  for (let i = 0; i < parts.length; i++) {
    const p = parts[i];
    if (!p) continue;
    if (/^[a-z]+_[0-9A-HJKMNP-TV-Z]{20,}$/i.test(p)) {
      parts[i] = ":id";
      continue;
    }
    if (/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(p)) {
      parts[i] = ":id";
      continue;
    }
    if (/^\d+$/.test(p) && p.length >= 2) {
      parts[i] = ":id";
      continue;
    }
    if (/^[0-9a-f]{16,}$/i.test(p)) {
      parts[i] = ":id";
      continue;
    }
  }
  return parts.join("/") || "/";
}
function recordApiRequest(route, latencyMs, status) {
  const now = currentMinute();
  if (Math.random() < 0.01) pruneOldMinutes(now);
  let minute = store.get(now);
  if (!minute) {
    minute = /* @__PURE__ */ new Map();
    store.set(now, minute);
  }
  let bucket = minute.get(route);
  if (!bucket) {
    bucket = newBucket();
    minute.set(route, bucket);
  }
  bucket.count++;
  bucket.latency_sum += latencyMs;
  if (bucket.latencies.length < MAX_LATENCY_SAMPLES_PER_BUCKET) {
    bucket.latencies.push(latencyMs);
  } else {
    const idx = Math.floor(Math.random() * MAX_LATENCY_SAMPLES_PER_BUCKET);
    bucket.latencies[idx] = latencyMs;
  }
  if (status >= 500) bucket.status_5xx++;
  else if (status >= 400) bucket.status_4xx++;
  else if (status >= 300) bucket.status_3xx++;
  else if (status >= 200) bucket.status_2xx++;
}
function percentile(sorted, p) {
  if (sorted.length === 0) return null;
  const idx = Math.min(sorted.length - 1, Math.floor(p / 100 * sorted.length));
  return sorted[idx];
}
function snapshotApiMetrics(opts = {}) {
  const windowMs = opts.windowMs ?? MAX_MINUTES * MINUTE_MS;
  const windowMinutes = Math.ceil(windowMs / MINUTE_MS);
  const now = currentMinute();
  const from = now - windowMinutes + 1;
  pruneOldMinutes(now);
  const routeAgg = /* @__PURE__ */ new Map();
  const timeline = [];
  for (let k = from; k <= now; k++) {
    const minute = store.get(k);
    let minuteCount = 0;
    let minuteErrors = 0;
    if (minute) {
      for (const [route, bucket] of minute) {
        let agg = routeAgg.get(route);
        if (!agg) {
          agg = newBucket();
          routeAgg.set(route, agg);
        }
        agg.count += bucket.count;
        agg.latency_sum += bucket.latency_sum;
        agg.status_2xx += bucket.status_2xx;
        agg.status_3xx += bucket.status_3xx;
        agg.status_4xx += bucket.status_4xx;
        agg.status_5xx += bucket.status_5xx;
        for (const l of bucket.latencies) {
          if (agg.latencies.length < 3e4) agg.latencies.push(l);
        }
        minuteCount += bucket.count;
        minuteErrors += bucket.status_5xx;
      }
    }
    timeline.push({
      minute_ms: k * MINUTE_MS,
      count: minuteCount,
      rps: minuteCount / 60,
      error_count: minuteErrors
    });
  }
  const routes = [];
  let totalCount = 0;
  let total2xx = 0;
  let total3xx = 0;
  let total4xx = 0;
  let total5xx = 0;
  const allLatencies = [];
  for (const [route, agg] of routeAgg) {
    const sorted = [...agg.latencies].sort((a, b) => a - b);
    const mean = agg.count > 0 ? agg.latency_sum / agg.count : null;
    const errors = agg.status_5xx;
    routes.push({
      route,
      count: agg.count,
      rps: agg.count / (windowMinutes * 60),
      p50_ms: percentile(sorted, 50),
      p95_ms: percentile(sorted, 95),
      p99_ms: percentile(sorted, 99),
      mean_ms: mean,
      status_2xx: agg.status_2xx,
      status_3xx: agg.status_3xx,
      status_4xx: agg.status_4xx,
      status_5xx: agg.status_5xx,
      error_rate: agg.count > 0 ? errors / agg.count : 0
    });
    totalCount += agg.count;
    total2xx += agg.status_2xx;
    total3xx += agg.status_3xx;
    total4xx += agg.status_4xx;
    total5xx += agg.status_5xx;
    for (const l of agg.latencies) {
      if (allLatencies.length < 3e4) allLatencies.push(l);
    }
  }
  routes.sort((a, b) => b.count - a.count);
  const totalSorted = allLatencies.sort((a, b) => a - b);
  const totalErrors = total5xx;
  return {
    window_ms: windowMs,
    window_minutes: windowMinutes,
    now_ms: Date.now(),
    totals: {
      count: totalCount,
      rps: totalCount / (windowMinutes * 60),
      p50_ms: percentile(totalSorted, 50),
      p95_ms: percentile(totalSorted, 95),
      p99_ms: percentile(totalSorted, 99),
      status_2xx: total2xx,
      status_3xx: total3xx,
      status_4xx: total4xx,
      status_5xx: total5xx,
      error_rate: totalCount > 0 ? totalErrors / totalCount : 0
    },
    routes,
    timeline
  };
}
function resetApiMetrics() {
  store.clear();
}

export {
  normalizeRoute,
  recordApiRequest,
  snapshotApiMetrics,
  resetApiMetrics
};
