import { create } from "zustand";
import { DEFAULT_SETTINGS, normalizeHunterName } from "./data";
import { SaveRepository } from "./saveRepository";
import { AccountRepository, type AccountResult } from "./accountRepository";
import { onlineRepository, type OnlineSaveSlot } from "./onlineRepository";
import type { AppScreen, GameModeId, GameSettings, HunterSave, SaveSlotId, SaveSlotState } from "./types";
import type { AbilityId, BossId, HealerClassId, InventoryItem, RunBuffId } from "../game/types";
import { RUN_BUFF_ORDER, RUN_BUFFS } from "../game/roguelike";
import { upgradeGearSlot, type GearOwnerId, type GearSlotId } from "../game/progression/gear";
import {
  equipActiveInfusion,
  equipPassiveInfusion,
  infusionsForOwner,
} from "../game/progression/infusions";
import { normalizeDifficultySlug, rollBossReward, type BossRewardAward, type DifficultySlug } from "../game/progression/loot";
import { highestEndlessBossKillsAfterDefeat, highestRoguelikeRoundAfterDefeat } from "../game/progression/hunterStats";

const repository = new SaveRepository();
const accounts = new AccountRepository();
const SETTINGS_KEY = "i-want-to-heal:settings:v1";
const onlineSaveQueues = new Map<SaveSlotId, Promise<HunterSave>>();

function writeServerSaveSerially(save: HunterSave): Promise<HunterSave> {
  const previous = onlineSaveQueues.get(save.slotId);
  const next = (previous ? previous.catch(() => save) : Promise.resolve(save))
    .then(() => onlineRepository.writeSave(save));
  onlineSaveQueues.set(save.slotId, next);
  void next.finally(() => {
    if (onlineSaveQueues.get(save.slotId) === next) onlineSaveQueues.delete(save.slotId);
  }).catch(() => undefined);
  return next;
}

function loadSettings(): GameSettings {
  try {
    const saved = localStorage.getItem(SETTINGS_KEY);
    return saved ? { ...DEFAULT_SETTINGS, ...JSON.parse(saved) } : DEFAULT_SETTINGS;
  } catch {
    return DEFAULT_SETTINGS;
  }
}

function persistSettings(settings: GameSettings) {
  try {
    localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
  } catch {
    // Settings remain active for this session when storage is unavailable.
  }
}

function accountNotice(result: Extract<AccountResult, { ok: false }>, action: "sign-in" | "create") {
  switch (result.reason) {
    case "missing-credentials": return "Enter both username and password.";
    case "account-exists": return "Account already exists. Sign in with its password.";
    case "invalid-password": return "Username or password is incorrect.";
    case "server-unavailable": return "Online server is unreachable. Continue offline or try again.";
    case "invalid-request": return result.message ?? (action === "create" ? "Account could not be created." : "Sign-in failed.");
  }
}

function refreshLocalSlots(current: readonly SaveSlotState[]): SaveSlotState[] {
  return repository.listLocal().map((slot) => ({
    ...slot,
    online: current.find((candidate) => candidate.id === slot.id)?.online ?? null,
  }));
}

function mergeServerSlots(serverSlots: readonly OnlineSaveSlot[]): SaveSlotState[] {
  return repository.listLocal().map((slot) => ({
    ...slot,
    online: serverSlots.find((candidate) => candidate.slotId === slot.id)?.save ?? null,
  }));
}

function replaceOnlineSlot(current: readonly SaveSlotState[], save: HunterSave): SaveSlotState[] {
  return refreshLocalSlots(current).map((slot) => slot.id === save.slotId ? { ...slot, online: save } : slot);
}

