import type { Stage } from '../scenes/stage';
import { Samurai } from '../sim/samurai';
import { judge, type Order, type RunResult, type Verdict } from './judging';
import { recordGrade } from './progress';

/** A day introduces exactly ONE new pressure (never two) and stays beatable by
 *  a scripted-but-honest run. angerRamp scales every sin — the samurai's
 *  patience shortens on the rush-hour day. */
export interface Day {
  order: Order;
  pressure: string;
  angerRamp: number;
}

export const DAYS: Day[] = [
  { order: { name: 'Open Tomato Sando', layers: ['bread', 'tomato', 'bread'], needsCut: true, maxSeconds: 45 }, pressure: 'the basics', angerRamp: 1 },
  { order: { name: 'Cheese & Tomato', layers: ['bread', 'cheese', 'tomato', 'bread'], needsCut: true, maxSeconds: 45 }, pressure: '+1 ingredient', angerRamp: 1 },
  { order: { name: 'Double Tomato', layers: ['bread', 'tomato', 'tomato', 'bread'], needsCut: true, maxSeconds: 45 }, pressure: 'wetter — twice the tomato', angerRamp: 1 },
  { order: { name: 'Katsu Sando', layers: ['bread', 'katsu', 'tomato', 'bread'], needsCut: true, maxSeconds: 45 }, pressure: 'a real cut — the katsu', angerRamp: 1 },
  { order: { name: 'Rush Hour', layers: ['bread', 'tomato', 'bread'], needsCut: true, maxSeconds: 22 }, pressure: 'the patience ramp', angerRamp: 1.8 },
  { order: { name: 'Tamago Sando', layers: ['bread', 'egg', 'bread'], needsCut: true, maxSeconds: 45 }, pressure: 'it rolls', angerRamp: 1 },
  { order: { name: 'Tofu, Gently', layers: ['bread', 'tofu', 'bread'], needsCut: true, maxSeconds: 45 }, pressure: 'fragile — pressing is death', angerRamp: 1.2 },
  { order: { name: 'THE TOWER', layers: ['bread', 'katsu', 'cheese', 'tomato', 'bread'], needsCut: true, maxSeconds: 60 }, pressure: 'five layers', angerRamp: 1.3 },
];

export const ORDERS: Order[] = DAYS.map((d) => d.order);

/**
 * The run controller. Holds the samurai + the current ticket, tallies the floor,
 * and turns a finished attempt into a Verdict. Kitchen sins feed the anger
 * meter; the meter topping out fires the string cut (wired to the stage).
 */
export type RunState = 'playing' | 'served' | 'dead';

const BOW_SECS = 0.5; // the dwell commit, straight from toastsim
const LATE_TICK = 3; // one servedLate bump per this many seconds overdue

export class Game {
  samurai = new Samurai();
  day = 0;
  order: Order = DAYS[0].order;
  drops = 0;
  seconds = 0; // ticket timer (seconds)
  state: RunState = 'playing';
  /** 0..1 bow-dwell progress while holding still over a correct sando. */
  dwell = 0;
  lastVerdict: Verdict | null = null;
  private lateBumped = 0;

  constructor(private stage: Stage) {
    this.samurai.onStrike = () => {
      this.state = 'dead';
      this.stage.stringCut();
    };
  }

  /** Is the physical stack exactly the ordered layers? */
  matchesOrder(): boolean {
    const kinds = this.stage.sando.kinds();
    return kinds.length === this.order.layers.length && kinds.every((k, i) => k === this.order.layers[i]);
  }

  /** Live-play heartbeat: clock, overdue anger, and the bow-dwell commit.
   *  `barStill` = the player is holding the control bar still. */
  updateLive(dtSec: number, barStill: boolean): void {
    if (this.state !== 'playing' || this.samurai.struck) return;
    this.seconds += dtSec;
    const over = this.seconds - this.order.maxSeconds;
    if (over > 0 && Math.floor(over / LATE_TICK) > this.lateBumped) {
      this.lateBumped = Math.floor(over / LATE_TICK);
      this.samurai.bump('servedLate');
    }
    const ready = this.matchesOrder() && this.stage.sando.stats().standing;
    if (ready && barStill) {
      this.dwell += dtSec / BOW_SECS;
      if (this.dwell >= 1) {
        this.state = 'served';
        this.lastVerdict = this.serve(true);
        recordGrade(this.day, this.lastVerdict.grade);
        this.stage.showScorecard(this.lastVerdict);
      }
    } else {
      this.dwell = Math.max(0, this.dwell - dtSec * 2);
    }
  }

  /** The string-cut post-mortem: judged with nothing served. */
  failVerdict(): Verdict {
    const run = this.buildRun(false, this.seconds);
    run.service.served = false;
    run.service.correct = false;
    return judge(this.order, run);
  }

  /** Load day `n` (clamped): its order + its samurai patience ramp. */
  setDay(n: number): Day {
    this.day = Math.max(0, Math.min(DAYS.length - 1, n));
    const d = DAYS[this.day];
    this.order = d.order;
    this.samurai.ramp = d.angerRamp;
    this.drops = 0;
    this.seconds = 0;
    this.state = 'playing';
    this.dwell = 0;
    this.lateBumped = 0;
    this.lastVerdict = null;
    return d;
  }

  reset(order: Order = this.order): void {
    this.samurai.reset();
    this.order = order;
    this.drops = 0;
    this.seconds = 0;
    this.state = 'playing';
    this.dwell = 0;
    this.lateBumped = 0;
    this.lastVerdict = null;
  }

  // ---- kitchen sins → anger ----
  drop(): void {
    this.drops++;
    this.samurai.bump('drop');
  }
  splat(): void {
    this.samurai.bump('splat');
  }
  crimeCut(): void {
    this.samurai.bump('crimeCut');
  }
  offstage(): void {
    this.samurai.bump('offstage');
  }

  private buildRun(bowHeld: boolean, seconds: number): RunResult {
    const cut = this.stage.cutter.last;
    const s = this.stage.sando.stats();
    const matches = this.matchesOrder();
    return {
      cut: cut ? { cv: cut.cv, quality: cut.quality, pieces: cut.pieces } : { cv: 1, quality: 'none', pieces: 0 },
      sando: { layers: s.layers, leanDeg: s.leanDeg, comDrift: s.comDrift, standing: s.standing, matches },
      service: { served: true, bowHeld, seconds, correct: matches && s.standing },
      floor: { splatMass: this.stage.cutter.stats().splatMass, drops: this.drops },
    };
  }

  /** Score the current sando as served. Sheathes on a clean correct serve,
   *  angers on the wrong dish. Returns the verdict for the scorecard. */
  serve(bowHeld = true, seconds = this.seconds): Verdict {
    const run = this.buildRun(bowHeld, seconds);
    const v = judge(this.order, run);
    if (run.service.correct && run.floor.splatMass < 1) this.samurai.bump('cleanServe');
    else if (!run.sando.matches || !run.sando.standing) this.samurai.bump('servedWrong');
    return v;
  }
}
