/**
 * Capacity autoscaling signal (§5).
 *
 * Turns a product's point-in-time `FleetMetrics` (queue depth, seat utilization,
 * and the derived saturation alerts: `no_live_capacity`, `saturated`,
 * `queue_starvation`) into a concrete, auditable scale recommendation an
 * external scaler can consume (pull model — `GET /fleet/autoscale`). PURE +
 * synchronous: the route does the I/O (compute metrics) and hands the snapshot
 * here, keeping the policy fully unit-testable and free of side effects.
 *
 * The recommendation is advisory: it never starts/stops factories itself, it
 * reports a desired seat target + delta + the raw signals that drove it, so a
 * downstream controller (or a human) makes the actual call.
 */

import type { FleetMetrics } from './coordinator.js';

export type ScalingAction = 'scale_out' | 'scale_in' | 'hold';

/** Tunable thresholds (route resolves these from `FLEET_AUTOSCALE_*` env). */
export interface ScalingThresholds {
  /** Seat utilization at/above which a backlog should trigger scale-out (%). */
  scaleOutUtilizationPct: number;
  /** Seat utilization at/below which idle capacity may be reclaimed (%). */
  scaleInUtilizationPct: number;
  /** Max seats to recommend adding in a single step (rate-limits scale-out). */
  maxScaleOutStep: number;
  /** Never recommend scaling below this many seats (keeps a warm floor). */
  minSeats: number;
}

export const DEFAULT_SCALING_THRESHOLDS: ScalingThresholds = {
  scaleOutUtilizationPct: 85,
  scaleInUtilizationPct: 20,
  maxScaleOutStep: 5,
  minSeats: 0,
};

export interface ScalingSignals {
  queueDepth: number;
  blocked: number;
  active: number;
  seatsUsed: number;
  seatsTotal: number;
  utilizationPct: number;
  /** Runnable backlog driving scale-out pressure (queued jobs; blocked excluded). */
  pressure: number;
  /** Codes of the alerts currently firing for the product. */
  alertCodes: string[];
}

export interface ScalingRecommendation {
  productId: string;
  action: ScalingAction;
  /** Absolute recommended seat target. */
  recommendedSeats: number;
  /** recommendedSeats - current seatsTotal (positive = add, negative = remove). */
  delta: number;
  /** Human-readable justification. */
  reason: string;
  /** Urgency: `critical` when work is queued with zero usable capacity. */
  urgency: 'critical' | 'warning' | 'none';
  /** Set when anti-flap hysteresis held back an otherwise-actionable signal. */
  suppressed?: boolean;
  /** Milliseconds remaining on the cooldown when `suppressed` is true. */
  cooldownRemainingMs?: number;
  signals: ScalingSignals;
  generatedAt: string;
}

/** Clamp to a non-negative integer. */
function clampNonNeg(n: number): number {
  return Math.max(0, Math.round(n));
}

/** Parse a non-negative number from env, falling back when unset/invalid. */
function envNum(name: string): number | undefined {
  const raw = process.env[name];
  if (raw === undefined || raw.trim() === '') return undefined;
  const n = Number(raw);
  return Number.isFinite(n) && n >= 0 ? n : undefined;
}

/**
 * Resolve threshold overrides from `FLEET_AUTOSCALE_*` env vars (any unset key
 * falls back to {@link DEFAULT_SCALING_THRESHOLDS}). Isolated here so the route
 * stays a thin pass-through and the defaults remain unit-testable.
 */
export function scalingThresholdsFromEnv(): Partial<ScalingThresholds> {
  const out: Partial<ScalingThresholds> = {};
  const outPct = envNum('FLEET_AUTOSCALE_SCALE_OUT_PCT');
  const inPct = envNum('FLEET_AUTOSCALE_SCALE_IN_PCT');
  const step = envNum('FLEET_AUTOSCALE_MAX_STEP');
  const floor = envNum('FLEET_AUTOSCALE_MIN_SEATS');
  if (outPct !== undefined) out.scaleOutUtilizationPct = outPct;
  if (inPct !== undefined) out.scaleInUtilizationPct = inPct;
  if (step !== undefined) out.maxScaleOutStep = step;
  if (floor !== undefined) out.minSeats = floor;
  return out;
}

