import { create } from 'zustand';
import {
  createInitialEnemies,
  THE_QUEST_ITEMS,
  THE_QUEST_NPCS,
  THE_QUEST_QUESTS,
  THE_QUEST_SKILLS,
  type EnemyDefinition,
  type EquipmentSlot,
  type InventoryItem,
  type NpcDefinition,
  type QuestDefinition,
  type SkillDefinition,
} from './theQuestData';

export type CombatTextEntry = {
  id: string;
  value: string;
  color: string;
};

export type EquipmentState = Record<EquipmentSlot, string | null>;

export type QuestProgressState = QuestDefinition & {
  currentCount: number;
  readyToTurnIn: boolean;
  completed: boolean;
};

export type EnemyState = EnemyDefinition & {
  hp: number;
  mp: number;
  alive: boolean;
};

export type CombatState = {
  currentTargetId: string | null;
  comboCount: number;
  comboExpiresAt: number;
  peakCombo: number;
  chainStep: number;
  lastDamage: number;
  lastHitCritical: boolean;
  lastLootLabel: string | null;
  lastRewardLabel: string | null;
  activeSkillId: string | null;
  activeSkillEndsAt: number;
  activeSkillLabel: string | null;
};

export type PlayerVitals = {
  level: number;
  exp: number;
  expToNext: number;
  hp: number;
  maxHp: number;
  mp: number;
  maxMp: number;
  attack: number;
  spellPower: number;
  defense: number;
  critRate: number;
  manaAbsorbChance: number;
  gold: number;
  gems: number;
};

export type JoystickState = {
  active: boolean;
  x: number;
  y: number;
};

export type MinimapState = {
  player: { x: number; y: number };
  enemies: Array<{ id: string; x: number; y: number; alive: boolean }>;
  npcs: Array<Pick<NpcDefinition, 'id' | 'name' | 'x' | 'y'>>;
};

export type PerformanceState = {
  isMobile: boolean;
  lowEffects: boolean;
};

export type PlayerLocationState = {
  x: number;
  y: number;
  zoneLabel: string;
  placeLabel: string;
  updatedAt: number;
};

export type StageState = {
  current: number;
  enemyResetVersion: number;
  transitionVersion: number;
};

export type QueuedRecoveryEffect = {
  itemId: string;
  label: string;
  icon: string;
  healPerTick: number;
  ticksRemaining: number;
  tickIntervalMs: number;
  nextTickAt: number;
};

export type ConsumableEffectsState = {
  queuedRecovery: QueuedRecoveryEffect[];
  autoHealMultiplier: number;
  autoHealBoostUntil: number;
  moveSpeedMultiplier: number;
  moveSpeedBoostUntil: number;
  arcaneSurgeMultiplier: number;
  arcaneSurgeUntil: number;
  manaWardReduction: number;
  manaWardUntil: number;
  familiarUntil: number;
  familiarPulseAt: number;
};

export type ManualSkillCastCommand = {
  id: number;
  skillId: string;
  requestedAt: number;
};

type OverlayPanel = 'character' | 'inventory' | 'equipment' | 'quests' | 'npcs' | 'settings' | null;

type PersistedEnemyProgress = {
  id: string;
  hp: number;
  alive: boolean;
};

type PersistedQuestState = {
  vitals: PlayerVitals;
  equipment: EquipmentState;
  inventory: InventoryItem[];
  quests: QuestProgressState[];
  enemies: PersistedEnemyProgress[];
  stageCurrent: number;
  autoCombat: boolean;
  activeNpcId: string | null;
  selectedInventoryId: string | null;
  selectedQuestId: string | null;
  playerLocation: PlayerLocationState;
  lastCombatLog: string;
  consumableEffects: ConsumableEffectsState;
  lastSavedAt: number;
};

type TheQuestStore = {
  vitals: PlayerVitals;
  equipment: EquipmentState;
  inventory: InventoryItem[];
  quests: QuestProgressState[];
  skills: SkillDefinition[];
  skillCooldowns: Record<string, number>;
  enemies: EnemyState[];
  npcs: NpcDefinition[];
  combat: CombatState;
  autoCombat: boolean;
  overlayPanel: OverlayPanel;
  activeNpcId: string | null;
  selectedInventoryId: string | null;
  selectedQuestId: string | null;
  combatText: CombatTextEntry | null;
  joystick: JoystickState;
  minimap: MinimapState;
  playerLocation: PlayerLocationState;
  performance: PerformanceState;
  stage: StageState;
  lastCombatLog: string;
  consumableEffects: ConsumableEffectsState;
  manualSkillCastCommand: ManualSkillCastCommand | null;
  setOverlayPanel: (panel: OverlayPanel) => void;
  setJoystick: (joystick: JoystickState) => void;
  setAutoCombat: (enabled: boolean) => void;
  setActiveNpc: (npcId: string | null) => void;
  setSelectedInventoryId: (itemId: string | null) => void;
  setSelectedQuestId: (questId: string | null) => void;
  setSkillCooldown: (skillId: string, readyAt: number) => void;
  reduceSkillCooldowns: (time: number) => void;
  setMinimap: (minimap: MinimapState) => void;
  setPlayerLocation: (location: PlayerLocationState) => void;
  setPerformance: (performance: PerformanceState) => void;
  setCurrentTarget: (enemyId: string | null) => void;
  setActiveSkill: (payload: { skillId: string; label: string; endsAt: number }) => void;
  clearExpiredSkillState: (time: number) => void;
  registerComboHit: (payload: {
    time: number;
    damage: number;
    critical: boolean;
    chainStep: number;
  }) => void;
  clearExpiredCombo: (time: number) => void;
  applyDamageToEnemy: (enemyId: string, amount: number) => void;
  useConsumable: (itemId: string) => void;
  equipItem: (itemId: string) => void;
  unequipItem: (slot: EquipmentSlot) => void;
  turnInQuest: (questId: string) => void;
  rewardEnemyKill: (enemyId: string) => void;
  respawnEnemy: (enemyId: string) => void;
  advanceStage: () => void;
  setEnemySnapshot: (enemies: EnemyState[]) => void;
  setCombatText: (entry: CombatTextEntry | null) => void;
  setLastCombatLog: (message: string) => void;
  setVitals: (updater: (current: PlayerVitals) => PlayerVitals) => void;
  setConsumableEffects: (updater: (current: ConsumableEffectsState) => ConsumableEffectsState) => void;
  requestManualSkillCast: (skillId: ManualSkillCastCommand['skillId']) => void;
  consumeManualSkillCast: (commandId: number) => void;
  reset: () => void;
};

