/**
 * Deployment Status Store
 *
 * External store (useSyncExternalStore-compatible) that holds the shared
 * deployment state keyed by targetId. Multiple components subscribed to
 * the same targetId see the same state — fixes the node/drawer-out-of-sync
 * bug and makes page-refresh hydration trivial.
 *
 * Mutation paths (deploy/stop/poll) go through server actions that wrap
 * use cases — this module stores state only and never contains business
 * logic.
 */
export const EMPTY_ENTRY = {
    status: null,
    url: null,
    targetType: null,
    hydrated: false,
    deployLoading: false,
    stopLoading: false,
    deployError: null,
};
export class DeploymentStatusStore {
    entries = new Map();
    listeners = new Map();
    globalListeners = new Set();
    /**
     * Set true after the first SSR seed runs. Because `ListDeploymentsUseCase`
     * returns the full universe of currently-tracked deployments, any
     * targetId NOT present in that list is definitively idle — there is no
     * need to fire a per-node `getDeploymentStatus` server action on mount.
     * Without this flag, every canvas node (most of which have no
     * deployment) was triggering one POST /control-center on initial render,
     * producing the burst of 30+ server-action calls observed on dashboard
     * load.
     */
    fullyHydrated = false;
    /** Has the store been seeded at least once from SSR? */
    isFullyHydrated() {
        return this.fullyHydrated;
    }
    /** Seed the store from SSR data (ListDeploymentsUseCase output). */
    hydrate(entries) {
        // Mark every SSR entry as hydrated and merge into the map.
        const seen = new Set();
        for (const entry of entries) {
            seen.add(entry.targetId);
            this.update(entry.targetId, {
                status: entry.state,
                url: entry.url,
                targetType: entry.targetType,
                hydrated: true,
            });
        }
        // Any previously tracked entries missing from the SSR list are stale —
        // mark them hydrated with null state so subscribers learn their
        // deployment is gone.
        for (const [targetId, existing] of this.entries) {
            if (!seen.has(targetId) && existing.hydrated && existing.status !== null) {
                this.update(targetId, { status: null, url: null });
            }
            else if (!seen.has(targetId) && !existing.hydrated) {
                this.update(targetId, { hydrated: true });
            }
        }
        this.fullyHydrated = true;
    }
    getEntry(targetId) {
        return this.entries.get(targetId) ?? EMPTY_ENTRY;
    }
    update(targetId, patch) {
        const current = this.entries.get(targetId) ?? EMPTY_ENTRY;
        const next = { ...current, ...patch };
        this.entries.set(targetId, next);
        this.notify(targetId);
    }
    /** Apply a status snapshot (from a server action result) to a targetId. */
    setStatus(targetId, status) {
        if (status === null) {
            this.update(targetId, { status: null, url: null, hydrated: true });
            return;
        }
        this.update(targetId, {
            status: status.state,
            url: status.url,
            hydrated: true,
        });
    }
    subscribe(targetId, listener) {
        let set = this.listeners.get(targetId);
        if (!set) {
            set = new Set();
            this.listeners.set(targetId, set);
        }
        set.add(listener);
        return () => {
            set?.delete(listener);
            if (set?.size === 0)
                this.listeners.delete(targetId);
        };
    }
    subscribeAll(listener) {
        this.globalListeners.add(listener);
        return () => {
            this.globalListeners.delete(listener);
        };
    }
    notify(targetId) {
        const set = this.listeners.get(targetId);
        if (set)
            for (const l of set)
                l();
        for (const l of this.globalListeners)
            l();
    }
}
