import { equipmentExpToNext, equipmentSlots } from '../data/battleItems';
import type { BattleBond, UnitData } from '../data/scenario';

export type BattleObjectiveSnapshot = {
  id: string;
  label: string;
  achieved: boolean;
  detail: string;
  rewardGold: number;
};

export type CampBondSnapshot = BattleBond & {
  battleExp: number;
};

export type CampMvpSnapshot = {
  unitId: string;
  name: string;
  damageDealt: number;
  defeats: number;
};

export type FirstBattleReport = {
  battleId: string;
  battleTitle: string;
  outcome: 'victory' | 'defeat';
  turnNumber: number;
  rewardGold: number;
  defeatedEnemies: number;
  totalEnemies: number;
  objectives: BattleObjectiveSnapshot[];
  units: UnitData[];
  bonds: CampBondSnapshot[];
  mvp?: CampMvpSnapshot;
  itemRewards: string[];
  completedCampDialogues: string[];
  completedCampVisits: string[];
  createdAt: string;
};

export type CampaignStep =
  | 'new'
  | 'prologue'
  | 'first-battle'
  | 'first-camp'
  | 'first-victory-story'
  | 'second-battle'
  | 'second-camp'
  | 'third-battle'
  | 'third-camp'
  | 'fourth-battle'
  | 'fourth-camp'
  | 'fifth-battle'
  | 'fifth-camp'
  | 'sixth-battle'
  | 'sixth-camp'
  | 'seventh-battle'
  | 'seventh-camp'
  | 'eighth-battle'
  | 'eighth-camp'
  | 'ninth-battle'
  | 'ninth-camp'
  | 'tenth-battle'
  | 'tenth-camp'
  | 'eleventh-battle'
  | 'eleventh-camp'
  | 'twelfth-battle'
  | 'twelfth-camp'
  | 'thirteenth-battle'
  | 'thirteenth-camp'
  | 'fourteenth-battle'
  | 'fourteenth-camp'
  | 'fifteenth-battle'
  | 'fifteenth-camp'
  | 'sixteenth-battle'
  | 'sixteenth-camp'
  | 'seventeenth-battle'
  | 'seventeenth-camp'
  | 'eighteenth-battle'
  | 'eighteenth-camp'
  | 'nineteenth-battle'
  | 'nineteenth-camp'
  | 'twentieth-battle'
  | 'twentieth-camp'
  | 'twenty-first-battle'
  | 'twenty-first-camp'
  | 'twenty-second-battle'
  | 'twenty-second-camp'
  | 'twenty-third-battle'
  | 'twenty-third-camp'
  | 'twenty-fourth-battle'
  | 'twenty-fourth-camp'
  | 'twenty-fifth-battle'
  | 'twenty-fifth-camp'
  | 'twenty-sixth-battle'
  | 'twenty-sixth-camp'
  | 'twenty-seventh-battle'
  | 'twenty-seventh-camp'
  | 'twenty-eighth-battle'
  | 'twenty-eighth-camp'
  | 'twenty-ninth-battle'
  | 'twenty-ninth-camp'
  | 'thirtieth-battle'
  | 'thirtieth-camp'
  | 'thirty-first-battle'
  | 'thirty-first-camp'
  | 'thirty-second-battle'
  | 'thirty-second-camp'
  | 'thirty-third-battle'
  | 'thirty-third-camp'
  | 'thirty-fourth-battle'
  | 'thirty-fourth-camp'
  | 'thirty-fifth-battle'
  | 'thirty-fifth-camp'
  | 'thirty-sixth-battle'
  | 'thirty-sixth-camp'
  | 'thirty-seventh-battle'
  | 'thirty-seventh-camp'
  | 'thirty-eighth-battle'
  | 'thirty-eighth-camp'
  | 'thirty-ninth-battle'
  | 'thirty-ninth-camp'
  | 'fortieth-battle'
  | 'fortieth-camp'
  | 'forty-first-battle'
  | 'forty-first-camp'
  | 'forty-second-battle'
  | 'forty-second-camp'
  | 'forty-third-battle'
  | 'forty-third-camp'
  | 'forty-fourth-battle'
  | 'forty-fourth-camp'
  | 'forty-fifth-battle'
  | 'forty-fifth-camp'
  | 'forty-sixth-battle'
  | 'forty-sixth-camp'
  | 'forty-seventh-battle'
  | 'forty-seventh-camp'
  | 'forty-eighth-battle'
  | 'forty-eighth-camp'
  | 'hanzhong-king-camp'
  | 'shu-han-foundation-camp'
  | 'baidi-entrustment-camp';

