import { DEFAULT_GROUP_HEAL_TARGETS, groupHealTargets, type PartyMember, type Spell } from '../game'
import { addBounceHeal, addHotEffect } from './rules'

export type SpellTargetBucket = 'direct' | 'hot' | 'shield' | 'damageReduction'

export type SpellTargetPlan = {
  directTargets: Set<string>
  hotTargets: Set<string>
  shieldTargets: Set<string>
  damageReductionTargets: Set<string>
  groupTargets: Set<string>
}

export type ExtraTargetMode = Partial<Record<Spell['kind'], SpellTargetBucket>>

function hasBlockedId(blockedIds: Iterable<string>, id: string) {
  if (blockedIds instanceof Set) return blockedIds.has(id)
  for (const blockedId of blockedIds) {
    if (blockedId === id) return true
  }
  return false
}

export function lowestHealthExtraTarget(party: PartyMember[], blockedIds: Iterable<string>) {
  let target: PartyMember | undefined
  let targetRatio = Number.POSITIVE_INFINITY
  for (const member of party) {
    if (member.health <= 0 || hasBlockedId(blockedIds, member.id)) continue
    const healthRatio = member.health / member.maxHealth
    if (healthRatio < targetRatio) {
      target = member
      targetRatio = healthRatio
    }
  }
  return target
}

export function addLowestHealthExtraTarget(
  plan: SpellTargetPlan,
  party: PartyMember[],
  bucket: SpellTargetBucket,
) {
  const targetSet = bucket === 'direct'
    ? plan.directTargets
    : bucket === 'hot'
      ? plan.hotTargets
      : bucket === 'shield'
        ? plan.shieldTargets
        : plan.damageReductionTargets
  const extra = lowestHealthExtraTarget(party, targetSet)
  if (extra) targetSet.add(extra.id)
}

export function buildSpellTargetPlan({
  party,
  spell,
  targetId,
  extraTargets,
  directTarget,
  hotTarget,
  shieldTarget,
  damageReductionTarget,
  groupTargetCount = DEFAULT_GROUP_HEAL_TARGETS,
  extraTargetMode,
  beforeExtraTargetBuckets = [],
}: {
  party: PartyMember[]
  spell: Spell
  targetId: string
  extraTargets: number
  directTarget: boolean
  hotTarget: boolean
  shieldTarget: boolean
  damageReductionTarget?: boolean
  groupTargetCount?: number
  extraTargetMode: ExtraTargetMode
  beforeExtraTargetBuckets?: SpellTargetBucket[]
}): SpellTargetPlan {
  const plan: SpellTargetPlan = {
    directTargets: new Set(directTarget ? [targetId] : []),
    hotTargets: new Set(hotTarget ? [targetId] : []),
    shieldTargets: new Set(shieldTarget ? [targetId] : []),
    damageReductionTargets: new Set(damageReductionTarget ? [targetId] : []),
    groupTargets: new Set(
      spell.kind === 'group'
        ? groupHealTargets(party, groupTargetCount).map((member) => member.id)
        : [],
    ),
  }

  beforeExtraTargetBuckets.forEach((bucket) => {
    addLowestHealthExtraTarget(plan, party, bucket)
  })

  const bucket = extraTargetMode[spell.kind] ?? 'direct'
  for (let index = 0; index < extraTargets; index += 1) {
    if (spell.kind === 'group') break
    addLowestHealthExtraTarget(plan, party, bucket)
  }

  return plan
}

export function clearNegativeEffects(member: PartyMember) {
  return {
    ...member,
    debuff: undefined,
    debuffTicks: undefined,
    poisonStacks: undefined,
    maxHealthPenaltyTicks: undefined,
    healingReductionTicks: undefined,
  }
}

export type FloatingHealEvent = {
  memberId: string
  value: number
}

