import { distance } from "./geometry";
import type { BossMotionState, BossState, MemberId, PartyMember, WorldPosition } from "./types";

export type AiCombatantId = Exclude<MemberId, "aelia">;

export type PartyAbilityId =
  | "sword_slash" | "shield_slam" | "revenge" | "sweeping_guard" | "bulwark_march"
  | "quick_shot" | "aimed_shot" | "rapid_fire" | "kill_shot" | "deadeye"
  | "arcane_bolt" | "ember_lance" | "arcane_burst" | "comet" | "overcharge"
  | "quick_cut" | "twin_fang" | "backstab" | "fan_of_blades" | "blade_flurry";

export const PARTY_ABILITY_NAMES: Record<PartyAbilityId, string> = {
  sword_slash: "Sword Slash",
  shield_slam: "Shield Slam",
  revenge: "Revenge",
  sweeping_guard: "Sweeping Guard",
  bulwark_march: "Bulwark March",
  quick_shot: "Quick Shot",
  aimed_shot: "Aimed Shot",
  rapid_fire: "Rapid Fire",
  kill_shot: "Kill Shot",
  deadeye: "Deadeye",
  arcane_bolt: "Arcane Bolt",
  ember_lance: "Ember Lance",
  arcane_burst: "Arcane Burst",
  comet: "Comet",
  overcharge: "Overcharge",
  quick_cut: "Quick Cut",
  twin_fang: "Twin Fang",
  backstab: "Backstab",
  fan_of_blades: "Fan of Blades",
  blade_flurry: "Blade Flurry",
};

export const PARTY_ABILITY_LOADOUTS: Record<AiCombatantId, readonly PartyAbilityId[]> = {
  brann: ["sword_slash", "shield_slam", "revenge", "sweeping_guard", "bulwark_march"],
  nia: ["quick_shot", "aimed_shot", "rapid_fire", "kill_shot", "deadeye"],
  orin: ["arcane_bolt", "ember_lance", "arcane_burst", "comet", "overcharge"],
  vale: ["quick_cut", "twin_fang", "backstab", "fan_of_blades", "blade_flurry"],
};

export interface PartyCombatTarget {
  instanceId: string;
  boss: BossState;
  motion: BossMotionState;
}

export interface PartyDamageEvent {
  id: number;
  at: number;
  sourceId: AiCombatantId;
  abilityId: PartyAbilityId;
  targetInstanceId: string;
  amount: number;
  secondary: boolean;
}

export interface PartyCombatAction {
  abilityId: PartyAbilityId;
  targetInstanceId: string;
  startedAt: number;
  completesAt: number;
  impactTimes: number[];
  nextImpactIndex: number;
  baseDamage: number;
  requiresStationary: boolean;
  multiplier: number;
}

export interface PartyVisualAction {
  abilityId: PartyAbilityId;
  targetInstanceId: string;
  startedAt: number;
  impactAt: number;
  endsAt: number;
}

export interface PartyCombatantState {
  id: AiCombatantId;
  readyAt: number;
  resource: number;
  points: number;
  cooldowns: Partial<Record<PartyAbilityId, number>>;
  activeAction: PartyCombatAction | null;
  visualAction: PartyVisualAction | null;
  overchargeStacks: number;
  bladeFlurryUntil: number;
  revengeReadyUntil: number;
  lastHp: number;
  damageDone: number;
}

export interface TankAuraState {
  expiresAt: number;
  radius: number;
  damageReduction: number;
}

export interface PartyCombatState {
  combatants: Record<AiCombatantId, PartyCombatantState>;
  tankAura: TankAuraState;
  nextEventId: number;
}

export interface PartyCombatContext {
  oldTime: number;
  time: number;
  party: PartyMember[];
  oldPositions: Record<MemberId, WorldPosition>;
  positions: Record<MemberId, WorldPosition>;
  targets: PartyCombatTarget[];
  upcomingMechanicRemaining: number;
  gearModifiers?: Record<AiCombatantId, { damage: number; cooldown: number }>;
}

interface AbilitySpec {
  id: PartyAbilityId;
  duration: number;
  impactOffsets: number[];
  damage: number;
  gcd: number;
  cooldown?: number;
  requiresStationary?: boolean;
}

