import {
  requireFeature
} from "./chunk-2N2KL4KM.js";
import {
  requireGlobalAdmin,
  tenantFilter
} from "./chunk-3F6VNSRK.js";
import {
  jsonOk,
  routeWrap
} from "./chunk-KKJ5D3II.js";
import {
  snapshotApiMetrics
} from "./chunk-D2XITRN6.js";
import {
  getDb,
  init_client
} from "./chunk-TLEKKGH7.js";
import {
  badRequest
} from "./chunk-EZYKRG4W.js";

// src/handlers/metrics.ts
init_client();
function zeroTotals() {
  return {
    session_count: 0,
    turn_count: 0,
    tool_call_count: 0,
    error_count: 0,
    active_seconds: 0,
    input_tokens: 0,
    output_tokens: 0,
    cache_read_input_tokens: 0,
    cache_creation_input_tokens: 0,
    cost_usd: 0
  };
}
function parseGroupBy(raw) {
  if (!raw) return "none";
  if (raw === "agent" || raw === "environment" || raw === "backend" || raw === "hour" || raw === "day" || raw === "api_key") {
    return raw;
  }
  return "none";
}
function groupByExpr(g) {
  switch (g) {
    case "agent":
      return "s.agent_id";
    case "environment":
      return "s.environment_id";
    case "backend":
      return "av.backend";
    case "api_key":
      return "COALESCE(s.api_key_id, '__unattributed__')";
    case "hour":
      return "strftime('%Y-%m-%dT%H:00', s.created_at/1000, 'unixepoch')";
    case "day":
      return "strftime('%Y-%m-%d', s.created_at/1000, 'unixepoch')";
    default:
      return "'all'";
  }
}
function parseTimeBucket(raw) {
  if (raw === "hour" || raw === "day" || raw === "week") return raw;
  return null;
}
function bucketStrftime(b) {
  switch (b) {
    case "hour":
      return "%Y-%m-%dT%H:00";
    case "day":
      return "%Y-%m-%d";
    case "week":
      return "%Y-%W";
  }
}
var BUCKET_MS = {
  hour: 60 * 60 * 1e3,
  day: 24 * 60 * 60 * 1e3,
  week: 7 * 24 * 60 * 60 * 1e3
};
var TOP_N = 10;
var MAX_POINTS_PER_SERIES = 200;
var MAX_TOTAL_CELLS = 2e3;
function zeroPoint(t) {
  return { t, cost_usd: 0, session_count: 0, turn_count: 0, error_count: 0 };
}
function handleKeyTimeSeries(db, from, to, bucket, tenantId) {
  const windowMs = to - from;
  const pointsInWindow = Math.ceil(windowMs / BUCKET_MS[bucket]);
  if (pointsInWindow > MAX_POINTS_PER_SERIES) {
    const next = { hour: "day", day: "week", week: "week" };
    throw badRequest(
      `time_bucket=${bucket} produces ${pointsInWindow} points for a ${Math.round(windowMs / (24 * 60 * 60 * 1e3))}d window (max ${MAX_POINTS_PER_SERIES}). Use time_bucket=${next[bucket]}.`
    );
  }
  const maxCells = (TOP_N + 2) * pointsInWindow;
  if (maxCells > MAX_TOTAL_CELLS) {
    throw badRequest(
      `combination exceeds the ${MAX_TOTAL_CELLS} cell cap (${maxCells} possible). Narrow window or use a larger bucket.`
    );
  }
  const strftime = bucketStrftime(bucket);
  const tenantClause = tenantId != null ? " AND tenant_id = ?" : "";
  const tenantClauseJoin = tenantId != null ? " AND s.tenant_id = ?" : "";
  const tenantArgs = tenantId != null ? [tenantId] : [];
  const topRows = db.prepare(
    `SELECT api_key_id AS key, SUM(usage_cost_usd) AS cost
         FROM sessions
        WHERE created_at BETWEEN ? AND ?
          AND api_key_id IS NOT NULL${tenantClause}
        GROUP BY api_key_id
        ORDER BY cost DESC
        LIMIT ?`
  ).all(from, to, ...tenantArgs, TOP_N);
  const topKeys = new Set(topRows.map((r) => r.key));
  const classifyKey = (raw2) => {
    if (raw2 == null) return "__unattributed__";
    if (topKeys.has(raw2)) return raw2;
    return "__other__";
  };
  const raw = db.prepare(
    `SELECT strftime(?, s.created_at/1000, 'unixepoch') AS t,
              s.api_key_id AS key,
              COUNT(*)                                AS session_count,
              COALESCE(SUM(s.usage_cost_usd), 0)      AS cost_usd,
              COALESCE(SUM(s.turn_count), 0)          AS turn_count
         FROM sessions s
        WHERE s.created_at BETWEEN ? AND ?${tenantClauseJoin}
        GROUP BY t, s.api_key_id`
  ).all(strftime, from, to, ...tenantArgs);
  const errorRaw = db.prepare(
    `SELECT strftime(?, e.received_at/1000, 'unixepoch') AS t,
              s.api_key_id AS key,
              COUNT(*) AS error_count
         FROM events e
         JOIN sessions s ON s.id = e.session_id
        WHERE e.type = 'session.error'
          AND e.received_at BETWEEN ? AND ?${tenantClauseJoin}
        GROUP BY t, s.api_key_id`
  ).all(strftime, from, to, ...tenantArgs);
  const labels = [];
  const step = BUCKET_MS[bucket];
  const format = (ms) => {
    const d = new Date(ms);
    if (bucket === "hour") return d.toISOString().slice(0, 13) + ":00";
    if (bucket === "day") return d.toISOString().slice(0, 10);
    const year = d.getUTCFullYear();
    const start = Date.UTC(year, 0, 1);
    const week = Math.floor((ms - start) / (7 * 24 * 3600 * 1e3));
    return `${year}-${String(week).padStart(2, "0")}`;
  };
  for (let t = from; t <= to; t += step) {
    const label = format(t);
    if (!labels.includes(label)) labels.push(label);
  }
  if (labels.length === 0) labels.push(format(from));
  const seriesByKey = /* @__PURE__ */ new Map();
  const touchSeries = (keyClass) => {
    let m = seriesByKey.get(keyClass);
    if (!m) {
      m = /* @__PURE__ */ new Map();
      for (const lbl of labels) m.set(lbl, zeroPoint(lbl));
      seriesByKey.set(keyClass, m);
    }
    return m;
  };
  for (const row of raw) {
    const cls = classifyKey(row.key);
    const series2 = touchSeries(cls);
    const p = series2.get(row.t) ?? zeroPoint(row.t);
    p.cost_usd += row.cost_usd;
    p.session_count += row.session_count;
    p.turn_count += row.turn_count;
    series2.set(row.t, p);
  }
  for (const row of errorRaw) {
    const cls = classifyKey(row.key);
    const series2 = touchSeries(cls);
    const p = series2.get(row.t) ?? zeroPoint(row.t);
    p.error_count += row.error_count;
    series2.set(row.t, p);
  }
  const keyMeta = /* @__PURE__ */ new Map();
  if (topKeys.size > 0) {
    const placeholders = Array.from(topKeys).map(() => "?").join(",");
    const tenantWhere = tenantId != null ? " AND tenant_id = ?" : "";
    const rows = db.prepare(`SELECT id, name FROM api_keys WHERE id IN (${placeholders})${tenantWhere}`).all(...topKeys, ...tenantArgs);
    for (const r of rows) keyMeta.set(r.id, r.name);
  }
  const totals = zeroTotals();
  for (const series2 of seriesByKey.values()) {
    for (const p of series2.values()) {
      totals.cost_usd += p.cost_usd;
      totals.session_count += p.session_count;
      totals.turn_count += p.turn_count;
      totals.error_count += p.error_count;
    }
  }
  const series = Array.from(seriesByKey.entries()).map(([key, pts]) => ({
    key,
    name: keyMeta.get(key) ?? (key.startsWith("__") ? key.replace(/^__|__$/g, "") : key),
    points: labels.map((lbl) => pts.get(lbl) ?? zeroPoint(lbl))
  }));
  return jsonOk({
    window: { from, to },
    group_by: "api_key",
    time_bucket: bucket,
    totals,
    series
  });
}
function handleGetMetrics(request) {
  return routeWrap(request, async ({ auth }) => {
    const url = new URL(request.url);
    const group = parseGroupBy(url.searchParams.get("group_by"));
    const from = Number(url.searchParams.get("from") ?? "0");
    const to = Number(url.searchParams.get("to") ?? `${Date.now()}`);
    const agentId = url.searchParams.get("agent_id");
    const environmentId = url.searchParams.get("environment_id");
    const timeBucket = parseTimeBucket(url.searchParams.get("time_bucket"));
    const tenantId = tenantFilter(auth);
    if (!Number.isFinite(from) || !Number.isFinite(to) || from < 0 || to < 0) {
      throw badRequest("from/to must be non-negative integers (ms since epoch)");
    }
    if (from > to) {
      throw badRequest("from must be <= to");
    }
    const db = getDb();
    if (group === "api_key") {
      requireFeature("per_key_analytics", "per-key analytics");
    }
    if (group === "api_key" && timeBucket) {
      return handleKeyTimeSeries(db, from, to, timeBucket, tenantId);
    }
    const groupExpr = groupByExpr(group);
    const params = [from, to];
    let filter = "s.created_at >= ? AND s.created_at <= ?";
    if (agentId) {
      filter += " AND s.agent_id = ?";
      params.push(agentId);
    }
    if (environmentId) {
      filter += " AND s.environment_id = ?";
      params.push(environmentId);
    }
    if (tenantId != null) {
      filter += " AND s.tenant_id = ?";
      params.push(tenantId);
    }
    const groupRows = db.prepare(
      `SELECT ${groupExpr}                                AS key,
                COUNT(*)                                    AS session_count,
                COALESCE(SUM(s.turn_count), 0)              AS turn_count,
                COALESCE(SUM(s.tool_calls_count), 0)        AS tool_call_count,
                COALESCE(SUM(s.active_seconds), 0)          AS active_seconds,
                COALESCE(SUM(s.usage_input_tokens), 0)      AS input_tokens,
                COALESCE(SUM(s.usage_output_tokens), 0)     AS output_tokens,
                COALESCE(SUM(s.usage_cache_read_input_tokens), 0)     AS cache_read_input_tokens,
                COALESCE(SUM(s.usage_cache_creation_input_tokens), 0) AS cache_creation_input_tokens,
                COALESCE(SUM(s.usage_cost_usd), 0)          AS cost_usd,
                0                                           AS error_count
         FROM sessions s
         JOIN agent_versions av
           ON av.agent_id = s.agent_id AND av.version = s.agent_version
         WHERE ${filter}
         GROUP BY ${groupExpr}
         ORDER BY turn_count DESC`
    ).all(...params);
    const errorFilterParams = [from, to];
    let errorFilter = "e.type = 'session.error' AND e.received_at >= ? AND e.received_at <= ?";
    if (agentId) {
      errorFilter += " AND s.agent_id = ?";
      errorFilterParams.push(agentId);
    }
    if (environmentId) {
      errorFilter += " AND s.environment_id = ?";
      errorFilterParams.push(environmentId);
    }
    if (tenantId != null) {
      errorFilter += " AND s.tenant_id = ?";
      errorFilterParams.push(tenantId);
    }
    const errorRows = db.prepare(
      `SELECT ${groupExpr} AS key, COUNT(*) AS error_count
         FROM events e
         JOIN sessions s        ON s.id = e.session_id
         JOIN agent_versions av ON av.agent_id = s.agent_id AND av.version = s.agent_version
         WHERE ${errorFilter}
         GROUP BY ${groupExpr}`
    ).all(...errorFilterParams);
    const errorsByKey = new Map(errorRows.map((r) => [r.key, r.error_count]));
    for (const row of groupRows) {
      row.error_count = errorsByKey.get(row.key) ?? 0;
    }
    const stopRows = db.prepare(
      `SELECT COALESCE(
                  json_extract(e.payload_json, '$.stop_reason.type'),
                  json_extract(e.payload_json, '$.stop_reason')
                ) AS stop_reason_value,
                COUNT(*) AS count
         FROM events e
         JOIN sessions s ON s.id = e.session_id
         WHERE e.type = 'session.status_idle'
           AND e.received_at BETWEEN ? AND ?
           ${agentId ? "AND s.agent_id = ?" : ""}
           ${environmentId ? "AND s.environment_id = ?" : ""}
           ${tenantId != null ? "AND s.tenant_id = ?" : ""}
         GROUP BY COALESCE(
                    json_extract(e.payload_json, '$.stop_reason.type'),
                    json_extract(e.payload_json, '$.stop_reason')
                  )`
    ).all(
      ...[
        from,
        to,
        ...agentId ? [agentId] : [],
        ...environmentId ? [environmentId] : [],
        ...tenantId != null ? [tenantId] : []
      ]
    );
    const stopReasons = {};
    for (const r of stopRows) {
      stopReasons[r.stop_reason_value ?? "unknown"] = r.count;
    }
    const toolDurationRows = db.prepare(
      `SELECT CAST(json_extract(e.payload_json, '$.duration_ms') AS REAL) AS d
         FROM events e
         JOIN sessions s ON s.id = e.session_id
         WHERE e.type = 'span.tool_call_end'
           AND e.received_at BETWEEN ? AND ?
           ${agentId ? "AND s.agent_id = ?" : ""}
           ${environmentId ? "AND s.environment_id = ?" : ""}
           ${tenantId != null ? "AND s.tenant_id = ?" : ""}
           AND json_extract(e.payload_json, '$.duration_ms') IS NOT NULL
         ORDER BY d ASC`
    ).all(
      ...[
        from,
        to,
        ...agentId ? [agentId] : [],
        ...environmentId ? [environmentId] : [],
        ...tenantId != null ? [tenantId] : []
      ]
    );
    const durations = toolDurationRows.map((r) => r.d).filter((d) => Number.isFinite(d));
    const pct = (p) => {
      if (durations.length === 0) return null;
      const idx = Math.min(durations.length - 1, Math.floor(p / 100 * durations.length));
      return durations[idx];
    };
    const totals = zeroTotals();
    for (const row of groupRows) {
      totals.session_count += row.session_count;
      totals.turn_count += row.turn_count;
      totals.tool_call_count += row.tool_call_count;
      totals.error_count += row.error_count;
      totals.active_seconds += row.active_seconds;
      totals.input_tokens += row.input_tokens;
      totals.output_tokens += row.output_tokens;
      totals.cache_read_input_tokens += row.cache_read_input_tokens;
      totals.cache_creation_input_tokens += row.cache_creation_input_tokens;
      totals.cost_usd += row.cost_usd;
    }
    return jsonOk({
      window: { from, to },
      group_by: group,
      totals,
      groups: group === "none" ? [] : groupRows,
      stop_reasons: stopReasons,
      tool_latency_p50_ms: pct(50),
      tool_latency_p95_ms: pct(95),
      tool_latency_p99_ms: pct(99),
      tool_call_sample_count: durations.length
    });
  });
}
function handleGetApiMetrics(request) {
  return routeWrap(request, async ({ auth }) => {
    requireGlobalAdmin(auth);
    const url = new URL(request.url);
    const wmRaw = Number(url.searchParams.get("window_minutes") ?? "60");
    if (!Number.isFinite(wmRaw) || wmRaw <= 0) {
      throw badRequest("window_minutes must be a positive integer");
    }
    const windowMinutes = Math.min(Math.max(Math.floor(wmRaw), 1), 60);
    return jsonOk(snapshotApiMetrics({ windowMs: windowMinutes * 6e4 }));
  });
}

export {
  handleGetMetrics,
  handleGetApiMetrics
};