export interface FrontendState {
  screen: AppScreen;
  accountId: string | null;
  slots: SaveSlotState[];
  selectedSlotId: SaveSlotId;
  activeSlotId: SaveSlotId | null;
  selectedMode: GameModeId;
  selectedBossId: BossId;
  selectedDifficultySlug: DifficultySlug;
  selectedGearOwnerId: GearOwnerId;
  selectedGearSlotId: GearSlotId;
  gearWorkshopMode: "upgrade" | "infusion";
  selectedInfusionId: string;
  selectedPassiveAbilityId: AbilityId;
  selectedPassiveInfusionId: RunBuffId;
  recentRewards: BossRewardAward[];
  settings: GameSettings;
  notice: string;
  restoreSession: () => Promise<boolean>;
  signIn: (username: string, password: string) => Promise<boolean>;
  createAccount: (username: string, password: string) => Promise<boolean>;
  continueOffline: () => void;
  signOut: () => void;
  navigate: (screen: AppScreen) => void;
  selectSlot: (slotId: SaveSlotId) => void;
  createSlot: (slotId: SaveSlotId, hunterName: string) => boolean;
  playSlot: (slotId: SaveSlotId) => void;
  deleteSlot: (slotId: SaveSlotId) => void;
  copySlot: (sourceId: SaveSlotId, targetId: SaveSlotId) => void;
  uploadSlot: (slotId: SaveSlotId) => Promise<void>;
  downloadSlot: (slotId: SaveSlotId) => Promise<void>;
  selectMode: (mode: GameModeId) => void;
  selectBoss: (bossId: BossId) => void;
  selectDifficulty: (difficultySlug: DifficultySlug) => void;
  selectGearOwner: (ownerId: GearOwnerId) => void;
  selectGearSlot: (slotId: GearSlotId) => void;
  selectGearWorkshopMode: (mode: "upgrade" | "infusion") => void;
  selectInfusion: (infusionId: string) => void;
  selectPassiveAbility: (abilityId: AbilityId) => void;
  selectPassiveInfusion: (passiveId: RunBuffId) => void;
  upgradeSelectedGear: () => boolean;
  equipSelectedInfusion: () => boolean;
  equipPassiveInfusion: (passiveId: RunBuffId) => boolean;
  selectHealerClass: (classId: HealerClassId) => void;
  updateActiveHealerInventory: (inventory: InventoryItem[]) => void;
  updateSetting: <K extends keyof GameSettings>(key: K, value: GameSettings[K]) => void;
  touchActiveSave: () => void;
  recordBossVictory: (bossId: BossId, difficultySlug: DifficultySlug) => BossRewardAward | null;
  recordRoguelikeDefeat: (round: number) => void;
  recordRogueTrialsEndlessDefeat: (bossKills: number) => void;
  clearRecentRewards: () => void;
  clearNotice: () => void;
}

function activeSave(slots: SaveSlotState[], activeSlotId: SaveSlotId | null): HunterSave | null {
  return slots.find((slot) => slot.id === activeSlotId)?.local ?? null;
}

