import { WebsocketProvider } from 'y-websocket';
import { IndexeddbPersistence } from 'y-indexeddb';
import * as Y from 'yjs';
/**
 * Open a collaboration session for `documentId`. Must be called in a browser
 * context (jsdom is fine for tests when a fake provider is injected).
 *
 * The returned `ydoc` is the document the editor binds to via TipTap's
 * `Collaboration` extension; `awareness` carries cursor + presence state.
 * Always call `destroy()` on unmount to evict the local awareness state and
 * close the socket.
 */
export function createCollaborationProvider(opts, deps = {}) {
    if (typeof globalThis === 'undefined' ||
        typeof globalThis.window === 'undefined') {
        // Note: jsdom defines `window`, so this only trips on bare-Node SSR.
        throw new Error('createCollaborationProvider can only be called in a browser context (window is undefined)');
    }
    if (!opts.documentId || !opts.url) {
        throw new Error('createCollaborationProvider requires both documentId and url');
    }
    const Ctor = deps.WebsocketProviderCtor ?? WebsocketProvider;
    const ydoc = new Y.Doc();
    const provider = new Ctor(opts.url, opts.documentId, ydoc, {
        connect: opts.connect ?? true,
        params: opts.params,
    });
    // Layer y-indexeddb on top of the y-websocket transport for offline
    // drafts. Both providers share the same Y.Doc, so they automatically
    // converge: IndexedDB hydrates the doc on load, the websocket pushes
    // pending updates to the gateway on reconnect.
    const offlineEnabled = opts.offline !== false;
    const offlineCheck = deps.isIndexedDbSupported ?? defaultIndexedDbSupportCheck;
    let offlineStatus = 'pending';
    const setOfflineStatus = (next) => {
        offlineStatus = next;
        opts.onOfflineStatus?.(next);
    };
    let offlinePersistence = null;
    if (!offlineEnabled) {
        setOfflineStatus('unsupported');
    }
    else if (!offlineCheck()) {
        setOfflineStatus('unsupported');
    }
    else {
        setOfflineStatus('loading');
        const PersistenceCtor = deps.IndexeddbPersistenceCtor ?? IndexeddbPersistence;
        try {
            offlinePersistence = new PersistenceCtor(
            // Namespace the IndexedDB key by documentId so multiple docs in
            // the same admin session don't collide.
            `actuate-cms:${opts.documentId}`, ydoc);
            // `whenSynced` is the canonical "snapshot replay finished" hook
            // from y-indexeddb (https://github.com/yjs/y-indexeddb).
            offlinePersistence.whenSynced
                .then(() => setOfflineStatus('ready'))
                .catch((err) => {
                opts.onError?.(err instanceof Error ? err : new Error('Offline persistence failed'));
                setOfflineStatus('error');
            });
        }
        catch (err) {
            opts.onError?.(err instanceof Error ? err : new Error('Offline persistence failed'));
            setOfflineStatus('error');
            offlinePersistence = null;
        }
    }
    // Seed local awareness with the user's presence info. The
    // `CollaborationCursor` TipTap extension reads `user.name` + `user.color`
    // out of awareness state and renders cursors accordingly.
    provider.awareness.setLocalState({
        user: {
            name: opts.user.name,
            color: opts.user.color,
            avatarUrl: opts.user.avatarUrl,
            userId: opts.user.userId,
        },
    });
    if (opts.onStatus) {
        const handler = (event) => {
            const status = normaliseStatus(event.status);
            if (status)
                opts.onStatus(status);
        };
        provider.on('status', handler);
    }
    if (opts.onError) {
        const handler = () => {
            // y-websocket emits this on socket close + on protocol errors. We
            // surface a generic Error so callers can render a toast without
            // sniffing y-websocket internals.
            opts.onError(new Error('Realtime connection error'));
        };
        provider.on('connection-error', handler);
    }
    const destroy = () => {
        try {
            provider.awareness.setLocalState(null);
        }
        catch {
            /* awareness may already be torn down */
        }
        try {
            provider.destroy();
        }
        catch {
            /* idempotent */
        }
        if (offlinePersistence) {
            try {
                // y-indexeddb keeps the IDB connection open until `destroy()`;
                // we don't drop the stored data (that's the point of offline
                // drafts), but we close the handle so the runtime can GC.
                offlinePersistence.destroy();
            }
            catch {
                /* idempotent */
            }
        }
        try {
            ydoc.destroy();
        }
        catch {
            /* idempotent */
        }
    };
    return {
        ydoc,
        provider,
        awareness: provider.awareness,
        offlinePersistence,
        get offlineStatus() {
            return offlineStatus;
        },
        destroy,
    };
}
function defaultIndexedDbSupportCheck() {
    return (typeof globalThis !== 'undefined' &&
        typeof globalThis.indexedDB !== 'undefined');
}
function normaliseStatus(value) {
    if (value === 'connected')
        return 'connected';
    if (value === 'connecting')
        return 'connecting';
    if (value === 'disconnected')
        return 'disconnected';
    return null;
}
export function summarisePresence(awareness) {
    const seenUserIds = new Set();
    const users = [];
    for (const [clientId, state] of awareness.getStates()) {
        const user = state?.user;
        if (!user || typeof user.name !== 'string')
            continue;
        if (user.userId) {
            if (seenUserIds.has(user.userId))
                continue;
            seenUserIds.add(user.userId);
        }
        users.push({ clientId, user });
    }
    return { selfClientId: awareness.clientID, users };
}
//# sourceMappingURL=collaboration-provider.js.map