import { create } from "zustand"; import type { PlayerSession } from "../app/types"; import { requestOnlineJson } from "../app/onlineAccountClient"; import { companionSpecReadinessFromCache, normalizeCompanionSpecReadinessPolicy, type CompanionSpecReadinessPolicy, type PublishedCompanionSpecReadinessPolicy, } from "./companionSpecReadiness"; interface CompanionSpecReadinessState extends PublishedCompanionSpecReadinessPolicy { readonly draft: CompanionSpecReadinessPolicy; readonly loading: boolean; readonly saving: boolean; readonly dirty: boolean; readonly error: string | null; refresh: (session?: PlayerSession | null) => Promise; setStatus: (specializationId: string, status: "ready" | "beta" | "needs-testing") => void; resetDraft: () => void; publish: (session?: PlayerSession | null) => Promise; } const CACHE_KEY = "healer-man.companion-spec-readiness.v1"; const LOCAL_GUI = import.meta.env.VITE_LOCAL_BALANCE_LAB_GUI === "true"; const LOCAL_PATH = "/api/local/balance-lab/companion-spec-readiness"; function cached(): PublishedCompanionSpecReadinessPolicy { return companionSpecReadinessFromCache(typeof window === "undefined" ? null : window.localStorage.getItem(CACHE_KEY)); } function cache(payload: PublishedCompanionSpecReadinessPolicy): void { try { window.localStorage.setItem(CACHE_KEY, JSON.stringify(payload)); } catch { /* optional offline cache */ } } async function request(pathname: string, init: RequestInit, session?: PlayerSession | null): Promise { if (!LOCAL_GUI) return requestOnlineJson(pathname, init, session?.accessToken); const headers = new Headers(init.headers); headers.set("Accept", "application/json"); if (init.body !== undefined) headers.set("Content-Type", "application/json"); const response = await fetch(LOCAL_PATH, { ...init, headers }); const body = await response.json().catch(() => ({})) as T & { error?: string }; if (!response.ok) throw new Error(body.error ?? `Local readiness request failed (${response.status}).`); return body; } const initial = cached(); export const useCompanionSpecReadinessStore = create((set, get) => ({ ...initial, draft: initial.config, loading: false, saving: false, dirty: false, error: null, refresh: async (session = null) => { set({ loading: true, error: null }); try { const gm = Boolean(session?.accessToken && session.roles?.includes("gm")); const pathname = gm ? "/api/gm/companion-spec-readiness" : "/api/companion-spec-readiness"; const payload = await request(pathname, {}, session); const normalized = { ...payload, config: normalizeCompanionSpecReadinessPolicy(payload.config) }; cache(normalized); set({ ...normalized, draft: normalized.config, loading: false, dirty: false }); } catch (error) { set({ loading: false, error: error instanceof Error ? error.message : "Could not load companion readiness." }); } }, setStatus: (specializationId, status) => set((state) => ({ draft: { schemaVersion: 1, statuses: { ...state.draft.statuses, [specializationId]: status }, }, dirty: true, error: null, })), resetDraft: () => set((state) => ({ draft: state.config, dirty: false, error: null })), publish: async (session = null) => { if (!LOCAL_GUI && (!session?.accessToken || !session.roles?.includes("gm"))) { set({ error: "Game Master access is required." }); return false; } set({ saving: true, error: null }); try { const draft = normalizeCompanionSpecReadinessPolicy(get().draft); const payload = await request( "/api/gm/companion-spec-readiness", { method: "PUT", body: JSON.stringify({ expectedRevision: get().revision, config: draft }) }, session, ); const normalized = { ...payload, config: normalizeCompanionSpecReadinessPolicy(payload.config) }; cache(normalized); set({ ...normalized, draft: normalized.config, saving: false, dirty: false }); return true; } catch (error) { set({ saving: false, error: error instanceof Error ? error.message : "Could not publish companion readiness." }); return false; } }, })); export function currentCompanionSpecReadinessPolicy(): PublishedCompanionSpecReadinessPolicy { const state = useCompanionSpecReadinessStore.getState(); return { revision: state.revision, updatedAt: state.updatedAt, updatedBy: state.updatedBy, config: state.config }; }