/**
 * garden_bench — WHERE DOES THE PROTECTION ACTUALLY GO?  [Lane C, SPRINT13 gate 1]
 *
 * The QA pass found one variable sighted three times: the sim's garden outcome
 * barely responds to whether a sail is up (sail DEAD at t=3 on the funnelled
 * buster still finished 83%). A's GARDEN_DRAIN comment names the suspect and
 * says the lever is the shadow GEOMETRY, not the constant. This bench is the
 * measurement that has to come before any fix.
 *
 * It drives the REAL chain — real site JSON, real world.js yard, real SailRig,
 * real skyfx exposure helpers, main.js's exact drain arithmetic — because a
 * bench that reimplements the drain measures a copy and proves nothing (the
 * lesson balance.test.js paid for twice: the fictional yard, and the missing
 * camera).
 *
 * For each yard × storm it flies two conditions and reports where they differ:
 *   HELD  — an honest bed-covering quad on RATED steel, re-repaired every step
 *           so a corner failure can never contaminate a GEOMETRY measurement
 *   BARE  — no sail at all, the floor
 * ...and buckets rain/hail shadow-over-bed by wind speed, which is the actual
 * question: protection is not one number, it decays as the wind builds.
 *
 * Deliberately a browser tool: the scoring chain needs `document`. Run it at
 * tools/garden_bench/bench.html; it prints a table and dumps JSON to console.
 */

import * as THREE from '../../web/world/vendor/three.module.js';
import { RiggingSession } from '../../web/world/js/rigging.js';
import { SailRig } from '../../web/world/js/sail.js';
import { createSkyFx } from '../../web/world/js/skyfx.js';
import { loadStorm, windForSite } from '../../web/world/js/weather.js';
import { createWorld, loadSite } from '../../web/world/js/world.js';
import { HARDWARE, FIXED_DT } from '../../web/world/js/contracts.js';

const [CARABINER, SHACKLE, RATED] = HARDWARE;

// main.js's exact numbers. Duplicated ONLY so a divergence names itself; the
// bench asserts they still match main.js at the bottom of the report.
const GARDEN_DRAIN = 0.9;
const W_HAIL = 5.0;
const W_RAIN = 0.25;

export const STORMS = [
  'storm_01_gentle', 'storm_02_wildnight', 'storm_03_southerly',
  'storm_03b_earlybuster', 'storm_02b_icenight',
];

/**
 * The honest bed-covering line in each yard — NOT the carport trap, NOT a rig
 * that misses. backyard's is balance.test's COVER_QUAD (C's sweep, the only
 * buyable one). site_02's is the site JSON's own _design note: the honest three
 * posts + the gum tree, 58% cover at 32.6 m².
 */
export const YARDS = [
  { site: 'backyard_01', quad: ['p1', 'p2', 'p3', 'p4'] },
  { site: 'site_02_corner_block', quad: ['q1', 'q2', 'q3', 'tr1'] },
];

/**
 * The real yard, read from world.js — never a copied anchor table, and never a
 * FROZEN one. Two landmines this builder now closes (SPRINT13, both caught by
 * D replaying through the real UI):
 *   1. The SITE def rides along because its wind block is half the yard's
 *      weather: the venturi lives HERE, not in any storm def. Measured without
 *      it, site_02 is a different, easier yard (third harness to learn this;
 *      windForSite is the fix and the story).
 *   2. The anchors are world.anchors THEMSELVES. The old remap to a static
 *      `sway: () => pos` froze the gum tree — a rig on a tree eats the tree's
 *      sway as dynamic load, and stripping it under-read every tr1 corner.
 *      The world is built on a re-pointable wind proxy (main.js-router style)
 *      so the sway closures sample whichever storm is currently flying.
 */
async function buildYard(siteId) {
  const scene = new THREE.Scene();
  let current = {
    sample: (p, t, o) => (o || new THREE.Vector3()).set(0, 0, 4),
    speedAt: () => 4, rainAt: () => 0, rainMmPerHour: () => 0,
    gustTelegraph: () => null, eventsBetween: () => [],
  };
  const proxy = {
    sample: (p, t, o) => current.sample(p, t, o || new THREE.Vector3()),
    speedAt: (p, t) => current.speedAt(p, t),
    rainAt: (t) => current.rainAt(t),
    rainMmPerHour: (t) => (current.rainMmPerHour ? current.rainMmPerHour(t) : 0),
    gustTelegraph: (t) => current.gustTelegraph(t),
    eventsBetween: (a, b) => current.eventsBetween(a, b),
    setSheltersFromTrees() {},
    get seed() { return current.seed ?? 1; },
    get def() { return current.def ?? {}; },
  };
  const siteDef = await loadSite(siteId);
  const world = createWorld(scene, { wind: proxy, site: siteDef });
  if (world.dress) { try { await world.dress(); } catch { /* graybox anchors are enough */ } }
  const anchors = world.anchors;
  return {
    anchors, bed: world.gardenBed, ids: anchors.map((a) => a.id),
    sunDir: world.sunDir, heightAt: world.heightAt,
    siteDef,
    use: (w) => { current = w; },
  };
}