const EMPTY_AURA: TankAuraState = { expiresAt: 0, radius: 3, damageReduction: 0.3 };
const RANGED_IDS: readonly AiCombatantId[] = ["nia", "orin"];
const VALE_CLEAVE_RADIUS = 3.6;
const VALE_MELEE_RANGE = 3.65;
const BRANN_MELEE_RANGE = 4.8;
// Calibrated against full two-boss rotations: intended 50–90 seconds, never over 100.
const PARTY_DAMAGE_SCALE = 2;

function combatant(id: AiCombatantId, hp: number): PartyCombatantState {
  return {
    id,
    readyAt: 0,
    resource: id === "vale" ? 100 : 0,
    points: 0,
    cooldowns: {},
    activeAction: null,
    visualAction: null,
    overchargeStacks: 0,
    bladeFlurryUntil: 0,
    revengeReadyUntil: 0,
    lastHp: hp,
    damageDone: 0,
  };
}

export function createPartyCombatState(party: PartyMember[]): PartyCombatState {
  const hp = (id: AiCombatantId) => party.find((member) => member.id === id)?.hp ?? 0;
  return {
    combatants: {
      brann: combatant("brann", hp("brann")),
      nia: combatant("nia", hp("nia")),
      orin: combatant("orin", hp("orin")),
      vale: combatant("vale", hp("vale")),
    },
    tankAura: { ...EMPTY_AURA },
    nextEventId: 1,
  };
}

function cloneCombatant(source: PartyCombatantState): PartyCombatantState {
  return {
    ...source,
    cooldowns: { ...source.cooldowns },
    activeAction: source.activeAction ? { ...source.activeAction, impactTimes: [...source.activeAction.impactTimes] } : null,
    visualAction: source.visualAction ? { ...source.visualAction } : null,
  };
}

function isReady(actor: PartyCombatantState, abilityId: PartyAbilityId, at: number) {
  return (actor.cooldowns[abilityId] ?? 0) <= at + 0.001;
}

function moving(id: AiCombatantId, context: PartyCombatContext) {
  const elapsed = Math.max(0.001, context.time - context.oldTime);
  return distance(context.oldPositions[id], context.positions[id]) / elapsed > 0.35;
}

function livingTargets(targets: PartyCombatTarget[]) {
  return targets.filter((target) => target.boss.hp > 0);
}

function targetsInRange(source: WorldPosition, targets: PartyCombatTarget[], range: number) {
  return livingTargets(targets).filter((target) => distance(source, target.motion.position) <= range);
}

function targetFor(id: AiCombatantId, context: PartyCombatContext, range = Number.POSITIVE_INFINITY) {
  const eligible = targetsInRange(context.positions[id], context.targets, range);
  if (!eligible.length) return undefined;
  if (id === "orin" && eligible.length > 1) return eligible[1];
  if (RANGED_IDS.includes(id)) return eligible[0];
  let nearest = eligible[0];
  for (let index = 1; index < eligible.length; index += 1) {
    if (distance(context.positions[id], eligible[index].motion.position) < distance(context.positions[id], nearest.motion.position)) nearest = eligible[index];
  }
  return nearest;
}

function partyNeedsBulwark(context: PartyCombatContext) {
  const brann = context.party.find((member) => member.id === "brann");
  const lowMembers = context.party.filter((member) => member.hp > 0 && member.hp / member.maxHp < 0.6).length;
  return context.upcomingMechanicRemaining <= 2 || (brann?.hp ?? 0) / Math.max(1, brann?.maxHp ?? 1) < 0.7 || lowMembers >= 2;
}