export const useFrontendStore = create<FrontendState>((set, get) => ({
  screen: "login",
  accountId: null,
  slots: repository.listLocal(),
  selectedSlotId: 1,
  activeSlotId: null,
  selectedMode: "roguelike-pve",
  selectedBossId: "bulldrome",
  selectedDifficultySlug: "initiate",
  selectedGearOwnerId: "priest",
  selectedGearSlotId: "weapon",
  gearWorkshopMode: "upgrade",
  selectedInfusionId: infusionsForOwner("priest")[0].id,
  selectedPassiveAbilityId: "mend",
  selectedPassiveInfusionId: "mend-echo",
  recentRewards: [],
  settings: loadSettings(),
  notice: "",

  restoreSession: async () => {
    try {
      const account = await accounts.session();
      if (!account) return false;
      const serverSlots = await onlineRepository.listSaves();
      set({ accountId: account.username, slots: mergeServerSlots(serverSlots), screen: "saves", notice: `Online session restored for ${account.username}.` });
      return true;
    } catch {
      return false;
    }
  },

  signIn: async (username, password) => {
    const result = await accounts.authenticate(username, password);
    if (!result.ok) {
      set({ notice: accountNotice(result, "sign-in") });
      return false;
    }
    try {
      const serverSlots = await onlineRepository.listSaves();
      set({ accountId: result.username, slots: mergeServerSlots(serverSlots), screen: "saves", notice: `Online sync connected as ${result.username}.` });
      return true;
    } catch {
      set({ notice: "Signed in, but server saves could not be loaded." });
      return false;
    }
  },
  createAccount: async (username, password) => {
    const result = await accounts.create(username, password);
    if (!result.ok) {
      set({ notice: accountNotice(result, "create") });
      return false;
    }
    try {
      const serverSlots = await onlineRepository.listSaves();
      set({ accountId: result.username, slots: mergeServerSlots(serverSlots), screen: "saves", notice: `Account created. Online sync connected as ${result.username}.` });
      return true;
    } catch {
      set({ notice: "Account created, but server saves could not be loaded." });
      return false;
    }
  },
  continueOffline: () => set({ accountId: null, slots: repository.listLocal(), screen: "saves", notice: "Offline saves ready." }),
  signOut: () => {
    void accounts.logout();
    set({ accountId: null, slots: repository.listLocal(), activeSlotId: null, screen: "login", notice: "Signed out. Offline saves remain on this device." });
  },
  navigate: (screen) => set({ screen, notice: "" }),
  selectSlot: (selectedSlotId) => set({ selectedSlotId, notice: "" }),
  createSlot: (slotId, rawHunterName) => {
    const hunterName = normalizeHunterName(rawHunterName);
    if (!hunterName) {
      set({ notice: "Enter a hunter name before creating the save." });
      return false;
    }
    repository.create(slotId, hunterName);
    set((state) => ({ slots: refreshLocalSlots(state.slots), selectedSlotId: slotId, notice: `${hunterName} created in offline slot ${slotId}.` }));
    return true;
  },
  playSlot: (slotId) => {
    const local = repository.touch(slotId);
    if (!local) return;
    set((state) => ({ activeSlotId: slotId, selectedSlotId: slotId, slots: refreshLocalSlots(state.slots), screen: "home", notice: "Save loaded." }));
  },
  deleteSlot: (slotId) => {
    repository.deleteLocal(slotId);
    set((state) => ({
      slots: refreshLocalSlots(state.slots),
      activeSlotId: state.activeSlotId === slotId ? null : state.activeSlotId,
      notice: `Local slot ${slotId} deleted. Online copy preserved.`,
    }));
  },
  copySlot: (sourceId, targetId) => {
    const copy = repository.copyLocal(sourceId, targetId);
    if (!copy) return;
    set((state) => ({ slots: refreshLocalSlots(state.slots), selectedSlotId: targetId, notice: `Slot ${sourceId} copied to slot ${targetId}.` }));
  },
  uploadSlot: async (slotId) => {
    const { accountId } = get();
    if (!accountId) return set({ notice: "Sign in before syncing online." });
    const local = repository.listLocal().find((slot) => slot.id === slotId)?.local;
    if (!local) return set({ notice: "No offline save to sync." });
    try {
      const uploaded = await writeServerSaveSerially(local);
      if (get().accountId === accountId) {
        set((state) => ({ slots: replaceOnlineSlot(state.slots, uploaded), notice: `Slot ${slotId} synced to TrueNAS.` }));
      }
    } catch (error) {
      set({ notice: error instanceof Error ? error.message : "Save upload failed." });
    }
  },
  downloadSlot: async (slotId) => {
    const { accountId } = get();
    if (!accountId) return set({ notice: "Sign in before downloading an online save." });
    try {
      await onlineSaveQueues.get(slotId)?.catch(() => undefined);
      const serverSave = await onlineRepository.readSave(slotId);
      if (!serverSave) return set({ notice: "No online version exists for this slot." });
      const downloaded = repository.replaceLocal(serverSave);
      if (get().accountId === accountId) {
        set((state) => ({ slots: replaceOnlineSlot(state.slots, downloaded), notice: `Slot ${slotId} downloaded from TrueNAS.` }));
      }
    } catch (error) {
      set({ notice: error instanceof Error ? error.message : "Save download failed." });
    }
  },
  selectMode: (selectedMode) => set({ selectedMode, screen: "mode", notice: "" }),
  selectBoss: (selectedBossId) => set({ selectedBossId, notice: "" }),
  selectDifficulty: (selectedDifficultySlug) => set({ selectedDifficultySlug: normalizeDifficultySlug(selectedDifficultySlug), notice: "" }),
  selectGearOwner: (selectedGearOwnerId) => set({
    selectedGearOwnerId,
    selectedInfusionId: infusionsForOwner(selectedGearOwnerId)[0].id,
    notice: "",
  }),
  selectGearSlot: (selectedGearSlotId) => set({ selectedGearSlotId, notice: "" }),
  selectGearWorkshopMode: (gearWorkshopMode) => set({ gearWorkshopMode, notice: "" }),
  selectInfusion: (selectedInfusionId) => set({ selectedInfusionId, notice: "" }),
  selectPassiveAbility: (selectedPassiveAbilityId) => {
    const selectedPassiveInfusionId = RUN_BUFF_ORDER.find((id) => RUN_BUFFS[id].abilityId === selectedPassiveAbilityId) ?? "mend-echo";
    set({ selectedPassiveAbilityId, selectedPassiveInfusionId, notice: "" });
  },
  selectPassiveInfusion: (selectedPassiveInfusionId) => set({
    selectedPassiveAbilityId: RUN_BUFFS[selectedPassiveInfusionId].abilityId,
    selectedPassiveInfusionId,
    notice: "",
  }),
  upgradeSelectedGear: () => {
    const { activeSlotId, accountId, selectedGearOwnerId, selectedGearSlotId } = get();
    if (!activeSlotId) return false;
    let message = "Gear upgrade failed.";
    let upgraded = false;
    repository.updateLocal(activeSlotId, (save) => {
      try {
        const result = upgradeGearSlot(save.gearProgress, save.materials, selectedGearOwnerId, selectedGearSlotId);
        upgraded = true;
        message = `${selectedGearOwnerId} ${selectedGearSlotId} upgraded to +${result.gearProgress[selectedGearOwnerId].slots[selectedGearSlotId].level}.`;
        return { ...save, gearProgress: result.gearProgress, materials: result.inventory };
      } catch (error) {
        message = error instanceof Error ? error.message : message;
        return save;
      }
    });
    set((state) => ({ slots: refreshLocalSlots(state.slots), notice: message }));
    return upgraded;
  },
  equipSelectedInfusion: () => {
    const { activeSlotId, accountId, selectedGearOwnerId, selectedGearSlotId, selectedInfusionId } = get();
    if (!activeSlotId) return false;
    let message = "Infusion failed.";
    let equipped = false;
    repository.updateLocal(activeSlotId, (save) => {
      try {
        const wasEquipped = save.gearProgress[selectedGearOwnerId].infusionAbilityId === selectedInfusionId;
        const result = equipActiveInfusion(save.gearProgress, save.materials, selectedGearOwnerId, selectedGearSlotId, selectedInfusionId);
        equipped = true;
        message = wasEquipped ? "Infusion already equipped." : `${selectedGearOwnerId} infusion equipped.`;
        return { ...save, gearProgress: result.gearProgress, materials: result.inventory };
      } catch (error) {
        message = error instanceof Error ? error.message : message;
        return save;
      }
    });
    set((state) => ({ slots: refreshLocalSlots(state.slots), notice: message }));
    return equipped;
  },
  equipPassiveInfusion: (passiveId) => {
    const { activeSlotId, accountId, selectedGearOwnerId } = get();
    if (!activeSlotId) return false;
    let message = "Passive infusion failed.";
    let equipped = false;
    repository.updateLocal(activeSlotId, (save) => {
      try {
        const gearProgress = equipPassiveInfusion(save.gearProgress, selectedGearOwnerId, passiveId);
        equipped = true;
        message = "Passive infusion equipped. Applies next encounter.";
        return { ...save, gearProgress };
      } catch (error) {
        message = error instanceof Error ? error.message : message;
        return save;
      }
    });
    set((state) => ({ slots: refreshLocalSlots(state.slots), notice: message }));
    return equipped;
  },
  selectHealerClass: (classId) => {
    const { activeSlotId, accountId } = get();
    if (!activeSlotId) return;
    const updated = repository.updateLocal(activeSlotId, (save) => ({ ...save, activeClassId: classId }));
    if (!updated) return;
    set({
      slots: refreshLocalSlots(get().slots),
      selectedGearOwnerId: classId,
      selectedInfusionId: infusionsForOwner(classId)[0].id,
      notice: `${updated.healers[classId].level > 1 ? "Level " + updated.healers[classId].level + " " : ""}${classId[0].toUpperCase() + classId.slice(1)} selected.`,
    });
  },
  updateActiveHealerInventory: (inventory) => {
    const { activeSlotId, accountId } = get();
    if (!activeSlotId) return;
    repository.updateLocal(activeSlotId, (save) => ({
      ...save,
      healers: {
        ...save.healers,
        [save.activeClassId]: { ...save.healers[save.activeClassId], inventory: structuredClone(inventory) },
      },
    }));
    set((state) => ({ slots: refreshLocalSlots(state.slots) }));
  },
  updateSetting: (key, value) => set((state) => {
    const settings = { ...state.settings, [key]: value };
    persistSettings(settings);
    return { settings, notice: "Settings saved offline." };
  }),
  touchActiveSave: () => {
    const { activeSlotId, accountId } = get();
    if (!activeSlotId) return;
    repository.touch(activeSlotId);
    set((state) => ({ slots: refreshLocalSlots(state.slots) }));
  },
  recordBossVictory: (bossId, difficultySlug) => {
    const { activeSlotId } = get();
    if (!activeSlotId) return null;
    let awarded: BossRewardAward | null = null;
    repository.updateLocal(activeSlotId, (save) => {
      const reward = rollBossReward(bossId, difficultySlug, save.materials, save.collectionLog);
      awarded = reward.award;
      const bossKills = { ...save.stats.bossKills, [bossId]: (save.stats.bossKills[bossId] ?? 0) + 1 };
      return {
        ...save,
        stats: { ...save.stats, totalBossKills: save.stats.totalBossKills + 1, flawlessClears: save.stats.flawlessClears + 1, bossKills },
        materials: reward.inventory,
        collectionLog: reward.collectionLog,
      };
    });
    set((state) => ({
      slots: refreshLocalSlots(state.slots),
      recentRewards: awarded ? [...state.recentRewards, awarded].slice(-12) : state.recentRewards,
      notice: awarded ? `${awarded.drop.name} x${awarded.quantity} saved.` : "Boss clear saved.",
    }));
    return awarded;
  },
  recordRoguelikeDefeat: (round) => {
    const { activeSlotId } = get();
    if (!activeSlotId) return;
    const updated = repository.updateLocal(activeSlotId, (save) => ({
      ...save,
      stats: {
        ...save.stats,
        highestRoguelikeRound: highestRoguelikeRoundAfterDefeat(save.stats.highestRoguelikeRound, round),
      },
    }));
    if (!updated) return;
    set((state) => ({ slots: refreshLocalSlots(state.slots) }));
  },
  recordRogueTrialsEndlessDefeat: (bossKills) => {
    const { activeSlotId } = get();
    if (!activeSlotId) return;
    const updated = repository.updateLocal(activeSlotId, (save) => ({
      ...save,
      stats: {
        ...save.stats,
        highestRogueTrialsEndlessKills: highestEndlessBossKillsAfterDefeat(save.stats.highestRogueTrialsEndlessKills, bossKills),
      },
    }));
    if (!updated) return;
    set((state) => ({ slots: refreshLocalSlots(state.slots) }));
  },
  clearRecentRewards: () => set({ recentRewards: [] }),
  clearNotice: () => set({ notice: "" }),
}));

