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

const DRUID_EFFECTS = new Set<HealingEffectId>(["regrowth", "rejuvenation", "lifebloom", "wild-growth"]);

export interface HealingEffectAdvanceResult {
  member: PartyMember;
  tickCount: number;
  tickCounts: Partial<Record<HealingEffectId, number>>;
  bloomHealing: number;
}

export function healingEffect(member: PartyMember, id: HealingEffectId): HealingOverTimeEffect | undefined {
  return member.healingEffects.find((effect) => effect.id === id);
}

export function setHealingEffect(member: PartyMember, effect: HealingOverTimeEffect): PartyMember {
  const existingIndex = member.healingEffects.findIndex((candidate) => candidate.id === effect.id);
  if (existingIndex < 0) return { ...member, healingEffects: [...member.healingEffects, effect] };
  const healingEffects = [...member.healingEffects];
  healingEffects[existingIndex] = effect;
  return { ...member, healingEffects };
}

export function removeHealingEffect(member: PartyMember, id: HealingEffectId): PartyMember {
  const healingEffects = member.healingEffects.filter((effect) => effect.id !== id);
  return healingEffects.length === member.healingEffects.length ? member : { ...member, healingEffects };
}

export function triggerHealingEffects(
  member: PartyMember,
  healingMultiplier: number,
  predicate: (effect: HealingOverTimeEffect) => boolean = () => true,
): { member: PartyMember; healing: number; triggered: number } {
  if (member.hp <= 0) return { member, healing: 0, triggered: 0 };
  let healing = 0;
  let triggered = 0;
  for (const effect of member.healingEffects) {
    if (!predicate(effect)) continue;
    healing += effect.healingPerTick * effect.stacks * healingMultiplier;
    triggered += 1;
  }
  return {
    member: healing > 0 ? { ...member, hp: Math.min(member.maxHp, member.hp + healing) } : member,
    healing,
    triggered,
  };
}

export function advanceHealingEffects(
  member: PartyMember,
  oldTime: number,
  time: number,
  healingMultiplier: number,
  flourishExpiresAt = 0,
): HealingEffectAdvanceResult {
  if (!member.healingEffects.length) return { member, tickCount: 0, tickCounts: {}, bloomHealing: 0 };
  let hp = member.hp;
  let tickCount = 0;
  const tickCounts: Partial<Record<HealingEffectId, number>> = {};
  let bloomHealing = 0;
  const healingEffects: HealingOverTimeEffect[] = [];

  for (const current of member.healingEffects) {
    if (current.expiresAt <= oldTime) continue;
    let nextTickAt = current.nextTickAt;
    const lastTickAt = Math.min(time, current.expiresAt);
    while (nextTickAt <= lastTickAt + 0.001) {
      if (hp > 0) {
        hp = Math.min(member.maxHp, hp + current.healingPerTick * current.stacks * healingMultiplier);
        tickCount += 1;
        tickCounts[current.id] = (tickCounts[current.id] ?? 0) + 1;
      }
      const accelerated = DRUID_EFFECTS.has(current.id) && flourishExpiresAt > nextTickAt;
      nextTickAt += accelerated ? Math.min(0.5, current.tickInterval) : current.tickInterval;
    }

    if (time >= current.expiresAt) {
      if (current.id === "lifebloom" && hp > 0) {
        const bloom = (current.expirationHealingPerStack ?? 10) * current.stacks * healingMultiplier;
        bloomHealing += bloom;
        hp = Math.min(member.maxHp, hp + bloom);
      }
      continue;
    }

    healingEffects.push({ ...current, nextTickAt });
  }

  return {
    member: { ...member, hp, healingEffects },
    tickCount,
    tickCounts,
    bloomHealing,
  };
}

export function flourishParty(party: readonly PartyMember[], time: number, extension = 5): PartyMember[] {
  return party.map((member) => {
    let changed = false;
    const healingEffects = member.healingEffects.map((effect) => {
      if (!DRUID_EFFECTS.has(effect.id) || effect.expiresAt <= time) return effect;
      changed = true;
      return {
        ...effect,
        expiresAt: effect.expiresAt + extension,
        nextTickAt: Math.min(effect.nextTickAt, time + 0.5),
      };
    });
    return changed ? { ...member, healingEffects } : member;
  });
}

export function chainHealIndexes(
  party: readonly PartyMember[],
  positions: Readonly<Record<MemberId, WorldPosition>>,
  startIndex: number,
  maxTargets: number,
  maxJumpDistance = 7,
): number[] {
  if (startIndex < 0 || party[startIndex]?.hp <= 0 || maxTargets <= 0) return [];
  const result = [startIndex];
  const visited = new Set<number>(result);
  while (result.length < maxTargets) {
    const previousIndex = result[result.length - 1];
    const previousPosition = positions[party[previousIndex].id];
    const candidate = party
      .map((member, index) => ({
        index,
        member,
        jumpDistance: distance(previousPosition, positions[member.id]),
      }))
      .filter(({ index, member, jumpDistance }) => !visited.has(index)
        && member.hp > 0
        && member.hp < member.maxHp
        && jumpDistance <= maxJumpDistance)
      .sort((left, right) => left.jumpDistance - right.jumpDistance
        || (left.member.hp / left.member.maxHp) - (right.member.hp / right.member.maxHp)
        || left.index - right.index)[0];
    if (!candidate) break;
    visited.add(candidate.index);
    result.push(candidate.index);
  }
  return result;
}

export function equalizeHealthPercentages(
  party: readonly PartyMember[],
  positions: Readonly<Record<MemberId, WorldPosition>>,
  center: WorldPosition,
  radius: number,
): PartyMember[] {
  const linkedIndexes = party
    .map((member, index) => ({ member, index }))
    .filter(({ member }) => member.hp > 0 && distance(positions[member.id], center) <= radius)
    .map(({ index }) => index);
  if (linkedIndexes.length < 2) return [...party];
  const averagePercent = linkedIndexes.reduce((sum, index) => sum + party[index].hp / party[index].maxHp, 0) / linkedIndexes.length;
  const linked = new Set(linkedIndexes);
  return party.map((member, index) => linked.has(index)
    ? { ...member, hp: Math.min(member.maxHp, member.maxHp * averagePercent) }
    : member);
}

export function resolveReactiveHealAfterDamage(
  before: PartyMember,
  after: PartyMember,
  time: number,
  healingMultiplier: number,
): { member: PartyMember; triggered: boolean } {
  const effect = before.reactiveHeal;
  const tookDamage = after.hp < before.hp || after.absorb < before.absorb;
  if (!effect || effect.id !== "earth-shield" || effect.charges <= 0 || effect.expiresAt <= time
    || effect.nextTriggerAt > time || after.hp <= 0 || !tookDamage) {
    return {
      member: effect && effect.expiresAt <= time ? { ...after, reactiveHeal: null } : after,
      triggered: false,
    };
  }
  const charges = effect.charges - 1;
  return {
    member: {
      ...after,
      hp: Math.min(after.maxHp, after.hp + effect.healingPerTrigger * healingMultiplier),
      reactiveHeal: charges > 0 ? { ...effect, charges, nextTriggerAt: time + 0.75 } : null,
    },
    triggered: true,
  };
}