const THE_QUEST_LOCATION_STORAGE_KEY = 'the-quest:player-location';
const THE_QUEST_STATE_STORAGE_KEY = 'the-quest:game-state';
const DEFAULT_PLAYER_POSITION = { x: 1080, y: 920 } as const;
const DEFAULT_STAGE = 1;
const PASSIVE_HP_RECOVERY_PER_SECOND = 0.9;
const PASSIVE_MP_RECOVERY_PER_SECOND = 0.6;
const OFFLINE_RECOVERY_RATE_MULTIPLIER = 0.2;

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

function buildPlaceLabel(x: number, y: number) {
  const horizontal = x < 920 ? '서쪽' : x > 1460 ? '동쪽' : '중앙';
  const vertical = y < 780 ? '북부' : y > 1120 ? '남부' : '중부';
  return `${horizontal} ${vertical} 지점`;
}

export function resolveTheQuestLocationMeta(x: number, y: number, npcs: NpcDefinition[]) {
  const clampedX = clamp(Math.round(x), 0, 2200);
  const clampedY = clamp(Math.round(y), 0, 1600);
  const nearbyNpc =
    npcs.find((npc) => Math.hypot(npc.x - clampedX, npc.y - clampedY) < 120) ?? null;

  let zoneLabel = '에메랄드 길목';
  if (clampedY < 780) {
    zoneLabel = '문포레스트 입구';
  } else if (clampedX > 1440 && clampedY < 980) {
    zoneLabel = '북부 포탈 숲';
  } else if (clampedX > 1500 && clampedY >= 980) {
    zoneLabel = '리버사이드 경계';
  } else if (clampedX < 980 && clampedY > 1040) {
    zoneLabel = '상인 야영지';
  }

  return {
    x: clampedX,
    y: clampedY,
    zoneLabel,
    placeLabel: nearbyNpc ? `${nearbyNpc.name} 주변` : buildPlaceLabel(clampedX, clampedY),
    updatedAt: Date.now(),
  } satisfies PlayerLocationState;
}

function readStoredPlayerLocation() {
  if (typeof window === 'undefined') {
    return null;
  }

  try {
    const rawValue = window.localStorage.getItem(THE_QUEST_LOCATION_STORAGE_KEY);
    if (!rawValue) {
      return null;
    }

    const parsed = JSON.parse(rawValue) as Partial<PlayerLocationState>;
    if (typeof parsed.x !== 'number' || typeof parsed.y !== 'number') {
      return null;
    }

    return {
      x: clamp(parsed.x, 0, 2200),
      y: clamp(parsed.y, 0, 1600),
      zoneLabel: typeof parsed.zoneLabel === 'string' && parsed.zoneLabel.trim() ? parsed.zoneLabel : '문포레스트 입구',
      placeLabel: typeof parsed.placeLabel === 'string' && parsed.placeLabel.trim() ? parsed.placeLabel : '중앙 중부 지점',
      updatedAt: typeof parsed.updatedAt === 'number' ? parsed.updatedAt : Date.now(),
    } satisfies PlayerLocationState;
  } catch {
    return null;
  }
}

function persistPlayerLocation(location: PlayerLocationState) {
  if (typeof window === 'undefined') {
    return;
  }

  try {
    window.localStorage.setItem(THE_QUEST_LOCATION_STORAGE_KEY, JSON.stringify(location));
  } catch {
    // Ignore storage write failures and keep gameplay state in memory.
  }
}

function isValidEquipmentSlot(slot: string): slot is EquipmentSlot {
  return ['weapon', 'helmet', 'shoulders', 'chest', 'belt', 'gloves', 'boots', 'ring', 'necklace', 'bracelet', 'artifact'].includes(slot);
}

function sanitizeEquipment(input: unknown, fallback: EquipmentState) {
  if (!input || typeof input !== 'object') {
    return fallback;
  }

  const equipment = { ...fallback };
  for (const [slot, itemId] of Object.entries(input as Record<string, unknown>)) {
    if (!isValidEquipmentSlot(slot)) {
      continue;
    }
    equipment[slot] = typeof itemId === 'string' && itemId.trim() ? itemId : null;
  }
  return equipment;
}

function sanitizeEnemyProgress(input: unknown) {
  if (!Array.isArray(input)) {
    return [];
  }

  return input
    .map((entry) => {
      if (!entry || typeof entry !== 'object') {
        return null;
      }

      const candidate = entry as Partial<PersistedEnemyProgress>;
      return typeof candidate.id === 'string' && typeof candidate.hp === 'number' && typeof candidate.alive === 'boolean'
        ? candidate
        : null;
    })
    .filter(Boolean) as PersistedEnemyProgress[];
}

function sanitizeInventory(input: unknown, fallback: InventoryItem[]) {
  if (!Array.isArray(input)) {
    return fallback;
  }

  const fallbackMap = new Map(fallback.map((item) => [item.id, item]));
  const storedEntries = input
    .map((entry) => {
      if (!entry || typeof entry !== 'object') {
        return null;
      }
      const item = entry as InventoryItem;
      if (typeof item.id !== 'string' || !fallbackMap.has(item.id)) {
        return null;
      }
      return item;
    })
    .filter(Boolean) as InventoryItem[];

  const storedMap = new Map(storedEntries.map((item) => [item.id, item]));
  return fallback.map((item) => {
    const storedItem = storedMap.get(item.id);
    if (!storedItem) {
      return { ...item };
    }

    return {
      ...item,
      quantity: typeof storedItem.quantity === 'number' ? storedItem.quantity : item.quantity,
    };
  });
}

function sanitizeQuests(input: unknown, fallback: QuestProgressState[]) {
  if (!Array.isArray(input)) {
    return fallback;
  }

  const fallbackMap = new Map(fallback.map((quest) => [quest.id, quest]));

  const storedQuests = input
    .map((entry) => {
      if (!entry || typeof entry !== 'object') {
        return null;
      }
      const quest = entry as Partial<QuestProgressState>;
      return typeof quest.id === 'string' ? quest : null;
    })
    .filter(Boolean) as Array<Partial<QuestProgressState> & { id: string }>;

  const storedMap = new Map(storedQuests.map((quest) => [quest.id, quest]));

  return fallback.map((quest) => {
    const storedQuest = storedMap.get(quest.id);
    if (!storedQuest) {
      return { ...quest };
    }

    const nextCount =
      typeof storedQuest.currentCount === 'number' && Number.isFinite(storedQuest.currentCount)
        ? clamp(Math.round(storedQuest.currentCount), 0, quest.requiredCount)
        : quest.currentCount;

    return {
      ...quest,
      currentCount: nextCount,
      readyToTurnIn: typeof storedQuest.readyToTurnIn === 'boolean' ? storedQuest.readyToTurnIn : quest.readyToTurnIn,
      completed: typeof storedQuest.completed === 'boolean' ? storedQuest.completed : quest.completed,
    };
  });
}

