// Phase C Task C3 — hand-rolled Prometheus metrics registry for host-cp.
//
// Emits exactly two metric families:
//   http_requests_total{service,route,method,status_code}  counter
//   http_request_duration_seconds{service,route,method}    histogram
//
// TAXONOMY COMPLIANCE (NON-NEGOTIABLE):
//   ONLY {service, route, method, status_code} labels allowed.
//   BANNED: world_id, trace_id, user_id, request_id, operator_id.
//   world_id surfaces via Prometheus exemplars in Phase D — NOT labels.
//
// No external npm deps — Prometheus text exposition is simple enough to
// produce with template literals. Avoids the prom-client footprint on a
// host-side service that has no other dependency on metrics tooling.

// ─── Route mapping ────────────────────────────────────────────────────────
//
// Raw req.url is a cardinality bomb: every unique URL is a new time series.
// We normalize dynamic path segments to stable patterns before labelling.
//
// RULES (first match wins):
//   /health                          → /health
//   /api/bootstrap                   → /api/bootstrap
//   /metrics                         → /metrics
//   /api/host-stream                 → /api/host-stream
//   /api/worlds/{id}/credentials/... → /api/worlds/:id/credentials/:action
//   /api/worlds/{id}/tunnels/...     → /api/worlds/:id/tunnels
//   /api/worlds/{id}/pr              → /api/worlds/:id/pr
//   /api/worlds/{id}/progress        → /api/worlds/:id/progress
//   /api/worlds (no id)              → /api/worlds
//   /api/world/{id}/**               → /api/world/:id/*  (proxy routes)
//   /api/admin/registry/...          → /api/admin/registry
//   /api/admin/upgrade               → /api/admin/upgrade
//   /api/admin/world-pr              → /api/admin/world-pr
//   /api/admin/world-pr/{id}         → /api/admin/world-pr/:id
//   /api/auth/credentials/...        → /api/auth/credentials
//   /api/auth/...                    → /api/auth
//   /api/plan/conversations/{id}/... → /api/plan/conversations/:id
//   /api/plan/conversations          → /api/plan/conversations
//   /api/plan/**                     → /api/plan
//   /api/auth/events                 → /api/auth/events
//   /api/version/status              → /api/version/status
//   /api/repos                       → /api/repos
//   /api/runbooks                    → /api/runbooks
//   /api/workspaces/match            → /api/workspaces/match
//   /api/workspaces                  → /api/workspaces
//   /api/projects                    → /api/projects
//   /api/processes/**                → /api/processes
//   /v1/chunks/**                    → /v1/chunks
//   /v1/worlds/**                    → /v1/worlds
//   /assets/**                       → /assets (SPA static assets)
//   (other GET to static paths)      → /static
//   (unknown)                        → /unknown

