/**
 * gardenfly.js — fly a rig line through the REAL scoring chain and return the
 * garden verdict the sim would hand the invoice.  [Lane B, SPRINT13 gate 1.2]
 *
 * This module exists because the audit's old ranking quantity — static cover%
 * of the taut attach-shape — predicted the wild night's garden BACKWARDS
 * (r = −0.42, THREADS [B] gate-1 evidence): it told three people a $20 rig was
 * bad while the sim paid it +$97. The quantity that scores is the FLOWN
 * vertical (hail) shadow through the storm: hail is 5.0 against rain's 0.25
 * (garden.js), stones fall near-vertical, and a sail either blocks the hail
 * over the bed or blocks nothing that matters. Only flying the real chain
 * measures that, so this is the audit's scoring engine now; cover% is demoted
 * to a geometry diagnostic in both front-ends.
 *
 * THE chain, one copy of it, shared by audit.html and garden_probe.html:
 *
 *   windForSite(stormDef, siteDef, anchors)          (C's shared builder —
 *     venturi + tree shelters, byte-for-byte main.js's site-load wiring)
 *     → SailRig.attach(ids, hw, tension)             (the real attach)
 *     → rig.step + sky.step(dt, t, {sail: rig})      (main.js's exact call)
 *     → createGarden(...).step(dt, hailExp, rainExp) (A's garden.js, no copy)
 *
 * NO calm settle, deliberately: in the real game the rig does not exist before
 * commit, and commit→attach→storm is one keypress (the rig.t fix's own words),
 * so the attach transient flies under STORM wind and counts. C measured the
 * phantom settle's skew and removed it from the bench the same day.
 *
 * Landmines this file exists to never re-arm (each earned this sprint):
 *
 *  · D's skipped-attach trap (skyfx.js:803): a harness that never runs
 *    attach() scores EVERY rig as bare — the bare-bed constant lies politely.
 *    flyGarden throws if the rig it built has no shadow mesh.
 *  · The venturi lives in the SITE def, not the storm def. THREE harnesses
 *    shipped that bug (my Sprint-11 audit, C's bench, D's first harness half);
 *    windForSite is the one door now, and the selftest mutation-checks that
 *    the funnel reaches this module's wind.
 *  · The FROZEN TREE (C's landmine 2): remapping anchors to `sway: () => pos`
 *    freezes the gum tree whose sway is the dynamic load a tr1 rig eats —
 *    q4 read 1.02 frozen vs 1.24 live. Callers hand this module
 *    world.anchors THEMSELVES and re-point the world's wind proxy via `use`
 *    so the sway closures sample the storm that is actually flying.
 *  · THE MARGIN RULE (C's residual): even corrected, benches under-read the
 *    real UI's peaks by ~5–15% on site_02 (cause unfound, in the pool). Until
 *    it is found, any corner within 15% of its effective rating is scored
 *    "breaks in the game" — a flight that holds on paper with q4 at 1.24 vs
 *    1.2 is D's 39.8 TATTERED in play, not the bench's 91.9 FULL. flyGarden
 *    reports `marginal` and callers must not print PASS over it.
 *
 * Per-corner peaks are labelled from `corners[k].anchorId`, never input order —
 * C's label-permutation warning (attach reorders picks into ring order).
 *
 * Browser-only by necessity: skyfx needs `document` (node throws), which is
 * why audit.mjs (node) prints winnability and POINTS HERE for garden truth.
 */

import { SailRig } from '../../web/world/js/sail.js';
import { windForSite } from '../../web/world/js/weather.js';
import { createSkyFx } from '../../web/world/js/skyfx.js';
import { createGarden } from '../../web/world/js/garden.js';
import { HARDWARE, FIXED_DT } from '../../web/world/js/contracts.js';
import { AUDIT } from './sweep.js';

// The margin rule's knob is AUDIT.MARGIN — sweep.js holds the ONE copy and
// both audit engines read it (a local copy here would be the exact drift this
// sprint spent itself killing).

/** Shop hardware by its contracts.js name ("rated shackle") — throws on a
 *  stranger, because a typo'd tier silently becoming a carabiner is exactly
 *  the setHardware() lesson from gate 3. */
export function hardwareByName(name) {
  const hw = HARDWARE.find((h) => h.name === name);
  if (!hw) throw new Error(`unknown hardware "${name}" — the shop sells: ${HARDWARE.map((h) => h.name).join(', ')}`);
  return hw;
}

/**
 * Fly one line (or a bare bed) and return the garden verdict.
 *
 * @param {object} o
 * @param {Array}  o.anchors   world.anchors THEMSELVES (live sway), or resolved
 *                            {id, type, pos, sway, ratingHint} for synthetic yards
 * @param {object} o.bed       garden bed rect {x, z, w, d}
 * @param {object} o.stormDef  storm JSON to fly
 * @param {object} [o.siteDef] site JSON — its wind.venturi is half the yard's
 *                            weather; omit ONLY for a site with no funnel
 * @param {function} [o.use]   yard's wind-proxy re-pointer (C's bench pattern):
 *                            called with this flight's wind so world.anchors'
 *                            tree-sway closures sample the storm actually flying
 * @param {Array}  [o.ids]     4 anchor ids; null/omitted = bare bed (the control)
 * @param {Array|object} [o.hw] hardware per pick (aligned with ids), or one tier for all 4
 * @param {number} [o.tension]
 * @returns {{ hp, state, byHail, byRain, cornersLost, peaks, marginal, cost }}
 */