function chooseAbility(actor: PartyCombatantState, at: number, isMoving: boolean, context: PartyCombatContext): AbilitySpec | null {
  const source = context.positions[actor.id];
  const rangedTarget = targetFor(actor.id, context);
  if (actor.id === "nia") {
    if (!rangedTarget) return null;
    if (rangedTarget.boss.hp / rangedTarget.boss.maxHp <= 0.25 && isReady(actor, "kill_shot", at)) return { id: "kill_shot", duration: 0.45, impactOffsets: [0.24], damage: 7, gcd: 1.1, cooldown: 10 };
    if (!isMoving && isReady(actor, "rapid_fire", at)) return { id: "rapid_fire", duration: 2, impactOffsets: [0.4, 0.8, 1.2, 1.6], damage: 2, gcd: 2, cooldown: 9, requiresStationary: true };
    if (!isMoving && actor.resource >= 50) return { id: "deadeye", duration: 1, impactOffsets: [1], damage: 10, gcd: 1.1, requiresStationary: true };
    if (!isMoving) return { id: "aimed_shot", duration: 1.5, impactOffsets: [1.5], damage: 4, gcd: 1.5, requiresStationary: true };
    return { id: "quick_shot", duration: 0.45, impactOffsets: [0.24], damage: 2, gcd: 1.15 };
  }

  if (actor.id === "orin") {
    if (!rangedTarget) return null;
    if (!isMoving && actor.overchargeStacks === 0 && isReady(actor, "overcharge", at)) return { id: "overcharge", duration: 0.35, impactOffsets: [], damage: 0, gcd: 0.7, cooldown: 20 };
    if (!isMoving && isReady(actor, "comet", at)) return { id: "comet", duration: 2, impactOffsets: [2], damage: 9, gcd: 2, cooldown: 12, requiresStationary: true };
    if (!isMoving && actor.points >= 3) return { id: "arcane_burst", duration: 1, impactOffsets: [1], damage: 8, gcd: 1.1, requiresStationary: true };
    if (isReady(actor, "ember_lance", at)) return { id: "ember_lance", duration: 0.45, impactOffsets: [0.24], damage: 2, gcd: 1.1, cooldown: 4 };
    if (isMoving) return null;
    return { id: "arcane_bolt", duration: 1.4, impactOffsets: [1.4], damage: 3, gcd: 1.4, requiresStationary: true };
  }

  if (actor.id === "vale") {
    const nearby = targetsInRange(source, context.targets, VALE_CLEAVE_RADIUS);
    const target = targetFor(actor.id, context, VALE_MELEE_RANGE);
    if (!target) return null;
    if (nearby.length > 1 && actor.bladeFlurryUntil <= at && actor.resource >= 20 && isReady(actor, "blade_flurry", at)) return { id: "blade_flurry", duration: 0.35, impactOffsets: [], damage: 0, gcd: 0.7, cooldown: 15 };
    if (nearby.length > 1 && actor.resource >= 35 && isReady(actor, "fan_of_blades", at)) return { id: "fan_of_blades", duration: 0.65, impactOffsets: [0.4], damage: 3, gcd: 1.1, cooldown: 6 };
    if (actor.points >= 3 && actor.resource >= 25) return { id: "backstab", duration: 0.7, impactOffsets: [0.45], damage: 5, gcd: 1.1 };
    if (nearby.length > 1 && actor.resource >= 30) return { id: "twin_fang", duration: 0.65, impactOffsets: [0.4], damage: 3, gcd: 1.1 };
    return { id: "quick_cut", duration: 0.55, impactOffsets: [0.32], damage: 2, gcd: 1.1 };
  }

  const target = targetFor(actor.id, context, BRANN_MELEE_RANGE);
  if (!target) return null;
  if (partyNeedsBulwark(context) && isReady(actor, "bulwark_march", at)) return { id: "bulwark_march", duration: 0.6, impactOffsets: [0.35], damage: 2, gcd: 1.1, cooldown: 30 };
  if (actor.revengeReadyUntil > at && isReady(actor, "revenge", at)) return { id: "revenge", duration: 0.6, impactOffsets: [0.35], damage: 3, gcd: 1.1, cooldown: 5 };
  if (isReady(actor, "shield_slam", at)) return { id: "shield_slam", duration: 0.6, impactOffsets: [0.35], damage: 2, gcd: 1.1, cooldown: 6 };
  if (targetsInRange(source, context.targets, BRANN_MELEE_RANGE).length > 1 && isReady(actor, "sweeping_guard", at)) return { id: "sweeping_guard", duration: 0.65, impactOffsets: [0.4], damage: 2, gcd: 1.1, cooldown: 4 };
  return { id: "sword_slash", duration: 0.6, impactOffsets: [0.35], damage: 1, gcd: 1.2 };
}

