/**
 * Prometheus exposition for fleet metrics (§4 ops export).
 *
 * Renders the per-product `FleetMetrics` + the process-wide reaper/GC counters +
 * the engine circuit-breaker snapshot into the Prometheus text format
 * (`text/plain; version=0.0.4`). PURE + synchronous — the route does the I/O
 * (scan products, compute metrics) and hands the snapshot here, which keeps the
 * formatting fully unit-testable.
 */

import type { FleetMetrics } from './coordinator.js';
import type { ReaperStats } from './reaper.js';
import type { EngineBreakerEntry } from './engine-breaker.js';
import type { ScalingAction, ScalingRecommendation } from './autoscaler.js';

/** Scale actions emitted as the `action` label on `fleet_autoscale_action`. */
const SCALING_ACTIONS: readonly ScalingAction[] = ['scale_out', 'scale_in', 'hold'];

/** One product's computed metrics, paired with its id for labelling. */
export interface ProductMetrics {
  productId: string;
  metrics: FleetMetrics;
  /** Optional raw autoscale recommendation — emitted as `fleet_autoscale_*`. */
  recommendation?: ScalingRecommendation;
}

export interface FleetPromInput {
  products: ProductMetrics[];
  reaper: ReaperStats;
  breakers: EngineBreakerEntry[];
}

/** Escape a Prometheus label value (backslash, double-quote, newline). */
function esc(v: string): string {
  return v.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
}

/** Render one labelled series line. `labels` is an ordered [key,value][]. */
function line(name: string, labels: [string, string][], value: number): string {
  if (labels.length === 0) return `${name} ${value}`;
  const inner = labels.map(([k, v]) => `${k}="${esc(v)}"`).join(',');
  return `${name}{${inner}} ${value}`;
}

interface Metric {
  name: string;
  help: string;
  type: 'gauge' | 'counter';
  rows: string[];
}

/**
 * Render the full fleet exposition. Deterministic ordering (products as given,
 * then global counters) so output diffs are stable and testable.
 */
