import Phaser from 'phaser';
import type { EventBus } from '../../../core/EventBus';
import { PALETTE } from '../../../data/outfits';
import type { Patron } from '../../../data/types';
import {
  BANGS_TO_BUST,
  OK_MS,
  PERFECT_MS,
  applyBang,
  freshStall,
  judgeBang,
  scoreStall,
  type StallOutcome,
  type StallState,
} from '../bangJudge';
import { STALL_BUSTED, STALL_MUFFLED, STALL_OFF_BEAT } from '../../../data/strings/floor';

const W = 640;
const H = 360;

const PINK = 0xd03470;
const GREEN = 0x9fe8a0;
const AMBER = 0xd8a020;
const PURPLE = 0x7b2fd0;
const INK = 0x0a0a12;

const DOOR_W = 250;
const DOOR_H = 160;
const DOOR_CX = 320;
const DOOR_CY = 145;
const DOOR_HINGE_X = DOOR_CX - DOOR_W / 2;
const DOOR_BOTTOM = DOOR_CY + DOOR_H / 2;

const BAR_W = 380;
const BAR_H = 14;
const BAR_CX = 320;
const BAR_CY = 292;

const BUST_HOLD_MS = 1600;
/** How far the door swings before it's "open" — a sliver of edge stays visible. */
const DOOR_SWUNG_SCALE = 0.06;

interface TickMark {
  beatIndex: number;
  /** The tick's arrival stamped on OUR clock, so bangs and beats compare directly. */
  atElapsedMs: number;
}

function shoeColour(p: Patron): number {
  const layer = p.outfit.find((l) => l.slot === 'shoes');
  if (!layer || layer.type === 'none') return 0x14141c;
  return PALETTE[layer.colour] ?? 0x14141c;
}

export class StallOverlay {
  private readonly root: Phaser.GameObjects.Container;
  private readonly doorNode: Phaser.GameObjects.Container;
  private readonly barFlash: Phaser.GameObjects.Rectangle;
  private readonly marker: Phaser.GameObjects.Rectangle;
  private readonly titleText: Phaser.GameObjects.Text;
  private readonly gradeText: Phaser.GameObjects.Text;
  private readonly flavourText: Phaser.GameObjects.Text;
  private readonly noteText: Phaser.GameObjects.Text;
  private readonly pips: Phaser.GameObjects.Rectangle[] = [];
  private readonly feet: Phaser.GameObjects.Rectangle[] = [];
  private readonly feetBaseX: number[] = [];
  private readonly offBeat: () => void;

  private open_ = false;
  private resolved = true;
  private done: ((r: StallOutcome) => void) | null = null;
  private state: StallState = freshStall();

  private elapsedMs = 0;
  private lastTick: TickMark | null = null;
  private bustHoldMs = 0;
  private shakeMs = 0;
  private shakeMag = 0;
  private flashMs = 0;

