export interface BeatTick {
  beatIndex: number;
  audioTimeMs: number;
}

export type BangGrade = 'perfect' | 'ok' | 'miss';

export const PERFECT_MS = 60;
export const OK_MS = 130;
export const BANGS_TO_BUST = 3;

const AGGRO_PER_MISS = 0.8;
const AGGRO_CAP = 6;
const VIBE_BUSTED = 9;
const VIBE_FIZZLED = -1.5;

/**
 * How far (ms, absolute) a bang landed from the NEAREST beat boundary implied by
 * lastTick. Boundaries are lastTick.audioTimeMs + k*beatIntervalMs for integer k
 * (k may be negative — a bang can land just BEFORE the tick it is judged against).
 */
export function beatError(bangTimeMs: number, lastTick: BeatTick, beatIntervalMs: number): number {
  const delta = bangTimeMs - lastTick.audioTimeMs;
  // Degenerate clock: every point is its own boundary, so the tick is all we have.
  if (!(beatIntervalMs > 0)) return Math.abs(delta);
  const nearest = Math.round(delta / beatIntervalMs) * beatIntervalMs;
  return Math.abs(delta - nearest);
}

export function judgeBang(bangTimeMs: number, lastTick: BeatTick, beatIntervalMs: number): BangGrade {
  const err = beatError(bangTimeMs, lastTick, beatIntervalMs);
  if (err <= PERFECT_MS) return 'perfect';
  if (err <= OK_MS) return 'ok';
  return 'miss';
}

export interface StallState {
  combo: number;
  bangs: number;
  busted: boolean;
  aggro: number;
  lastGrade: BangGrade | null;
}

export function freshStall(): StallState {
  return { combo: 0, bangs: 0, busted: false, aggro: 0, lastGrade: null };
}

/** Pure: returns the NEXT state, never mutates the input. */
export function applyBang(s: StallState, grade: BangGrade): StallState {
  const hit = grade !== 'miss';
  const combo = hit ? s.combo + 1 : 0;
  return {
    combo,
    bangs: s.bangs + 1,
    // Once the door gives, it stays given.
    busted: s.busted || combo >= BANGS_TO_BUST,
    aggro: hit ? s.aggro : s.aggro + AGGRO_PER_MISS,
    lastGrade: grade,
  };
}

export interface StallOutcome {
  vibeDelta: number;
  aggroDelta: number;
  solved: boolean;
}

/** Final scoring when the stall minigame ends (busted, or the player walked away). */
export function scoreStall(s: StallState): StallOutcome {
  return {
    vibeDelta: s.busted ? VIBE_BUSTED : VIBE_FIZZLED,
    aggroDelta: Math.min(s.aggro, AGGRO_CAP),
    solved: s.busted,
  };
}