export type CampaignUnitProgressSnapshot = {
  unitId: string;
  name: string;
  level: number;
  exp: number;
  hp: number;
  maxHp: number;
  equipment: UnitData['equipment'];
};

export type CampaignBondProgressSnapshot = {
  id: string;
  title: string;
  level: number;
  exp: number;
  battleExp: number;
};

export type CampaignReserveTrainingSnapshot = {
  unitId: string;
  name: string;
  expGained: number;
  equipmentExpGained: number;
  bondExpGained?: number;
  focusId?: CampaignReserveTrainingFocusId;
  focusLabel?: string;
  level: number;
  exp: number;
};

export type CampaignReserveTrainingFocusId = 'balanced' | 'class-practice' | 'bond-practice';

export type CampaignReserveTrainingFocusDefinition = {
  id: CampaignReserveTrainingFocusId;
  label: string;
  summary: string;
  expGained: number;
  equipmentExpGained: number;
  bondExpGained: number;
};

export const defaultCampaignReserveTrainingFocusId: CampaignReserveTrainingFocusId = 'balanced';

export const campaignReserveTrainingFocusDefinitions: CampaignReserveTrainingFocusDefinition[] = [
  {
    id: 'balanced',
    label: '균형 훈련',
    summary: '대기 무장의 경험치와 장비 숙련을 고르게 올립니다.',
    expGained: 6,
    equipmentExpGained: 2,
    bondExpGained: 0
  },
  {
    id: 'class-practice',
    label: '병과 숙련',
    summary: '대기 무장의 장비 경험치를 더 올려 병과 운용을 다듬습니다.',
    expGained: 4,
    equipmentExpGained: 5,
    bondExpGained: 0
  },
  {
    id: 'bond-practice',
    label: '공명 합숙',
    summary: '대기 무장의 개인 성장은 낮지만 관련 공명 경험치를 함께 올립니다.',
    expGained: 5,
    equipmentExpGained: 1,
    bondExpGained: 4
  }
];

export type CampaignBattleSettlement = {
  battleId: string;
  battleTitle: string;
  outcome: FirstBattleReport['outcome'];
  rewardGold: number;
  itemRewards: string[];
  objectives: BattleObjectiveSnapshot[];
  units: CampaignUnitProgressSnapshot[];
  bonds: CampaignBondProgressSnapshot[];
  reserveTraining?: CampaignReserveTrainingSnapshot[];
  completedAt: string;
};

export type CampaignState = {
  version: 1;
  updatedAt: string;
  step: CampaignStep;
  activeSaveSlot: number;
  gold: number;
  roster: UnitData[];
  bonds: CampBondSnapshot[];
  inventory: Record<string, number>;
  selectedSortieUnitIds: string[];
  reserveTrainingFocus: CampaignReserveTrainingFocusId;
  completedCampDialogues: string[];
  completedCampVisits: string[];
  battleHistory: Record<string, CampaignBattleSettlement>;
  latestBattleId?: string;
  firstBattleReport?: FirstBattleReport;
};

export const campaignStorageKey = 'heros-web:campaign-state';
export const campaignSaveSlotCount = 3;