function sanitizeVitals(input: unknown, fallback: PlayerVitals) {
  if (!input || typeof input !== 'object') {
    return fallback;
  }

  const vitals = input as Partial<PlayerVitals>;
  const keys: Array<keyof PlayerVitals> = [
    'level',
    'exp',
    'expToNext',
    'hp',
    'maxHp',
    'mp',
    'maxMp',
    'attack',
    'spellPower',
    'defense',
    'critRate',
    'manaAbsorbChance',
    'gold',
    'gems',
  ];

  return keys.reduce<PlayerVitals>(
    (acc, key) => ({
      ...acc,
      [key]: typeof vitals[key] === 'number' && Number.isFinite(vitals[key]) ? vitals[key] : fallback[key],
    }),
    { ...fallback },
  );
}

function sanitizePlayerLocation(input: unknown, fallback: PlayerLocationState) {
  if (!input || typeof input !== 'object') {
    return fallback;
  }

  const location = input as Partial<PlayerLocationState>;
  if (typeof location.x !== 'number' || typeof location.y !== 'number') {
    return fallback;
  }

  return resolveTheQuestLocationMeta(location.x, location.y, THE_QUEST_NPCS);
}

function sanitizeStageCurrent(input: unknown) {
  if (typeof input !== 'number' || !Number.isFinite(input)) {
    return DEFAULT_STAGE;
  }

  return Math.max(DEFAULT_STAGE, Math.round(input));
}

function sanitizeConsumableEffects(input: unknown) {
  const fallback: ConsumableEffectsState = {
    queuedRecovery: [],
    autoHealMultiplier: 1,
    autoHealBoostUntil: 0,
    moveSpeedMultiplier: 1,
    moveSpeedBoostUntil: 0,
    arcaneSurgeMultiplier: 1,
    arcaneSurgeUntil: 0,
    manaWardReduction: 0,
    manaWardUntil: 0,
    familiarUntil: 0,
    familiarPulseAt: 0,
  };
  if (!input || typeof input !== 'object') {
    return fallback;
  }

  const candidate = input as Partial<ConsumableEffectsState>;
  const queuedRecovery = Array.isArray(candidate.queuedRecovery)
    ? candidate.queuedRecovery
        .map((entry) => {
          if (!entry || typeof entry !== 'object') {
            return null;
          }

          const effect = entry as Partial<QueuedRecoveryEffect>;
          if (
            typeof effect.itemId !== 'string' ||
            typeof effect.label !== 'string' ||
            typeof effect.icon !== 'string' ||
            typeof effect.healPerTick !== 'number' ||
            typeof effect.ticksRemaining !== 'number' ||
            typeof effect.tickIntervalMs !== 'number' ||
            typeof effect.nextTickAt !== 'number'
          ) {
            return null;
          }

          return {
            itemId: effect.itemId,
            label: effect.label,
            icon: effect.icon,
            healPerTick: Math.max(1, Math.round(effect.healPerTick)),
            ticksRemaining: Math.max(0, Math.round(effect.ticksRemaining)),
            tickIntervalMs: Math.max(250, Math.round(effect.tickIntervalMs)),
            nextTickAt: effect.nextTickAt,
          } satisfies QueuedRecoveryEffect;
        })
        .filter((entry): entry is QueuedRecoveryEffect => Boolean(entry) && entry.ticksRemaining > 0)
    : [];

  return {
    queuedRecovery,
    autoHealMultiplier:
      typeof candidate.autoHealMultiplier === 'number' && Number.isFinite(candidate.autoHealMultiplier)
        ? Math.max(1, candidate.autoHealMultiplier)
        : fallback.autoHealMultiplier,
    autoHealBoostUntil:
      typeof candidate.autoHealBoostUntil === 'number' && Number.isFinite(candidate.autoHealBoostUntil)
        ? candidate.autoHealBoostUntil
        : fallback.autoHealBoostUntil,
    moveSpeedMultiplier:
      typeof candidate.moveSpeedMultiplier === 'number' && Number.isFinite(candidate.moveSpeedMultiplier)
        ? Math.max(1, candidate.moveSpeedMultiplier)
        : fallback.moveSpeedMultiplier,
    moveSpeedBoostUntil:
      typeof candidate.moveSpeedBoostUntil === 'number' && Number.isFinite(candidate.moveSpeedBoostUntil)
        ? candidate.moveSpeedBoostUntil
        : fallback.moveSpeedBoostUntil,
    arcaneSurgeMultiplier:
      typeof candidate.arcaneSurgeMultiplier === 'number' && Number.isFinite(candidate.arcaneSurgeMultiplier)
        ? Math.max(1, candidate.arcaneSurgeMultiplier)
        : fallback.arcaneSurgeMultiplier,
    arcaneSurgeUntil:
      typeof candidate.arcaneSurgeUntil === 'number' && Number.isFinite(candidate.arcaneSurgeUntil)
        ? candidate.arcaneSurgeUntil
        : fallback.arcaneSurgeUntil,
    manaWardReduction:
      typeof candidate.manaWardReduction === 'number' && Number.isFinite(candidate.manaWardReduction)
        ? Math.max(0, candidate.manaWardReduction)
        : fallback.manaWardReduction,
    manaWardUntil:
      typeof candidate.manaWardUntil === 'number' && Number.isFinite(candidate.manaWardUntil)
        ? candidate.manaWardUntil
        : fallback.manaWardUntil,
    familiarUntil:
      typeof candidate.familiarUntil === 'number' && Number.isFinite(candidate.familiarUntil)
        ? candidate.familiarUntil
        : fallback.familiarUntil,
    familiarPulseAt:
      typeof candidate.familiarPulseAt === 'number' && Number.isFinite(candidate.familiarPulseAt)
        ? candidate.familiarPulseAt
        : fallback.familiarPulseAt,
  } satisfies ConsumableEffectsState;
}

function readStoredQuestState() {
  if (typeof window === 'undefined') {
    return null;
  }

  try {
    const rawValue = window.localStorage.getItem(THE_QUEST_STATE_STORAGE_KEY);
    if (!rawValue) {
      return null;
    }

    return JSON.parse(rawValue) as Partial<PersistedQuestState>;
  } catch {
    return null;
  }
}

