import Phaser from 'phaser';
import { CONTRABAND } from '../../../data/contraband';
import type { Patron } from '../../../data/types';
import { PATDOWN_INTRO } from '../../../data/strings/floor';
import { PATDOWN_MS, POCKET_COUNT, foundLine, scorePatDown } from '../patDown';
import type { PatDownResult, Pocket } from '../patDown';

const VIEW_W = 640;
const VIEW_H = 360;
const DEPTH = 5000;

/** patDown.ts lays pockets out on a 64x96 outline; blow it up so 12px zones are clickable. */
const SCALE = 2.5;
const OUTLINE_X = 46;
const OUTLINE_Y = 62;
const ZONE = 24;

const BIN_X = 430;
const BIN_Y = 190;
const BIN_W = 160;
const BIN_H = 170;

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

const MONO = 'monospace';

interface TokenArt {
  readonly w: number;
  readonly h: number;
  readonly colour: number;
}

/** Placeholder art, one entry per CONTRABAND spriteHint. Real sprites land in the v0.4 pass. */
const TOKEN_ART: Readonly<Record<string, TokenArt>> = {
  baggie: { w: 14, h: 10, colour: 0xf2f2f0 },
  flask: { w: 12, h: 22, colour: 0xb8c0c8 },
  goonSack: { w: 24, h: 16, colour: 0xc8ccd4 },
  tinnies: { w: 18, h: 20, colour: AMBER },
  kebab: { w: 12, h: 26, colour: 0xd8cea4 },
  someoneElsesId: { w: 22, h: 14, colour: 0xdad4c4 },
  budgie: { w: 18, h: 14, colour: 0x6fd06f },
  vapes: { w: 22, h: 12, colour: PURPLE },
  marker: { w: 8, h: 24, colour: 0x24242c },
  snags: { w: 24, h: 12, colour: 0xe08c94 },
};

const DEFAULT_ART: TokenArt = { w: 16, h: 14, colour: 0x9aa0b0 };
const NAME_BY_ID = new Map(CONTRABAND.map((c) => [c.id, c.name] as const));

interface ZoneView {
  readonly g: Phaser.GameObjects.Graphics;
  readonly label: Phaser.GameObjects.Text;
  readonly hit: Phaser.GameObjects.Zone;
}

interface TokenView {
  readonly root: Phaser.GameObjects.Container;
  readonly body: Phaser.GameObjects.Rectangle;
  readonly label: Phaser.GameObjects.Text;
  pocketIndex: number;
  itemId: string;
  homeX: number;
  homeY: number;
  grabOffX: number;
  grabOffY: number;
  /** Owns a pocket's item this round. One token per pocket, so slots are never recycled mid-round. */
  live: boolean;
  /** In or past the bin tween: no longer draggable, no longer flapping. */
  gone: boolean;
}

// Dashes along each edge, corners left open — reads as a rounded dashed hint box
// without paying for an actual rounded path per zone.
function dashRect(g: Phaser.GameObjects.Graphics, x: number, y: number, w: number, h: number): void {
  const step = 6;
  const dash = 3;
  const r = 3;
  for (let px = x + r; px < x + w - r; px += step) {
    const len = Math.min(dash, x + w - r - px);
    g.lineBetween(px, y, px + len, y);
    g.lineBetween(px, y + h, px + len, y + h);
  }
  for (let py = y + r; py < y + h - r; py += step) {
    const len = Math.min(dash, y + h - r - py);
    g.lineBetween(x, py, x, py + len);
    g.lineBetween(x + w, py, x + w, py + len);
  }
}

/**
 * The timed pat-down. Thin shell: all scoring lives in patDown.ts — this only
 * flips `revealed` / `binned` on the caller's pockets, which is the agreed channel.
 */
export class PatDownOverlay {
  private readonly scene: Phaser.Scene;
  private readonly root: Phaser.GameObjects.Container;
  private readonly timerBar: Phaser.GameObjects.Rectangle;
  private readonly timerText: Phaser.GameObjects.Text;
  private readonly lineText: Phaser.GameObjects.Text;
  private readonly logText: Phaser.GameObjects.Text;
  private readonly binGeom = new Phaser.Geom.Rectangle(BIN_X - BIN_W / 2, BIN_Y - BIN_H / 2, BIN_W, BIN_H);
  private readonly zones: ZoneView[] = [];
  private readonly tokens: TokenView[] = [];

  private pockets: Pocket[] = [];
  private patron: Patron | null = null;
  private doneFn: ((r: PatDownResult) => void) | null = null;
  private remainingMs = 0;
  private flapMs = 0;
  private open_ = false;
  private found: string[] = [];