/** @param {string} pathname */
export function pathToRoute(pathname) {
  // Normalize trailing slash for matching (keep bare / as /)
  const p = pathname.length > 1 ? pathname.replace(/\/$/, '') : pathname;

  if (p === '/health') return '/health';
  if (p === '/api/bootstrap') return '/api/bootstrap';
  if (p === '/metrics') return '/metrics';
  if (p === '/api/host-stream') return '/api/host-stream';
  if (p === '/api/auth/events') return '/api/auth/events';
  if (p === '/api/version/status') return '/api/version/status';
  if (p === '/api/repos') return '/api/repos';
  if (p === '/api/runbooks') return '/api/runbooks';
  if (p === '/api/workspaces/match') return '/api/workspaces/match';
  if (p === '/api/workspaces') return '/api/workspaces';
  if (p === '/api/projects') return '/api/projects';
  if (p === '/api/worlds') return '/api/worlds';
  if (p === '/api/plan/conversations' || p === '/api/plan/personas') return p;
  if (p === '/api/admin/upgrade') return '/api/admin/upgrade';
  if (p === '/api/admin/world-pr') return '/api/admin/world-pr';
  if (p === '/api/admin/registry') return '/api/admin/registry';
  if (p.startsWith('/api/worlds/')) {
    if (p.includes('/credentials/')) return '/api/worlds/:id/credentials/:action';
    if (p.includes('/tunnels')) return '/api/worlds/:id/tunnels';
    if (p.endsWith('/pr')) return '/api/worlds/:id/pr';
    if (p.endsWith('/progress')) return '/api/worlds/:id/progress';
    return '/api/worlds/:id';
  }
  if (p.startsWith('/api/world/')) return '/api/world/:id/*';
  if (p.startsWith('/api/admin/registry/')) return '/api/admin/registry';
  if (p.startsWith('/api/admin/world-pr/')) return '/api/admin/world-pr/:id';
  if (p.startsWith('/api/auth/credentials')) return '/api/auth/credentials';
  if (p.startsWith('/api/auth/')) return '/api/auth';
  if (p.startsWith('/api/plan/conversations/')) return '/api/plan/conversations/:id';
  if (p.startsWith('/api/plan/')) return '/api/plan';
  if (p.startsWith('/api/processes') || p.startsWith('/api/servers')) return '/api/processes';
  if (p.startsWith('/v1/chunks')) return '/v1/chunks';
  if (p.startsWith('/v1/worlds')) return '/v1/worlds';
  if (p.startsWith('/assets/')) return '/assets';
  // SPA HTML fallback routes (GET / and SPA sub-routes like /worlds, /plan/...)
  if (p === '/' || p.startsWith('/worlds') || p.startsWith('/plan') || p.startsWith('/workspaces')) return '/static';
  return '/unknown';
}

// ─── In-memory registry ───────────────────────────────────────────────────

const HISTOGRAM_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5];

/** @type {Map<string, number>} labelSet → count */
const _counters = new Map();

/**
 * Per label-set histogram state.
 * @type {Map<string, {buckets: number[], sum: number, count: number}>}
 */
const _histograms = new Map();

/** @param {string[]} parts label values in canonical order */
function _labelKey(parts) {
  return parts.join('\x00');
}

/**
 * Reset all metrics. FOR TESTS ONLY — never call in production code.
 * Exported as a separate name so it's invisible to consumers that only
 * import the named exports they need.
 */
export function _resetForTest() {
  _counters.clear();
  _histograms.clear();
}

/**
 * Increment http_requests_total counter.
 *
 * @param {string} service
 * @param {string} route    — MUST be a normalized route pattern
 * @param {string} method
 * @param {string} statusCode
 */
export function incRequest(service, route, method, statusCode) {
  const key = _labelKey([service, route, method, statusCode]);
  _counters.set(key, (_counters.get(key) ?? 0) + 1);
}

/**
 * Observe http_request_duration_seconds.
 *
 * @param {string} service
 * @param {string} route
 * @param {string} method
 * @param {number} seconds
 */
export function observeDuration(service, route, method, seconds) {
  const key = _labelKey([service, route, method]);
  let h = _histograms.get(key);
  if (!h) {
    // buckets[i] = count of observations where seconds <= HISTOGRAM_BUCKETS[i]
    // but stored as INCREMENTAL per-range so cumulation happens on render.
    // Each bucket[i] = count that fell in range (HISTOGRAM_BUCKETS[i-1], HISTOGRAM_BUCKETS[i]].
    h = { buckets: new Array(HISTOGRAM_BUCKETS.length).fill(0), sum: 0, count: 0 };
    _histograms.set(key, h);
  }
  // Find the first bucket boundary that accommodates this observation.
  // Increment only that bucket; render accumulates for the exposition.
  let placed = false;
  for (let i = 0; i < HISTOGRAM_BUCKETS.length; i++) {
    if (seconds <= HISTOGRAM_BUCKETS[i]) {
      h.buckets[i]++;
      placed = true;
      break;
    }
  }
  // Observations beyond the last bucket are counted in h.count only;
  // the +Inf bucket in the exposition equals h.count.
  if (!placed) {
    // No bucket captured it — it lands in +Inf only.
  }
  h.sum += seconds;
  h.count++;
}

