import { AVAILABLE_BOSS_IDS } from "../bossCatalog";
import type { BossId, HealerAbilityId } from "../types";
import {
  emptyChallengeCounts,
  emptyChallengeMetrics,
  evaluateChallenge,
  mergeChallengeMetrics,
  selectChallenge,
} from "./challenges";
import {
  canRecruitCandidate,
  generatePartyDraftOffers,
  recruitCandidate,
} from "./party";
import { createRandomState, shuffleWithRandom } from "./random";
import {
  applyRewardChoice,
  buyShopOffer,
  generateRewardChest,
  generateRunShop,
  restParty,
  revivePartyMember,
  sellBagItem,
} from "./rewards";
import {
  addSpellToAbilityLoadout,
  canPickSpell,
  generateSpellDraftOffers,
  removeSpellFromAbilityLoadout,
} from "./spells";
import type {
  PartyVitalUpdate,
  RandomState,
  RpgRoguelikeAction,
  RpgRoguelikeRunConfig,
  RpgRoguelikeRunState,
} from "./types";
import {
  ACT_BOSS_COUNT,
  BOSSES_PER_ACT,
  MAX_ACTIVE_ROSTER,
  PARTY_DRAFT_WAVE_COUNT,
  PARTY_RECRUITS_PER_WAVE,
  SPELL_DRAFT_WAVE_COUNT,
  TOTAL_BOSS_COUNT,
} from "./types";

function uniqueBossPool(pool: readonly BossId[]): BossId[] {
  return [...new Set(pool)];
}

export function generateBossRoute(
  source: RandomState,
  requestedPool: readonly BossId[] = AVAILABLE_BOSS_IDS,
): { route: BossId[]; random: RandomState } {
  const pool = uniqueBossPool(requestedPool);
  if (!pool.length) throw new Error("RPG Roguelike requires at least one boss.");
  const route: BossId[] = [];
  let random = source;
  while (route.length < TOTAL_BOSS_COUNT) {
    const shuffled = shuffleWithRandom(random, pool);
    random = shuffled.random;
    for (const bossId of shuffled.values) {
      if (route.length >= TOTAL_BOSS_COUNT) break;
      if (pool.length > 1 && route[route.length - 1] === bossId) continue;
      route.push(bossId);
    }
  }
  return { route, random };
}

function syncPartyVitals(
  state: RpgRoguelikeRunState,
  updates: readonly PartyVitalUpdate[] | undefined,
  playerHp?: number,
): RpgRoguelikeRunState {
  if (!updates?.length && playerHp === undefined) return state;
  const byId = new Map((updates ?? []).map((update) => [update.instanceId, update.hp]));
  const roster = state.roster.map((member) => {
    const hp = byId.get(member.instanceId);
    return hp === undefined
      ? member
      : { ...member, hp: Math.max(0, Math.min(member.stats.maxHp, hp)) };
  });
  return {
    ...state,
    playerHp: playerHp === undefined ? state.playerHp : Math.max(0, Math.min(100, playerHp)),
    roster,
  };
}

function prepareEncounter(state: RpgRoguelikeRunState): RpgRoguelikeRunState {
  if (state.bossIndex >= TOTAL_BOSS_COUNT) return { ...state, phase: "victory", currentChallenge: null };
  if (state.bossIndex >= ACT_BOSS_COUNT) {
    return { ...state, phase: "boss-briefing", currentChallenge: null, lastChallengeResult: null };
  }
  const selected = selectChallenge(state.random, state.challengeCounts);
  return {
    ...state,
    random: selected.random,
    phase: "challenge-briefing",
    challengeCounts: selected.counts,
    currentChallenge: { objective: selected.objective, metrics: emptyChallengeMetrics() },
    lastChallengeResult: null,
  };
}

export function createRpgRoguelikeRun(config: RpgRoguelikeRunConfig): RpgRoguelikeRunState {
  let random = createRandomState(config.seed);
  const route = generateBossRoute(random, config.bossPool);
  random = route.random;
  const partyOffers = generatePartyDraftOffers(random, 0);
  random = partyOffers.random;
  return {
    version: 1,
    seed: random.seed,
    random,
    phase: "party-draft",
    partyDraft: { waveIndex: 0, offers: partyOffers.offers, recruitedThisWaveIds: [] },
    spellDraft: null,
    roster: [],
    selectedSpellIds: [],
    abilityLoadout: {},
    spellRanks: {},
    playerHp: 100,
    bossRoute: route.route,
    bossIndex: 0,
    bossesDefeated: 0,
    challengeCounts: emptyChallengeCounts(),
    currentChallenge: null,
    lastChallengeResult: null,
    nextChestQualityBonus: 0,
    pendingReward: null,
    equipment: {},
    bag: [],
    currency: Math.max(0, Math.floor(config.startingCurrency ?? 80)),
    shop: null,
  };
}

