import { DAYS, DAY_LENGTH, RECIPES, TIP_BONUS, TIP_THRESHOLD } from './recipes';
import type { DayConfig, FlyingDish, Guest, Phase } from './types';

export const SLOT_X = [78, 195, 312];
export const GUEST_BOTTOM = 590;
export const COUNTER_TOP = 540;
const BEST_KEY = 'shortstack.bestDay';

export interface DayStats {
  earned: number;
  served: number;
  lost: number;
}

export class Game {
  phase: Phase = 'title';
  day = 1;
  coins = 0;
  totalEarned = 0;
  stars = 5;
  timeLeft = DAY_LENGTH;
  guests: Guest[] = [];
  flying: FlyingDish[] = [];
  stats: DayStats = { earned: 0, served: 0, lost: 0 };
  bestDay = 1;
  cookingGuestId: number | null = null;

  private spawnTimer = 1;
  private nextId = 1;

  onGuestLost: (g: Guest) => void = () => {};
  onServed: (g: Guest, amount: number) => void = () => {};

  constructor() {
    const raw = localStorage.getItem(BEST_KEY);
    const n = raw === null ? NaN : parseInt(raw, 10);
    if (!Number.isNaN(n)) this.bestDay = Math.min(4, Math.max(1, n));
  }

  cfg(): DayConfig {
    return DAYS[Math.min(this.day, DAYS.length) - 1];
  }

  private saveBest(v: number): void {
    if (v > this.bestDay) {
      this.bestDay = v;
      try {
        localStorage.setItem(BEST_KEY, String(v));
      } catch {
        // private mode etc.
      }
    }
  }

  startDay(): void {
    this.coins = 0;
    this.stars = 5;
    this.timeLeft = DAY_LENGTH;
    this.guests = [];
    this.flying = [];
    this.stats = { earned: 0, served: 0, lost: 0 };
    this.spawnTimer = 1;
    this.cookingGuestId = null;
    this.phase = 'playing';
    this.saveBest(this.day);
  }

  update(dt: number): void {
    if (this.phase !== 'playing') return;
    this.timeLeft = Math.max(0, this.timeLeft - dt);

    this.spawnTimer -= dt;
    if (this.spawnTimer <= 0 && this.timeLeft > 8) {
      if (this.trySpawn()) {
        const c = this.cfg();
        this.spawnTimer = c.spawnMin + Math.random() * (c.spawnMax - c.spawnMin);
      } else {
        this.spawnTimer = 0.8;
      }
    }

    for (const g of [...this.guests]) {
      g.bob += dt;
      if (g.state === 'arriving') {
        const tx = SLOT_X[g.slot];
        g.x += (tx - g.x) * Math.min(1, dt * 4.2);
        if (Math.abs(g.x - tx) < 2) {
          g.x = tx;
          g.state = 'waiting';
        }
      } else if (g.state === 'waiting') {
        g.patience -= dt;
        if (g.patience <= 0) this.loseGuest(g);
      } else {
        g.leaveT += dt;
        g.x += (g.state === 'leaving-happy' ? -1 : 1) * dt * 240;
        if (g.x < -130 || g.x > 520 || g.leaveT > 2.5) {
          this.guests = this.guests.filter((o) => o !== g);
        }
      }
    }

    for (const f of [...this.flying]) {
      f.t += dt * 2.6;
      const k = Math.min(1, f.t);
      const e = 1 - (1 - k) * (1 - k);
      f.x = f.fromX + (f.toX - f.fromX) * e;
      f.y = f.fromY + (f.toY - f.fromY) * e - Math.sin(k * Math.PI) * 60;
      if (k >= 1) this.flying = this.flying.filter((o) => o !== f);
    }

    if (this.timeLeft <= 0) this.endDay();
  }

  private trySpawn(): boolean {
    const taken = new Set(
      this.guests.filter((g) => g.state === 'arriving' || g.state === 'waiting').map((g) => g.slot),
    );
    const free = [0, 1, 2].filter((s) => !taken.has(s));
    if (free.length === 0) return false;
    const slot = free[Math.floor(Math.random() * free.length)];
    const c = this.cfg();
    const recipeIdx = c.recipes[Math.floor(Math.random() * c.recipes.length)];
    const usedSprites = new Set(this.guests.map((g) => g.spriteIdx));
    const spritePool = [0, 1, 2, 3].filter((i) => !usedSprites.has(i));
    const spriteIdx =
      spritePool.length > 0
        ? spritePool[Math.floor(Math.random() * spritePool.length)]
        : Math.floor(Math.random() * 4);
    this.guests.push({
      id: this.nextId++,
      slot,
      recipeIdx,
      state: 'arriving',
      x: 470,
      patience: c.patience,
      patienceMax: c.patience,
      spriteIdx,
      bob: Math.random() * 10,
      leaveT: 0,
    });
    return true;
  }

  guestById(id: number): Guest | undefined {
    return this.guests.find((g) => g.id === id);
  }

  private loseGuest(g: Guest): void {
    g.state = 'leaving-angry';
    g.leaveT = 0;
    this.stars = Math.max(0, this.stars - 1);
    this.stats.lost++;
    if (this.cookingGuestId === g.id) this.cookingGuestId = null;
    this.onGuestLost(g);
  }

  /** Serve a waiting guest. Returns coins earned (0 if the guest can't be served). */
  serveGuest(g: Guest, mult: number): number {
    if (g.state !== 'waiting' && g.state !== 'arriving') return 0;
    const recipe = RECIPES[g.recipeIdx];
    let amount = Math.round(recipe.coins * mult);
    if (g.patience / g.patienceMax > TIP_THRESHOLD) amount += TIP_BONUS;
    this.coins += amount;
    this.stats.earned += amount;
    this.stats.served++;
    g.state = 'leaving-happy';
    g.leaveT = 0;
    if (this.cookingGuestId === g.id) this.cookingGuestId = null;
    this.onServed(g, amount);
    return amount;
  }

  targetMet(): boolean {
    return this.coins >= this.cfg().target;
  }

  private endDay(): void {
    this.totalEarned += this.coins;
    this.cookingGuestId = null;
    this.phase = 'recap';
  }

  /** Advance from the recap screen. */
  advance(): void {
    if (this.phase !== 'recap') return;
    if (!this.targetMet()) {
      this.phase = 'dayintro'; // retry same day
      return;
    }
    if (this.day >= DAYS.length) {
      this.saveBest(4);
      this.phase = 'win';
      return;
    }
    this.day++;
    this.phase = 'dayintro';
  }

  resetToTitle(): void {
    this.phase = 'title';
    this.day = 1;
    this.totalEarned = 0;
    this.guests = [];
    this.flying = [];
  }

  /** e2e hook: serve the first waiting guest at full quality. */
  serveTest(): number {
    const g = this.guests.find((o) => o.state === 'waiting');
    if (!g) return 0;
    return this.serveGuest(g, 1);
  }
}