  constructor(
    scene: Phaser.Scene,
    private readonly sfx: { play(name: 'clickerClunk'): void } | null = null,
  ) {
    this.scene = scene;
    this.root = scene.add.container(0, 0);

    this.root.add(scene.add.rectangle(VIEW_W / 2, VIEW_H / 2, VIEW_W, VIEW_H, 0x05050a, 0.82));
    this.root.add(
      scene.add.rectangle(VIEW_W / 2, VIEW_H / 2, 600, 328, 0x0d0d16, 0.96).setStrokeStyle(1, PINK, 0.6),
    );

    this.root.add(scene.add.rectangle(32, 30, 576, 8, 0x1c1c2a).setOrigin(0, 0));
    this.timerBar = scene.add.rectangle(32, 30, 576, 8, GREEN).setOrigin(0, 0);
    this.root.add(this.timerBar);

    this.root.add(scene.add.text(32, 44, PATDOWN_INTRO, { fontFamily: MONO, fontSize: '9px', color: '#d03470' }));
    this.timerText = scene.add
      .text(608, 44, '', { fontFamily: MONO, fontSize: '9px', color: '#9fe8a0' })
      .setOrigin(1, 0);
    this.root.add(this.timerText);

    this.buildOutline();
    this.buildBin();

    for (let i = 0; i < POCKET_COUNT; i++) this.zones.push(this.buildZone(i));
    for (let i = 0; i < POCKET_COUNT; i++) this.tokens.push(this.buildToken());

    this.logText = scene.add.text(524, 104, '', {
      fontFamily: MONO,
      fontSize: '7px',
      color: '#d8a020',
      wordWrap: { width: 88 },
      lineSpacing: 2,
    });
    this.root.add(this.logText);

    this.lineText = scene.add.text(46, 316, '', {
      fontFamily: MONO,
      fontSize: '8px',
      color: '#e8e4d8',
      wordWrap: { width: 390 },
    });
    this.root.add(this.lineText);

    this.root.add(
      scene.add
        .text(608, 318, 'CLICK pockets · DRAG to bin · ESC done', {
          fontFamily: MONO,
          fontSize: '7px',
          color: '#556',
        })
        .setOrigin(1, 0),
    );

    this.root.setDepth(DEPTH).setScrollFactor(0, 0, true).setVisible(false);
    scene.input.on('dragstart', this.onDragStart);
    scene.input.on('drag', this.onDrag);
    scene.input.on('dragend', this.onDragEnd);
  }

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

  open(args: { patron: Patron; pockets: Pocket[] }, done: (r: PatDownResult) => void): void {
    if (this.open_) return;
    this.patron = args.patron;
    this.pockets = args.pockets;
    this.doneFn = done;
    this.remainingMs = PATDOWN_MS;
    this.flapMs = 0;
    this.found = [];
    this.open_ = true;

    this.lineText.setText('');
    this.logText.setText('');

    for (let i = 0; i < this.zones.length; i++) {
      const z = this.zones[i];
      const p = this.pockets[i];
      if (z === undefined) continue;
      const on = p !== undefined;
      z.g.setVisible(on);
      z.label.setVisible(on);
      z.hit.setVisible(on);
      if (z.hit.input) z.hit.input.enabled = on;
      if (on) this.paintZone(i);
    }

    for (const t of this.tokens) this.hideToken(t);

    this.root.setVisible(true);
    this.tick();
  }

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

    for (const t of this.tokens) {
      // The budgie is alive and it would like everyone to know.
      if (t.live && !t.gone && t.itemId === 'budgie') t.root.setRotation(Math.sin(this.flapMs / 70) * 0.3);
    }

