import { freshParty } from "../data";
import type { AiCombatantId } from "../partyCombat";
import {
  EMPTY_MEMBER_GEAR_MODIFIERS,
  type EncounterGearModifiers,
  type MemberGearModifiers,
} from "../progression/gearEffects";
import type { HealerAbilityId, HealerClassId, PartyCombatKitId, PartyMember } from "../types";
import { assignRosterToCombatSlots } from "./party";
import type {
  CombatPartyAssignment,
  PartyArchetypeId,
  PartyRosterMember,
  PartyVitalUpdate,
  RpgRoguelikeRunState,
  RunEquipment,
  RunGearOwnerId,
  RunGearStatId,
} from "./types";
import { MAX_SPELL_RANK } from "./types";

export const RPG_COMBAT_SLOT_IDS: readonly AiCombatantId[] = ["brann", "nia", "orin", "vale"];

export const SPELL_POWER_PER_RANK = 0.12;
export const SPELL_COOLDOWN_REDUCTION_PER_RANK = 0.05;

export type RpgPartyDamageProfiles = Record<AiCombatantId, {
  readonly singleTarget: number;
  readonly areaDamage: number;
}>;

export interface RpgCombatProjection {
  /** Existing combat systems keep their stable ids; runProfile carries drafted identity. */
  readonly party: PartyMember[];
  readonly gearModifiers: EncounterGearModifiers;
  readonly damageProfiles: RpgPartyDamageProfiles;
}

export interface ProjectRpgCombatOptions {
  readonly healerClassId: HealerClassId;
  readonly playerName: string;
}

type CombatProjectionSource = Pick<RpgRoguelikeRunState, "roster" | "equipment">;

const PERCENT_SCALE = 100;

function clamp(value: number, minimum: number, maximum: number): number {
  return Math.max(minimum, Math.min(maximum, value));
}

function normalizedPercent(value: number): number {
  return Math.max(0, Number.isFinite(value) ? value : 0) / PERCENT_SCALE;
}

function ownerGearStats(equipment: RunEquipment, ownerId: RunGearOwnerId): Record<RunGearStatId, number> {
  const totals: Record<RunGearStatId, number> = { damage: 0, maxHealth: 0, haste: 0 };
  for (const item of Object.values(equipment[ownerId] ?? {})) {
    if (item) totals[item.statId] += normalizedPercent(item.statValue);
  }
  return totals;
}

/**
 * Converts additive percentage-point run gear into simulation multipliers.
 * Haste uses reciprocal scaling so future gear tiers cannot create zero cooldowns.
 */
export function runGearMultipliers(
  equipment: RunEquipment,
  ownerId: RunGearOwnerId,
): { readonly maxHealth: number; readonly damage: number; readonly cooldown: number } {
  const stats = ownerGearStats(equipment, ownerId);
  return {
    maxHealth: 1 + stats.maxHealth,
    damage: 1 + stats.damage,
    cooldown: 1 / (1 + stats.haste),
  };
}

function visualArchetype(archetypeId: PartyArchetypeId): NonNullable<PartyMember["runProfile"]>["visualArchetype"] {
  if (archetypeId === "knight-tank" || archetypeId.startsWith("warrior-")) return "knight";
  if (archetypeId === "ranger-damage") return "ranger";
  if (archetypeId === "mage-damage" || archetypeId === "warlock-damage") return "mage";
  return "rogue";
}

export function combatKitForRosterMember(member: PartyRosterMember): PartyCombatKitId {
  if (member.role === "Tank") return "tank";
  if (member.archetypeId === "mage-damage") return "caster";
  if (member.archetypeId === "ranger-damage" || member.archetypeId === "warlock-damage") return "ranged";
  return "melee";
}

function memberModifiers(member: PartyRosterMember | null, equipment: RunEquipment): MemberGearModifiers {
  if (!member) return { ...EMPTY_MEMBER_GEAR_MODIFIERS };
  const gear = runGearMultipliers(equipment, member.instanceId);
  return {
    ...EMPTY_MEMBER_GEAR_MODIFIERS,
    maxHealth: gear.maxHealth,
    moveSpeed: member.stats.moveSpeed,
    damage: gear.damage,
    cooldown: gear.cooldown,
    // Defense has no renderer dependency. Existing combat applies this field to
    // hazards; other damage kinds can consume it when their shared API supports it.
    hazardDamageTaken: clamp(1 / Math.max(0.01, member.stats.defense), 0.65, 1.25),
  };
}

function healerModifiers(equipment: RunEquipment): MemberGearModifiers {
  const gear = runGearMultipliers(equipment, "player");
  return {
    ...EMPTY_MEMBER_GEAR_MODIFIERS,
    maxHealth: gear.maxHealth,
    healingPower: gear.damage,
    cooldown: gear.cooldown,
  };
}

/** Run-only player gear treats weapon damage as healing power. */
export function createRpgHealerModifiers(equipment: RunEquipment): MemberGearModifiers {
  return healerModifiers(equipment);
}

function encounterGearModifiers(
  combatParty: CombatPartyAssignment,
  equipment: RunEquipment,
): EncounterGearModifiers {
  return {
    aelia: healerModifiers(equipment),
    brann: memberModifiers(combatParty.brann, equipment),
    nia: memberModifiers(combatParty.nia, equipment),
    orin: memberModifiers(combatParty.orin, equipment),
    vale: memberModifiers(combatParty.vale, equipment),
  };
}