const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: CampaignStep }> = {
  'first-battle-zhuo-commandery': { victory: 'first-camp', retry: 'first-battle' },
  'second-battle-yellow-turban-pursuit': { victory: 'second-camp', retry: 'second-battle' },
  'third-battle-guangzong-road': { victory: 'third-camp', retry: 'third-battle' },
  'fourth-battle-guangzong-camp': { victory: 'fourth-camp', retry: 'fourth-battle' },
  'fifth-battle-sishui-vanguard': { victory: 'fifth-camp', retry: 'fifth-battle' },
  'sixth-battle-jieqiao-relief': { victory: 'sixth-camp', retry: 'sixth-battle' },
  'seventh-battle-xuzhou-rescue': { victory: 'seventh-camp', retry: 'seventh-battle' },
  'eighth-battle-xiaopei-supply-road': { victory: 'eighth-camp', retry: 'eighth-battle' },
  'ninth-battle-xuzhou-gate-night-raid': { victory: 'ninth-camp', retry: 'ninth-battle' },
  'tenth-battle-xuzhou-breakout': { victory: 'tenth-camp', retry: 'tenth-battle' },
  'eleventh-battle-xudu-refuge-road': { victory: 'eleventh-camp', retry: 'eleventh-battle' },
  'twelfth-battle-xiapi-outer-siege': { victory: 'twelfth-camp', retry: 'twelfth-battle' },
  'thirteenth-battle-xiapi-final': { victory: 'thirteenth-camp', retry: 'thirteenth-battle' },
  'fourteenth-battle-cao-break': { victory: 'fourteenth-camp', retry: 'fourteenth-battle' },
  'fifteenth-battle-yuan-refuge-road': { victory: 'fifteenth-camp', retry: 'fifteenth-battle' },
  'sixteenth-battle-liu-biao-refuge': { victory: 'sixteenth-camp', retry: 'sixteenth-battle' },
  'seventeenth-battle-wolong-visit-road': { victory: 'seventeenth-camp', retry: 'seventeenth-battle' },
  'eighteenth-battle-bowang-ambush': { victory: 'eighteenth-camp', retry: 'eighteenth-battle' },
  'nineteenth-battle-changban-refuge': { victory: 'nineteenth-camp', retry: 'nineteenth-battle' },
  'twentieth-battle-jiangdong-envoy': { victory: 'twentieth-camp', retry: 'twentieth-battle' },
  'twenty-first-battle-red-cliffs-vanguard': { victory: 'twenty-first-camp', retry: 'twenty-first-battle' },
  'twenty-second-battle-red-cliffs-fire': { victory: 'twenty-second-camp', retry: 'twenty-second-battle' },
  'twenty-third-battle-jingzhou-south-entry': { victory: 'twenty-third-camp', retry: 'twenty-third-battle' },
  'twenty-fourth-battle-guiyang-persuasion': { victory: 'twenty-fourth-camp', retry: 'twenty-fourth-battle' },
  'twenty-fifth-battle-wuling-mountain-road': { victory: 'twenty-fifth-camp', retry: 'twenty-fifth-battle' },
  'twenty-sixth-battle-changsha-veteran': { victory: 'twenty-sixth-camp', retry: 'twenty-sixth-battle' },
  'twenty-seventh-battle-yizhou-relief-road': { victory: 'twenty-seventh-camp', retry: 'twenty-seventh-battle' },
  'twenty-eighth-battle-fu-pass-entry': { victory: 'twenty-eighth-camp', retry: 'twenty-eighth-battle' },
  'twenty-ninth-battle-luo-outer-wall': { victory: 'twenty-ninth-camp', retry: 'twenty-ninth-battle' },
  'thirtieth-battle-luofeng-ambush': { victory: 'thirtieth-camp', retry: 'thirtieth-battle' },
  'thirty-first-battle-luo-main-gate': { victory: 'thirty-first-camp', retry: 'thirty-first-battle' },
  'thirty-second-battle-mianzhu-gate': { victory: 'thirty-second-camp', retry: 'thirty-second-battle' },
  'thirty-third-battle-chengdu-surrender': { victory: 'thirty-third-camp', retry: 'thirty-third-battle' },
  'thirty-fourth-battle-jiameng-pass': { victory: 'thirty-fourth-camp', retry: 'thirty-fourth-battle' },
  'thirty-fifth-battle-yangping-scout': { victory: 'thirty-fifth-camp', retry: 'thirty-fifth-battle' },
  'thirty-sixth-battle-dingjun-vanguard': { victory: 'thirty-sixth-camp', retry: 'thirty-sixth-battle' },
  'thirty-seventh-battle-hanzhong-decisive': { victory: 'thirty-seventh-camp', retry: 'thirty-seventh-battle' },
  'thirty-eighth-battle-jing-defense': { victory: 'thirty-eighth-camp', retry: 'thirty-eighth-battle' },
  'thirty-ninth-battle-fan-castle-vanguard': { victory: 'thirty-ninth-camp', retry: 'thirty-ninth-battle' },
  'fortieth-battle-han-river-flood': { victory: 'fortieth-camp', retry: 'fortieth-battle' },
  'forty-first-battle-fan-castle-siege': { victory: 'forty-first-camp', retry: 'forty-first-battle' },
  'forty-second-battle-jing-rear-crisis': { victory: 'forty-second-camp', retry: 'forty-second-battle' },
  'forty-third-battle-gongan-collapse': { victory: 'forty-third-camp', retry: 'forty-third-battle' },
  'forty-fourth-battle-maicheng-isolation': { victory: 'forty-fourth-camp', retry: 'forty-fourth-battle' },
  'forty-fifth-battle-yiling-vanguard': { victory: 'forty-fifth-camp', retry: 'forty-fifth-battle' },
  'forty-sixth-battle-yiling-fire': { victory: 'forty-sixth-camp', retry: 'forty-sixth-battle' },
  'forty-seventh-battle-nanzhong-stabilization': { victory: 'forty-seventh-camp', retry: 'forty-seventh-battle' },
  'forty-eighth-battle-meng-huo-main-force': { victory: 'forty-eighth-camp', retry: 'forty-eighth-battle' }
};

