/**
 * SHADES — the week. Lane A owns this file.
 *
 * Five nights, one yard, one wallet. This is deliberately a thin wrapper around
 * the phase machine rather than a new system: main.js already knows how to run
 * forecast → prep → storm → aftermath, and the week's whole job is to answer
 * three questions between rounds — which storm is tonight, how much money do you
 * have, and is it over.
 *
 * Zero imports beyond contracts, and no THREE: the money and the ladder of
 * nights are pure data, so they're testable at fixed cost with no browser. The
 * only thing that touches the screen is main.js reading these numbers.
 */

import { START_BUDGET, HARDWARE } from './contracts.js';

/**
 * The ladder. Escalating, and each rung introduces one new idea rather than just
 * more wind — that's Lane C's authoring, not my ordering:
 *   1 Sea Breeze       gust 10.5, no hail   — the tutorial: nothing can hurt you
 *   2 Southerly Buster gust 20.0, hail      — the change, and hail, arrive
 *   3 Early Buster     gust 20.0, hail      — same teeth, change comes EARLY
 *   4 Wild Night       gust 30.0, hail      — the one the whole repo tuned for
 *   5 Ice Night        gust 28.5, hail      — less wind, more ice
 * Night 3 is the trap: it looks like night 2 on the forecast card and isn't.
 *
 * SPRINT10: a night is now a STORM and a SITE. Nights 1/2/4/5 are the backyard;
 * night 3 moves to the corner block — the same "looks like night 2 and isn't"
 * beat, sharpened: it's a new yard you've never rigged, its carport is a trap,
 * and its storm carries the southerly change that makes the venturi scream. The
 * plain-string form is still accepted (defaults to backyard_01) so nothing that
 * reads NIGHTS[i] as a storm key breaks.
 */
export const NIGHTS = [
  { storm: 'storm_01_gentle', site: 'backyard_01' },
  { storm: 'storm_03_southerly', site: 'backyard_01' },
  { storm: 'storm_03b_earlybuster', site: 'site_02_corner_block' },
  { storm: 'storm_02_wildnight', site: 'backyard_01' },
  { storm: 'storm_02b_icenight', site: 'backyard_01' },
];

/** A night entry, either shape → {storm, site}. */
export function nightAt(i) {
  const n = NIGHTS[i];
  return typeof n === 'string' ? { storm: n, site: 'backyard_01' } : n;
}

/**
 * The economy, in one place because it is the thing most likely to need tuning
 * once somebody actually plays five nights in a row.
 *
 * Shape (SPRINT8 §gate 1): pay = fee by severity + garden bonus + intact
 * hardware refund − collateral. Two of those needed a judgement call and both
 * are the difference between a week with stakes and a week you cannot lose:
 *
 *  - **A lost night pays a fraction of the fee, not zero and not all of it.**
 *    Zero is unrecoverable and reads as punishment; full pay means failure costs
 *    only the broken shackle, and the bank only ever climbs. DESIGN.md's warranty
 *    callout is the source: you turn up, you're just not getting paid for it.
 *  - **Hardware comes back at half.** Full refund makes gear effectively free —
 *    the only sink left is breakage, the bank grows every night, and "broke =
 *    game over" becomes unreachable. Half is also the honest number: a shackle
 *    that rode out a gale has been fatigued, its pin worked loose, and DESIGN.md
 *    already says flogging unscrews unmoused pins. You can re-use it; you can't
 *    call it new.
 */
export const PAY = {
  /**
   * Client's fee for the job, scaled off the storm's worst gust.
   *
   * The constants are set by the shape of the week, not by taste — measured by
   * playing all five nights at fixed skill. A fee has to be worth roughly
   * two-thirds of a good rig ($75): any smaller and one bad night is instant
   * game over (at `8 + gust` a disaster night paid $13 against a $75 rig and
   * ended the week on night ONE, which is a punishment, not a difficulty); any
   * larger and the bank runs away and night five is trivially affordable.
   * At `25 + 1.6·gust` a disaster leaves you at ~$26 — alive, but rigging the
   * next night on carabiners, which is the death spiral doing its job.
   */
  feeFor: (gustPeak) => Math.round(25 + gustPeak * 1.6),
  /** A lost night still pays this share — you turned up, it just went wrong. */
  lostNightShare: 0.35,
  /** Garden bonus at hp=100. Scales linearly; this is the main earner. */
  gardenBonusMax: 45,
  /** What surviving hardware is worth back. Fatigued, not new. */
  refundShare: 0.5,
};

/**
 * Below this you cannot rig four corners at all — four carabiners is the floor.
 *
 * ⚠️ MEASURED, OPEN, AND FOR A PLAYTEST TO SETTLE — not a guess, and written
 * here because the numbers are only visible once you run five nights:
 *
 *  1. **A competent week runs away with the money.** Holding 4/4 at hp 70 every
 *     night takes the bank 80 → 116 → 167 → 218 → 285 → 350. By night three you
 *     can already afford four rated shackles ($120) and the wild night stops
 *     being a decision. The fee is the lever; it is deliberately NOT tuned down
 *     yet, because the fix might equally be that later nights should cost more
 *     to rig (a bigger sail, a second bed) — that's Sprint 9's content question,
 *     not a constant I should quietly pick tonight.
 *  2. **A floor-scraping week may never actually go broke.** At $20 a cheap rig
 *     earns back roughly what it costs, so the bank can sit just above this line
 *     indefinitely, losing every garden. The grade fixes what the END CARD says
 *     about that (see gradeFor) but not whether 'gameover' is reachable at all
 *     from a slow bleed rather than one disaster.
 *
 * I could not settle either from the harness: my own bad-player model handed
 * back a fixed $30 of intact hardware no matter what it spent, which flattered
 * the bottom of the curve and is exactly the class of mistake this repo has
 * caught me making all project. Both need a human playing five nights.
 */