function applyOfflineRecovery(vitals: PlayerVitals, lastSavedAt: unknown) {
  if (typeof lastSavedAt !== 'number' || !Number.isFinite(lastSavedAt)) {
    return {
      vitals,
      recoveredHp: 0,
      recoveredMp: 0,
    };
  }

  const elapsedMs = Date.now() - lastSavedAt;
  if (elapsedMs <= 0) {
    return {
      vitals,
      recoveredHp: 0,
      recoveredMp: 0,
    };
  }

  const elapsedSeconds = elapsedMs / 1000;
  const recoveredHp = Math.max(0, Math.floor(elapsedSeconds * PASSIVE_HP_RECOVERY_PER_SECOND * OFFLINE_RECOVERY_RATE_MULTIPLIER));
  const recoveredMp = Math.max(0, Math.floor(elapsedSeconds * PASSIVE_MP_RECOVERY_PER_SECOND * OFFLINE_RECOVERY_RATE_MULTIPLIER));
  if (recoveredHp <= 0 && recoveredMp <= 0) {
    return {
      vitals,
      recoveredHp: 0,
      recoveredMp: 0,
    };
  }

  return {
    vitals: {
      ...vitals,
      hp: Math.min(vitals.maxHp, vitals.hp + recoveredHp),
      mp: Math.min(vitals.maxMp, vitals.mp + recoveredMp),
    },
    recoveredHp,
    recoveredMp,
  };
}

function buildPersistedQuestState(state: Pick<
  TheQuestStore,
  | 'vitals'
  | 'equipment'
  | 'inventory'
  | 'quests'
  | 'enemies'
  | 'stage'
  | 'autoCombat'
  | 'activeNpcId'
  | 'selectedInventoryId'
  | 'selectedQuestId'
  | 'playerLocation'
  | 'lastCombatLog'
  | 'consumableEffects'
>): PersistedQuestState {
  return {
    vitals: state.vitals,
    equipment: state.equipment,
    inventory: state.inventory,
    quests: state.quests,
    enemies: state.enemies.map(({ id, hp, alive }) => ({ id, hp, alive })),
    stageCurrent: state.stage.current,
    autoCombat: state.autoCombat,
    activeNpcId: state.activeNpcId,
    selectedInventoryId: state.selectedInventoryId,
    selectedQuestId: state.selectedQuestId,
    playerLocation: state.playerLocation,
    lastCombatLog: state.lastCombatLog,
    consumableEffects: state.consumableEffects,
    lastSavedAt: Date.now(),
  };
}

let persistQuestStateTimer: number | null = null;
let pendingPersistedQuestState: PersistedQuestState | null = null;
let lastPersistedQuestStateJson = '';

function createStageEnemies(stageCurrent: number) {
  const safeStage = Math.max(DEFAULT_STAGE, Math.round(stageCurrent));
  const hpMultiplier = 1 + (safeStage - 1) * 0.32;
  const attackMultiplier = 1 + (safeStage - 1) * 0.18;
  const rewardMultiplier = 1 + (safeStage - 1) * 0.2;
  const orbitOffset = (safeStage - 1) * 24;

  return createInitialEnemies().map((enemy, index) => {
    const columnOffset = index % 2 === 0 ? -orbitOffset : orbitOffset;
    const rowOffset = index < 3 ? -(safeStage - 1) * 18 : (safeStage - 1) * 18;
    const maxHp = Math.round(enemy.maxHp * hpMultiplier);
    const maxMp = Math.round(enemy.maxMp * (1 + (safeStage - 1) * 0.16));

    return {
      ...enemy,
      level: enemy.level + safeStage - 1,
      maxHp,
      maxMp,
      attack: Math.round(enemy.attack * attackMultiplier * (enemy.role === 'boss' ? 1.15 : 1)),
      expReward: Math.round(enemy.expReward * rewardMultiplier * (enemy.role === 'boss' ? 1.22 : 1)),
      goldReward: Math.round(enemy.goldReward * rewardMultiplier * (enemy.role === 'boss' ? 1.22 : 1)),
      x: clamp(enemy.x + columnOffset, 180, 2020),
      y: clamp(enemy.y + rowOffset, 180, 1420),
      hp: maxHp,
      mp: maxMp,
      alive: true,
    } satisfies EnemyState;
  });
}

function schedulePersistQuestState(state: Pick<
  TheQuestStore,
  | 'vitals'
  | 'equipment'
  | 'inventory'
  | 'quests'
  | 'enemies'
  | 'stage'
  | 'autoCombat'
  | 'activeNpcId'
  | 'selectedInventoryId'
  | 'selectedQuestId'
  | 'playerLocation'
  | 'lastCombatLog'
  | 'consumableEffects'
>) {
  if (typeof window === 'undefined') {
    return;
  }

  const snapshot = buildPersistedQuestState(state);
  const snapshotJson = JSON.stringify(snapshot);
  if (snapshotJson === lastPersistedQuestStateJson) {
    return;
  }
  pendingPersistedQuestState = snapshot;

  if (persistQuestStateTimer !== null) {
    return;
  }

  persistQuestStateTimer = window.setTimeout(() => {
    persistQuestStateTimer = null;
    if (!pendingPersistedQuestState) {
      return;
    }

    try {
      const nextJson = JSON.stringify(pendingPersistedQuestState);
      window.localStorage.setItem(THE_QUEST_STATE_STORAGE_KEY, nextJson);
      lastPersistedQuestStateJson = nextJson;
      pendingPersistedQuestState = null;
    } catch {
      // Ignore storage write failures and keep gameplay state in memory.
    }
  }, 180);
}

