import { create } from "zustand"; import type { PlayerSession } from "../app/types"; import { requestOnlineJson } from "../app/onlineAccountClient"; import { DEFAULT_MANASTORM_ADMIN_CONFIG, LEGACY_MANASTORM_ADMIN_CACHE_KEY, MANASTORM_ADMIN_CACHE_KEY, normalizeManastormAdminConfig, publicManastormAdminConfig, resolvedBossAdminCatalog, type ManastormAdminConfig, type PublishedManastormAdminConfig, } from "./manastormAdminConfig"; interface CachedPayload extends PublishedManastormAdminConfig {} function readCache(): CachedPayload { try { const raw = typeof window !== "undefined" ? window.localStorage.getItem(MANASTORM_ADMIN_CACHE_KEY) ?? window.localStorage.getItem(LEGACY_MANASTORM_ADMIN_CACHE_KEY) : null; const parsed = raw ? JSON.parse(raw) as Partial : null; if (!parsed || !Number.isInteger(parsed.revision) || (parsed.revision ?? -1) < 0) throw new Error("missing cache"); return { revision: parsed.revision!, updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : null, config: publicManastormAdminConfig(normalizeManastormAdminConfig(parsed.config)), }; } catch { return { revision: 0, updatedAt: null, config: DEFAULT_MANASTORM_ADMIN_CONFIG }; } } function writeCache(payload: PublishedManastormAdminConfig): void { try { window.localStorage.setItem(MANASTORM_ADMIN_CACHE_KEY, JSON.stringify({ ...payload, config: publicManastormAdminConfig(payload.config), })); } catch { // Private browsing and storage quotas must not prevent play. } } export interface ManastormAdminState extends PublishedManastormAdminConfig { readonly draft: ManastormAdminConfig; readonly loading: boolean; readonly saving: boolean; readonly error: string | null; readonly editingBossKey: string | null; readonly dirty: boolean; refresh: (session?: PlayerSession | null) => Promise; replaceDraft: (config: ManastormAdminConfig) => void; resetDraft: () => void; publish: (session: PlayerSession | null) => Promise; setEditingBossKey: (key: string | null) => void; } const cached = readCache(); export const useManastormAdminStore = create((set, get) => ({ ...cached, draft: cached.config, loading: false, saving: false, error: null, editingBossKey: null, dirty: false, refresh: async (session = null) => { set({ loading: true, error: null }); try { const gm = Boolean(session?.accessToken && session.roles?.includes("gm")); const payload = await requestOnlineJson( gm ? "/api/gm/manastorm-config" : "/api/manastorm-config", {}, gm ? session?.accessToken : undefined, ); const normalized = { ...payload, config: normalizeManastormAdminConfig(payload.config) }; writeCache(normalized); set({ ...normalized, draft: normalized.config, dirty: false, loading: false }); } catch (error) { set({ loading: false, error: error instanceof Error ? error.message : "Could not refresh Manastorm configuration." }); } }, replaceDraft: (draft) => set({ draft: normalizeManastormAdminConfig(draft), dirty: true, error: null }), resetDraft: () => set((state) => ({ draft: state.config, dirty: false, error: null })), publish: async (session) => { if (!session?.accessToken || !session.roles?.includes("gm")) { set({ error: "Game Master access is required." }); return false; } const invalidReadyBoss = resolvedBossAdminCatalog(get().draft).find((entry) => entry.status === "ready" && !entry.runnable); if (invalidReadyBoss) { set({ error: `${invalidReadyBoss.name} cannot be published as Ready: ${invalidReadyBoss.validationIssues.join(" ")}` }); return false; } set({ saving: true, error: null }); try { const payload = await requestOnlineJson("/api/gm/manastorm-config", { method: "PUT", body: JSON.stringify({ expectedRevision: get().revision, config: get().draft }), }, session.accessToken); const normalized = { ...payload, config: normalizeManastormAdminConfig(payload.config) }; writeCache(normalized); set({ ...normalized, draft: normalized.config, dirty: false, saving: false }); return true; } catch (error) { set({ saving: false, error: error instanceof Error ? error.message : "Could not publish Manastorm configuration." }); return false; } }, setEditingBossKey: (editingBossKey) => set({ editingBossKey }), }));