import { distance } from "../geometry";
import { ARENA_CENTER } from "../arena";
import type { BossId, BossMotionState, BossState, CircleHazard, CircleHazardKind, MemberId, PartyMember, PoolTelegraph, WorldPosition } from "../types";
import type { BossMechanicContext, BossMechanicEvent } from "./types";

const PERSISTENT_HAZARD_KINDS = new Set<CircleHazardKind>(["venom_pool", "lava_pool", "hourglass", "soul_rift"]);
const HAZARD_LABELS = {
  venom_pool: "Venom pool",
  skyfall: "Skyfall",
  quake: "Fracture Quake",
  lava_pool: "Lava pool",
  stinger_eruption: "Stinger Eruption",
  hourglass: "Hourglass zone",
  tidal_burst: "Crushing Tide",
  soul_rift: "Soul rift",
  crownfall: "Crownfall",
  royal_shockwave: "Royal Shockwave",
} as const;

/**
 * Standard idle location after a mechanic. Dual encounters keep distinct
 * center-adjacent slots through formationOffsetX instead of drifting to a
 * displaced tank at the edge of the room.
 */
export function returnBossToArenaCenter(motion: BossMotionState, delta: number, speed: number) {
  const targetX = ARENA_CENTER[0] + motion.formationOffsetX;
  const targetZ = ARENA_CENTER[1];
  const dx = targetX - motion.position[0];
  const dz = targetZ - motion.position[1];
  const remaining = Math.hypot(dx, dz);
  const step = speed * delta;
  if (remaining < 0.0001 || remaining <= step) {
    motion.position[0] = targetX;
    motion.position[1] = targetZ;
    return true;
  }
  motion.position[0] += (dx / remaining) * step;
  motion.position[1] += (dz / remaining) * step;
  return false;
}

export function createCircleHazard({
  id,
  kind,
  center,
  innerRadius,
  radius,
  activatesAt,
  duration,
  damage,
  tickInterval,
}: {
  id: string;
  kind: CircleHazardKind;
  center: WorldPosition;
  innerRadius?: number;
  radius: number;
  activatesAt: number;
  duration: number;
  damage: number;
  tickInterval?: number;
}): CircleHazard {
  return {
    id,
    kind,
    center: [...center],
    innerRadius,
    radius,
    activatesAt,
    expiresAt: activatesAt + duration,
    damage,
    tickInterval,
    nextDamageAt: {},
    resolved: false,
    hitIds: [],
  };
}

export function createBaseMotion(bossId: BossId): BossMotionState {
  return {
    bossId,
    activeMechanicId: null,
    formationOffsetX: 0,
    mode: "holding",
    position: [0, -8.2],
    chargeStart: [0, -8.2],
    chargeEnd: [0, 5.5],
    chargeTargetId: "nia",
    chargeHitIds: [],
    phaseEndsAt: 0,
    chargeCount: 0,
    pounceTargetId: "aelia",
    pounceCenter: [0, 4.5],
    nextMechanicAt: Number.POSITIVE_INFINITY,
    mechanicCount: 0,
    phaseStartedAt: 0,
    mechanicHitIds: [],
    mechanicNextDamageAt: {},
    tetherIds: [],
    tetherBreakDistance: 0,
    breathAngle: 0,
    breathStartAngle: 0,
    breathEndAngle: 0,
    hazards: [],
    slashLanes: [],
    poolMechanicCount: 0,
    poolTelegraphs: [],
  };
}

export function cloneMotion(source: BossMotionState): BossMotionState {
  return {
    ...source,
    position: [source.position[0], source.position[1]],
    chargeStart: [source.chargeStart[0], source.chargeStart[1]],
    chargeEnd: [source.chargeEnd[0], source.chargeEnd[1]],
    chargeHitIds: [...source.chargeHitIds],
    pounceCenter: [source.pounceCenter[0], source.pounceCenter[1]],
    mechanicHitIds: [...source.mechanicHitIds],
    mechanicNextDamageAt: { ...source.mechanicNextDamageAt },
    tetherIds: [...source.tetherIds],
    hazards: source.hazards.map((hazard) => ({
      ...hazard,
      center: [hazard.center[0], hazard.center[1]],
      nextDamageAt: { ...hazard.nextDamageAt },
      hitIds: [...hazard.hitIds],
    })),
    slashLanes: source.slashLanes.map((lane) => ({
      ...lane,
      start: [lane.start[0], lane.start[1]],
      end: [lane.end[0], lane.end[1]],
    })),
    poolTelegraphs: source.poolTelegraphs.map((telegraph): PoolTelegraph => ({
      ...telegraph,
      center: [telegraph.center[0], telegraph.center[1]],
      start: telegraph.start && [telegraph.start[0], telegraph.start[1]],
      end: telegraph.end && [telegraph.end[0], telegraph.end[1]],
      tiles: telegraph.tiles?.map((tile) => ({ ...tile, center: [tile.center[0], tile.center[1]] })),
      soulSiphon: telegraph.soulSiphon && {
        ...telegraph.soulSiphon,
        ghostPosition: [telegraph.soulSiphon.ghostPosition[0], telegraph.soulSiphon.ghostPosition[1]],
        wardPosition: [telegraph.soulSiphon.wardPosition[0], telegraph.soulSiphon.wardPosition[1]],
      },
      hitIds: [...telegraph.hitIds],
    })),
  };
}