export function flyGarden({ anchors, bed, stormDef, siteDef = null, use = null, ids = null, hw = null, tension = 1.0 }) {
  const wind = windForSite(stormDef, siteDef, anchors);
  use?.(wind);

  let rig = null, cost = 0;
  if (ids) {
    const hwArr = Array.isArray(hw) ? hw : Array(4).fill(hw ?? hardwareByName('rated shackle'));
    cost = hwArr.reduce((s, h) => s + (h.cost || 0), 0);
    // shade cloth (porosity 0.30): the fabric a competent player flies — same as sweep.js
    rig = new SailRig({ anchors, gridN: 10, porosity: 0.30 }).attach(ids, hwArr, tension);
    // D's skipped-attach trap: a rig without its shadow mesh scores as a bare
    // bed and the number LOOKS plausible. Refuse to fly it.
    if (!rig.rigged || !rig.pos || !rig.tris || !rig.tris.length) {
      throw new Error('flyGarden: rig is not attached (no shadow mesh) — a bypassed attach scores ' +
        'every rig as bare (skyfx.js:803, D\'s landmine). Fix the harness, do not trust the number.');
    }
  }

  const sky = createSkyFx({ wind, night: true });
  const garden = createGarden({ setPlants() {} });   // THE model — garden.js, main.js's own

  const n = Math.round(stormDef.duration / FIXED_DT);
  for (let i = 0; i < n; i++) {
    const t = i * FIXED_DT;
    if (rig) rig.step(FIXED_DT, wind, t);
    sky.step(FIXED_DT, t, { sail: rig });            // main.js's exact third param
    garden.step(FIXED_DT, sky.gardenHailExposure(bed, t), sky.gardenExposure(bed, t));
  }
  sky.dispose?.();

  // labelled from the rig's OWN anchorId — attach reorders picks into ring
  // order, and a peak quoted against input order swaps corners (C's warning).
  const peaks = rig ? rig.corners.map((c) => {
    const eff = c.hw.rating * (c.anchor.ratingHint ?? 1);
    return { id: c.anchorId, peakN: Math.round(c.peakLoad), effN: Math.round(eff),
      headroom: +(1 - c.peakLoad / eff).toFixed(3) };
  }) : [];

  return {
    hp: +garden.hp.toFixed(1),
    state: garden.state,                             // 'full' | 'tattered' | 'dead' — garden.js's own thresholds
    byHail: +garden.damage.hail.toFixed(1),
    byRain: +garden.damage.rain.toFixed(1),
    cornersLost: rig ? rig.corners.filter((c) => c.broken).length : 4,
    peaks,
    // C's margin rule: corners that held on paper but sit within MARGIN of
    // their effective rating — in the real game these break. A prediction
    // carrying names here is NOT a clean hold, whatever the hp says.
    marginal: peaks.filter((p) => !rig.corners.find((c) => c.anchorId === p.id).broken && p.headroom < AUDIT.MARGIN)
      .map((p) => p.id),
    cost,
  };
}

/**
 * Judge a site against its pinned `separation` block (A's gate-1.4 ruling,
 * backyard_01.json): fly the pinned recipe AND a bare bed on the block's own
 * storm, and answer the only question the target asks — does the best line the
 * budget buys read FULL and WIN, and does a bare bed LOSE the night.
 *
 * `heldWin` is main.js's win rule (hp >= 50 && corners lost < 2) — EXACTLY the
 * rule, so this stays in lockstep with a.test's pin of the same block.
 * `heldClean` is the margin rule's opinion, kept SEPARATE from `ok` on
 * purpose: the pinned target is A's ruling and this module doesn't get to
 * overrule it with a working rule whose cause is still unfound — but a pinned
 * line carrying a corner inside the 15% band is a knife edge the front-end
 * must SAY (it's a flag for A/D, not a verdict).
 *
 * @returns {{ held, bare, heldOk, heldWin, heldClean, bareOk, ok }}
 */
export function flySeparation({ anchors, bed, separation, stormDef, siteDef = null, use = null }) {
  const ids = separation.line.map((l) => l.anchor);
  const hw = separation.line.map((l) => hardwareByName(l.hw));
  const tension = separation.tension ?? 1.0;
  const held = flyGarden({ anchors, bed, stormDef, siteDef, use, ids, hw, tension });
  const bare = flyGarden({ anchors, bed, stormDef, siteDef, use });
  const heldOk = held.hp > separation.heldMustExceed;
  const heldWin = held.hp >= 50 && held.cornersLost < 2;
  const heldClean = held.marginal.length === 0;
  const bareOk = bare.hp < separation.bareMustLoseBelow;
  return { held, bare, heldOk, heldWin, heldClean, bareOk, ok: heldOk && heldWin && bareOk };
}