export type CampaignSaveSlotSummary = {
  slot: number;
  exists: boolean;
  updatedAt?: string;
  step?: CampaignStep;
  gold?: number;
  battleTitle?: string;
};

let campaignState: CampaignState | undefined;

export function createInitialCampaignState(): CampaignState {
  return {
    version: 1,
    updatedAt: new Date().toISOString(),
    step: 'new',
    activeSaveSlot: 1,
    gold: 0,
    roster: [],
    bonds: [],
    inventory: {},
    selectedSortieUnitIds: [],
    reserveTrainingFocus: defaultCampaignReserveTrainingFocusId,
    completedCampDialogues: [],
    completedCampVisits: [],
    battleHistory: {}
  };
}

export function startNewCampaign() {
  const state = createInitialCampaignState();
  state.step = 'prologue';
  return setCampaignState(state);
}

export function getCampaignState() {
  return cloneCampaignState(ensureCampaignState());
}

export function setCampaignState(state: CampaignState) {
  campaignState = normalizeCampaignState(state);
  campaignState.updatedAt = new Date().toISOString();
  persistCampaignState(campaignState);
  return cloneCampaignState(campaignState);
}

export function loadCampaignState(slot?: number) {
  campaignState = readStoredCampaignState(slot) ?? readStoredCampaignState() ?? readLatestSlottedCampaignState() ?? createInitialCampaignState();
  return cloneCampaignState(campaignState);
}

export function saveCampaignState(state = ensureCampaignState(), slot = state.activeSaveSlot) {
  campaignState = normalizeCampaignState(state);
  campaignState.activeSaveSlot = normalizeSlot(slot);
  campaignState.updatedAt = new Date().toISOString();
  persistCampaignState(campaignState);
  return cloneCampaignState(campaignState);
}

export function resetCampaignState() {
  campaignState = createInitialCampaignState();
  tryStorage()?.removeItem(campaignStorageKey);
  for (let slot = 1; slot <= campaignSaveSlotCount; slot += 1) {
    tryStorage()?.removeItem(campaignSaveSlotKey(slot));
  }
  return cloneCampaignState(campaignState);
}

export function hasCampaignSave() {
  return Boolean(tryStorage()?.getItem(campaignStorageKey) ?? readLatestSlottedCampaignState());
}

export function listCampaignSaveSlots(): CampaignSaveSlotSummary[] {
  return Array.from({ length: campaignSaveSlotCount }, (_, index) => {
    const slot = index + 1;
    const state = readStoredCampaignState(slot);
    if (!state) {
      return { slot, exists: false };
    }
    return {
      slot,
      exists: true,
      updatedAt: state.updatedAt,
      step: state.step,
      gold: state.gold,
      battleTitle: state.firstBattleReport?.battleTitle
    };
  });
}

