/**
 * Signal strain — the continuous half of THE SCREEN's vocabulary.
 *
 * Events give edges (brownout on/off); the snapshot gives quantities. Strain is what the
 * factory feels BEFORE the seizure: the reserve draining toward empty, the decoders running
 * hot. Strain is weather; the corruption ledger is climate. It stays deliberately subordinate.
 *
 * Pure and snapshot-shaped on purpose: no DOM, no GL, so the "healthy factory stays sterile"
 * guarantee is a unit test rather than a promise.
 */
import type { SimSnapshot } from '../contracts';

/**
 * The two dreads (v4). They are deliberately different questions:
 *
 *   tremor — "how many seconds until dark?"   ACUTE. The cliff edge. Only exists while
 *            something is actually draining, and it spikes as the runway runs out.
 *   pallor — "how much cushion do I have at all?"   CHRONIC. Sits there even in surplus.
 *            A full tank draining fast is a crisis; a near-empty tank coasting is a way of
 *            life, and it should feel like malaise rather than alarm.
 *
 * A factory with 4 seconds left and full tanks reads as a jolt; one permanently scraping the
 * bottom of an empty tank reads as washed-out and grim. Same bandwidth system, two feelings.
 */
export interface Strain {
  /** 0..1 tremor/desync judder as the bandwidth reserve runs out */
  tremor: number;
  /** 0..1 slow feverish shimmer from aggregate machine heat */
  fever: number;
  /** 0..1 pallor: how empty the tanks are, regardless of whether anything is draining */
  pallor: number;
}

export const NO_STRAIN: Strain = { tremor: 0, fever: 0, pallor: 0 };

/**
 * How much runway counts as calm. Reserve deeper than this reads as healthy; dread ramps up
 * as the factory's remaining seconds fall toward zero.
 */
export const STRAIN_HORIZON_SECONDS = 8;

/** Machines idling at a trace of heat are not a fever. */
const HEAT_FLOOR = 0.01;

const clamp01 = (x: number) => Math.min(1, Math.max(0, x));

/**
 * Read-only peek at the snapshot (contracts v3). The snapshot is never retained: everything
 * needed collapses to two scalars here and now.
 */
export function computeStrain(snap?: SimSnapshot): Strain {
  if (!snap) return NO_STRAIN;

  // --- tremor: seconds of reserve remaining at the current deficit.
  // Per the v3 units ruling, `stored` is bandwidth-SECONDS while gen/draw are bandwidth per
  // second — so stored/deficit is literally "how long until the lights go out".
  const { gen, draw, stored, capacity } = snap.bandwidth;
  const deficit = draw - gen;
  let tremor = 0;
  if (deficit > 0) {
    const secondsLeft = Math.max(0, stored) / deficit;
    tremor = 1 - clamp01(secondsLeft / STRAIN_HORIZON_SECONDS);
  }
  // A factory in surplus is calm no matter how little it has banked — that is what keeps
  // strain out of the NOTHING IS WRONG era, where gen and draw are both zero.

  // --- pallor: how empty the tanks are. Unlike tremor this does NOT care about draining;
  // it is the standing cost of living hand-to-mouth.
  // No tanks built = no cushion to miss = no pallor. That is not a special case for the boot
  // state, it is the honest reading: you cannot be low on a reserve you never built.
  const pallor = capacity && capacity > 0 ? 1 - clamp01(Math.max(0, stored) / capacity) : 0;

  // --- fever: how hot are the machines that actually run hot.
  // Averaging across every entity would drown a glowing decoder in a sea of cold belts.
  let sum = 0;
  let hot = 0;
  for (const e of snap.entities) {
    if (e.heat > HEAT_FLOOR) {
      sum += e.heat;
      hot++;
    }
  }
  const fever = hot > 0 ? clamp01(sum / hot) : 0;

  return { tremor, fever, pallor };
}