/**
 * Compute a scale recommendation for one product. Decision order (highest
 * urgency first):
 *
 *  1. `no_live_capacity` (queued work, zero live healthy seats) → CRITICAL
 *     scale-out, even from a cold zero-seat fleet.
 *  2. `saturated` / `queue_starvation` / high utilization with a backlog →
 *     scale-out by the smaller of the backlog and `maxScaleOutStep`.
 *  3. Idle (no backlog, no in-flight work, utilization ≤ scale-in floor) with
 *     slack seats above `minSeats` → reclaim idle capacity.
 *  4. Otherwise hold.
 *
 * A `budget_exhausted` alert SUPPRESSES scale-out (new claims are blocked by the
 * budget, so more seats can't drain the queue) — it degrades to hold.
 */
export function recommendScaling(
  metrics: FleetMetrics,
  thresholds: Partial<ScalingThresholds> = {}
): ScalingRecommendation {
  const t = { ...DEFAULT_SCALING_THRESHOLDS, ...thresholds };
  const { queueDepth, blocked, active } = metrics.jobs;
  const { seatsUsed, seatsTotal, utilizationPct } = metrics.factories;
  const alertCodes = metrics.alerts.map(a => a.code);
  const pressure = queueDepth;

  const signals: ScalingSignals = {
    queueDepth,
    blocked,
    active,
    seatsUsed,
    seatsTotal,
    utilizationPct,
    pressure,
    alertCodes,
  };

  const hold = (reason: string): ScalingRecommendation => ({
    productId: metrics.productId,
    action: 'hold',
    recommendedSeats: seatsTotal,
    delta: 0,
    reason,
    urgency: 'none',
    signals,
    generatedAt: metrics.generatedAt,
  });

  const scaleOut = (
    addSeats: number,
    reason: string,
    urgency: 'critical' | 'warning'
  ): ScalingRecommendation => {
    const add = Math.min(Math.max(1, addSeats), t.maxScaleOutStep);
    const recommendedSeats = clampNonNeg(seatsTotal + add);
    return {
      productId: metrics.productId,
      action: 'scale_out',
      recommendedSeats,
      delta: recommendedSeats - seatsTotal,
      reason,
      urgency,
      signals,
      generatedAt: metrics.generatedAt,
    };
  };

  // Budget exhaustion blocks claims — more seats won't help; do not scale out.
  if (alertCodes.includes('budget_exhausted')) {
    return hold('Budget exhausted — claims are blocked, so scaling out cannot drain the queue.');
  }

  // 1. No usable capacity for queued work — most urgent.
  if (alertCodes.includes('no_live_capacity')) {
    return scaleOut(
      pressure,
      `${queueDepth} job(s) queued with no live, healthy factory — provision capacity now.`,
      'critical'
    );
  }

  // 2. Saturation / starvation / sustained high utilization with a backlog.
  if (alertCodes.includes('saturated')) {
    return scaleOut(
      pressure,
      `All ${seatsTotal} seat(s) busy with ${queueDepth} job(s) still queued — add capacity.`,
      'warning'
    );
  }
  if (alertCodes.includes('queue_starvation')) {
    return scaleOut(
      pressure,
      `Oldest queued job has waited past the starvation threshold — add capacity to drain it.`,
      'warning'
    );
  }
  if (pressure > 0 && seatsTotal > 0 && utilizationPct >= t.scaleOutUtilizationPct) {
    return scaleOut(
      pressure,
      `Seat utilization ${utilizationPct}% ≥ ${t.scaleOutUtilizationPct}% with ${queueDepth} queued — add capacity.`,
      'warning'
    );
  }

  // 3. Idle slack — reclaim capacity down to the floor (never below in-use seats).
  const idle = queueDepth === 0 && active === 0;
  if (idle && seatsTotal > t.minSeats && utilizationPct <= t.scaleInUtilizationPct) {
    const recommendedSeats = clampNonNeg(Math.max(t.minSeats, seatsUsed));
    if (recommendedSeats < seatsTotal) {
      return {
        productId: metrics.productId,
        action: 'scale_in',
        recommendedSeats,
        delta: recommendedSeats - seatsTotal,
        reason: `No queued or in-flight work and ${utilizationPct}% utilization — reclaim idle seats down to ${recommendedSeats}.`,
        urgency: 'none',
        signals,
        generatedAt: metrics.generatedAt,
      };
    }
  }

  return hold('Capacity is balanced against current demand.');
}

