/**
 * broom.js — DESIGN.md's funniest correct mechanic.  (Lane D, SPRINT5 §Lane D-1)
 *
 * A flat sail pools water. The water is heavier than anything else in the game and it will pull the
 * rig down. The fix is a bloke with a broom walking under the belly and poking it upward — at which
 * point forty kilos of cold water arrives on his head. That is both the correct engineering answer
 * and the joke, and they are the same thing, which is the best kind of mechanic.
 *
 * Everything here defers to the asset. E baked the intent into broom_01_v1.glb:
 *   grip_anchor  extras.carry_type = "broom"
 *   poke_tip     extras.use = "push the pond up from under the sail; soft end, won't hole the cloth"
 *                — on the BRISTLE end, deliberately: a broomstick jabbed at a loaded sail holes it.
 *   root         extras.anim_hint = "reuse Crank/Dig for the poke — no new Mixamo needed"
 * so the carry type, the working end and the animation are all read, not invented.
 *
 * Seam with Lane B (posted in THREADS before either of us built): sailRig.ponds -> [{node,mass,pos}]
 * and drainPondAt(node) -> kg dumped. Duck-typed: with no ponding landed the broom still carries,
 * walks and refuses to poke thin air, and the moment B lands it the whole thing lights up.
 */
// No top-level THREE/GLTFLoader import, deliberately: the vendored addons import the bare specifier
// 'three', which only resolves under index.html's importmap — so importing them here would drag the
// whole GL chain into d.test.js and cost this lane its headless suite (node resolves relative paths
// only). The pond logic below is the part worth asserting and it is pure; the view is loaded
// dynamically, which never happens outside a browser.
export const BROOM_URL = './models/broom_01_v1.glb';

export const BROOM_TUNE = {
  pokeSecs: 1.5,            // matches B's "drain over ~1.5 s"
  reachUp: 3.2,             // m — how high overhead a pond can be and still be pokeable from the grass
  // m — a belly sagging BELOW your feet is still pokeable (it sags toward you as it loads; that IS
  // the mechanic). Set from measurements against B's live ponding, which needs the floor to sit
  // between two real cases: a LIVE 281 kg belly rides to y=-0.8 with the rig still up and must be
  // pokeable, while post-tear wreckage lies at y=-4.5 and has nothing coherent to poke. -1.5 splits
  // them with room either side. (-0.6 refused the 281 kg pond — measured, not theorised.)
  sagFloor: -1.5,
  standRadius: 2.0,         // m — how near the pond's ground shadow you must be
  drainRadius: 2,           // grid radius B's drainPondAt tapers over (its default; explicit here)

  // CALIBRATED against what a poke actually SHEDS — not against the pond's total mass, which is the
  // mistake my Sprint-5 guesses (15/60/120) and my first pass at these (60/150/350) both made.
  // Two measurements, one from Lane B and one taken here against their live sim:
  //   · B: a 1.5 s poke on a loaded ~310 kg belly sheds ~290 kg.
  //   · here: a real mid-storm 169 kg pond sheds ~106 kg per poke.
  // (The shed can exceed the pond's net change — rain keeps refilling while you poke. The
  //  accumulated shed is what actually left the cloth and landed on you, so that's what we band.)
  // Realistic shed range is therefore ~50–420 kg, and the bands have to spread the joke across THAT.
  //
  // The gradient is the mechanic, not just the gag. Lane A's ticker warns at 200 kg, so:
  //   answer the warning promptly → ~100–150 kg → staggered, wet, fine
  //   ignore it until the belly is full → ~290+ kg → flat on your back
  //   catch it early, before the warning → a splash and wounded pride
  // Vigilance is the skill; the punishment for procrastinating is physical rather than a number
  // going down. NOTE: gate 1's balance pass moves pond masses — d.test.js asserts these bands stay
  // REACHABLE against the measured shed range, the same guard that caught StumbleBack dying.
  // Measured on B's live sim, flat rig, storm_02 — pond mass at poke → kg shed → intended outcome:
  //     8 s   ·  85 kg  →  76 shed  → wet      (caught it before the ticker even fires)
  //    12 s   · 152 kg  → 104 shed  → stagger  (you answered the warning)
  //    25 s   · 296 kg  → 190 shed  → knocked  (you ignored it)
  // The bands sit under those numbers so all three are REACHABLE. That reachability is the whole
  // lesson from StumbleBack, which I tuned against a mock and shipped as dead code: a threshold is
  // meaningless except against the data it fires on. d.test.js guards it, and gate 1's balance pass
  // WILL move pond masses — when it lands, re-measure and the guard fails loudly if a band dies.
  splashKg: 55,             // below this it's a splash — FX only; the state machine ignores it
  staggerKg: 100,           // a real poke on a pond you were warned about
  knockKg: 180,             // a belly you ignored, coming down all at once
};