/**
 * Fly one condition and record the geometry as a time series.
 * @param {'held'|'bare'} mode
 */
async function fly(yard, stormName, mode) {
  const def = await loadStorm(stormName);
  // windForSite, NOT createWind + storm-def venturi. The old line here read
  // `def.wind.venturi` off the STORM def, where a venturi never lives — it
  // LOOKED right and never fired, so every site_02 number this bench produced
  // before 2026-07-18 was flown with the funnel OFF (D caught it replaying the
  // $80 line through the real UI: 91.5 FULL became 39.8 TATTERED). Same bug
  // B's site_audit shipped in Sprint 11. The helper carries the story.
  const wind = windForSite(def, yard.siteDef, yard.anchors);
  yard.use(wind);   // point the yard's tree-sway closures at this flight's wind

  let rig = null;
  if (mode === 'held') {
    const s = new RiggingSession({ anchors: yard.anchors, budget: 9999 });
    for (const id of yard.quad) {
      const r = s.rig(id);
      if (!r.ok) return { skipped: `cannot rig ${id}: ${r.reason ?? 'refused'}` };
    }
    // RATED on every corner: this is a geometry bench, so buy the strongest
    // steel in the game and ignore the $80 budget on purpose. Money is B's
    // question; this one is "where does the shadow land".
    for (const id of yard.quad) { const r = s.setHardware(id, RATED);
      if (!r.ok) throw new Error(`setHardware ${id} refused: ${r.reason} — flying hardware it did not buy`); }
    s.setTension(1.0);
    rig = s.commit(new SailRig({ anchors: yard.anchors, gridN: 10 }));
  }

  // NO calm settle (correction, 2026-07-18): in the real game the rig does not
  // exist before commit, and commit → attach → advance() → storm land in one
  // keypress (sail.js attach, B's clock-reset comment). The attach transient
  // rides the storm's own calm opening exactly as it does for a player — and
  // because SailRig samples wind at its INTERNAL clock, a 12 s settle here
  // skewed every storm sample 12 s off the authored curve.

  const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
  camera.position.set(0, 2, 8);
  const sky = createSkyFx({ wind, night: true, camera });

  const bed = yard.bed;
  const probe = new THREE.Vector3(bed.x, 1.7, bed.z);
  let hp = 100, byHail = 0, byRain = 0, repairs = 0;
  const rows = [];

  const steps = Math.round(def.duration / FIXED_DT);
  for (let i = 0; i < steps; i++) {
    const t = i * FIXED_DT;
    if (rig) {
      rig.step(FIXED_DT, wind, t);
      // HELD means held. A corner that lets go turns this into a hardware
      // measurement instead of a geometry one, so put it straight back.
      for (let k = 0; k < rig.corners.length; k++) {
        if (rig.corners[k].broken) { rig.repairCorner(k, RATED); repairs++; }
      }
    }
    sky.step(FIXED_DT, t, rig ? { sail: rig } : {});

    const hailExp = sky.gardenHailExposure(bed, t);
    const rainExp = sky.gardenExposure(bed, t);
    // main.js's exact drain
    const dHail = W_HAIL * hailExp * GARDEN_DRAIN * FIXED_DT;
    const dRain = W_RAIN * rainExp * GARDEN_DRAIN * FIXED_DT;
    const total = Math.min(hp, dHail + dRain);
    if (total > 0) {
      const scale = total / (dHail + dRain);
      byHail += dHail * scale; byRain += dRain * scale;
      hp -= total;
    }

    // sample the geometry a few times a second — the grids only rebuild at 10 Hz
    if (i % 6 === 0) {
      rows.push({
        t,
        speed: wind.speedAt(probe, t),
        rainShadow: sky.rainShadowOver(bed),
        hailShadow: sky.hailShadowOver(bed),
        rainExp, hailExp,
        hail: sky.hailAmount,
      });
    }
  }
  sky.dispose?.();
  return {
    hp: Math.round(hp * 10) / 10,
    byHail: Math.round(byHail * 10) / 10,
    byRain: Math.round(byRain * 10) / 10,
    repairs,
    rows,
    // the SUN coverage — reported for context only. It is emphatically not the
    // rain/hail geometry, and the gap between them is half of what this bench
    // exists to show.
    cover: rig ? rig.coverageOver(bed, yard.sunDir, yard.heightAt) : 0,
  };
}