export type FrontendSnapshot = Omit<FrontendState,
  | "restoreSession"
  | "signIn"
  | "createAccount"
  | "continueOffline"
  | "signOut"
  | "navigate"
  | "selectSlot"
  | "createSlot"
  | "playSlot"
  | "deleteSlot"
  | "copySlot"
  | "uploadSlot"
  | "downloadSlot"
  | "selectMode"
  | "selectBoss"
  | "selectDifficulty"
  | "selectGearOwner"
  | "selectGearSlot"
  | "selectGearWorkshopMode"
  | "selectInfusion"
  | "selectPassiveAbility"
  | "selectPassiveInfusion"
  | "upgradeSelectedGear"
  | "equipSelectedInfusion"
  | "equipPassiveInfusion"
  | "selectHealerClass"
  | "updateActiveHealerInventory"
  | "updateSetting"
  | "touchActiveSave"
  | "recordBossVictory"
  | "recordRoguelikeDefeat"
  | "recordRogueTrialsEndlessDefeat"
  | "clearRecentRewards"
  | "clearNotice"
>;

export function getFrontendSnapshot(): FrontendSnapshot {
  const {
    restoreSession: _restoreSession,
    signIn: _signIn,
    createAccount: _createAccount,
    continueOffline: _continueOffline,
    signOut: _signOut,
    navigate: _navigate,
    selectSlot: _selectSlot,
    createSlot: _createSlot,
    playSlot: _playSlot,
    deleteSlot: _deleteSlot,
    copySlot: _copySlot,
    uploadSlot: _uploadSlot,
    downloadSlot: _downloadSlot,
    selectMode: _selectMode,
    selectBoss: _selectBoss,
    selectDifficulty: _selectDifficulty,
    selectGearOwner: _selectGearOwner,
    selectGearSlot: _selectGearSlot,
    selectGearWorkshopMode: _selectGearWorkshopMode,
    selectInfusion: _selectInfusion,
    selectPassiveAbility: _selectPassiveAbility,
    selectPassiveInfusion: _selectPassiveInfusion,
    upgradeSelectedGear: _upgradeSelectedGear,
    equipSelectedInfusion: _equipSelectedInfusion,
    equipPassiveInfusion: _equipPassiveInfusion,
    selectHealerClass: _selectHealerClass,
    updateActiveHealerInventory: _updateActiveHealerInventory,
    updateSetting: _updateSetting,
    touchActiveSave: _touchActiveSave,
    recordBossVictory: _recordBossVictory,
    recordRoguelikeDefeat: _recordRoguelikeDefeat,
    recordRogueTrialsEndlessDefeat: _recordRogueTrialsEndlessDefeat,
    clearRecentRewards: _clearRecentRewards,
    clearNotice: _clearNotice,
    ...snapshot
  } = useFrontendStore.getState();
  return snapshot;
}

export function useActiveHunter(): HunterSave | null {
  return useFrontendStore((state) => activeSave(state.slots, state.activeSlotId));
}
