/**
 * envelope.js — the STORM-side failure envelope, per anchor.  [Lane C, SPRINT12]
 *
 * SPRINT12 gate 2.4: B measures the post-ratingHint failure envelope through the
 * cloth sim (site_audit: settle, fly the storm, read peak corner loads in N).
 * This is the SECOND harness on the same envelope, measured from the other side:
 * no cloth, no rig, no tension — just what the STORM delivers at each anchor's
 * position, through the same wind everyone samples (createWind + the site's
 * venturi + the tree shelters, exactly as main.js wires them at site load).
 *
 * Two harnesses, one number — the repo rule. What must agree:
 *   · ORDERING: anchors ranked by wind-side pressure/hint should rank the same
 *     way B's peak corner loads do, once quad geometry is accounted for. An
 *     anchor whose load outranks its wind exposure has a variable to find.
 *   · TIMING: B's peak corner load should land near the wind-side tPeak (cloth
 *     lag is under a second; the venturi's alignment window is tens of seconds).
 * What will NOT agree, by construction: absolute newtons. Corner load carries
 * quad geometry, cloth porosity, tension and rain mass — all B's side. This
 * harness deliberately knows none of it, which is what makes it independent.
 *
 * Per anchor, per storm:
 *   peak     highest local horizontal wind speed over the storm, m/s
 *            (wind.speedAt — the anemometer number, same units as every probe
 *             table in THREADS)
 *   tPeak    when it happened, s
 *   peakPa   peak dynamic pressure, Pa: ½ρ·v²·(1+downFrac²) — the (1+d²) folds
 *            the downdraft back in, since cloth feels the full vector while
 *            speedAt deliberately reads horizontal-only
 *   dose     ∫ v² dt over the whole storm, m²/s — exposure × time, so a long
 *            grind and a single spike stop looking alike
 *   paPerHint  peakPa / ratingHint — the wind-side "who blows first" ranking.
 *            B's wiring is `load > hw.rating * ratingHint`, so dividing the
 *            delivered pressure by the hint predicts failure ORDER for equal
 *            hardware and equal geometry. Prediction, not measurement — B's
 *            harness is the one that owns the geometry.
 *
 * Sampling matches sweep.js's flight exactly: FIXED_DT steps at t = i·dt over
 * the storm's duration, same seed path (createWind(def) — def.seed), so the two
 * harnesses fly the SAME storm, not merely the same JSON.
 */

import { createWind } from '../../web/world/js/weather.js';
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';

export const ENVELOPE = {
  RHO: 1.225,          // kg/m³, sea-level air — the constant, named once
  DT: FIXED_DT,
};

/**
 * Measure the storm at every anchor.
 *
 * @param {object} o
 * @param {Array}  o.anchors   resolved anchors: { id, type, pos:{x,y,z}, ratingHint? }
 *                             — pass the DRESSED world.anchors (browser) or a
 *                             verified dump; graybox positions measure a yard
 *                             that does not ship (site_audit's lesson).
 * @param {object} o.stormDef  parsed storm JSON
 * @param {Array}  [o.venturi] the SITE's funnel zones (siteDef.wind.venturi) —
 *                             a sweep without it flies an easier yard than ships
 * @param {Array}  [o.probes]  extra { id, pos } points (bed centre, throat…)
 *                             measured alongside, hint fixed at 1
 * @param {number} [o.dt]
 * @returns {{ rows, downFrac, duration }} rows sorted by paPerHint, descending
 */
export function stormEnvelope({ anchors, stormDef, venturi = [], probes = [], dt = FIXED_DT }) {
  const wind = createWind(stormDef);
  wind.setVenturi(venturi);
  // exactly main.js:447 — every tree anchor casts a shadow, defaults and all
  wind.setSheltersFromTrees(anchors.filter((a) => a.type === 'tree'));
  const downFrac = wind.core.downFrac;
  const vecFold = 1 + downFrac * downFrac;   // |v|² = h²·(1+d²) when vy = -d·h

  const rows = [
    ...anchors.map((a) => ({
      id: a.id, type: a.type, probe: false,
      hint: Number.isFinite(a.ratingHint) ? a.ratingHint : 1,
      pos: { x: a.pos.x, z: a.pos.z },
      peak: 0, tPeak: 0, dose: 0,
    })),
    ...probes.map((p) => ({
      id: p.id, type: 'probe', probe: true, hint: 1,
      pos: { x: p.pos.x, z: p.pos.z },
      peak: 0, tPeak: 0, dose: 0,
    })),
  ];

  const duration = stormDef.duration ?? 90;
  const n = Math.round(duration / dt);
  for (let i = 0; i <= n; i++) {
    const t = i * dt;
    for (const r of rows) {
      const s = wind.speedAt(r.pos, t);
      if (s > r.peak) { r.peak = s; r.tPeak = t; }
      r.dose += s * s * dt;
    }
  }

  for (const r of rows) {
    r.peakPa = 0.5 * ENVELOPE.RHO * r.peak * r.peak * vecFold;
    r.paPerHint = r.peakPa / (r.hint > 0 ? r.hint : 1);
    // what B's wiring makes each shop tier hold HERE: rating × hint, in N
    r.effN = HARDWARE.map((h) => h.rating * r.hint);
  }
  rows.sort((a, b) => b.paPerHint - a.paPerHint);
  return { rows, downFrac, duration };
}