// ── Anti-flap hysteresis (§5) ─────────────────────────────────────────────────
//
// The raw recommender reacts to the instantaneous snapshot, which can oscillate
// (scale_out on a queue blip, then scale_in the moment it drains). Hysteresis
// suppresses a *direction reversal* within a cooldown window so a consumer that
// acts on the signal cannot thrash capacity. A CRITICAL scale-out (queued work,
// zero usable capacity) is never debounced — that is a genuine emergency.

/** Last emitted scale decision for a product (the cooldown anchor). */
export interface ScaleState {
  action: ScalingAction;
  at: number;
}

/** Default cooldown between opposite-direction scale actions (5 minutes). */
export const DEFAULT_COOLDOWN_MS = 300_000;

/**
 * Apply anti-flap hysteresis to a raw recommendation. PURE: given the raw
 * recommendation, the product's prior emitted state, the current time, and the
 * cooldown, returns the (possibly suppressed) recommendation plus the next state
 * to persist. The cooldown anchor only advances when an action is actually
 * emitted, so a suppressed signal keeps counting down from the real last action.
 */
export function applyHysteresis(
  raw: ScalingRecommendation,
  prior: ScaleState | undefined,
  nowMs: number,
  opts: { cooldownMs: number }
): { recommendation: ScalingRecommendation; nextState: ScaleState | undefined } {
  // Holds never anchor the cooldown and never need suppressing.
  if (raw.action === 'hold') return { recommendation: raw, nextState: prior };

  // A capacity emergency always wins — never debounce a critical scale-out.
  const isCriticalScaleOut = raw.action === 'scale_out' && raw.urgency === 'critical';

  const elapsed = prior ? nowMs - prior.at : Number.POSITIVE_INFINITY;
  const isReversal = !!prior && prior.action !== 'hold' && prior.action !== raw.action;

  if (isReversal && elapsed < opts.cooldownMs && !isCriticalScaleOut) {
    const remaining = opts.cooldownMs - elapsed;
    const suppressed: ScalingRecommendation = {
      ...raw,
      action: 'hold',
      recommendedSeats: raw.signals.seatsTotal,
      delta: 0,
      urgency: 'none',
      suppressed: true,
      cooldownRemainingMs: remaining,
      reason: `Hysteresis: holding '${raw.action}' for ${Math.ceil(remaining / 1000)}s after a recent '${prior!.action}' (anti-flap cooldown).`,
    };
    // Keep the original anchor so the cooldown keeps elapsing.
    return { recommendation: suppressed, nextState: prior };
  }

  // Emitted (allowed) — advance the cooldown anchor to now.
  return { recommendation: raw, nextState: { action: raw.action, at: nowMs } };
}

/** Process-wide per-product cooldown anchors (mirrors reaper/breaker in-mem state). */
const scaleStates = new Map<string, ScaleState>();

/** Test seam: clear all hysteresis state. */
export function _resetScaleStates(): void {
  scaleStates.clear();
}

/** Snapshot of the current per-product hysteresis anchors (observability/tests). */
export function getScaleStates(): Record<string, ScaleState> {
  return Object.fromEntries(scaleStates);
}

/** Resolve the cooldown from `FLEET_AUTOSCALE_COOLDOWN_SEC` (default 5m). */
export function cooldownMsFromEnv(): number {
  const sec = envNum('FLEET_AUTOSCALE_COOLDOWN_SEC');
  return sec !== undefined ? sec * 1000 : DEFAULT_COOLDOWN_MS;
}

/**
 * Stateful recommendation: computes the raw signal then applies anti-flap
 * hysteresis against this process's per-product cooldown state. This is what the
 * actionable `/fleet/autoscale[/all]` endpoints serve. The Prometheus exposition
 * deliberately uses the RAW recommender instead, so a scrape never mutates the
 * cooldown anchors.
 */
export function recommendScalingStateful(
  metrics: FleetMetrics,
  thresholds: Partial<ScalingThresholds> = {},
  opts?: { nowMs?: number; cooldownMs?: number }
): ScalingRecommendation {
  const raw = recommendScaling(metrics, thresholds);
  const nowMs = opts?.nowMs ?? Date.now();
  const cooldownMs = opts?.cooldownMs ?? cooldownMsFromEnv();
  const prior = scaleStates.get(metrics.productId);
  const { recommendation, nextState } = applyHysteresis(raw, prior, nowMs, { cooldownMs });
  if (nextState) scaleStates.set(metrics.productId, nextState);
  return recommendation;
}