/** Mirrors draft replacement capacity so controller/UI never expose inert removals. */
export function canRemovePartyMember(state: RpgRoguelikeRunState, memberId: string): boolean {
  const draft = state.partyDraft;
  if (state.phase !== "party-draft"
    || !draft
    || draft.waveIndex === 0
    || !state.roster.some((member) => member.instanceId === memberId)) return false;
  if (draft.waveIndex < PARTY_DRAFT_WAVE_COUNT - 1) return true;

  const removingCurrentPick = draft.recruitedThisWaveIds.includes(memberId);
  const picksAfterRemoval = draft.recruitedThisWaveIds.length - Number(removingCurrentPick);
  const maximumFinalRoster = state.roster.length - 1 + (PARTY_RECRUITS_PER_WAVE - picksAfterRemoval);
  return maximumFinalRoster >= MAX_ACTIVE_ROSTER;
}

function reducePartyDraft(state: RpgRoguelikeRunState, action: RpgRoguelikeAction): RpgRoguelikeRunState {
  const draft = state.partyDraft;
  if (state.phase !== "party-draft" || !draft) return state;
  if (action.type === "party-recruit") {
    const candidate = draft.offers.find((offer) => offer.candidateId === action.candidateId);
    if (!candidate
      || state.roster.some((member) => member.instanceId === candidate.candidateId)
      || !canRecruitCandidate(state.roster, draft.recruitedThisWaveIds.length)) return state;
    const roster = [...state.roster, recruitCandidate(candidate)];
    return {
      ...state,
      roster,
      partyDraft: { ...draft, recruitedThisWaveIds: [...draft.recruitedThisWaveIds, candidate.candidateId] },
    };
  }
  if (action.type === "party-remove") {
    if (!canRemovePartyMember(state, action.memberId)) return state;
    return {
      ...state,
      roster: state.roster.filter((member) => member.instanceId !== action.memberId),
      partyDraft: {
        ...draft,
        recruitedThisWaveIds: draft.recruitedThisWaveIds.filter((id) => id !== action.memberId),
      },
    };
  }
  if (action.type !== "party-next-wave") return state;
  if (draft.waveIndex < PARTY_DRAFT_WAVE_COUNT - 1) {
    const remainingWaves = PARTY_DRAFT_WAVE_COUNT - draft.waveIndex - 1;
    if (state.roster.length + remainingWaves * PARTY_RECRUITS_PER_WAVE < MAX_ACTIVE_ROSTER) return state;
    const generated = generatePartyDraftOffers(state.random, draft.waveIndex + 1);
    return {
      ...state,
      random: generated.random,
      partyDraft: { waveIndex: draft.waveIndex + 1, offers: generated.offers, recruitedThisWaveIds: [] },
    };
  }
  if (state.roster.length !== MAX_ACTIVE_ROSTER || !state.roster.some((member) => member.role === "Tank")) return state;
  const generated = generateSpellDraftOffers(state.random);
  return {
    ...state,
    random: generated.random,
    phase: "spell-draft",
    partyDraft: null,
    spellDraft: {
      waveIndex: 0,
      offers: generated.offers,
      pickedThisWaveIds: [],
      seenOfferIds: generated.offers,
    },
  };
}

function reduceSpellDraft(state: RpgRoguelikeRunState, action: RpgRoguelikeAction): RpgRoguelikeRunState {
  const draft = state.spellDraft;
  if (state.phase !== "spell-draft" || !draft) return state;
  if (action.type === "spell-pick") {
    if (!draft.offers.includes(action.spellId)
      || !canPickSpell(state.selectedSpellIds, draft.pickedThisWaveIds.length, action.spellId)) return state;
    const selectedSpellIds = [...state.selectedSpellIds, action.spellId];
    return {
      ...state,
      selectedSpellIds,
      abilityLoadout: addSpellToAbilityLoadout(state.abilityLoadout, action.spellId),
      spellDraft: { ...draft, pickedThisWaveIds: [...draft.pickedThisWaveIds, action.spellId] },
    };
  }
  if (action.type === "spell-remove") {
    if (draft.waveIndex === 0) return state;
    if (!state.selectedSpellIds.includes(action.spellId)) return state;
    const selectedSpellIds = state.selectedSpellIds.filter((spellId) => spellId !== action.spellId);
    return {
      ...state,
      selectedSpellIds,
      abilityLoadout: removeSpellFromAbilityLoadout(state.abilityLoadout, action.spellId),
      spellDraft: {
        ...draft,
        pickedThisWaveIds: draft.pickedThisWaveIds.filter((spellId) => spellId !== action.spellId),
      },
    };
  }
  if (action.type !== "spell-next-wave") return state;
  if (draft.waveIndex < SPELL_DRAFT_WAVE_COUNT - 1) {
    const generated = generateSpellDraftOffers(state.random, draft.seenOfferIds);
    return {
      ...state,
      random: generated.random,
      spellDraft: {
        waveIndex: draft.waveIndex + 1,
        offers: generated.offers,
        pickedThisWaveIds: [],
        seenOfferIds: [...draft.seenOfferIds, ...generated.offers],
      },
    };
  }
  if (!state.selectedSpellIds.length) return state;
  return prepareEncounter({ ...state, spellDraft: null });
}