export function createRpgEncounterGearModifiers(source: CombatProjectionSource): EncounterGearModifiers {
  return encounterGearModifiers(assignRosterToCombatSlots(source.roster), source.equipment);
}

export function createRpgDamageProfiles(combatParty: CombatPartyAssignment): RpgPartyDamageProfiles {
  return {
    brann: combatParty.brann
      ? { singleTarget: combatParty.brann.stats.singleTarget, areaDamage: combatParty.brann.stats.areaDamage }
      : { singleTarget: 0, areaDamage: 0 },
    nia: combatParty.nia
      ? { singleTarget: combatParty.nia.stats.singleTarget, areaDamage: combatParty.nia.stats.areaDamage }
      : { singleTarget: 0, areaDamage: 0 },
    orin: combatParty.orin
      ? { singleTarget: combatParty.orin.stats.singleTarget, areaDamage: combatParty.orin.stats.areaDamage }
      : { singleTarget: 0, areaDamage: 0 },
    vale: combatParty.vale
      ? { singleTarget: combatParty.vale.stats.singleTarget, areaDamage: combatParty.vale.stats.areaDamage }
      : { singleTarget: 0, areaDamage: 0 },
  };
}

function projectRosterMember(
  slotId: AiCombatantId,
  member: PartyRosterMember,
  modifier: MemberGearModifiers,
): PartyMember {
  const maxHp = Math.max(1, Math.round(member.stats.maxHp * modifier.maxHealth));
  const healthRatio = member.stats.maxHp > 0 ? clamp(member.hp / member.stats.maxHp, 0, 1) : 0;
  return {
    id: slotId,
    name: member.name,
    className: member.className,
    role: member.role,
    color: member.color,
    maxHp,
    hp: member.hp <= 0 ? 0 : maxHp * healthRatio,
    absorb: 0,
    healingEffects: [],
    reactiveHeal: null,
    knockedUntil: 0,
    debuffs: [],
    runProfile: {
      instanceId: member.instanceId,
      combatProfileId: member.archetypeId,
      combatKitId: combatKitForRosterMember(member),
      tier: member.rarity,
      visualArchetype: visualArchetype(member.archetypeId),
    },
  };
}

/**
 * Projects immutable run state into the legacy five-slot combat shape. Missing
 * draft slots stay absent instead of creating targetable placeholder allies.
 */
export function projectRpgCombat(
  source: CombatProjectionSource,
  options: ProjectRpgCombatOptions,
): RpgCombatProjection {
  const combatParty = assignRosterToCombatSlots(source.roster);
  const gearModifiers = encounterGearModifiers(combatParty, source.equipment);
  const healerBase = freshParty(options.healerClassId, options.playerName)[0];
  const healerMaxHp = Math.max(1, Math.round(healerBase.maxHp * gearModifiers.aelia.maxHealth));
  const healer: PartyMember = { ...healerBase, maxHp: healerMaxHp, hp: healerMaxHp };
  const party: PartyMember[] = [healer];

  for (const slotId of RPG_COMBAT_SLOT_IDS) {
    const member = combatParty[slotId];
    if (member) party.push(projectRosterMember(slotId, member, gearModifiers[slotId]));
  }

  return {
    party,
    gearModifiers,
    damageProfiles: createRpgDamageProfiles(combatParty),
  };
}

/**
 * Converts combat health back to gear-independent run health. This prevents an
 * armor swap from permanently inflating or reducing a companion's saved HP.
 */
export function extractRpgPartyVitals(
  party: readonly PartyMember[],
  combatParty: CombatPartyAssignment,
): PartyVitalUpdate[] {
  const vitals: PartyVitalUpdate[] = [];
  for (const slotId of RPG_COMBAT_SLOT_IDS) {
    const rosterMember = combatParty[slotId];
    if (!rosterMember) continue;
    const combatMember = party.find((member) => member.id === slotId);
    if (!combatMember) continue;
    const ratio = combatMember.maxHp > 0 ? clamp(combatMember.hp / combatMember.maxHp, 0, 1) : 0;
    vitals.push({ instanceId: rosterMember.instanceId, hp: rosterMember.stats.maxHp * ratio });
  }
  return vitals;
}

/** Converts gear-adjusted healer health back to the run's 100 HP base. */
export function extractRpgPlayerVital(party: readonly PartyMember[]): number {
  const healer = party.find((member) => member.id === "aelia");
  if (!healer || healer.maxHp <= 0) return 0;
  return 100 * clamp(healer.hp / healer.maxHp, 0, 1);
}

export function normalizedSpellRank(
  ranks: Partial<Record<HealerAbilityId, number>>,
  spellId: HealerAbilityId,
): number {
  const value = ranks[spellId] ?? 0;
  return Number.isFinite(value) ? clamp(Math.floor(value), 0, MAX_SPELL_RANK) : 0;
}

/** Applies to direct healing, absorbs, and periodic-heal magnitudes. */
export function spellRankPowerMultiplier(
  ranks: Partial<Record<HealerAbilityId, number>>,
  spellId: HealerAbilityId,
): number {
  return 1 + normalizedSpellRank(ranks, spellId) * SPELL_POWER_PER_RANK;
}

/** Applies only to the upgraded spell, before the player's run-gear haste. */
export function spellRankCooldownMultiplier(
  ranks: Partial<Record<HealerAbilityId, number>>,
  spellId: HealerAbilityId,
): number {
  return 1 - normalizedSpellRank(ranks, spellId) * SPELL_COOLDOWN_REDUCTION_PER_RANK;
}