function createInitialState() {
  const storedLocation = readStoredPlayerLocation();
  const initialLocation =
    storedLocation ?? resolveTheQuestLocationMeta(DEFAULT_PLAYER_POSITION.x, DEFAULT_PLAYER_POSITION.y, THE_QUEST_NPCS);
  const storedState = readStoredQuestState();
  const initialStageCurrent = sanitizeStageCurrent(storedState?.stageCurrent);
  const enemyStates = createStageEnemies(initialStageCurrent);

  const baseState = {
    vitals: {
      level: 15,
      exp: 420,
      expToNext: 800,
      hp: 276,
      maxHp: 276,
      mp: 168,
      maxMp: 168,
      attack: 28,
      spellPower: 72,
      defense: 24,
      critRate: 0.18,
      manaAbsorbChance: 0.14,
      gold: 23456,
      gems: 1250,
    },
    equipment: {
      weapon: 'apprentice-staff',
      helmet: 'scribe-hood',
      shoulders: 'moon-spaulders',
      chest: 'astral-mantle',
      belt: 'mist-belt',
      gloves: 'rune-gloves',
      boots: 'trail-boots',
      ring: 'moon-ring',
      necklace: 'river-charm',
      bracelet: 'ember-bracelet',
      artifact: 'forest-relic',
    } satisfies EquipmentState,
    inventory: THE_QUEST_ITEMS.map((item) => ({ ...item })),
    quests: THE_QUEST_QUESTS.map((quest) => ({
      ...quest,
      currentCount: 0,
      readyToTurnIn: false,
      completed: false,
    })),
    skills: THE_QUEST_SKILLS,
    skillCooldowns: Object.fromEntries(THE_QUEST_SKILLS.map((skill) => [skill.id, 0])),
    enemies: enemyStates,
    npcs: THE_QUEST_NPCS,
    combat: {
      currentTargetId: null,
      comboCount: 0,
      comboExpiresAt: 0,
      peakCombo: 0,
      chainStep: 0,
      lastDamage: 0,
      lastHitCritical: false,
      lastLootLabel: null,
      lastRewardLabel: null,
      activeSkillId: null,
      activeSkillEndsAt: 0,
      activeSkillLabel: null,
    } satisfies CombatState,
    autoCombat: false,
    overlayPanel: null,
    activeNpcId: 'lyra',
    selectedInventoryId: 'apprentice-staff',
    selectedQuestId: 'slime-hunt',
    combatText: null,
    joystick: { active: false, x: 0, y: 0 },
    playerLocation: initialLocation,
    performance: {
      isMobile: false,
      lowEffects: false,
    } satisfies PerformanceState,
    stage: {
      current: initialStageCurrent,
      enemyResetVersion: 0,
      transitionVersion: 0,
    } satisfies StageState,
    minimap: {
      player: { x: initialLocation.x, y: initialLocation.y },
      enemies: enemyStates.map(({ id, x, y, alive }) => ({ id, x, y, alive })),
      npcs: THE_QUEST_NPCS.map(({ id, name, x, y }) => ({ id, name, x, y })),
    },
    lastCombatLog: '리라: 포탈 숲에서 슬라임을 정리하고 브롬에게 보고하세요.',
    consumableEffects: {
      queuedRecovery: [],
      autoHealMultiplier: 1,
      autoHealBoostUntil: 0,
      moveSpeedMultiplier: 1,
      moveSpeedBoostUntil: 0,
      arcaneSurgeMultiplier: 1,
      arcaneSurgeUntil: 0,
      manaWardReduction: 0,
      manaWardUntil: 0,
      familiarUntil: 0,
      familiarPulseAt: 0,
    } satisfies ConsumableEffectsState,
    manualSkillCastCommand: null,
  };
  if (!storedState) {
    return baseState;
  }

  const enemyProgressMap = new Map(sanitizeEnemyProgress(storedState.enemies).map((enemy) => [enemy.id, enemy]));
  const mergedEnemies = baseState.enemies.map((enemy) => {
    const storedEnemy = enemyProgressMap.get(enemy.id);
    if (!storedEnemy) {
      return enemy;
    }

    return {
      ...enemy,
      hp: clamp(storedEnemy.hp, 0, enemy.maxHp),
      alive: storedEnemy.alive && storedEnemy.hp > 0,
    };
  });

  const nextLocation = sanitizePlayerLocation(storedState.playerLocation, initialLocation);
  const restoredVitals = sanitizeVitals(storedState.vitals, baseState.vitals);
  const offlineRecovery = applyOfflineRecovery(restoredVitals, storedState.lastSavedAt);
  const restoredLog =
    offlineRecovery.recoveredHp > 0 || offlineRecovery.recoveredMp > 0
      ? `오프라인 회복 · HP +${offlineRecovery.recoveredHp} · MP +${offlineRecovery.recoveredMp}`
      : typeof storedState.lastCombatLog === 'string'
      ? storedState.lastCombatLog
      : baseState.lastCombatLog;

  return {
    ...baseState,
    vitals: offlineRecovery.vitals,
    equipment: sanitizeEquipment(storedState.equipment, baseState.equipment),
    inventory: sanitizeInventory(storedState.inventory, baseState.inventory),
    quests: syncQuestCollectionProgress(sanitizeQuests(storedState.quests, baseState.quests), sanitizeInventory(storedState.inventory, baseState.inventory)),
    enemies: mergedEnemies,
    stage: {
      current: initialStageCurrent,
      enemyResetVersion: 0,
      transitionVersion: 0,
    },
    autoCombat: typeof storedState.autoCombat === 'boolean' ? storedState.autoCombat : baseState.autoCombat,
    activeNpcId: typeof storedState.activeNpcId === 'string' ? storedState.activeNpcId : baseState.activeNpcId,
    selectedInventoryId:
      typeof storedState.selectedInventoryId === 'string' ? storedState.selectedInventoryId : baseState.selectedInventoryId,
    selectedQuestId: typeof storedState.selectedQuestId === 'string' ? storedState.selectedQuestId : baseState.selectedQuestId,
    playerLocation: nextLocation,
    consumableEffects: sanitizeConsumableEffects(storedState.consumableEffects),
    minimap: {
      ...baseState.minimap,
      player: { x: nextLocation.x, y: nextLocation.y },
      enemies: mergedEnemies.map(({ id, x, y, alive }) => ({ id, x, y, alive })),
    },
    lastCombatLog: restoredLog,
  };
}

function findInventoryItem(inventory: InventoryItem[], itemId: string) {
  return inventory.find((item) => item.id === itemId) ?? null;
}

function findInventoryQuantity(inventory: InventoryItem[], itemId: string) {
  return Math.max(0, findInventoryItem(inventory, itemId)?.quantity ?? 0);
}

function applyEquipmentBonuses(
  base: PlayerVitals,
  inventory: InventoryItem[],
  nextEquipment: EquipmentState,
  currentEquipment: EquipmentState = nextEquipment,
) {
  const buildEquippedItems = (equipment: EquipmentState) =>
    Object.values(equipment)
      .map((itemId) => (itemId ? findInventoryItem(inventory, itemId) : null))
      .filter(Boolean) as InventoryItem[];

  const equipped = buildEquippedItems(nextEquipment);
  const currentEquipped = buildEquippedItems(currentEquipment);

  const currentBonusAttack = currentEquipped.reduce((sum, item) => sum + (item.attack ?? 0), 0);
  const currentBonusDefense = currentEquipped.reduce((sum, item) => sum + (item.defense ?? 0), 0);
  const currentBonusCritRate = currentEquipped.reduce((sum, item) => sum + (item.critRate ?? 0), 0);
  const currentBonusSpellPower = currentEquipped.reduce((sum, item) => sum + (item.spellPower ?? 0), 0);
  const currentBonusMaxMp = currentEquipped.reduce((sum, item) => sum + (item.maxMpBonus ?? 0), 0);
  const currentBonusManaAbsorbChance = currentEquipped.reduce((sum, item) => sum + (item.manaAbsorbChance ?? 0), 0);

  const baseAttack = Math.max(0, base.attack - currentBonusAttack);
  const baseDefense = Math.max(0, base.defense - currentBonusDefense);
  const baseCritRate = Math.max(0, base.critRate - currentBonusCritRate);
  const baseSpellPower = Math.max(0, base.spellPower - currentBonusAttack - currentBonusSpellPower);
  const baseMaxMp = Math.max(0, base.maxMp - currentBonusMaxMp);
  const baseManaAbsorbChance = Math.max(0, base.manaAbsorbChance - currentBonusManaAbsorbChance);

  const bonusAttack = equipped.reduce((sum, item) => sum + (item.attack ?? 0), 0);
  const bonusDefense = equipped.reduce((sum, item) => sum + (item.defense ?? 0), 0);
  const bonusCritRate = equipped.reduce((sum, item) => sum + (item.critRate ?? 0), 0);
  const bonusSpellPower = equipped.reduce((sum, item) => sum + (item.spellPower ?? 0), 0);
  const bonusMaxMp = equipped.reduce((sum, item) => sum + (item.maxMpBonus ?? 0), 0);
  const bonusManaAbsorbChance = equipped.reduce((sum, item) => sum + (item.manaAbsorbChance ?? 0), 0);
  const nextMaxMp = baseMaxMp + bonusMaxMp;

  return {
    ...base,
    hp: Math.min(base.hp, base.maxHp),
    maxMp: nextMaxMp,
    mp: Math.min(base.mp, nextMaxMp),
    attack: baseAttack + bonusAttack,
    spellPower: baseSpellPower + bonusAttack + bonusSpellPower,
    defense: baseDefense + bonusDefense,
    critRate: baseCritRate + bonusCritRate,
    manaAbsorbChance: Math.min(0.5, baseManaAbsorbChance + bonusManaAbsorbChance),
  };
}

