import Phaser from 'phaser';
import { renderDoll } from '../../patrons/doll';
import { dollPlan } from '../../patrons/dollPlan';
import { drunkStage } from '../../rules/drunk';
import type { Patron } from '../../data/types';
import { CLAIM_LINES, DRUNK_GREETINGS, OWNER_HINTS, PATRON_GREETINGS } from '../../data/strings/door';
import { encounterById, type EncounterId } from '../../rules/encounters';
import { MONO } from './ui';
import { ZONE_BANDS, ZONE_ORDER, describeZone, greetingIndex, type InspectZone } from './inspect';

// The patron at the rope, three times life size so the outfit actually reads.
// Hover zones map 1:1 onto the doll's y bands — you look at the shoes by
// looking at the shoes.

const SCALE = 3;
const DOLL_W = 32;
const DOLL_H = 48;

export class PatronUpView {
  private root: Phaser.GameObjects.Container | null = null;
  private doll: Phaser.GameObjects.Image | null = null;
  private tooltip: Phaser.GameObjects.Container | null = null;
  private patron: Patron | null = null;
  private swayT = 0;
  private swayPx = 0;
  /** The bubble currently on screen. Beats REPLACE it; they do not stack. */
  private bubble: Phaser.GameObjects.GameObject[] = [];

  constructor(
    private readonly scene: Phaser.Scene,
    private readonly x: number,
    /** y of the patron's FEET */
    private readonly footY: number,
  ) {}

  get current(): Patron | null {
    return this.patron;
  }

  show(p: Patron, encounter?: EncounterId): void {
    this.clear();
    this.patron = p;
    const { scene } = this;

    const root = scene.add.container(this.x, this.footY).setDepth(300);
    this.root = root;

    // a puddle-ish shadow so they are standing ON something
    root.add(scene.add.ellipse(0, 2, DOLL_W * SCALE * 0.6, 10, 0x000000, 0.45));

    const key = renderDoll(scene, p, 'queue');
    const doll = scene.add.image(0, 0, key).setOrigin(0.5, 1).setScale(SCALE);
    this.doll = doll;
    this.swayPx = dollPlan(p, 'queue').swayPx;
    root.add(doll);

    // hover zones, in doll-local coordinates scaled up
    for (const zone of ZONE_ORDER) {
      const band = ZONE_BANDS[zone];
      const h = (band.y1 - band.y0) * SCALE;
      const cy = -DOLL_H * SCALE + (band.y0 + (band.y1 - band.y0) / 2) * SCALE;
      const hit = scene.add
        .rectangle(0, cy, DOLL_W * SCALE * 0.8, h, 0xffffff, 0)
        .setInteractive({ useHandCursor: true });
      hit.on('pointerover', () => this.showTooltip(zone, cy));
      hit.on('pointerout', () => this.hideTooltip());
      root.add(hit);
    }

    this.speak(p, encounter);

    // step-up: they walk in from the queue side and settle
    root.x = this.x - 90;
    root.setAlpha(0);
    scene.tweens.add({ targets: root, x: this.x, alpha: 1, duration: 380, ease: 'Quad.easeOut' });
  }

  private speak(p: Patron, encounter?: EncounterId): void {
    if (!this.root) return;

    // A scripted patron says their own words, in beats, and says them over the
    // top of nothing else — the whole point is that they are a person rather
    // than a draw from a pool.
    const script = encounter ? encounterById(encounter) : undefined;
    if (script) {
      for (const beat of script.beats) this.sayLater(beat.line, beat.afterMs);
      return;
    }

    const { scene } = this;
    const drunk = drunkStage(p.intoxication);
    // knowsOwner is checked FIRST and separately: the owner's mate is the trap
    // (design §3.1), so his line has to be the one that drops the name, not a
    // generic "i'm on the list" shared with every influencer.
    const pool = p.flags.knowsOwner
      ? OWNER_HINTS
      : p.flags.claimsGuestList
        ? CLAIM_LINES
        : drunk === 'messy' || drunk === 'maggot' || drunk === 'loose'
          ? DRUNK_GREETINGS
          : PATRON_GREETINGS;
    const line = pool[greetingIndex(p, pool.length)] ?? PATRON_GREETINGS[0]!;

    const bubbleY = -DOLL_H * SCALE - 16;
    const label = scene.add
      .text(0, bubbleY, `"${line}"`, {
        fontFamily: MONO,
        fontSize: '8px',
        color: '#d8d0c0',
        align: 'center',
        wordWrap: { width: 150 },
      })
      .setOrigin(0.5, 1);
    const bg = scene.add
      .rectangle(0, bubbleY - label.height / 2 + 1, label.width + 10, label.height + 6, 0x14121a, 0.85)
      .setStrokeStyle(1, 0x3a3448);
    this.root.add([bg, label]);
    this.bubble = [bg, label];

    scene.tweens.add({ targets: [bg, label], alpha: 0, delay: 3600, duration: 700 });
  }