/**
 * @param {THREE.Object3D} scene
 * @param {object} world     contracts World (dressed)
 * @param {object} interact  Lane D's Interact
 * @param {object} player    PlayerSim
 * @param {object} getRig    () => sailRig — a getter, because rigSail() REPLACES the rig object
 */
export function createBroom(scene, world, interact, player, getRig) {
  const state = { carried: false, view: null, tune: { ...BROOM_TUNE }, pokeKg: 0 };

  // Home: against the shed wall. E ships it standing on its head, which is how it lives there.
  const home = { x: 8.2, y: 0, z: 7.0 };
  if (world.shedTable && world.shedTable.pos) {
    home.x = world.shedTable.pos.x - 0.8;
    home.z = world.shedTable.pos.z + 1.0;
  }
  home.y = world.heightAt ? world.heightAt(home.x, home.z) : 0;

  if (scene) {
    import('../vendor/addons/loaders/GLTFLoader.js').then(({ GLTFLoader }) => {
      new GLTFLoader().load(BROOM_URL, (g) => {
        g.scene.traverse((o) => { if (o.isMesh) { o.castShadow = true; o.frustumCulled = false; } });
        state.view = g.scene;
        scene.add(g.scene);
        sync();
      }, undefined, () => { /* no asset: the mechanic still runs, you just can't see the broom */ });
    }).catch(() => { /* headless (selftest/node): logic only, no view */ });
  }

  function sync() {
    if (!state.view) return;
    state.view.visible = !state.carried;
    state.view.position.set(home.x, home.y, home.z);
    state.view.rotation.set(0, 0.9, 0.16);   // slouched against the shed wall
  }

  /**
   * Lane B's frozen contract (contracts.js SailRig): pondCentroid() -> {x,y,z,mass,node}|null.
   * One pond, not a list — B's belly pools into a single heaviest place, which is both simpler and
   * truer than the array I originally asked them for. Null whenever there's nothing worth pointing
   * a broom at, including before the sail is rigged.
   */
  const pond = () => {
    const rig = getRig && getRig();
    if (!rig || typeof rig.pondCentroid !== 'function') return null;
    const c = rig.pondCentroid();
    return c && c.mass > 0 ? c : null;
  };

  /**
   * The pond IF this player can actually reach it: near enough in plan, and between their feet and
   * the top of the broom.
   *
   * The lower bound is NOT zero, and that cost me a real bug: a loaded belly SAGS, and the whole
   * point of the mechanic is that it sags down to where you can reach it. Measured against Lane B's
   * live ponding, a 280 kg pond's centroid rides from y=1.2 down through y=-0.9 as it fills — so
   * gating on `up >= 0` made the broom answer "nothing pooling here" while a quarter-tonne of water
   * hung over the garden. Sagging toward you is the mechanic working, not a reason to refuse.
   * Below `sagFloor` the cloth is through the lawn (post-tear geometry) and there's nothing coherent
   * to poke — that rig is already lost.
   */
  function targetPond() {
    const c = pond();
    if (!c) return null;
    const d = Math.hypot(c.x - player.pos.x, c.z - player.pos.z);
    const up = c.y - (player.pos.y + player.climbY);
    if (d > state.tune.standRadius) return null;
    if (up > state.tune.reachUp || up < state.tune.sagFloor) return null;
    return c;
  }

  /** Where to stand: the pond's shadow on the grass. */
  const pokeSpot = () => {
    const c = pond();
    return c ? { x: c.x, y: 0, z: c.z } : null;
  };

  const wired = [];

  // 1. take the broom off the shed wall — a third carry type, so it queues behind the same hands
  wired.push(interact.register({
    id: 'broom_take',
    pos: () => (state.carried ? null : home),
    radius: 1.5,
    holdSecs: 0.7,
    clip: 'PickUp',
    label: (p) => (p.carrying ? 'hands full' : 'take the broom'),
    canUse: (p) => !state.carried && !p.carrying && p.climbY < 0.02,
    onDone: (p, t) => { state.carried = true; p.pickUp('broom', t); sync(); },
  }));

  // 2. put it back
  wired.push(interact.register({
    id: 'broom_drop',
    pos: () => (state.carried ? { x: home.x, y: home.y, z: home.z } : null),
    radius: 1.5,
    holdSecs: 0.4,
    clip: 'PickUp',
    label: 'put the broom back',
    canUse: (p) => state.carried && p.carrying === 'broom',
    onDone: (p, t) => { state.carried = false; p.drop(t); sync(); },
  }));

  // 3. THE POKE. Stand under the belly, push up, wear it.
  wired.push(interact.register({
    id: 'broom_poke',
    pos: pokeSpot,
    radius: state.tune.standRadius,
    holdSecs: state.tune.pokeSecs,
    clip: 'Crank',                      // E's anim_hint — no new Mixamo needed
    label: (p) => {
      if (p.carrying !== 'broom') return 'you need the broom';
      const c = targetPond();
      if (!c) return 'nothing pooling here';
      return `push the water off (${Math.round(c.mass)} kg)`;
    },
    // physical gates only — never player.state (see interact.register's note; it cancels its own hold)
    canUse: (p) => {
      const rig = getRig && getRig();
      return p.carrying === 'broom' && !!(rig && rig.drainPondAt) && !!targetPond();
    },
    // B's drainPondAt is PER-FRAME and returns the kg shed by that call, so the total is only
    // knowable by accumulating across the hold. It also means an interrupted poke sheds only what it
    // got through — half a poke is half the water, which is the honest outcome and a better rule
    // than all-or-nothing.
    onHold: (p, dt) => {
      const rig = getRig && getRig();
      const c = targetPond();
      if (!rig || !c) return;
      const shed = rig.drainPondAt(c.node, dt, state.tune.drainRadius);
      if (typeof shed === 'number' && shed > 0) state.pokeKg += shed;
    },
    onDone: (p, t) => {
      const kg = state.pokeKg;
      state.pokeKg = 0;
      if (kg > 0) onWater(p, kg, t);
    },
  }));

  /**
   * The payoff. All of it lands on the player, because they are standing directly underneath it —
   * that is not a bug in the plan, it IS the plan.
   *
   * Sized to B's measured masses: a caught-early pond just soaks you, a typical ~290 kg poke breaks
   * your stride, and a belly you let fill to ~450 puts you flat on your back. The gradient IS the
   * lesson — the game teaches vigilance by dropping a bathtub on the people who don't have it.
   */
  function onWater(p, kg, t) {
    p.events.push({ type: 'doused', kg, t });
    const T = state.tune;
    if (kg >= T.knockKg) {
      // straight down and backwards: a bathtub arriving on your head does not blow you downwind
      p.knockdown(t, -Math.sin(p.facing), -Math.cos(p.facing));
    } else if (kg >= T.staggerKg) {
      p.staggerHit(t);
    }
    // below splashKg: you just get wet, which is its own reward
  }

  return {
    get carried() { return state.carried; },
    get home() { return home; },
    get pokeKg() { return state.pokeKg; },
    tune: state.tune,
    pond,
    targetPond,
    pokeSpot,
    onWater,
    update() { sync(); },
    dispose() { wired.forEach((un) => un()); if (state.view) scene.remove(state.view); },
  };
}