export function markCampaignStep(step: CampaignStep) {
  const state = ensureCampaignState();
  state.step = step;
  state.updatedAt = new Date().toISOString();
  persistCampaignState(state);
  return cloneCampaignState(state);
}

export function setCampaignReserveTrainingFocus(focusId: CampaignReserveTrainingFocusId) {
  const state = ensureCampaignState();
  state.reserveTrainingFocus = normalizeReserveTrainingFocusId(focusId);
  state.updatedAt = new Date().toISOString();
  persistCampaignState(state);
  return cloneCampaignState(state);
}

export function setFirstBattleReport(report: FirstBattleReport) {
  const state = ensureCampaignState();
  const reportClone = cloneReport(report);
  const battleId = reportClone.battleId;
  const previousSettlement = state.battleHistory[battleId];
  const completedCampDialogues =
    battleId === 'first-battle-zhuo-commandery' ? [...reportClone.completedCampDialogues] : [...state.completedCampDialogues];
  const completedCampVisits =
    battleId === 'first-battle-zhuo-commandery' ? [...(reportClone.completedCampVisits ?? [])] : [...state.completedCampVisits];

  reportClone.completedCampDialogues = completedCampDialogues;
  reportClone.completedCampVisits = completedCampVisits;
  state.firstBattleReport = reportClone;
  const stepRule = campaignBattleSteps[battleId] ?? campaignBattleSteps['first-battle-zhuo-commandery'];
  const victoryStep: CampaignStep = stepRule.victory;
  const retryStep: CampaignStep = stepRule.retry;
  state.step = reportClone.outcome === 'victory' ? victoryStep : retryStep;
  state.gold = Math.max(0, state.gold - (previousSettlement?.rewardGold ?? 0) + reportClone.rewardGold);
  state.roster = mergeRosterProgress(state.roster, reportClone.units.filter((unit) => unit.faction === 'ally'));
  state.bonds = reportClone.bonds.map(cloneBondSnapshot);
  const reserveTraining = previousSettlement || reportClone.outcome !== 'victory'
    ? previousSettlement?.reserveTraining ?? []
    : applyReserveTrainingProgress(
      state.roster,
      state.bonds,
      reportClone.units.filter((unit) => unit.faction === 'ally'),
      state.reserveTrainingFocus
    );
  if (!previousSettlement && reportClone.outcome === 'victory') {
    reportClone.bonds = state.bonds.map(cloneBondSnapshot);
  }
  state.completedCampDialogues = completedCampDialogues;
  state.completedCampVisits = completedCampVisits;
  state.inventory = applyRewardDelta(state.inventory, previousSettlement?.itemRewards ?? [], -1);
  state.inventory = applyRewardDelta(state.inventory, reportClone.itemRewards, 1);
  state.battleHistory[battleId] = createBattleSettlement(reportClone, reserveTraining);
  state.latestBattleId = battleId;
  state.updatedAt = new Date().toISOString();

  persistCampaignState(state);
}

export function getFirstBattleReport() {
  const report = ensureCampaignState().firstBattleReport;
  return report ? cloneReport(report) : undefined;
}

export function applyCampBondExp(dialogueId: string, bondId: string, amount: number) {
  const state = ensureCampaignState();
  if (!state.firstBattleReport) {
    return undefined;
  }

  if (!state.completedCampDialogues.includes(dialogueId)) {
    state.completedCampDialogues.push(dialogueId);
  }
  if (!state.firstBattleReport.completedCampDialogues.includes(dialogueId)) {
    state.firstBattleReport.completedCampDialogues.push(dialogueId);
  }

  const campaignBond = state.bonds.find((candidate) => candidate.id === bondId);
  const reportBond = state.firstBattleReport.bonds.find((candidate) => candidate.id === bondId);
  if (!campaignBond || !reportBond) {
    return undefined;
  }

  advanceBond(campaignBond, amount);
  reportBond.level = campaignBond.level;
  reportBond.exp = campaignBond.exp;
  reportBond.battleExp = campaignBond.battleExp;
  state.updatedAt = new Date().toISOString();

  persistCampaignState(state);
  return cloneReport(state.firstBattleReport);
}

