import type { PartyMember } from '../game'
import {
  clamp,
  healAmount,
  memberHotEffects,
  tickHotEffects,
} from './rules'

export type MemberTickInput = {
  member: PartyMember
  party: PartyMember[]
  damage: number
  hotHealing: number
  hotTicks: 'effects' | 'ticks'
  healingMultiplier?: number
  damageReductionMultiplier?: number
  damageReductionRounding?: 'round' | 'ceil'
  shieldedDamageMultiplier?: number
  applyDebuff?: {
    label: string
    ticks: number
  }
  applyPoisonStacks?: boolean
  poisonDamage?: (stacks: number) => number
  applyMaxHealthPenaltyTicks?: number
  applyHealingReductionTicks?: number
  decrementDamageReduction?: boolean
  useBounceHeals?: boolean
  jumpTarget?: () => PartyMember | undefined
}

export type MemberTickResult = {
  member: PartyMember
  floatingHeal: number
  jumpedBounceHeals: Array<{
    targetId: string
    heal: NonNullable<PartyMember['bounceHeals']>[number]
  }>
}

/**
 * Applies one combat tick to a party member. Screen-level engines still decide
 * who is targeted and when fights end; this helper owns repeated health math:
 * damage reduction, shields, HoT healing, debuff timers, poison, and bounce heals.
 */
export function advanceMemberTick({
  member,
  party,
  damage,
  hotHealing,
  hotTicks,
  healingMultiplier = 1,
  damageReductionMultiplier = 0.5,
  damageReductionRounding = 'round',
  shieldedDamageMultiplier,
  applyDebuff,
  applyPoisonStacks = false,
  poisonDamage = () => 0,
  applyMaxHealthPenaltyTicks,
  applyHealingReductionTicks,
  decrementDamageReduction = false,
  useBounceHeals = false,
  jumpTarget,
}: MemberTickInput): MemberTickResult {
  if (member.health <= 0) {
    return { member, floatingHeal: 0, jumpedBounceHeals: [] }
  }

  let nextDamage = damage
  const nextPoisonStacks = applyPoisonStacks
    ? Math.max(1, (member.poisonStacks ?? 0) + 1)
    : member.poisonStacks ?? 0
  if (nextPoisonStacks > 0) nextDamage += poisonDamage(nextPoisonStacks)

  if ((member.damageReductionTicks ?? 0) > 0) {
    const reducedDamage = nextDamage * damageReductionMultiplier
    nextDamage = damageReductionRounding === 'ceil'
      ? Math.ceil(reducedDamage)
      : Math.round(reducedDamage)
  }
  if (member.shield > 0 && shieldedDamageMultiplier !== undefined) {
    nextDamage = Math.round(nextDamage * shieldedDamageMultiplier)
  }

  const absorbed = Math.min(member.shield, nextDamage)
  const hotEffects = hotTicks === 'effects' ? memberHotEffects(member) : []
  let healing = hotTicks === 'effects'
    ? hotEffects.reduce((total, effect) => total + healAmount(member, effect.power, healingMultiplier), 0)
    : member.hotTicks > 0 ? healAmount(member, hotHealing, healingMultiplier) : 0

  let nextBounceHeals = [...(member.bounceHeals ?? [])]
  const jumpedBounceHeals: MemberTickResult['jumpedBounceHeals'] = []
  if (useBounceHeals && nextDamage > 0 && nextBounceHeals.length > 0) {
    nextBounceHeals = nextBounceHeals.flatMap((effect) => {
      healing += healAmount(member, effect.power, healingMultiplier)
      const nextCharges = effect.charges - 1
      if (nextCharges <= 0) return []
      const target = jumpTarget?.() ?? party.find((candidate) => candidate.health > 0 && candidate.id !== member.id) ?? member
      jumpedBounceHeals.push({
        targetId: target.id,
        heal: { ...effect, charges: nextCharges },
      })
      return []
    })
  }

  const nextMaxHealthPenaltyTicks = applyMaxHealthPenaltyTicks !== undefined
    ? applyMaxHealthPenaltyTicks
    : Math.max(0, (member.maxHealthPenaltyTicks ?? 0) - 1)
  const nextHealingReductionTicks = applyHealingReductionTicks !== undefined
    ? applyHealingReductionTicks
    : Math.max(0, (member.healingReductionTicks ?? 0) - 1)
  const nextDebuffTicks = applyDebuff
    ? applyDebuff.ticks
    : Math.max(0, (member.debuffTicks ?? 0) - 1)
  const nextEffectiveMaxHealth = Math.max(1, Math.round(
    member.maxHealth * (nextMaxHealthPenaltyTicks > 0 ? 0.75 : 1),
  ))

  return {
    member: {
      ...member,
      health: clamp(
        clamp(member.health + healing, 0, nextEffectiveMaxHealth) - nextDamage + absorbed,
        0,
        nextEffectiveMaxHealth,
      ),
      shield: Math.max(0, member.shield - nextDamage),
      hotTicks: hotTicks === 'effects' ? 0 : Math.max(0, member.hotTicks - 1),
      hotEffects: hotTicks === 'effects' ? tickHotEffects(hotEffects) : member.hotEffects,
      bounceHeals: useBounceHeals ? nextBounceHeals : member.bounceHeals,
      damageReductionTicks: decrementDamageReduction
        ? Math.max(0, (member.damageReductionTicks ?? 0) - 1)
        : member.damageReductionTicks,
      debuff: nextDebuffTicks > 0
        ? applyDebuff?.label ?? member.debuff
        : undefined,
      debuffTicks: nextDebuffTicks > 0 ? nextDebuffTicks : undefined,
      poisonStacks: nextPoisonStacks,
      maxHealthPenaltyTicks: nextMaxHealthPenaltyTicks,
      healingReductionTicks: nextHealingReductionTicks,
    },
    floatingHeal: healing,
    jumpedBounceHeals,
  }
}

export function attachJumpedBounceHeals(
  party: PartyMember[],
  jumpedBounceHeals: MemberTickResult['jumpedBounceHeals'],
) {
  if (jumpedBounceHeals.length === 0) return party
  const jumpedByTarget = new Map<string, NonNullable<PartyMember['bounceHeals']>>()
  for (const jump of jumpedBounceHeals) {
    const current = jumpedByTarget.get(jump.targetId)
    if (current) current.push(jump.heal)
    else jumpedByTarget.set(jump.targetId, [jump.heal])
  }
  return party.map((member) => {
    const jumped = jumpedByTarget.get(member.id)
    if (!jumped || jumped.length === 0) return member
    return {
      ...member,
      bounceHeals: [
        ...(member.bounceHeals ?? []),
        ...jumped,
      ],
    }
  })
}