export type SpellEffectProfile = {
  /**
   * Profiles keep mode-specific spell math out of screen components. PvE uses
   * stacked HoT effect objects; PvP modes mostly use legacy hotTicks. Stadium
   * also applies dampening through the power callbacks below.
   */
  modeName: string
  heal: (member: PartyMember, power: number, multiplier: number) => number
  healingMultiplier: (member: PartyMember) => number
  power: {
    direct: (spell: Spell) => number
    cleanse: (spell: Spell) => number
    groupHeal: (spell: Spell) => number
    groupAbsorb: (spell: Spell) => number
    shield: (sourcePower: number, strength?: number) => number
  }
  hot: {
    mode: 'effects' | 'ticks'
    defaultTicks: number
    groupTicks: number
    radianceTicks: number
    merge: 'replace' | 'max'
    groupMerge?: 'replace' | 'max'
  }
  effects: {
    renewSpell?: Spell | null
    shieldSpell?: Spell | null
    groupAbsorbOnly: (spell: Spell) => boolean
    groupHotOnly: (spell: Spell) => boolean
    groupAppliesShield: (spell: Spell) => boolean
    groupAppliesHot: (spell: Spell) => boolean
    shieldAppliesHot: (spell: Spell) => boolean
    hotSpellForDirect: (spell: Spell) => Spell
  }
  ratios: {
    groupShield: number
    directShield: number
  }
  damageReductionTicks: number
  floatingHeals: {
    group: boolean
    direct: boolean
    cleanse: boolean
  }
  bounceHeals: boolean
}

function applyHot(
  member: PartyMember,
  spell: Spell,
  profile: SpellEffectProfile,
  ticks = profile.hot.defaultTicks,
  merge = profile.hot.merge,
) {
  if (profile.hot.mode === 'effects') {
    return {
      ...member,
      hotTicks: 0,
      hotEffects: addHotEffect(member, spell, ticks),
    }
  }
  return {
    ...member,
    hotTicks: merge === 'max'
      ? Math.max(member.hotTicks, ticks)
      : ticks,
  }
}

function recordFloatingHeal(
  events: FloatingHealEvent[],
  enabled: boolean,
  member: PartyMember,
  nextHealth: number,
) {
  if (enabled && nextHealth > member.health) {
    events.push({ memberId: member.id, value: nextHealth - member.health })
  }
}