export function chooseLivingTarget(
  party: PartyMember[],
  order: readonly MemberId[],
  startIndex: number,
) {
  for (let offset = 0; offset < order.length; offset += 1) {
    const candidate = order[(startIndex + offset) % order.length];
    if (party.some((member) => member.id === candidate && member.hp > 0)) return candidate;
  }
  return order[0];
}

export function applyMelee(
  boss: BossState,
  motion: BossMotionState,
  party: PartyMember[],
  positions: Record<MemberId, WorldPosition>,
  time: number,
  interval: number,
  amount: number,
  damageMember: BossMechanicContext["damageMember"],
) {
  while (boss.nextMeleeAt <= time) {
    if (motion.mode === "holding") {
      const tankIndex = party.findIndex((member) => member.id === "brann");
      if (tankIndex >= 0 && party[tankIndex].hp > 0) {
        party[tankIndex] = damageMember(party[tankIndex], amount, positions.brann, boss.nextMeleeAt);
      }
    }
    boss.nextMeleeAt += interval;
  }
}

export function resolveCircleHazards(
  motion: BossMotionState,
  party: PartyMember[],
  positions: Record<MemberId, WorldPosition>,
  time: number,
  damageMember: BossMechanicContext["damageMember"],
  events: BossMechanicEvent[],
) {
  let updatedParty = party;
  for (const hazard of motion.hazards) {
    if (time < hazard.activatesAt || time >= hazard.expiresAt) continue;
    const newlyHit: MemberId[] = [];
    updatedParty = updatedParty.map((member) => {
      if (member.hp <= 0) return member;
      const hazardDistance = distance(positions[member.id], hazard.center);
      const inside = hazardDistance <= hazard.radius && hazardDistance >= (hazard.innerRadius ?? 0);
      if (!inside) {
        delete hazard.nextDamageAt[member.id];
        return member;
      }
      if (!hazard.hitIds.includes(member.id)) {
        hazard.hitIds.push(member.id);
        newlyHit.push(member.id);
      }
      if (!PERSISTENT_HAZARD_KINDS.has(hazard.kind)) {
        return hazard.resolved || hazard.hitIds.includes(member.id) && !newlyHit.includes(member.id)
          ? member
          : damageMember(member, hazard.damage, positions[member.id], time, "hazard");
      }

      let next = member;
      let tickAt = hazard.nextDamageAt[member.id] ?? time;
      while (tickAt <= time + 0.001) {
        next = damageMember(next, hazard.damage, positions[member.id], tickAt, "hazard");
        tickAt += hazard.tickInterval ?? 1;
      }
      hazard.nextDamageAt[member.id] = tickAt;
      return next;
    });
    if (newlyHit.length && !hazard.resolved) {
      const pulseKind = hazard.kind === "venom_pool" ? "venom" : hazard.kind === "skyfall" || hazard.kind === "stinger_eruption" || hazard.kind === "hourglass" ? "skyfall" : "boss";
      events.push({ at: time, message: `${HAZARD_LABELS[hazard.kind]} catches ${newlyHit.length} ally${newlyHit.length === 1 ? "" : "ies"}.`, tone: "danger", pulseKind });
    }
    if (!PERSISTENT_HAZARD_KINDS.has(hazard.kind) && (newlyHit.length || time >= hazard.activatesAt)) hazard.resolved = true;
  }
  motion.hazards = motion.hazards.filter((hazard) => hazard.expiresAt > time);
  return updatedParty;
}

export function memberName(party: PartyMember[], memberId: MemberId) {
  return party.find((member) => member.id === memberId)?.name ?? memberId;
}