function mergeInventoryDrop(inventory: InventoryItem[], itemId: string, quantity: number) {
  if (quantity <= 0) {
    return inventory;
  }

  const itemIndex = inventory.findIndex((item) => item.id === itemId);
  if (itemIndex >= 0) {
    const nextInventory = [...inventory];
    const currentItem = nextInventory[itemIndex];
    nextInventory[itemIndex] = {
      ...currentItem,
      quantity: Math.max(1, (currentItem.quantity ?? 1) + quantity),
    };
    return nextInventory;
  }

  const template = THE_QUEST_ITEMS.find((item) => item.id === itemId);
  if (!template) {
    return inventory;
  }

  return [...inventory, { ...template, quantity: template.quantity ? template.quantity + quantity : quantity }];
}

function consumeInventoryItem(inventory: InventoryItem[], itemId: string, quantity: number) {
  if (quantity <= 0) {
    return inventory;
  }

  return inventory
    .map((item) => {
      if (item.id !== itemId) {
        return item;
      }

      return {
        ...item,
        quantity: Math.max(0, (item.quantity ?? 0) - quantity),
      };
    })
    .filter((item) => item.id !== itemId || item.category !== 'consumable' || (item.quantity ?? 0) > 0)
    .filter((item) => item.id !== itemId || item.category === 'consumable' || (item.quantity ?? 0) > 0);
}

function syncQuestCollectionProgress(quests: QuestProgressState[], inventory: InventoryItem[]) {
  return quests.map((quest) => {
    if (quest.completed || quest.progressType !== 'collect') {
      return quest;
    }

    const collectedCount = Math.min(quest.requiredCount, findInventoryQuantity(inventory, quest.turnInItemId ?? quest.target));
    return {
      ...quest,
      currentCount: collectedCount,
      readyToTurnIn: collectedCount >= quest.requiredCount,
    };
  });
}

function rollEnemyDrops(enemy: EnemyState) {
  return enemy.lootTable.reduce<Array<{ itemId: string; quantity: number }>>((drops, loot) => {
    if (Math.random() > loot.chance) {
      return drops;
    }

    const min = loot.minQuantity ?? 1;
    const max = loot.maxQuantity ?? min;
    const quantity = Math.max(min, Math.round(min + Math.random() * (max - min)));
    drops.push({ itemId: loot.itemId, quantity });
    return drops;
  }, []);
}

function formatLootLabel(drops: Array<{ itemId: string; quantity: number }>) {
  if (!drops.length) {
    return null;
  }

  return drops
    .map((drop) => {
      const item = THE_QUEST_ITEMS.find((entry) => entry.id === drop.itemId);
      return `${item?.name ?? drop.itemId} x${drop.quantity}`;
    })
    .join(', ');
}