  private clearBubble(): void {
    for (const o of this.bubble) o.destroy();
    this.bubble = [];
  }

  /** One speech bubble, `afterMs` from the moment they stepped up. */
  private sayLater(line: string, afterMs: number): void {
    this.scene.time.delayedCall(afterMs, () => {
      // The patron may have been ruled on and walked off before this beat was
      // due — a stale bubble floating over an empty rope is the bug here.
      if (!this.root || this.patron === null) return;
      const { scene } = this;
      // An exchange is a sequence, not a pile: beat two replaces beat one.
      this.clearBubble();
      const bubbleY = -DOLL_H * SCALE - 16;
      const label = scene.add
        .text(0, bubbleY, `"${line}"`, {
          fontFamily: MONO,
          fontSize: '8px',
          color: '#e8dcc4',
          align: 'center',
          wordWrap: { width: 190 },
        })
        .setOrigin(0.5, 1);
      const bg = scene.add
        .rectangle(0, bubbleY - label.height / 2 + 1, label.width + 10, label.height + 6, 0x14121a, 0.9)
        .setStrokeStyle(1, 0x584a68);
      this.root.add([bg, label]);
      this.bubble = [bg, label];
      scene.tweens.add({ targets: [bg, label], alpha: 0, delay: 3200, duration: 700 });
    });
  }

  private showTooltip(zone: InspectZone, localY: number): void {
    this.hideTooltip();
    if (!this.root || !this.patron) return;
    const { scene } = this;

    const lines = describeZone(this.patron, zone);
    const c = scene.add.container(56, localY).setDepth(310);
    const label = scene.add
      .text(6, 0, lines.join('\n'), {
        fontFamily: MONO,
        fontSize: '7px',
        color: '#f0e4d0',
        wordWrap: { width: 120 },
      })
      .setOrigin(0, 0.5);
    const bg = scene.add
      .rectangle(0, 0, label.width + 14, label.height + 8, 0x0d0b12, 0.94)
      .setOrigin(0, 0.5)
      .setStrokeStyle(1, 0x5a4a68);
    c.add([bg, label]);
    this.root.add(c);
    this.tooltip = c;
  }

  private hideTooltip(): void {
    this.tooltip?.destroy();
    this.tooltip = null;
  }

  /** Called every frame — the sway is the drunk tell you watch for. */
  update(deltaMs: number): void {
    if (!this.doll || this.swayPx <= 0) return;
    this.swayT += deltaMs / 1000;
    const speed = 2.2 - this.swayPx * 0.35;
    this.doll.x = Math.sin(this.swayT * speed) * this.swayPx * SCALE * 0.5;
    this.doll.setAngle(Math.sin(this.swayT * speed * 0.7) * this.swayPx * 0.9);
  }

  /** They walk off left after a denial: the defeated slouch. */
  walkOffDenied(onDone?: () => void): void {
    const root = this.root;
    this.patron = null;
    this.hideTooltip();
    if (!root) {
      onDone?.();
      return;
    }
    this.root = null;
    this.doll?.setAngle(9); // the slouch
    this.scene.tweens.add({
      targets: root,
      x: root.x - 170,
      y: root.y + 6,
      alpha: 0,
      duration: 900,
      ease: 'Quad.easeIn',
      onComplete: () => {
        root.destroy();
        onDone?.();
      },
    });
  }

  /** They walk right, into the venue. */
  walkInAdmitted(doorX: number, onDone?: () => void): void {
    const root = this.root;
    this.patron = null;
    this.hideTooltip();
    if (!root) {
      onDone?.();
      return;
    }
    this.root = null;
    this.scene.tweens.add({
      targets: root,
      x: doorX,
      y: root.y - 26,
      scale: 0.55,
      alpha: 0,
      duration: 820,
      ease: 'Quad.easeIn',
      onComplete: () => {
        root.destroy();
        onDone?.();
      },
    });
  }

  clear(): void {
    this.clearBubble();
    this.hideTooltip();
    this.root?.destroy();
    this.root = null;
    this.doll = null;
    this.patron = null;
    this.swayT = 0;
  }
}
