/**
 * sweep.js — the winnability sweep, shared by BOTH front-ends.  [Lane B, SPRINT10]
 *
 * There is exactly one copy of "is this site winnable" and this is it. audit.mjs
 * (node, fast, but blind to GLB-dressed anchors) and audit.html (browser, reads
 * the fully dressed createWorld(site).anchors) both import auditSweep and only
 * differ in how they GET the anchors and how they PRINT the result. A tool built
 * to catch reimplemented-formula drift must not carry two copies of its own math.
 *
 * Pure given its inputs: hand it resolved anchors (each {id, type, pos, sway}),
 * the bed rect, and the storm + calm-day defs. It flies the same settle+storm the
 * game flies and returns ranked rows + a verdict. No I/O, no process, no DOM.
 */

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

/** Audit knobs, in one place so both front-ends and any future site agree. */
export const AUDIT = {
  BAND: { lo: 18, hi: 45 },   // A's a.test rigging band, m²
  MIN_COVER: 0.25,            // A's "shades the bed" bar
  SETTLE_S: 12,               // D's settle — a player plays through prep
  PRE_GUST_S: 3,              // hold the settle inside gentle's pre-gust window (balance.test)
  SPARE_COST: 15,             // a $15 spare is the difference between a repair and a prayer
  CALM_STORM: 'storm_01_gentle',
};

/** Ground-plane area, shoelace over the ring — the same formula a.test uses. */
export function areaOf(q) {
  const r = orderRing(q);
  let a = 0;
  for (let i = 0, j = r.length - 1; i < r.length; j = i++) a += (r[j].pos.x + r[i].pos.x) * (r[j].pos.z - r[i].pos.z);
  return Math.abs(a / 2);
}

/** Cheapest hardware that holds a corner at `peakN` newtons, or null if the shop can't. */
export const tierFor = (peakN) => HARDWARE.find((h) => h.rating >= peakN) || null;

/**
 * Sweep every bed-covering quad and price the cheapest holding line.
 * @param {object} o
 * @param {Array}  o.anchors   resolved anchors: { id, type, pos:{x,y,z}, sway }
 * @param {object} o.bed       garden bed rect { x, z, w, d }
 * @param {object} o.stormDef  the storm JSON to fly
 * @param {object} o.calmDef   the calm-day JSON to settle on (storm_01_gentle)
 * @returns {{ cands, rows, winners, verdict:{ ok:boolean, code:string, best } }}
 */
export function auditSweep({ anchors, bed, stormDef, calmDef }) {
  // 1. every quad, in the rigging band, that shades the bed
  const cands = [];
  for (let a = 0; a < anchors.length; a++) for (let b = a + 1; b < anchors.length; b++)
    for (let c = b + 1; c < anchors.length; c++) for (let d = c + 1; d < anchors.length; d++) {
      const q = [anchors[a], anchors[b], anchors[c], anchors[d]];
      const area = areaOf(q);
      if (area < AUDIT.BAND.lo || area > AUDIT.BAND.hi) continue;
      let rig;
      try {
        rig = new SailRig({ anchors, gridN: 10 }).attach(q.map((x) => x.id), Array(4).fill(HARDWARE[2]), 1.0);
      } catch { continue; }                       // degenerate ring
      const cover = rig.coverageOver(bed, { x: 0, y: 1, z: 0 });
      if (cover >= AUDIT.MIN_COVER) cands.push({ ids: q.map((x) => x.id), area, cover });
    }

  if (!cands.length) return { cands, rows: [], winners: [], verdict: { ok: false, code: 'no-cover', best: null } };

  // 2. peak corner loads, settled the way the game settles, on the real storm.
  //    Every clause here is a bug the first draft shipped:
  //      · createWind + setSheltersFromTrees — trees knock a hole downwind.
  //      · settle on the CALM day on a RUNNING clock (main.js's prep), not storm
  //        wind frozen at t=0 (that read 1.94 kN of standing load vs the true 0.40).
  //      · resetPeaks() at entry — peakLoad is peak-since-ATTACH otherwise, folding
  //        the settle transient into the storm peak.
  const trees = anchors.filter((a) => a.type === 'tree');
  const wind = createWind(stormDef);
  wind.setSheltersFromTrees(trees);
  const calmWind = createWind(calmDef);
  calmWind.setSheltersFromTrees(trees);

  const rows = [];
  for (const cnd of cands) {
    // shade cloth (porosity 0.30): the fabric a competent player takes into a windy night
    const rig = new SailRig({ anchors, gridN: 10, porosity: 0.30 })
      .attach(cnd.ids, Array(4).fill({ name: 'audit', cost: 0, rating: Infinity }), 1.0);
    for (let i = 0, n = Math.round(AUDIT.SETTLE_S / FIXED_DT); i < n; i++) {
      rig.step(FIXED_DT, calmWind, (i * FIXED_DT) % AUDIT.PRE_GUST_S);
    }
    rig.resetPeaks();                                                     // ← the storm starts HERE
    for (let i = 0; i < stormDef.duration * 60; i++) rig.step(FIXED_DT, wind, i * FIXED_DT);

    const tiers = rig.corners.map((c) => ({ id: c.anchorId, peak: c.peakLoad, tier: tierFor(c.peakLoad) }));
    const unholdable = tiers.filter((c) => !c.tier);
    const hw = tiers.reduce((s, c) => s + (c.tier ? c.tier.cost : 0), 0);
    rows.push({ ...cnd, tiers, unholdable, hw, total: hw + AUDIT.SPARE_COST,
      affordable: !unholdable.length && hw <= START_BUDGET });
  }
  rows.sort((a, b) => (a.affordable === b.affordable ? a.hw - b.hw : a.affordable ? -1 : 1));

  const winners = rows.filter((r) => r.affordable);
  return {
    cands, rows, winners,
    verdict: winners.length
      ? { ok: true, code: 'pass', best: winners[0] }
      : { ok: false, code: 'unaffordable', best: rows[0] },
  };
}