export const useTheQuestStore = create<TheQuestStore>((set) => ({
  ...createInitialState(),
  setOverlayPanel: (overlayPanel) => set({ overlayPanel }),
  setJoystick: (joystick) => set({ joystick }),
  setAutoCombat: (autoCombat) => set({ autoCombat }),
  setActiveNpc: (activeNpcId) => set({ activeNpcId }),
  setSelectedInventoryId: (selectedInventoryId) => set({ selectedInventoryId }),
  setSelectedQuestId: (selectedQuestId) => set({ selectedQuestId }),
  setSkillCooldown: (skillId, readyAt) =>
    set((state) => ({
      skillCooldowns: {
        ...state.skillCooldowns,
        [skillId]: readyAt,
      },
    })),
  reduceSkillCooldowns: (time) =>
    set((state) => ({
      skillCooldowns: Object.fromEntries(
        Object.entries(state.skillCooldowns).map(([skillId, readyAt]) => [skillId, Math.max(0, readyAt <= time ? 0 : readyAt)]),
      ),
    })),
  setMinimap: (minimap) => set({ minimap }),
  setPlayerLocation: (playerLocation) => {
    persistPlayerLocation(playerLocation);
    set({ playerLocation });
  },
  setPerformance: (performance) => set({ performance }),
  setConsumableEffects: (updater) =>
    set((state) => ({
      consumableEffects: updater(state.consumableEffects),
    })),
  setCurrentTarget: (currentTargetId) =>
    set((state) => ({
      combat:
        state.combat.currentTargetId === currentTargetId
          ? state.combat
          : {
              ...state.combat,
              currentTargetId,
            },
    })),
  setActiveSkill: ({ skillId, label, endsAt }) =>
    set((state) => ({
      combat: {
        ...state.combat,
        activeSkillId: skillId,
        activeSkillLabel: label,
        activeSkillEndsAt: endsAt,
      },
    })),
  clearExpiredSkillState: (time) =>
    set((state) => {
      if (!state.combat.activeSkillId || state.combat.activeSkillEndsAt > time) {
        return {};
      }

      return {
        combat: {
          ...state.combat,
          activeSkillId: null,
          activeSkillLabel: null,
          activeSkillEndsAt: 0,
        },
      };
    }),
  registerComboHit: ({ time, damage, critical, chainStep }) =>
    set((state) => {
      const comboCount = state.combat.comboExpiresAt > time ? state.combat.comboCount + 1 : 1;
      return {
        combat: {
          ...state.combat,
          comboCount,
          comboExpiresAt: time + 1500,
          peakCombo: Math.max(state.combat.peakCombo, comboCount),
          chainStep,
          lastDamage: damage,
          lastHitCritical: critical,
          lastRewardLabel: null,
        },
      };
    }),
  clearExpiredCombo: (time) =>
    set((state) => {
      if (state.combat.comboExpiresAt > time || state.combat.comboCount === 0) {
        return {};
      }

      return {
        combat: {
          ...state.combat,
          comboCount: 0,
          comboExpiresAt: 0,
          chainStep: 0,
          lastHitCritical: false,
        },
      };
    }),
  applyDamageToEnemy: (enemyId, amount) =>
    set((state) => ({
      enemies: state.enemies.map((enemy) =>
        enemy.id === enemyId
          ? {
              ...enemy,
              hp: Math.max(0, enemy.hp - amount),
              alive: enemy.hp - amount > 0,
            }
          : enemy,
      ),
    })),
  useConsumable: (itemId) =>
    set((state) => {
      const now = Date.now();
      const inventory = state.inventory
        .map((item) => {
          if (item.id !== itemId || item.category !== 'consumable') {
            return item;
          }

          const nextQuantity = Math.max(0, (item.quantity ?? 0) - 1);
          return {
            ...item,
            quantity: nextQuantity,
          };
        })
        .filter((item) => item.category !== 'consumable' || (item.quantity ?? 0) > 0);

      const item = findInventoryItem(state.inventory, itemId);
      if (!item || item.category !== 'consumable') {
        return {};
      }

      let nextCombatText: CombatTextEntry | null = null;
      let nextLog = `${item.name} 사용 완료`;
      let nextConsumableEffects = state.consumableEffects;

      if ((item.healOverTimeTotal ?? 0) > 0) {
        const totalTicks = Math.max(1, item.healOverTimeTicks ?? 1);
        const healPerTick = Math.max(1, Math.round((item.healOverTimeTotal ?? 0) / totalTicks));
        const tickIntervalMs = Math.max(250, item.healOverTimeIntervalMs ?? 1000);
        const lastQueued = state.consumableEffects.queuedRecovery.at(-1);
        const nextTickAt = lastQueued
          ? lastQueued.nextTickAt + lastQueued.tickIntervalMs * Math.max(0, lastQueued.ticksRemaining)
          : now + tickIntervalMs;

        nextConsumableEffects = {
          ...state.consumableEffects,
          queuedRecovery: [
            ...state.consumableEffects.queuedRecovery,
            {
              itemId: item.id,
              label: item.name,
              icon: item.icon,
              healPerTick,
              ticksRemaining: totalTicks,
              tickIntervalMs,
              nextTickAt,
            },
          ],
        };
        nextCombatText = {
          id: `${itemId}-${now}`,
          value: `${healPerTick} HP x${totalTicks}`,
          color: '#7df0b3',
        };
        nextLog = `${item.name} 순차 회복 예약`;
      } else if ((item.autoHealMultiplier ?? 0) > 1) {
        nextConsumableEffects = {
          ...state.consumableEffects,
          autoHealMultiplier: item.autoHealMultiplier ?? 1,
          autoHealBoostUntil: now + (item.effectDurationMs ?? 0),
        };
        nextCombatText = {
          id: `${itemId}-${now}`,
          value: `AUTO x${item.autoHealMultiplier}`,
          color: '#9fd7ff',
        };
        nextLog = `${item.name} 적용 완료`;
      } else if ((item.moveSpeedMultiplier ?? 0) > 1) {
        nextConsumableEffects = {
          ...state.consumableEffects,
          moveSpeedMultiplier: item.moveSpeedMultiplier ?? 1,
          moveSpeedBoostUntil: now + (item.effectDurationMs ?? 0),
        };
        nextCombatText = {
          id: `${itemId}-${now}`,
          value: `SPD x${item.moveSpeedMultiplier}`,
          color: '#ffd576',
        };
        nextLog = `${item.name} 적용 완료`;
      }

      return {
        inventory,
        selectedInventoryId: inventory[0]?.id ?? null,
        combatText: nextCombatText,
        consumableEffects: nextConsumableEffects,
        lastCombatLog: nextLog,
      };
    }),
  equipItem: (itemId) =>
    set((state) => {
      const item = findInventoryItem(state.inventory, itemId);
      if (!item || !item.slot) {
        return {};
      }

      const equipment = {
        ...state.equipment,
        [item.slot]: item.id,
      };

      return {
        equipment,
        selectedInventoryId: item.id,
        vitals: applyEquipmentBonuses(state.vitals, state.inventory, equipment, state.equipment),
        lastCombatLog: `${item.name} 장착`,
      };
    }),
  unequipItem: (slot) =>
    set((state) => {
      const equipment = {
        ...state.equipment,
        [slot]: null,
      };

      const slotLabelMap: Record<EquipmentSlot, string> = {
        weapon: '무기',
        helmet: '투구',
        shoulders: '견갑',
        chest: '갑옷',
        belt: '벨트',
        gloves: '장갑',
        boots: '신발',
        ring: '반지',
        necklace: '목걸이',
        bracelet: '팔찌',
        artifact: '유물',
      };

      return {
        equipment,
        vitals: applyEquipmentBonuses(state.vitals, state.inventory, equipment, state.equipment),
        lastCombatLog: `${slotLabelMap[slot]} 해제`,
      };
    }),
  turnInQuest: (questId) =>
    set((state) => {
      const quest = state.quests.find((entry) => entry.id === questId);
      if (!quest) {
        return {};
      }

      if (quest.completed) {
        return {
          lastCombatLog: `${quest.title} 보상은 이미 수령했습니다.`,
        };
      }

      if (!quest.readyToTurnIn) {
        return {
          lastCombatLog: `${quest.title} 목표를 먼저 완료하세요.`,
        };
      }

      if (state.activeNpcId !== quest.turnInNpcId) {
        const npcName = THE_QUEST_NPCS.find((npc) => npc.id === quest.turnInNpcId)?.name ?? '담당 NPC';
        return {
          lastCombatLog: `${npcName}에게 가서 퀘스트를 보고하세요.`,
        };
      }

      let inventory = state.inventory;
      if (quest.turnInItemId) {
        const ownedCount = findInventoryQuantity(inventory, quest.turnInItemId);
        if (ownedCount < quest.requiredCount) {
          return {
            lastCombatLog: `${quest.title} 제출 재료가 부족합니다.`,
          };
        }
        inventory = consumeInventoryItem(inventory, quest.turnInItemId, quest.requiredCount);
      }

      if (quest.rewardItemId) {
        inventory = mergeInventoryDrop(inventory, quest.rewardItemId, 1);
      }

      const totalExp = state.vitals.exp + quest.rewardExp;
      const didLevelUp = totalExp >= state.vitals.expToNext;
      const vitals = {
        ...state.vitals,
        gold: state.vitals.gold + quest.rewardGold,
        exp: didLevelUp ? totalExp - state.vitals.expToNext : totalExp,
        expToNext: didLevelUp ? Math.round(state.vitals.expToNext * 1.2) : state.vitals.expToNext,
        level: didLevelUp ? state.vitals.level + 1 : state.vitals.level,
        maxHp: didLevelUp ? state.vitals.maxHp + 24 : state.vitals.maxHp,
        hp: didLevelUp ? state.vitals.maxHp + 24 : state.vitals.hp,
        maxMp: didLevelUp ? state.vitals.maxMp + 8 : state.vitals.maxMp,
        mp: didLevelUp ? state.vitals.maxMp + 8 : state.vitals.mp,
      };

      const quests = syncQuestCollectionProgress(
        state.quests.map((entry) =>
          entry.id === questId
            ? {
                ...entry,
                currentCount: entry.requiredCount,
                readyToTurnIn: false,
                completed: true,
              }
            : entry,
        ),
        inventory,
      );

      const rewardItemName = quest.rewardItemId ? findInventoryItem(THE_QUEST_ITEMS, quest.rewardItemId)?.name ?? null : null;

      return {
        vitals,
        inventory,
        quests,
        selectedQuestId: questId,
        selectedInventoryId: quest.rewardItemId ?? state.selectedInventoryId,
        combatText: {
          id: `quest-reward-${questId}-${Date.now()}`,
          value: `+${quest.rewardExp} EXP`,
          color: '#ffe27a',
        },
        combat: {
          ...state.combat,
          lastRewardLabel: `퀘스트 보상 · +${quest.rewardExp} EXP · +${quest.rewardGold} G`,
        },
        lastCombatLog: `${quest.title} 보고 완료${rewardItemName ? ` · ${rewardItemName} 획득` : ''}${didLevelUp ? ' · 레벨 업!' : ''}`,
      };
    }),
  rewardEnemyKill: (enemyId) =>
    set((state) => {
      const enemy = state.enemies.find((entry) => entry.id === enemyId);
      if (!enemy) {
        return {};
      }

      const drops = rollEnemyDrops(enemy);
      const inventory = drops.reduce((currentInventory, drop) => mergeInventoryDrop(currentInventory, drop.itemId, drop.quantity), state.inventory);
      const lootLabel = formatLootLabel(drops);

      const nextQuests = syncQuestCollectionProgress(
        state.quests.map((quest) => {
          if (quest.completed || quest.progressType !== 'kill' || quest.target !== 'slime') {
            return quest;
          }

          const nextCount = Math.min(quest.requiredCount, quest.currentCount + 1);
          return {
            ...quest,
            currentCount: nextCount,
            readyToTurnIn: nextCount >= quest.requiredCount,
          };
        }),
        inventory,
      );

      const leveledExp = state.vitals.exp + enemy.expReward;
      const didLevelUp = leveledExp >= state.vitals.expToNext;
      const nextVitals = {
        ...state.vitals,
        gold: state.vitals.gold + enemy.goldReward,
        exp: didLevelUp ? leveledExp - state.vitals.expToNext : leveledExp,
        expToNext: didLevelUp ? Math.round(state.vitals.expToNext * 1.2) : state.vitals.expToNext,
        level: didLevelUp ? state.vitals.level + 1 : state.vitals.level,
        maxHp: didLevelUp ? state.vitals.maxHp + 24 : state.vitals.maxHp,
        hp: didLevelUp ? state.vitals.maxHp + 24 : state.vitals.hp,
        maxMp: didLevelUp ? state.vitals.maxMp + 8 : state.vitals.maxMp,
        mp: didLevelUp ? state.vitals.maxMp + 8 : state.vitals.mp,
      };

      const activeQuest = nextQuests.find((quest) => quest.id === state.selectedQuestId) ?? nextQuests[0];

      return {
        vitals: nextVitals,
        inventory,
        quests: nextQuests,
        selectedQuestId: activeQuest?.id ?? null,
        combatText: {
          id: `${enemyId}-${Date.now()}`,
          value: `+${enemy.expReward} EXP`,
          color: '#ffe27a',
        },
        combat: {
          ...state.combat,
          lastLootLabel: lootLabel,
          lastRewardLabel: `+${enemy.expReward} EXP · +${enemy.goldReward} G`,
        },
        lastCombatLog: `${enemy.name} 처치${lootLabel ? ` · ${lootLabel}` : ''}${didLevelUp ? ' · 레벨 업!' : ''}`,
      };
    }),
  respawnEnemy: (enemyId) =>
    set((state) => {
      const stageEnemy = createStageEnemies(state.stage.current).find((enemy) => enemy.id === enemyId);
      if (!stageEnemy) {
        return {};
      }

      return {
        enemies: state.enemies.map((enemy) => (enemy.id === enemyId ? stageEnemy : enemy)),
        stage: {
          ...state.stage,
          enemyResetVersion: state.stage.enemyResetVersion + 1,
        },
      };
    }),
  advanceStage: () =>
    set((state) => {
      const nextStageCurrent = state.stage.current + 1;
      const nextEnemies = createStageEnemies(nextStageCurrent);
      const nextLocation = resolveTheQuestLocationMeta(DEFAULT_PLAYER_POSITION.x, DEFAULT_PLAYER_POSITION.y, THE_QUEST_NPCS);

      return {
        enemies: nextEnemies,
        combat: {
          ...state.combat,
          currentTargetId: null,
        },
        stage: {
          current: nextStageCurrent,
          enemyResetVersion: state.stage.enemyResetVersion + 1,
          transitionVersion: state.stage.transitionVersion + 1,
        },
        minimap: {
          ...state.minimap,
          player: { x: nextLocation.x, y: nextLocation.y },
          enemies: nextEnemies.map(({ id, x, y, alive }) => ({ id, x, y, alive })),
        },
        playerLocation: nextLocation,
        lastCombatLog: `스테이지 ${nextStageCurrent} 진입 · 포탈 이동과 함께 몬스터가 모두 초기화되었습니다.`,
      };
    }),
  setEnemySnapshot: (enemies) => set({ enemies }),
  setCombatText: (combatText) => set({ combatText }),
  setLastCombatLog: (lastCombatLog) => set({ lastCombatLog }),
  setVitals: (updater) => set((state) => ({ vitals: updater(state.vitals) })),
  requestManualSkillCast: (skillId) =>
    set(() => ({
      manualSkillCastCommand: {
        id: Date.now() + Math.floor(Math.random() * 1000),
        skillId,
        requestedAt: Date.now(),
      },
    })),
  consumeManualSkillCast: (commandId) =>
    set((state) =>
      state.manualSkillCastCommand?.id === commandId
        ? {
            manualSkillCastCommand: null,
          }
        : {},
    ),
  reset: () => set(createInitialState()),
}));

useTheQuestStore.subscribe((state) => {
  schedulePersistQuestState(state);
});