/** Bucket a time series by wind speed — protection is not one number. */
function byWind(rows) {
  const buckets = [[0, 10], [10, 15], [15, 20], [20, 25], [25, 99]];
  return buckets.map(([lo, hi]) => {
    const r = rows.filter((x) => x.speed >= lo && x.speed < hi);
    if (!r.length) return { band: `${lo}-${hi}`, n: 0 };
    const mean = (f) => r.reduce((a, b) => a + f(b), 0) / r.length;
    return {
      band: `${lo}-${hi}`,
      n: r.length,
      kmh: `${Math.round(lo * 3.6)}-${Math.round(hi * 3.6)}`,
      rainShadow: mean((x) => x.rainShadow),
      hailShadow: mean((x) => x.hailShadow),
    };
  }).filter((b) => b.n > 0);
}

const f2 = (v) => (v == null ? '   —  ' : v.toFixed(2).padStart(6));
const f1 = (v) => (v == null ? '  — ' : v.toFixed(1).padStart(5));

export async function runBench(log) {
  const out = [];
  log('# GARDEN BENCH — where does the protection actually go?');
  log('# HELD = honest bed-covering quad, RATED steel, re-repaired every step (geometry only)');
  log('# BARE = no sail. Drain is main.js exact: hail×5.0 + rain×0.25, ×0.9.\n');

  for (const y of YARDS) {
    const yard = await buildYard(y.site);
    // Print the funnel in the header, B's audit.html pattern: a reader must be
    // able to SEE which wind these numbers were flown in.
    const vl = yard.siteDef.wind?.venturi ?? [];
    const vtxt = vl.length
      ? vl.map((v) => `throat (${v.x},${v.z}) gain ${v.gain} r${v.radius}`).join(' · ')
      : 'none';
    log(`\n## ${y.site}   bed ${yard.bed.w}×${yard.bed.d} at (${yard.bed.x}, ${yard.bed.z})   line ${y.quad.join('+')}   venturi: ${vtxt}`);

    for (const storm of STORMS) {
      const held = await fly({ ...yard, quad: y.quad }, storm, 'held');
      if (held.skipped) { log(`  ${storm}: SKIPPED — ${held.skipped}`); continue; }
      const bare = await fly({ ...yard, quad: y.quad }, storm, 'bare');
      const sep = bare.hp === 0 && held.hp === 0 ? 0 : held.hp - bare.hp;
      out.push({ site: y.site, storm, held, bare, sep });

      log(`\n  ### ${storm}   cover ${(held.cover * 100).toFixed(0)}%   repairs ${held.repairs}`);
      log(`      GARDEN HP    held ${f1(held.hp)}   bare ${f1(bare.hp)}   SEPARATION ${f1(sep)}`);
      log(`      held  damage: hail ${f1(held.byHail)}  rain ${f1(held.byRain)}`);
      log(`      bare  damage: hail ${f1(bare.byHail)}  rain ${f1(bare.byRain)}`);
      log('      wind band     km/h      rainShadow(held)   hailShadow(held)');
      for (const b of byWind(held.rows)) {
        log(`      ${b.band.padStart(6)} m/s  ${b.kmh.padStart(8)}      ${f2(b.rainShadow)}             ${f2(b.hailShadow)}`);
      }
    }
  }

  log('\n\n# THE HEADLINE');
  log('# site                        storm                    held   bare    sep');
  for (const r of out) {
    log(`# ${r.site.padEnd(26)} ${r.storm.padEnd(22)} ${f1(r.held.hp)}  ${f1(r.bare.hp)}  ${f1(r.sep)}`);
  }
  console.log('GARDEN_BENCH_JSON', JSON.stringify(out.map((r) => ({
    site: r.site, storm: r.storm, held: r.held.hp, bare: r.bare.hp, sep: r.sep,
    heldHail: r.held.byHail, heldRain: r.held.byRain,
    bareHail: r.bare.byHail, bareRain: r.bare.byRain,
    cover: r.held.cover,
  }))));
  return out;
}