  constructor(
    scene: Phaser.Scene,
    bus: EventBus,
    private readonly beatIntervalMs: number,
    private readonly sfx: { play(name: 'doorBang'): void } | null = null,
  ) {
    const t = (x: number, y: number, size: number, colour: string): Phaser.GameObjects.Text =>
      scene.add.text(x, y, '', { fontFamily: 'monospace', fontSize: `${size}px`, color: colour });

    this.root = scene.add.container(0, 0);

    this.root.add(scene.add.rectangle(W / 2, H / 2, W, H, 0x000000, 0.82));
    this.root.add(scene.add.rectangle(W / 2, 180, 470, 310, INK).setStrokeStyle(1, PINK, 0.7));

    this.titleText = t(105, 40, 9, '#d03470');
    this.root.add(this.titleText);

    // Stall interior: revealed as the door swings away.
    this.root.add(scene.add.rectangle(DOOR_CX, DOOR_CY, DOOR_W, DOOR_H, 0x05050a));
    this.root.add(scene.add.rectangle(DOOR_CX, DOOR_CY - 30, 40, 60, 0x11111a)); // cistern, in shadow

    // Door hangs off its hinge so the bust can swing it, not slide it.
    this.doorNode = scene.add.container(DOOR_HINGE_X, DOOR_CY);
    const face = scene.add.rectangle(DOOR_W / 2, 0, DOOR_W, DOOR_H, 0x2b2018).setStrokeStyle(1, 0x120c08);
    this.doorNode.add(face);
    this.doorNode.add(scene.add.rectangle(DOOR_W / 2, -34, DOOR_W - 40, 54, 0x342519));
    this.doorNode.add(scene.add.rectangle(DOOR_W / 2, 36, DOOR_W - 40, 54, 0x342519));
    this.doorNode.add(scene.add.rectangle(DOOR_W - 26, 0, 14, 5, 0xc0c0c8)); // latch
    this.doorNode.add(scene.add.rectangle(DOOR_W - 26, -12, 22, 9, 0x8a1020)); // ENGAGED tab
    this.doorNode.add(
      scene.add
        .text(DOOR_W - 26, -12, 'ENGAGED', { fontFamily: 'monospace', fontSize: '5px', color: '#f0c0c8' })
        .setOrigin(0.5),
    );
    this.doorNode.add(
      scene.add
        .text(DOOR_W / 2, -46, 'DAZZA WOZ HERE', { fontFamily: 'monospace', fontSize: '7px', color: '#5c4630' })
        .setOrigin(0.5)
        .setAngle(-6),
    );
    this.root.add(this.doorNode);

    // The infraction itself: two pairs of feet in a one-person cubicle.
    this.root.add(scene.add.rectangle(DOOR_CX, DOOR_BOTTOM + 7, DOOR_W, 14, 0x1c1c24));
    for (let i = 0; i < 4; i++) {
      const pairX = i < 2 ? DOOR_CX - 64 : DOOR_CX + 52;
      const x = pairX + (i % 2) * 13;
      this.feetBaseX.push(x);
      const foot = scene.add.rectangle(x, DOOR_BOTTOM + 6, 9, 6, 0x14141c);
      this.feet.push(foot);
      this.root.add(foot);
    }

    this.flavourText = t(120, 244, 8, '#c8c8d4').setWordWrapWidth(400);
    this.root.add(this.flavourText);
    this.noteText = t(120, 268, 7, '#6a6a80').setWordWrapWidth(400);
    this.root.add(this.noteText);

    // Beat bar: the full width is one beat, dead centre is the kick.
    const zone = (ms: number, colour: number, alpha: number): Phaser.GameObjects.Rectangle => {
      const frac = beatIntervalMs > 0 ? Math.min(1, (2 * ms) / beatIntervalMs) : 1;
      return scene.add.rectangle(BAR_CX, BAR_CY, BAR_W * frac, BAR_H, colour, alpha);
    };
    this.root.add(scene.add.rectangle(BAR_CX, BAR_CY, BAR_W, BAR_H, 0x14141c).setStrokeStyle(1, 0x30304a));
    this.root.add(zone(OK_MS, AMBER, 0.22));
    this.root.add(zone(PERFECT_MS, GREEN, 0.32));
    this.barFlash = scene.add.rectangle(BAR_CX, BAR_CY, BAR_W, BAR_H, GREEN, 0);
    this.root.add(this.barFlash);
    this.marker = scene.add.rectangle(BAR_CX, BAR_CY, 3, BAR_H + 8, 0xffffff);
    this.root.add(this.marker);

    this.gradeText = t(BAR_CX + BAR_W / 2 + 6, BAR_CY - 5, 8, '#9fe8a0');
    this.root.add(this.gradeText);

    for (let i = 0; i < BANGS_TO_BUST; i++) {
      const pip = scene.add.rectangle(BAR_CX - (BANGS_TO_BUST - 1) * 8 + i * 16, 314, 11, 7, PURPLE, 0.25);
      this.pips.push(pip);
      this.root.add(pip);
    }

    this.root.add(
      scene.add
        .text(BAR_CX, 328, 'SPACE bang on the door · ESC walk away', {
          fontFamily: 'monospace',
          fontSize: '7px',
          color: '#556',
        })
        .setOrigin(0.5, 0),
    );

    this.root.setScrollFactor(0, 0, true);
    this.root.setDepth(5200);
    this.root.setVisible(false);

    this.offBeat = bus.on('beat:tick', (p) => {
      if (!this.open_) return;
      this.lastTick = { beatIndex: p.beatIndex, atElapsedMs: this.elapsedMs };
    });
  }

  get isOpen(): boolean {
    return this.open_;
  }

  open(args: { stallId: string; patrons: Patron[] }, done: (r: StallOutcome) => void): void {
    this.done = done;
    this.resolved = false;
    this.open_ = true;
    this.state = freshStall();
    this.elapsedMs = 0;
    this.lastTick = null;
    this.bustHoldMs = 0;
    this.shakeMs = 0;
    this.shakeMag = 0;
    this.flashMs = 0;

    this.titleText.setText(`CUBICLE ${args.stallId.toUpperCase()} — occupied. Twice.`);
    this.flavourText.setText('Two pairs of shoes. One cubicle. Knock on the beat, mate.');
    this.noteText.setText('');
    this.gradeText.setText('');

    this.feet.forEach((foot, i) => {
      const patron = args.patrons[i < 2 ? 0 : 1];
      foot.setVisible(patron !== undefined);
      foot.setAlpha(1);
      foot.x = this.feetBaseX[i] ?? foot.x;
      if (patron) foot.setFillStyle(shoeColour(patron));
    });

    this.doorNode.setScale(1, 1);
    this.doorNode.x = DOOR_HINGE_X;
    this.doorNode.y = DOOR_CY;
    this.barFlash.setAlpha(0);
    this.paintPips();
    this.root.setVisible(true);
  }