    if (this.remainingMs <= 0) {
      this.remainingMs = 0;
      this.tick();
      this.resolve();
      return;
    }
    this.tick();
  }

  handleKey(key: string): void {
    if (!this.open_) return;
    // The scene uppercases before routing, so 'Escape' never matched and ESC was
    // dead here while the on-screen hint promised it worked.
    const k = key.toUpperCase();
    if (k === 'ESC' || k === 'ESCAPE') this.resolve();
  }

  destroy(): void {
    this.scene.input.off('dragstart', this.onDragStart);
    this.scene.input.off('drag', this.onDrag);
    this.scene.input.off('dragend', this.onDragEnd);
    this.root.destroy(true);
    this.doneFn = null;
    this.open_ = false;
  }

  // ---- build (constructor only) ----

  private buildOutline(): void {
    const g = this.scene.add.graphics();
    const px = (v: number): number => OUTLINE_X + v * SCALE;
    const py = (v: number): number => OUTLINE_Y + v * SCALE;
    const box = (x: number, y: number, w: number, h: number): void => {
      g.fillRect(px(x), py(y), w * SCALE, h * SCALE);
      g.strokeRect(px(x), py(y), w * SCALE, h * SCALE);
    };
    g.fillStyle(0x14141f, 1);
    g.lineStyle(1, 0x3c3c5a, 1);
    g.fillCircle(px(32), py(10), 8 * SCALE);
    g.strokeCircle(px(32), py(10), 8 * SCALE);
    box(30, 17, 4, 4); // neck
    box(20, 21, 24, 29); // torso
    box(2, 24, 18, 5); // arms out, mate
    box(44, 24, 18, 5);
    box(20, 50, 24, 10); // hips
    box(21, 60, 9, 34); // legs
    box(34, 60, 9, 34);
    this.root.add(g);
  }

  private buildBin(): void {
    const s = this.scene;
    this.root.add(
      s.add.rectangle(BIN_X, BIN_Y, BIN_W, BIN_H, 0x120c1c, 0.9).setStrokeStyle(1, GREEN, 0.75),
    );
    this.root.add(s.add.rectangle(BIN_X, BIN_Y - BIN_H / 2, BIN_W + 12, 12, GREEN, 0.22));
    this.root.add(s.add.rectangle(BIN_X, BIN_Y - BIN_H / 2 + 2, 90, 4, 0x05050a)); // slot
    this.root.add(
      s.add
        .text(BIN_X, BIN_Y - BIN_H / 2 + 18, 'AMNESTY BIN', {
          fontFamily: MONO,
          fontSize: '9px',
          color: '#9fe8a0',
        })
        .setOrigin(0.5, 0),
    );
    this.root.add(
      s.add
        .text(BIN_X, BIN_Y + BIN_H / 2 - 16, 'no questions asked', {
          fontFamily: MONO,
          fontSize: '7px',
          color: '#4a6a50',
        })
        .setOrigin(0.5, 0),
    );
  }

  private buildZone(index: number): ZoneView {
    const s = this.scene;
    const g = s.add.graphics();
    const label = s.add.text(0, 0, '', { fontFamily: MONO, fontSize: '6px', color: '#6f7aa8' }).setOrigin(0.5, 0);
    const hit = s.add.zone(0, 0, ZONE + 8, ZONE + 8).setInteractive({ useHandCursor: true });
    hit.on('pointerdown', () => this.revealPocket(index));
    this.root.add([g, label, hit]);
    return { g, label, hit };
  }

  private buildToken(): TokenView {
    const s = this.scene;
    const root = s.add.container(0, 0);
    const body = s.add.rectangle(0, 0, 16, 14, 0xffffff).setStrokeStyle(1, 0x05050a, 0.8);
    const label = s.add
      .text(0, 12, '', { fontFamily: MONO, fontSize: '6px', color: '#e8e4d8', align: 'center' })
      .setOrigin(0.5, 0);
    root.add([body, label]);
    root.setSize(40, 40).setInteractive(new Phaser.Geom.Rectangle(-20, -20, 40, 40), Phaser.Geom.Rectangle.Contains);
    s.input.setDraggable(root);
    this.root.add(root);
    return {
      root,
      body,
      label,
      pocketIndex: -1,
      itemId: '',
      homeX: 0,
      homeY: 0,
      grabOffX: 0,
      grabOffY: 0,
      live: false,
      gone: false,
    };
  }

  // ---- runtime ----

  private tick(): void {
    const frac = Phaser.Math.Clamp(this.remainingMs / PATDOWN_MS, 0, 1);
    this.timerBar.setSize(Math.max(576 * frac, 0.001), 8);
    this.timerBar.setFillStyle(frac > 0.5 ? GREEN : frac > 0.2 ? AMBER : PINK, 1);
    this.timerText.setText(`${(this.remainingMs / 1000).toFixed(1)}s`);
    this.timerText.setColor(frac > 0.5 ? '#9fe8a0' : frac > 0.2 ? '#d8a020' : '#d03470');
  }

  private paintZone(index: number): void {
    const z = this.zones[index];
    const p = this.pockets[index];
    if (z === undefined || p === undefined) return;
    const cx = OUTLINE_X + (p.x + 6) * SCALE;
    const cy = OUTLINE_Y + (p.y + 6) * SCALE;
    const half = ZONE / 2;

    z.hit.setPosition(cx, cy);
    z.label.setPosition(cx, cy + half + 2).setText(p.label);

    z.g.clear();
    if (!p.revealed) {
      z.g.lineStyle(1, 0x7b8cd0, 0.9);
      dashRect(z.g, cx - half, cy - half, ZONE, ZONE);
      z.label.setColor('#6f7aa8');
      return;
    }
    const hot = p.itemId !== undefined && !p.binned;
    const c = hot ? AMBER : GREEN;
    z.g.fillStyle(c, 0.12);
    z.g.fillRect(cx - half, cy - half, ZONE, ZONE);
    z.g.lineStyle(1, c, 0.9);
    z.g.strokeRect(cx - half, cy - half, ZONE, ZONE);
    z.label.setColor(hot ? '#d8a020' : '#9fe8a0');
  }

  private revealPocket(index: number): void {
    if (!this.open_) return;
    const p = this.pockets[index];
    if (p === undefined || p.revealed) return;
    p.revealed = true;
    this.paintZone(index);
    if (p.itemId !== undefined) this.spawnToken(index, p.itemId);
  }

  private spawnToken(index: number, itemId: string): void {
    const p = this.pockets[index];
    const t = this.tokens.find((x) => !x.live);
    if (p === undefined || t === undefined) return;

    const art = TOKEN_ART[itemId] ?? DEFAULT_ART;
    t.itemId = itemId;
    t.pocketIndex = index;
    t.live = true;
    t.gone = false;
    t.homeX = OUTLINE_X + (p.x + 6) * SCALE;
    t.homeY = OUTLINE_Y + (p.y + 6) * SCALE;

    t.body.setSize(art.w, art.h).setFillStyle(art.colour, 1);
    t.label.setText(NAME_BY_ID.get(itemId) ?? itemId).setPosition(0, art.h / 2 + 2);
    t.root.setPosition(t.homeX, t.homeY).setScale(1).setAlpha(1).setRotation(0).setVisible(true);
    if (t.root.input) t.root.input.enabled = true;
    this.root.bringToTop(t.root);
  }

  private hideToken(t: TokenView): void {
    this.scene.tweens.killTweensOf(t.root);
    t.live = false;
    t.gone = false;
    t.itemId = '';
    t.pocketIndex = -1;
    t.root.setVisible(false).setRotation(0).setScale(1).setAlpha(1);
    if (t.root.input) t.root.input.enabled = false;
  }

  private tokenFor(obj: Phaser.GameObjects.GameObject): TokenView | undefined {
    return this.tokens.find((t) => t.live && !t.gone && t.root === obj);
  }

  private readonly onDragStart = (pointer: Phaser.Input.Pointer, obj: Phaser.GameObjects.GameObject): void => {
    if (!this.open_) return;
    const t = this.tokenFor(obj);
    if (t === undefined) return;
    // Overlay is scrollFactor 0, so track screen-space pointer, not world drag coords.
    t.grabOffX = t.root.x - pointer.x;
    t.grabOffY = t.root.y - pointer.y;
    this.root.bringToTop(t.root);
  };

  private readonly onDrag = (pointer: Phaser.Input.Pointer, obj: Phaser.GameObjects.GameObject): void => {
    if (!this.open_) return;
    const t = this.tokenFor(obj);
    if (t === undefined) return;
    t.root.setPosition(pointer.x + t.grabOffX, pointer.y + t.grabOffY);
  };

  private readonly onDragEnd = (_pointer: Phaser.Input.Pointer, obj: Phaser.GameObjects.GameObject): void => {
    if (!this.open_) return;
    const t = this.tokenFor(obj);
    if (t === undefined) return;
    if (this.binGeom.contains(t.root.x, t.root.y)) this.binToken(t);
    else this.scene.tweens.add({ targets: t.root, x: t.homeX, y: t.homeY, duration: 160, ease: 'Back.easeOut' });
  };

  private binToken(t: TokenView): void {
    const p = this.pockets[t.pocketIndex];
    if (p === undefined) return;
    p.binned = true;
    this.sfx?.play('clickerClunk');
    const itemId = t.itemId;

    if (t.root.input) t.root.input.enabled = false;
    t.gone = true; // stops the flap and the drag, but keeps the slot so nothing reuses it mid-tween
    this.scene.tweens.add({
      targets: t.root,
      x: BIN_X,
      y: BIN_Y,
      scale: 0.2,
      alpha: 0,
      rotation: 0,
      duration: 240,
      ease: 'Quad.easeIn',
      onComplete: () => t.root.setVisible(false),
    });

    this.lineText.setText(foundLine(itemId));
    this.found.push(NAME_BY_ID.get(itemId) ?? itemId);
    this.logText.setText(this.found.join('\n'));
    this.paintZone(t.pocketIndex);
  }

  private resolve(): void {
    const done = this.doneFn;
    const patron = this.patron;
    if (!this.open_ || done === null || patron === null) return;
    this.open_ = false;
    this.doneFn = null;
    this.root.setVisible(false);
    done(scorePatDown(patron, this.pockets));
  }
}