export function applyCampVisitReward(
  visitId: string,
  reward: { bondId?: string; bondExp?: number; gold?: number; itemRewards?: string[] }
) {
  const state = ensureCampaignState();
  if (!state.firstBattleReport) {
    return undefined;
  }

  if (state.completedCampVisits.includes(visitId)) {
    return cloneCampaignState(state);
  }

  state.completedCampVisits.push(visitId);
  if (!state.firstBattleReport.completedCampVisits.includes(visitId)) {
    state.firstBattleReport.completedCampVisits.push(visitId);
  }

  if (reward.bondId && reward.bondExp && reward.bondExp > 0) {
    const campaignBond = state.bonds.find((candidate) => candidate.id === reward.bondId);
    const reportBond = state.firstBattleReport.bonds.find((candidate) => candidate.id === reward.bondId);
    if (campaignBond) {
      advanceBond(campaignBond, reward.bondExp);
      if (reportBond) {
        reportBond.level = campaignBond.level;
        reportBond.exp = campaignBond.exp;
        reportBond.battleExp = campaignBond.battleExp;
      }
    }
  }

  if (reward.gold && reward.gold > 0) {
    state.gold += reward.gold;
  }

  if (reward.itemRewards?.length) {
    state.inventory = applyRewardDelta(state.inventory, reward.itemRewards, 1);
  }

  state.updatedAt = new Date().toISOString();
  persistCampaignState(state);
  return cloneCampaignState(state);
}

export function ensureCampaignRosterUnits(units: UnitData[], bonds: BattleBond[] = []) {
  const state = ensureCampaignState();
  let changed = false;
  const existingUnitIds = new Set(state.roster.map((unit) => unit.id));

  units.forEach((unit) => {
    if (existingUnitIds.has(unit.id)) {
      return;
    }
    state.roster.push(cloneUnit(unit));
    existingUnitIds.add(unit.id);
    changed = true;
  });

  const existingBondIds = new Set(state.bonds.map((bond) => bond.id));
  bonds.forEach((bond) => {
    if (existingBondIds.has(bond.id)) {
      return;
    }
    state.bonds.push(createCampBondSnapshot(bond));
    existingBondIds.add(bond.id);
    changed = true;
  });

  if (state.firstBattleReport) {
    const reportUnitIds = new Set(state.firstBattleReport.units.map((unit) => unit.id));
    units.forEach((unit) => {
      if (reportUnitIds.has(unit.id)) {
        return;
      }
      state.firstBattleReport?.units.push(cloneUnit(unit));
      reportUnitIds.add(unit.id);
      changed = true;
    });

    const reportBondIds = new Set(state.firstBattleReport.bonds.map((bond) => bond.id));
    bonds.forEach((bond) => {
      if (reportBondIds.has(bond.id)) {
        return;
      }
      state.firstBattleReport?.bonds.push(createCampBondSnapshot(bond));
      reportBondIds.add(bond.id);
      changed = true;
    });
  }

  if (changed) {
    state.updatedAt = new Date().toISOString();
    persistCampaignState(state);
  }

  return cloneCampaignState(state);
}

function ensureCampaignState() {
  if (!campaignState) {
    campaignState = readStoredCampaignState() ?? createInitialCampaignState();
  }
  return campaignState;
}

function normalizeCampaignState(state: CampaignState): CampaignState {
  const normalized = cloneCampaignState(state);
  normalized.version = 1;
  normalized.updatedAt = normalized.updatedAt || new Date().toISOString();
  normalized.activeSaveSlot = normalizeSlot(normalized.activeSaveSlot);
  normalized.gold = normalized.gold ?? 0;
  normalized.roster = (normalized.roster ?? []).map(cloneUnit);
  normalized.bonds = (normalized.bonds ?? []).map(cloneBondSnapshot);
  normalized.completedCampDialogues = [...new Set(normalized.completedCampDialogues ?? [])];
  normalized.completedCampVisits = [...new Set(normalized.completedCampVisits ?? [])];
  normalized.inventory = { ...(normalized.inventory ?? {}) };
  normalized.selectedSortieUnitIds = [...new Set(normalized.selectedSortieUnitIds ?? [])];
  normalized.reserveTrainingFocus = normalizeReserveTrainingFocusId(normalized.reserveTrainingFocus);
  normalized.battleHistory = normalized.battleHistory ?? {};
  if (normalized.firstBattleReport) {
    normalized.firstBattleReport = cloneReport(normalized.firstBattleReport);
  }
  return normalized;
}