function completeReward(state: RpgRoguelikeRunState, choiceId: string): RpgRoguelikeRunState {
  const choice = state.pendingReward?.choices.find((candidate) => candidate.id === choiceId);
  if (!choice || state.phase !== "reward") return state;
  let next = applyRewardChoice(state, choice);
  next = { ...next, pendingReward: null };
  if (state.bossIndex === TOTAL_BOSS_COUNT - 1) return { ...next, phase: "victory" };

  const bossIndex = state.bossIndex + 1;
  if (state.bossesDefeated <= ACT_BOSS_COUNT && state.bossesDefeated % BOSSES_PER_ACT === 0) {
    const generated = generateRunShop(next.random, next, state.bossesDefeated / BOSSES_PER_ACT);
    return { ...next, random: generated.random, bossIndex, phase: "shop", shop: generated.shop };
  }
  return prepareEncounter({ ...next, bossIndex });
}

export function reduceRpgRoguelikeRun(
  state: RpgRoguelikeRunState,
  action: RpgRoguelikeAction,
): RpgRoguelikeRunState {
  if (state.phase === "victory" || state.phase === "defeat") return state;
  if (state.phase === "party-draft") return reducePartyDraft(state, action);
  if (state.phase === "spell-draft") return reduceSpellDraft(state, action);

  if (action.type === "party-vitals") return syncPartyVitals(state, action.vitals, action.playerHp);
  if (action.type === "challenge-start" && state.phase === "challenge-briefing" && state.currentChallenge) {
    return { ...state, phase: "challenge-active" };
  }
  if (action.type === "challenge-progress" && state.phase === "challenge-active" && state.currentChallenge) {
    return {
      ...state,
      currentChallenge: {
        ...state.currentChallenge,
        metrics: mergeChallengeMetrics(state.currentChallenge.metrics, action.metrics),
      },
    };
  }
  if (action.type === "challenge-complete" && state.phase === "challenge-active" && state.currentChallenge) {
    const metrics = mergeChallengeMetrics(state.currentChallenge.metrics, action.metrics ?? {});
    const evaluated = evaluateChallenge(state.currentChallenge.objective, metrics);
    const result = action.forcedFailure ? { ...evaluated, succeeded: false } : evaluated;
    return {
      ...state,
      phase: "boss-briefing",
      currentChallenge: null,
      lastChallengeResult: result,
      currency: state.currency + (result.succeeded ? result.objective.rewardCurrency : 0),
      nextChestQualityBonus: result.succeeded ? result.objective.chestQualityBonus : 0,
    };
  }
  if (action.type === "boss-start" && state.phase === "boss-briefing") return { ...state, phase: "boss-combat" };
  if (action.type === "boss-lost" && state.phase === "boss-combat") {
    return { ...syncPartyVitals(state, action.vitals, action.playerHp), phase: "defeat", pendingReward: null };
  }
  if (action.type === "boss-won" && state.phase === "boss-combat") {
    const synced = syncPartyVitals(state, action.vitals, action.playerHp);
    return { ...synced, phase: "boss-cleared", bossesDefeated: state.bossesDefeated + 1 };
  }
  if (action.type === "reward-open" && state.phase === "boss-cleared" && !state.pendingReward) {
    const baseQuality = Math.min(3, Math.floor(state.bossIndex / BOSSES_PER_ACT));
    const generated = generateRewardChest(state.random, state, baseQuality + state.nextChestQualityBonus);
    return {
      ...state,
      random: generated.random,
      phase: "reward",
      pendingReward: generated.chest,
      nextChestQualityBonus: 0,
    };
  }
  if (action.type === "reward-choose") return completeReward(state, action.choiceId);
  if (state.phase === "shop") {
    if (action.type === "shop-buy") return buyShopOffer(state, action.offerId);
    if (action.type === "shop-sell") return sellBagItem(state, action.itemId);
    if (action.type === "shop-rest") return restParty(state);
    if (action.type === "shop-revive") return revivePartyMember(state, action.memberId);
    if (action.type === "shop-leave") return prepareEncounter({ ...state, phase: "boss-briefing", shop: null });
  }
  return state;
}

export function selectCurrentBossId(state: RpgRoguelikeRunState): BossId | null {
  return state.bossRoute[state.bossIndex] ?? null;
}

export function selectCanRecruit(state: RpgRoguelikeRunState): boolean {
  return state.phase === "party-draft"
    && Boolean(state.partyDraft)
    && canRecruitCandidate(state.roster, state.partyDraft?.recruitedThisWaveIds.length ?? 0);
}

export function selectCanPickSpell(state: RpgRoguelikeRunState, spellId: HealerAbilityId): boolean {
  return state.phase === "spell-draft"
    && Boolean(state.spellDraft?.offers.includes(spellId))
    && canPickSpell(state.selectedSpellIds, state.spellDraft?.pickedThisWaveIds.length ?? 0, spellId);
}

export function isRpgRoguelikeTerminal(state: RpgRoguelikeRunState): boolean {
  return state.phase === "victory" || state.phase === "defeat";
}