export function applySpellEffectProfile({
  party,
  spell,
  targetId,
  plan,
  profile,
}: {
  party: PartyMember[]
  spell: Spell
  targetId: string
  plan: SpellTargetPlan
  profile: SpellEffectProfile
}) {
  const floatingHeals: FloatingHealEvent[] = []
  const nextParty = party.map((member) => {
    if (member.health <= 0) return member

    if (spell.kind === 'group') {
      if (!plan.groupTargets.has(member.id)) return member
      const isGroupAbsorb = profile.effects.groupAbsorbOnly(spell)
      if (profile.effects.groupHotOnly(spell)) {
        return applyHot(member, spell, profile, profile.hot.groupTicks, profile.hot.groupMerge)
      }

      const power = isGroupAbsorb
        ? profile.power.groupAbsorb(spell)
        : profile.power.groupHeal(spell)
      const nextHealth = isGroupAbsorb
        ? member.health
        : profile.heal(member, power, profile.healingMultiplier(member))
      recordFloatingHeal(floatingHeals, profile.floatingHeals.group, member, nextHealth)

      let nextMember: PartyMember = {
        ...member,
        health: nextHealth,
        shield: profile.effects.groupAppliesShield(spell)
          ? Math.max(
            member.shield,
            isGroupAbsorb
              ? power
              : profile.power.shield(
                profile.effects.shieldSpell?.power ?? spell.power,
                profile.ratios.groupShield,
              ),
          )
          : member.shield,
      }
      if (profile.effects.groupAppliesHot(spell) && profile.effects.renewSpell) {
        nextMember = applyHot(nextMember, profile.effects.renewSpell, profile, profile.hot.radianceTicks, profile.hot.groupMerge)
      } else if (profile.effects.groupAppliesHot(spell)) {
        nextMember = applyHot(nextMember, spell, profile, profile.hot.groupTicks, profile.hot.groupMerge)
      }
      return nextMember
    }

    if (
      !plan.directTargets.has(member.id)
      && !plan.hotTargets.has(member.id)
      && !plan.shieldTargets.has(member.id)
      && !plan.damageReductionTargets.has(member.id)
      && !(member.id === targetId && spell.kind === 'bounce_heal' && profile.bounceHeals)
    ) return member

    if (spell.kind === 'shield') {
      let nextMember: PartyMember = {
        ...member,
        shield: Math.max(member.shield, profile.power.shield(spell.power)),
      }
      if (plan.hotTargets.has(member.id) || profile.effects.shieldAppliesHot(spell)) {
        nextMember = applyHot(nextMember, profile.effects.renewSpell ?? spell, profile)
      }
      return nextMember
    }

    if (spell.kind === 'damage_reduction') {
      return {
        ...member,
        damageReductionTicks: profile.hot.merge === 'max'
          ? Math.max(member.damageReductionTicks ?? 0, profile.damageReductionTicks)
          : profile.damageReductionTicks,
        hotTicks: plan.hotTargets.has(member.id)
          ? (profile.hot.merge === 'max'
            ? Math.max(member.hotTicks, profile.hot.defaultTicks)
            : profile.hot.defaultTicks)
          : member.hotTicks,
      }
    }

    if (spell.kind === 'bounce_heal' && profile.bounceHeals) {
      return { ...member, bounceHeals: addBounceHeal(member, spell) }
    }

    if (spell.kind === 'cleanse') {
      const nextHealth = profile.heal(member, profile.power.cleanse(spell), profile.healingMultiplier(member))
      recordFloatingHeal(floatingHeals, profile.floatingHeals.cleanse, member, nextHealth)
      let nextMember: PartyMember = {
        ...clearNegativeEffects(member),
        health: nextHealth,
        shield: plan.shieldTargets.has(member.id)
          ? Math.max(
            member.shield,
            profile.power.shield(profile.effects.shieldSpell?.power ?? spell.power),
          )
          : member.shield,
      }
      if (plan.hotTargets.has(member.id)) {
        nextMember = applyHot(nextMember, profile.effects.renewSpell ?? spell, profile)
      }
      return nextMember
    }

    const nextHealth = plan.directTargets.has(member.id)
      ? profile.heal(member, profile.power.direct(spell), profile.healingMultiplier(member))
      : member.health
    recordFloatingHeal(floatingHeals, profile.floatingHeals.direct, member, nextHealth)
    let nextMember: PartyMember = {
      ...member,
      health: nextHealth,
      shield: plan.shieldTargets.has(member.id)
        ? Math.max(
          member.shield,
          profile.power.shield(
            profile.effects.shieldSpell?.power ?? spell.power,
            profile.ratios.directShield,
          ),
        )
        : member.shield,
    }
    if (plan.hotTargets.has(member.id)) {
      nextMember = applyHot(nextMember, profile.effects.hotSpellForDirect(spell), profile)
    }
    return nextMember
  })

  return { party: nextParty, floatingHeals }
}

export function advanceFreeCastProgress({
  enabled,
  wasReady,
  castsTowardFree,
}: {
  enabled: boolean
  wasReady: boolean
  castsTowardFree: number
}) {
  if (!enabled) {
    return {
      castsTowardFree,
      freeCastReady: wasReady,
    }
  }
  if (wasReady) {
    return {
      castsTowardFree: 0,
      freeCastReady: false,
    }
  }
  const nextCasts = castsTowardFree + 1
  return {
    castsTowardFree: nextCasts >= 5 ? 0 : nextCasts,
    freeCastReady: nextCasts >= 5,
  }
}