function normalizeReserveTrainingFocusId(focusId?: string): CampaignReserveTrainingFocusId {
  return campaignReserveTrainingFocusDefinitions.some((focus) => focus.id === focusId)
    ? focusId as CampaignReserveTrainingFocusId
    : defaultCampaignReserveTrainingFocusId;
}

function reserveTrainingFocusDefinition(focusId?: string) {
  const normalizedId = normalizeReserveTrainingFocusId(focusId);
  return campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === normalizedId) ?? campaignReserveTrainingFocusDefinitions[0];
}

function readStoredCampaignState(slot?: number) {
  const raw = tryStorage()?.getItem(slot ? campaignSaveSlotKey(slot) : campaignStorageKey);
  if (!raw) {
    return undefined;
  }

  try {
    const parsed = JSON.parse(raw) as CampaignState;
    if (!parsed || parsed.version !== 1) {
      return undefined;
    }
    return normalizeCampaignState(parsed);
  } catch {
    return undefined;
  }
}

function persistCampaignState(state: CampaignState) {
  const storage = tryStorage();
  storage?.setItem(campaignStorageKey, JSON.stringify(state));
  storage?.setItem(campaignSaveSlotKey(state.activeSaveSlot), JSON.stringify(state));
}

function tryStorage() {
  if (typeof window === 'undefined') {
    return undefined;
  }
  return window.localStorage;
}

function campaignSaveSlotKey(slot: number) {
  return `${campaignStorageKey}:slot-${normalizeSlot(slot)}`;
}

function normalizeSlot(slot: number | undefined) {
  if (!slot || Number.isNaN(slot)) {
    return 1;
  }
  return Math.min(campaignSaveSlotCount, Math.max(1, Math.floor(slot)));
}

function readLatestSlottedCampaignState() {
  const latest = listCampaignSaveSlots()
    .filter((slot) => slot.exists && slot.updatedAt)
    .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))[0];
  return latest ? readStoredCampaignState(latest.slot) : undefined;
}

function advanceBond(bond: CampBondSnapshot, amount: number) {
  bond.battleExp += amount;
  bond.exp += amount;
  while (bond.exp >= 100) {
    bond.exp -= 100;
    bond.level = Math.min(100, bond.level + 1);
  }
}

function createBattleSettlement(report: FirstBattleReport, reserveTraining: CampaignReserveTrainingSnapshot[] = []): CampaignBattleSettlement {
  return {
    battleId: report.battleId,
    battleTitle: report.battleTitle,
    outcome: report.outcome,
    rewardGold: report.rewardGold,
    itemRewards: [...report.itemRewards],
    objectives: report.objectives.map((objective) => ({ ...objective })),
    units: report.units
      .filter((unit) => unit.faction === 'ally')
      .map((unit) => ({
        unitId: unit.id,
        name: unit.name,
        level: unit.level,
        exp: unit.exp,
        hp: unit.hp,
        maxHp: unit.maxHp,
        equipment: cloneUnit(unit).equipment
      })),
    bonds: report.bonds.map((bond) => ({
      id: bond.id,
      title: bond.title,
      level: bond.level,
      exp: bond.exp,
      battleExp: bond.battleExp
    })),
    reserveTraining: reserveTraining.map((entry) => ({ ...entry })),
    completedAt: report.createdAt
  };
}

function applyRewardDelta(inventory: Record<string, number>, rewards: string[], direction: 1 | -1) {
  return rewards.reduce<Record<string, number>>((nextInventory, reward) => {
    const { label, amount } = parseRewardLabel(reward);
    nextInventory[label] = Math.max(0, (nextInventory[label] ?? 0) + amount * direction);
    if (nextInventory[label] === 0) {
      delete nextInventory[label];
    }
    return nextInventory;
  }, { ...inventory });
}