export function renderFleetMetricsProm(input: FleetPromInput): string {
  const m: Record<string, Metric> = {};
  const def = (name: string, type: Metric['type'], help: string): Metric =>
    (m[name] ??= { name, help, type, rows: [] });

  for (const { productId, metrics, recommendation } of input.products) {
    const p: [string, string][] = [['product', productId]];
    def('fleet_jobs_total', 'gauge', 'Total jobs for the product').rows.push(
      line('fleet_jobs_total', p, metrics.jobs.total)
    );
    def('fleet_queue_depth', 'gauge', 'Jobs currently queued').rows.push(
      line('fleet_queue_depth', p, metrics.jobs.queueDepth)
    );
    def('fleet_jobs_blocked', 'gauge', 'Jobs blocked on unmet dependencies').rows.push(
      line('fleet_jobs_blocked', p, metrics.jobs.blocked)
    );
    def('fleet_jobs_active', 'gauge', 'Jobs in an active (claimed/running) stage').rows.push(
      line('fleet_jobs_active', p, metrics.jobs.active)
    );
    def('fleet_oldest_queued_age_ms', 'gauge', 'Age of the oldest queued job (ms)').rows.push(
      line('fleet_oldest_queued_age_ms', p, metrics.jobs.oldestQueuedAgeMs ?? 0)
    );
    // Per-stage histogram of the job lifecycle.
    const stage = def('fleet_jobs_by_stage', 'gauge', 'Jobs by lifecycle stage');
    for (const [s, n] of Object.entries(metrics.jobs.byStage)) {
      stage.rows.push(
        line(
          'fleet_jobs_by_stage',
          [
            ['product', productId],
            ['stage', s],
          ],
          n
        )
      );
    }
    def('fleet_factories_total', 'gauge', 'Registered factories').rows.push(
      line('fleet_factories_total', p, metrics.factories.total)
    );
    def('fleet_factories_live', 'gauge', 'Factories seen within the stale window').rows.push(
      line('fleet_factories_live', p, metrics.factories.live)
    );
    def('fleet_factories_stale', 'gauge', 'Factories past the heartbeat stale window').rows.push(
      line('fleet_factories_stale', p, metrics.factories.stale)
    );
    def('fleet_seats_used', 'gauge', 'Occupied factory seats').rows.push(
      line('fleet_seats_used', p, metrics.factories.seatsUsed)
    );
    def('fleet_seats_total', 'gauge', 'Total advertised factory seats').rows.push(
      line('fleet_seats_total', p, metrics.factories.seatsTotal)
    );
    def('fleet_utilization_pct', 'gauge', 'Seat utilization percentage').rows.push(
      line('fleet_utilization_pct', p, metrics.factories.utilizationPct)
    );

    // One active-alert series per alert code (value 1), labelled by level.
    const alert = def('fleet_alert_active', 'gauge', 'Active fleet alert (1 = firing)');
    for (const a of metrics.alerts) {
      alert.rows.push(
        line(
          'fleet_alert_active',
          [
            ['product', productId],
            ['code', a.code],
            ['level', a.level],
          ],
          1
        )
      );
    }

    if (metrics.budget) {
      def('fleet_budget_spent_usd', 'gauge', 'Budget spent this window (USD)').rows.push(
        line('fleet_budget_spent_usd', p, metrics.budget.spentUsd)
      );
      def('fleet_budget_ceiling_usd', 'gauge', 'Budget ceiling (USD)').rows.push(
        line('fleet_budget_ceiling_usd', p, metrics.budget.ceilingUsd)
      );
      if (metrics.budget.projectedUsd !== null) {
        def('fleet_budget_projected_usd', 'gauge', 'Projected end-of-window spend (USD)').rows.push(
          line('fleet_budget_projected_usd', p, metrics.budget.projectedUsd)
        );
      }
    }

    // ── Capacity autoscaling signal (§5) — the RAW recommendation ──
    if (recommendation) {
      def(
        'fleet_autoscale_recommended_seats',
        'gauge',
        'Recommended total factory seats for the product'
      ).rows.push(line('fleet_autoscale_recommended_seats', p, recommendation.recommendedSeats));
      def(
        'fleet_autoscale_delta',
        'gauge',
        'Recommended seat change (positive = scale out, negative = scale in)'
      ).rows.push(line('fleet_autoscale_delta', p, recommendation.delta));
      def('fleet_autoscale_pressure', 'gauge', 'Runnable backlog driving scale-out').rows.push(
        line('fleet_autoscale_pressure', p, recommendation.signals.pressure)
      );
      // One-hot the current action so a dashboard can count e.g. scale_out products.
      const action = def(
        'fleet_autoscale_action',
        'gauge',
        'Current scale recommendation (1 = active) labelled by action'
      );
      for (const a of SCALING_ACTIONS) {
        action.rows.push(
          line(
            'fleet_autoscale_action',
            [
              ['product', productId],
              ['action', a],
            ],
            recommendation.action === a ? 1 : 0
          )
        );
      }
    }
  }

  // ── Process-wide reaper / GC counters ──
  const r = input.reaper.totals;
  const rc = (name: string, help: string, value: number): void => {
    def(name, 'counter', help).rows.push(line(name, [], value));
  };
  rc(
    'fleet_reaper_expired_reclaimed_total',
    'Jobs reclaimed from expired leases',
    r.expiredReclaimed
  );
  rc('fleet_reaper_stale_reclaimed_total', 'Jobs reclaimed from stale factories', r.staleReclaimed);
  rc('fleet_reaper_leases_deleted_total', 'Finished leases garbage-collected', r.leasesDeleted);
  rc('fleet_reaper_jobs_deleted_total', 'Terminal jobs garbage-collected', r.jobsDeleted);
  def('fleet_reaper_running', 'gauge', 'Reaper loop running (1/0)').rows.push(
    line('fleet_reaper_running', [], input.reaper.running ? 1 : 0)
  );

  // ── Engine circuit breakers (process-wide) ──
  const open = def(
    'fleet_engine_breaker_open',
    'gauge',
    'Engine breaker tripped (1 = OPEN/HALF_OPEN)'
  );
  let openCount = 0;
  for (const b of input.breakers) {
    if (b.state === 'CLOSED') continue;
    openCount += 1;
    open.rows.push(
      line(
        'fleet_engine_breaker_open',
        [
          ['factory', b.factoryId],
          ['engine', b.engine],
          ['state', b.state],
        ],
        1
      )
    );
  }
  def(
    'fleet_engine_breaker_open_count',
    'gauge',
    'Number of tripped (factory,engine) breakers'
  ).rows.push(line('fleet_engine_breaker_open_count', [], openCount));

  const out: string[] = [];
  for (const metric of Object.values(m)) {
    out.push(`# HELP ${metric.name} ${metric.help}`);
    out.push(`# TYPE ${metric.name} ${metric.type}`);
    out.push(...metric.rows);
  }
  out.push('');
  return out.join('\n');
}

/**
 * Authorize a metrics scrape. When `FLEET_METRICS_TOKEN` is set, a matching
 * `Authorization: Bearer <token>` is accepted (the Prometheus scrape path).
 * Returns false when the token is unset (caller falls back to admin auth) or
 * the bearer does not match — so the endpoint is never world-readable by default.
 */
export function scrapeTokenMatches(authHeader: string | undefined): boolean {
  const token = (process.env.FLEET_METRICS_TOKEN ?? '').trim();
  if (!token) return false;
  if (!authHeader) return false;
  const m = authHeader.match(/^Bearer\s+(.+)$/i);
  return m?.[1] === token;
}