  handleKey(key: string): void {
    if (!this.open_) return;
    const k = key.toUpperCase();
    if (k === 'ESC' || k === 'ESCAPE') return this.resolve();
    // Door's already open; the last beat is just theatre.
    if (this.bustHoldMs > 0) return;
    if (k === 'SPACE' || k === ' ') this.bang();
  }

  update(deltaMs: number): void {
    if (!this.open_) return;
    this.elapsedMs += deltaMs;

    if (this.shakeMs > 0) this.shakeMs = Math.max(0, this.shakeMs - deltaMs);
    if (this.flashMs > 0) {
      this.flashMs = Math.max(0, this.flashMs - deltaMs);
      this.barFlash.setAlpha((this.flashMs / 180) * 0.5);
    }

    if (this.bustHoldMs > 0) {
      this.bustHoldMs = Math.max(0, this.bustHoldMs - deltaMs);
      const progress = 1 - this.bustHoldMs / BUST_HOLD_MS;
      const swing = Math.min(1, progress * 2.2); // door goes fast, the shame lingers
      this.doorNode.setScale(1 - swing * (1 - DOOR_SWUNG_SCALE), 1);
      this.feet.forEach((foot, i) => {
        const base = this.feetBaseX[i] ?? foot.x;
        foot.x = base + (i < 2 ? -1 : 1) * swing * 70;
        foot.setAlpha(1 - swing);
      });
      if (this.bustHoldMs === 0) this.resolve();
      return;
    }

    this.doorNode.x = DOOR_HINGE_X + this.shakeWobble();
    this.doorNode.y = DOOR_CY + this.shakeWobble() * 0.4;
    this.marker.x = BAR_CX - BAR_W / 2 + this.beatPhase() * BAR_W;
    this.marker.setAlpha(this.lastTick ? 1 : 0.25);
  }

  destroy(): void {
    this.offBeat();
    this.done = null;
    this.open_ = false;
    this.root.destroy(true);
  }

  private bang(): void {
    const tick = this.lastTick;
    // No beat authority yet — swinging at silence shouldn't cost you a combo.
    if (!tick) return;
    this.sfx?.play('doorBang');

    const grade = judgeBang(
      this.elapsedMs,
      { beatIndex: tick.beatIndex, audioTimeMs: tick.atElapsedMs },
      this.beatIntervalMs,
    );
    const brokeCombo = grade === 'miss' && this.state.combo > 0;
    this.state = applyBang(this.state, grade);

    this.shakeMs = grade === 'miss' ? 90 : 220;
    this.shakeMag = grade === 'miss' ? 1.5 : 4.5;
    this.flashMs = 180;
    this.barFlash.setFillStyle(grade === 'perfect' ? GREEN : grade === 'ok' ? AMBER : PINK);
    this.gradeText.setText(grade.toUpperCase());
    this.gradeText.setColor(grade === 'perfect' ? '#9fe8a0' : grade === 'ok' ? '#d8a020' : '#d03470');
    this.paintPips();

    if (this.state.busted) {
      this.flavourText.setText(this.line(STALL_BUSTED));
      this.noteText.setText('');
      this.bustHoldMs = BUST_HOLD_MS;
      this.gradeText.setText('BUSTED');
      return;
    }
    if (grade === 'miss') {
      this.flavourText.setText(this.line(STALL_MUFFLED));
      this.noteText.setText(brokeCombo ? STALL_OFF_BEAT : '');
    } else {
      this.flavourText.setText('The door rattles. Something in there stops talking.');
      this.noteText.setText('');
    }
  }

  /** Deterministic line pick — the beat index is the only entropy we're allowed. */
  private line(lines: readonly string[]): string {
    const i = ((this.lastTick?.beatIndex ?? 0) + this.state.bangs) % lines.length;
    return lines[i] ?? lines[0] ?? '';
  }

  /** 0..1 across the bar, offset so a kick lands the marker dead centre. */
  private beatPhase(): number {
    if (!this.lastTick || !(this.beatIntervalMs > 0)) return 0.5;
    const since = (this.elapsedMs - this.lastTick.atElapsedMs) / this.beatIntervalMs;
    return ((since % 1) + 1.5) % 1;
  }

  private shakeWobble(): number {
    if (this.shakeMs <= 0) return 0;
    return Math.sin(this.shakeMs * 0.9) * this.shakeMag * (this.shakeMs / 220);
  }

  private paintPips(): void {
    this.pips.forEach((pip, i) => {
      const lit = i < this.state.combo;
      pip.setFillStyle(lit ? GREEN : PURPLE, lit ? 0.95 : 0.25);
    });
  }

  private resolve(): void {
    if (this.resolved) return;
    this.resolved = true;
    this.open_ = false;
    this.root.setVisible(false);
    const done = this.done;
    this.done = null;
    done?.(scoreStall(this.state));
  }
}