function mergeRosterProgress(currentRoster: UnitData[], deployedUnits: UnitData[]) {
  const merged = new Map<string, UnitData>();
  currentRoster.forEach((unit) => merged.set(unit.id, cloneUnit(unit)));
  deployedUnits.forEach((unit) => merged.set(unit.id, cloneUnit(unit)));
  return Array.from(merged.values());
}

function applyReserveTrainingProgress(
  roster: UnitData[],
  bonds: CampBondSnapshot[],
  deployedUnits: UnitData[],
  focusId: CampaignReserveTrainingFocusId
): CampaignReserveTrainingSnapshot[] {
  const focus = reserveTrainingFocusDefinition(focusId);
  const deployedIds = new Set(deployedUnits.map((unit) => unit.id));
  const reserveUnits = roster.filter((unit) => unit.faction === 'ally' && !deployedIds.has(unit.id));
  const reserveIds = new Set(reserveUnits.map((unit) => unit.id));
  const bondExpByUnit = new Map<string, number>();

  if (focus.bondExpGained > 0 && reserveIds.size > 0) {
    bonds.forEach((bond) => {
      const reservePartners = bond.unitIds.filter((unitId) => reserveIds.has(unitId));
      if (reservePartners.length === 0) {
        return;
      }
      advanceBond(bond, focus.bondExpGained);
      reservePartners.forEach((unitId) => {
        bondExpByUnit.set(unitId, (bondExpByUnit.get(unitId) ?? 0) + focus.bondExpGained);
      });
    });
  }

  return reserveUnits
    .map((unit) => {
      const expGained = focus.expGained;
      const equipmentExpGained = focus.equipmentExpGained;
      advanceUnitExp(unit, expGained);
      advanceReserveEquipment(unit, equipmentExpGained);
      return {
        unitId: unit.id,
        name: unit.name,
        expGained,
        equipmentExpGained,
        bondExpGained: bondExpByUnit.get(unit.id) ?? 0,
        focusId: focus.id,
        focusLabel: focus.label,
        level: unit.level,
        exp: unit.exp
      };
    });
}

function advanceUnitExp(unit: UnitData, amount: number) {
  let exp = unit.exp + amount;
  while (unit.level < 99 && exp >= 100) {
    exp -= 100;
    unit.level += 1;
    unit.maxHp += 2;
    unit.hp = Math.min(unit.maxHp, unit.hp + 2);
    unit.attack += 1;
  }
  unit.exp = unit.level >= 99 ? Math.min(exp, 100) : exp;
}

function advanceReserveEquipment(unit: UnitData, amount: number) {
  equipmentSlots.forEach((slot) => {
    const state = unit.equipment[slot];
    let exp = state.exp + amount;
    while (state.level < 9 && exp >= equipmentExpToNext(state.level)) {
      exp -= equipmentExpToNext(state.level);
      state.level += 1;
    }
    state.exp = state.level >= 9 ? Math.min(exp, equipmentExpToNext(state.level)) : exp;
  });
}

function parseRewardLabel(reward: string) {
  const normalized = reward.replace(/\s+/g, ' ').trim();
  const match = normalized.match(/^(.*?)(?:\s*[+xX]?\s*(\d+))$/);

  if (!match) {
    return { label: normalized, amount: 1 };
  }

  return {
    label: match[1].trim() || normalized,
    amount: Number(match[2] ?? 1)
  };
}

function cloneCampaignState(state: CampaignState): CampaignState {
  return JSON.parse(JSON.stringify(state)) as CampaignState;
}

function cloneReport(report: FirstBattleReport): FirstBattleReport {
  const cloned = JSON.parse(JSON.stringify(report)) as FirstBattleReport;
  cloned.completedCampVisits = [...new Set(cloned.completedCampVisits ?? [])];
  return cloned;
}

function cloneUnit(unit: UnitData): UnitData {
  return JSON.parse(JSON.stringify(unit)) as UnitData;
}

function cloneBondSnapshot(bond: CampBondSnapshot): CampBondSnapshot {
  return {
    ...bond,
    unitIds: [...bond.unitIds] as [string, string]
  };
}

function createCampBondSnapshot(bond: BattleBond): CampBondSnapshot {
  return {
    ...bond,
    unitIds: [...bond.unitIds] as [string, string],
    battleExp: 0
  };
}