function applyStartCosts(actor: PartyCombatantState, spec: AbilitySpec, at: number, state: PartyCombatState) {
  if (spec.cooldown) actor.cooldowns[spec.id] = at + spec.cooldown;
  if (spec.id === "quick_shot") actor.resource = Math.min(100, actor.resource + 10);
  if (spec.id === "aimed_shot") actor.resource = Math.min(100, actor.resource + 15);
  if (spec.id === "deadeye") actor.resource -= 50;
  if (spec.id === "arcane_bolt") actor.points = Math.min(3, actor.points + 1);
  if (spec.id === "arcane_burst") actor.points = 0;
  if (spec.id === "overcharge") actor.overchargeStacks = 3;
  if (spec.id === "quick_cut") actor.points = Math.min(5, actor.points + 1);
  if (spec.id === "twin_fang") actor.resource -= 30;
  if (spec.id === "backstab") { actor.resource -= 25; actor.points = Math.max(0, actor.points - 3); }
  if (spec.id === "fan_of_blades") actor.resource -= 35;
  if (spec.id === "blade_flurry") { actor.resource -= 20; actor.bladeFlurryUntil = at + 8; }
  if (spec.id === "bulwark_march") state.tankAura.expiresAt = at + 6;
}

function startAction(actor: PartyCombatantState, spec: AbilitySpec, target: PartyCombatTarget, at: number, state: PartyCombatState) {
  applyStartCosts(actor, spec, at, state);
  let multiplier = 1;
  if (actor.id === "orin" && spec.damage > 0 && actor.overchargeStacks > 0) {
    multiplier = 1.25;
    actor.overchargeStacks -= 1;
  }
  actor.readyAt = at + spec.gcd;
  actor.activeAction = {
    abilityId: spec.id,
    targetInstanceId: target.instanceId,
    startedAt: at,
    completesAt: at + spec.duration,
    impactTimes: spec.impactOffsets.map((offset) => at + offset),
    nextImpactIndex: 0,
    baseDamage: spec.damage,
    requiresStationary: spec.requiresStationary ?? false,
    multiplier,
  };
  actor.visualAction = {
    abilityId: spec.id,
    targetInstanceId: target.instanceId,
    startedAt: at,
    impactAt: spec.impactOffsets.length ? at + spec.impactOffsets[0] : at + spec.duration,
    endsAt: at + Math.max(spec.duration, spec.gcd),
  };
}

function damageTarget(
  state: PartyCombatState,
  actor: PartyCombatantState,
  action: PartyCombatAction,
  target: PartyCombatTarget,
  amount: number,
  at: number,
  secondary: boolean,
  events: PartyDamageEvent[],
) {
  if (target.boss.hp <= 0 || amount <= 0) return;
  const dealt = Math.min(target.boss.hp, amount * action.multiplier * PARTY_DAMAGE_SCALE);
  target.boss.hp -= dealt;
  actor.damageDone += dealt;
  events.push({ id: state.nextEventId++, at, sourceId: actor.id, abilityId: action.abilityId, targetInstanceId: target.instanceId, amount: dealt, secondary });
}

function resolveImpact(state: PartyCombatState, actor: PartyCombatantState, action: PartyCombatAction, at: number, context: PartyCombatContext, targets: PartyCombatTarget[], events: PartyDamageEvent[]) {
  const source = context.positions[actor.id];
  const range = actor.id === "vale" ? VALE_MELEE_RANGE : actor.id === "brann" ? BRANN_MELEE_RANGE : Number.POSITIVE_INFINITY;
  let target = targets.find((entry) => entry.instanceId === action.targetInstanceId && entry.boss.hp > 0 && distance(source, entry.motion.position) <= range);
  target ??= targetFor(actor.id, context, range);
  if (!target) return;

  if (action.abilityId === "fan_of_blades" || action.abilityId === "sweeping_guard") {
    const radius = action.abilityId === "fan_of_blades" ? VALE_CLEAVE_RADIUS : BRANN_MELEE_RANGE;
    for (const nearby of targetsInRange(source, targets, radius)) damageTarget(state, actor, action, nearby, action.baseDamage, at, nearby.instanceId !== target.instanceId, events);
    return;
  }

  damageTarget(state, actor, action, target, action.baseDamage, at, false, events);
  const secondaryTargets = targetsInRange(source, targets, actor.id === "vale" ? VALE_CLEAVE_RADIUS : BRANN_MELEE_RANGE).filter((entry) => entry.instanceId !== target!.instanceId);
  if (action.abilityId === "twin_fang") {
    for (const secondary of secondaryTargets) damageTarget(state, actor, action, secondary, 2, at, true, events);
  } else if (actor.id === "vale" && actor.bladeFlurryUntil > at && !["fan_of_blades", "blade_flurry"].includes(action.abilityId)) {
    for (const secondary of secondaryTargets) damageTarget(state, actor, action, secondary, action.baseDamage * 0.5, at, true, events);
  }
}

