// Fight brewing (design §3.2). Unlike the stall and pat-down, this one is not a
// modal minigame — it spends the floor's only real resource, where you are
// standing. Pure: no Phaser, no bus. The scene owns spawning and cooldowns.

import { FIGHT_SHOVES } from '../../data/strings/floor';

export const FIGHT_FUSE_MS = 7000;
export const BREAK_RADIUS_PX = 26;
// A fight is an EVENT, not a heartbeat. At 20s an idle night produced 28 brawls,
// which swamped every other meter on the floor and generated 28 heat strikes
// against a run cap of 3. At 150s a busy night brews four or five.
export const FIGHT_COOLDOWN_MS = 150_000;
export const FIGHT_MIN_DIST_PX = 34;

// Both must be at least this drunk to square up. 0.45 is exactly where
// drunkStage() flips tipsy -> loose, so this is a loose-and-above problem: a
// merely tipsy pair never kicks off.
export const FIGHT_MIN_INTOX = 0.45;

/** Escalation rungs the fuse burns through; also the cap on `shoves`. */
const SHOVE_STEPS = 5;

export type FightPhase = 'shoving' | 'broken' | 'swung';

export interface FightState {
  aId: string;
  bId: string;
  msLeft: number;
  phase: FightPhase;
  /** Cosmetic 0..SHOVE_STEPS tell; monotonic, derived from the burnt fuse. */
  shoves: number;
}

export interface FightOutcome {
  vibeDelta: number;
  aggroDelta: number;
  heat: boolean;
}

// You still got shoved about breaking it up, hence the aggro on a win.
const BROKEN_VIBE = 6;
const BROKEN_AGGRO = 1;
// A landed swing is the worst thing that can happen on the floor (design §3.2).
const SWUNG_VIBE = -12;
const SWUNG_AGGRO = 20;

export function freshFight(aId: string, bId: string): FightState {
  return { aId, bId, msLeft: FIGHT_FUSE_MS, phase: 'shoving', shoves: 0 };
}

/** Rungs burnt so far. Clamped both ends so a mangled msLeft can't escape 0..5. */
function shovesFor(msLeft: number): number {
  const burnt = FIGHT_FUSE_MS - msLeft;
  const step = Math.floor(burnt / (FIGHT_FUSE_MS / SHOVE_STEPS));
  return Math.max(0, Math.min(SHOVE_STEPS, step));
}

/** Shove chatter for the current rung. Clamped — FIGHT_SHOVES has no 6th line. */
export function shoveLine(shoves: number): string {
  const i = Math.max(0, Math.min(FIGHT_SHOVES.length - 1, Math.floor(shoves)));
  return FIGHT_SHOVES[i] ?? '';
}

/**
 * Is the player physically between A and B? True when the point is within
 * `radius` of the A-B SEGMENT (not the infinite line) — so standing past either
 * shoulder does not count. Handle the degenerate A==B case without dividing by zero.
 */
export function isBetween(
  px: number,
  py: number,
  ax: number,
  ay: number,
  bx: number,
  by: number,
  radius: number = BREAK_RADIUS_PX,
): boolean {
  const abx = bx - ax;
  const aby = by - ay;
  const apx = px - ax;
  const apy = py - ay;
  const lenSq = abx * abx + aby * aby;
  // Two patrons standing in the same spot: the segment is a point.
  const t = lenSq > 0 ? Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq)) : 0;
  const dx = apx - abx * t;
  const dy = apy - aby * t;
  return dx * dx + dy * dy <= radius * radius;
}

/** Pure: returns the next state, never mutates the input. */
export function stepFight(s: FightState, deltaMs: number, intervening: boolean): FightState {
  // Once it's broken or swung it's history — later frames can't relitigate it.
  if (s.phase !== 'shoving') return s;

  if (intervening) {
    return { ...s, phase: 'broken' };
  }

  const msLeft = Math.max(0, s.msLeft - deltaMs);
  return {
    ...s,
    msLeft,
    phase: msLeft <= 0 ? 'swung' : 'shoving',
    // Never goes backwards: msLeft only ever decreases while shoving.
    shoves: Math.max(s.shoves, shovesFor(msLeft)),
  };
}

export function scoreFight(s: FightState): FightOutcome {
  if (s.phase === 'broken') {
    return { vibeDelta: BROKEN_VIBE, aggroDelta: BROKEN_AGGRO, heat: false };
  }
  if (s.phase === 'swung') {
    return { vibeDelta: SWUNG_VIBE, aggroDelta: SWUNG_AGGRO, heat: true };
  }
  return { vibeDelta: 0, aggroDelta: 0, heat: false };
}

/** Both drunk enough and close enough to kick off. */
export function fightEligible(aIntox: number, bIntox: number, distPx: number): boolean {
  return (
    aIntox >= FIGHT_MIN_INTOX && bIntox >= FIGHT_MIN_INTOX && distPx <= FIGHT_MIN_DIST_PX
  );
}