// ─── Prometheus text exposition ───────────────────────────────────────────

/** Escape label value per Prometheus text format (backslash, newline, quote). */
function escapeLabelValue(v) {
  return String(v).replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
}

/**
 * Build the `{k1="v1",k2="v2",...}` label-set string.
 * @param {Record<string, string>} labels
 */
function labelSet(labels) {
  const parts = Object.entries(labels).map(
    ([k, v]) => `${k}="${escapeLabelValue(v)}"`,
  );
  return `{${parts.join(',')}}`;
}

/**
 * Render the complete Prometheus text exposition.
 * @returns {string}
 */
export function renderMetrics() {
  const lines = [];

  // ── http_requests_total ─────────────────────────────────────────────
  lines.push('# HELP http_requests_total Total number of HTTP requests handled.');
  lines.push('# TYPE http_requests_total counter');
  for (const [key, count] of _counters) {
    const [service, route, method, status_code] = key.split('\x00');
    lines.push(
      `http_requests_total${labelSet({ service, route, method, status_code })} ${count}`,
    );
  }

  // ── http_request_duration_seconds ───────────────────────────────────
  lines.push('# HELP http_request_duration_seconds HTTP request duration in seconds (histogram).');
  lines.push('# TYPE http_request_duration_seconds histogram');
  for (const [key, h] of _histograms) {
    const [service, route, method] = key.split('\x00');
    const base = { service, route, method };
    // Cumulative buckets: le=X must be ≥ sum of all observations ≤ X.
    let cumulative = 0;
    for (let i = 0; i < HISTOGRAM_BUCKETS.length; i++) {
      cumulative += h.buckets[i];
      lines.push(
        `http_request_duration_seconds_bucket${labelSet({ ...base, le: String(HISTOGRAM_BUCKETS[i]) })} ${cumulative}`,
      );
    }
    lines.push(
      `http_request_duration_seconds_bucket${labelSet({ ...base, le: '+Inf' })} ${h.count}`,
    );
    lines.push(`http_request_duration_seconds_sum${labelSet(base)} ${h.sum}`);
    lines.push(`http_request_duration_seconds_count${labelSet(base)} ${h.count}`);
  }

  lines.push(''); // trailing newline
  return lines.join('\n');
}

// ─── Request instrumentation wrapper ─────────────────────────────────────

/**
 * Wrap an async request handler so every request is instrumented.
 *
 * The wrapper:
 *   1. Derives a stable route pattern from req.url.
 *   2. Starts a high-resolution timer.
 *   3. Calls the original handler.
 *   4. Records counter + histogram using the response's status code.
 *
 * Status code capture: we monkey-patch res.writeHead and res.end to intercept
 * the status before it's sent. Falls back to res.statusCode (which Node sets
 * implicitly on .end() when no explicit writeHead call was made).
 *
 * @param {string} serviceName  — emitted as the `service` label
 * @param {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void>} handler
 * @returns {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void>}
 */
export function instrumentHandler(serviceName, handler) {
  return async (req, res) => {
    const start = performance.now();

    // Intercept status code by wrapping writeHead.
    let capturedStatus = null;
    const origWriteHead = res.writeHead.bind(res);
    res.writeHead = (status, ...rest) => {
      capturedStatus = status;
      return origWriteHead(status, ...rest);
    };

    try {
      await handler(req, res);
    } finally {
      const durationSec = (performance.now() - start) / 1000;
      const urlObj = new URL(req.url ?? '/', `http://localhost`);
      const route = pathToRoute(urlObj.pathname);
      const method = (req.method ?? 'GET').toUpperCase();
      const statusCode = String(capturedStatus ?? res.statusCode ?? 200);

      incRequest(serviceName, route, method, statusCode);
      observeDuration(serviceName, route, method, durationSec);
    }
  };
}