export function advancePartyCombat(source: PartyCombatState, context: PartyCombatContext) {
  const state: PartyCombatState = {
    combatants: {
      brann: cloneCombatant(source.combatants.brann),
      nia: cloneCombatant(source.combatants.nia),
      orin: cloneCombatant(source.combatants.orin),
      vale: cloneCombatant(source.combatants.vale),
    },
    tankAura: { ...source.tankAura },
    nextEventId: source.nextEventId,
  };
  const targets = context.targets.map((target) => ({ ...target, boss: { ...target.boss } }));
  const combatContext: PartyCombatContext = { ...context, targets };
  const events: PartyDamageEvent[] = [];
  const elapsed = context.time - context.oldTime;

  for (const id of ["brann", "nia", "orin", "vale"] as const) {
    const actor = state.combatants[id];
    const member = context.party.find((entry) => entry.id === id);
    if (!member || member.hp <= 0) continue;
    if (member.knockedUntil > context.time) {
      actor.activeAction = null;
      actor.readyAt = Math.max(actor.readyAt, member.knockedUntil);
      continue;
    }
    if (id === "vale") actor.resource = Math.min(100, actor.resource + 12 * elapsed);
    if (id === "brann" && member.hp < actor.lastHp) actor.revengeReadyUntil = context.time + 5;
    actor.lastHp = member.hp;
    const isMoving = moving(id, combatContext);
    if (isMoving && actor.activeAction?.requiresStationary) {
      actor.activeAction = null;
      actor.readyAt = Math.max(actor.readyAt, context.oldTime + 0.2);
      if (actor.visualAction) actor.visualAction.endsAt = context.oldTime;
    }

    for (let safety = 0; safety < 12; safety += 1) {
      const action = actor.activeAction;
      if (action) {
        while (action.nextImpactIndex < action.impactTimes.length && action.impactTimes[action.nextImpactIndex] <= context.time + 0.001) {
          resolveImpact(state, actor, action, action.impactTimes[action.nextImpactIndex], combatContext, targets, events);
          action.nextImpactIndex += 1;
        }
        if (action.completesAt > context.time + 0.001) break;
        actor.activeAction = null;
        continue;
      }

      const startAt = Math.max(context.oldTime, actor.readyAt);
      if (startAt > context.time + 0.001) break;
      const baseSpec = chooseAbility(actor, startAt, isMoving, combatContext);
      const modifier = context.gearModifiers?.[id];
      const spec = baseSpec && modifier ? {
        ...baseSpec,
        duration: baseSpec.duration * modifier.cooldown,
        impactOffsets: baseSpec.impactOffsets.map((offset) => offset * modifier.cooldown),
        damage: baseSpec.damage * modifier.damage,
        gcd: baseSpec.gcd * modifier.cooldown,
        cooldown: baseSpec.cooldown === undefined ? undefined : baseSpec.cooldown * modifier.cooldown,
      } : baseSpec;
      if (!spec) { actor.readyAt = context.time + 0.1; break; }
      const range = id === "vale" ? VALE_MELEE_RANGE : id === "brann" ? BRANN_MELEE_RANGE : Number.POSITIVE_INFINITY;
      const target = targetFor(id, combatContext, range);
      if (!target) { actor.readyAt = context.time + 0.1; break; }
      startAction(actor, spec, target, startAt, state);
    }
  }

  return { state, events };
}

export function tankAuraProtects(position: WorldPosition, tankPosition: WorldPosition, aura: TankAuraState, time: number) {
  return aura.expiresAt > time && distance(position, tankPosition) <= aura.radius;
}