export const BROKE_BELOW = HARDWARE[0].cost * 4;

/**
 * How the week is remembered, by gardens actually held.
 *
 * SPRINT8 gave Lane A the win wording, and this is the ruling: the end card is
 * the last thing SHADES says to a player, so it does not get to lie the way the
 * aftermath verdict used to. Three grades, and E's copy carries two of them —
 * their primary for a clean week, their own alternate for a scraped one. The
 * third is mine, because E never wrote words for "you stayed solvent while the
 * client's garden died four times", and until this was measured nobody knew the
 * game could even produce that ending.
 *
 * @param {number} held gardens saved, 0..5
 */
export function gradeFor(held) {
  if (held >= 5) return 'clean';
  if (held >= 3) return 'scraped';
  return 'solvent';
}

/** Worst gust a storm def can throw — the severity signal the fee reads. */
export function gustPeakOf(def) {
  const base = Math.max(...def.baseCurve.map((p) => p[1]));
  const g = def.gusts ?? {};
  return base + (g.powBase ?? 0) + (g.powRamp ?? 0);
}

/**
 * @typedef {object} Settlement
 * @property {number} fee        client's payment for the job
 * @property {number} bonus      garden bonus
 * @property {number} refund     intact hardware recovered, at half
 * @property {number} collateral what your failure broke of theirs
 * @property {number} pay        fee + bonus + refund − collateral
 * @property {number} bankBefore
 * @property {number} bankAfter
 * @property {'continue'|'win'|'gameover'} outcome
 */

/**
 * @param {object} [opts]
 * @param {number} [opts.bank=START_BUDGET]
 */
export function createWeek(opts = {}) {
  const startBank = opts.bank ?? START_BUDGET;
  let index = 0;          // 0-based into NIGHTS
  let bank = startBank;
  let done = false;
  /** @type {Settlement[]} */
  const log = [];

  const week = {
    /** 1-based, for humans. "Night 3 of 5". */
    get night() { return index + 1; },
    get nights() { return NIGHTS.length; },
    get stormKey() { return nightAt(index).storm; },
    /** SPRINT10: which yard tonight is on. main.js loads it. */
    get site() { return nightAt(index).site; },
    get bank() { return bank; },
    get isFinalNight() { return index === NIGHTS.length - 1; },
    get over() { return done; },
    get log() { return log; },

    reset() {
      index = 0; bank = startBank; done = false; log.length = 0;
      return week;
    },

    /**
     * Settle the night just played and move the ladder on.
     *
     * @param {object} run   main.js's scoreRun() result
     * @param {object} def   tonight's storm def (for severity)
     * @param {number} spent what prep actually cost out of the bank
     * @returns {Settlement}
     */
    settle(run, def, spent) {
      const bankBefore = bank;
      const fullFee = PAY.feeFor(gustPeakOf(def));
      const fee = run.win ? fullFee : Math.round(fullFee * PAY.lostNightShare);
      const bonus = Math.round((Math.max(0, run.hp) / 100) * PAY.gardenBonusMax);

      // Only hardware still on an unbroken corner comes home, and at half.
      const intact = (run.intactHardwareValue ?? 0);
      const refund = Math.round(intact * PAY.refundShare);

      const collateral = (run.collateral ?? []).reduce((s, c) => s + c.cost, 0);
      const pay = fee + bonus + refund - collateral;

      bank = Math.max(0, bankBefore - spent + pay);

      const held = log.filter((n) => n.won).length + (run.win ? 1 : 0);

      // Outcome is decided AFTER the money moves: you can win the last night and
      // still be broke, and the week should say you finished it.
      //
      // Reaching night five is NOT winning, and the first draft of this said it
      // was. Measured: a player who rigs $20 of carabiners every night loses
      // four gardens out of five, never dips under the broke line because a
      // cheap rig barely costs anything, and got handed "THE WEEK HELD —
      // everything's still where you put it" over four dead gardens. Outlasting
      // the week isn't surviving it. So the WORDS scale with what you actually
      // held (see grade); the outcome only says whether you're still trading.
      let outcome;
      if (week.isFinalNight) outcome = 'win';
      else if (bank < BROKE_BELOW) outcome = 'gameover';
      else outcome = 'continue';
      done = outcome !== 'continue';

      const s = { fee, fullFee, bonus, refund, collateral, pay, spent,
        bankBefore, bankAfter: bank, outcome, night: week.night, held,
        grade: gradeFor(held), stormKey: week.stormKey, won: !!run.win };
      log.push(s);
      return s;
    },

    /** Move to the next night. No-op once the week is done. */
    advance() {
      if (!done && index < NIGHTS.length - 1) index++;
      return week;
    },
  };
  return week;
}
