/**
 * CollabSync — orchestrates multi-doc CRDT sync between local workbooks
 * and a remote collaboration server.
 *
 * Each workbook in the SheetEnginePool gets a SyncBridge. Local mutations
 * are debounced and pushed as Yrs diffs. Incoming binary frames from the
 * server are routed to the correct bridge by doc ID.
 *
 * Engine creation uses `createHeadlessEngine({ yrsState })` which calls
 * `compute_init_from_yrs_state` in Rust — this preserves exact Yrs item
 * identities so that subsequent `syncApply` diffs merge correctly.
 * (See mog/plans/active/colab/round-7/root-cause.md for why the JSON
 * snapshot path is broken.)
 */
import { createHeadlessEngine } from '../new-sheet/sdk-types.js';
import { SyncBridge } from './sync-bridge.js';
// ── CollabSync ──────────────────────────────────────────────────────────
export class CollabSync {
    client;
    pool;
    bridges = new Map();
    debounceTimers = new Map();
    unsubscribers = [];
    workbookEventUnsubs = new Map();
    destroyed = false;
    /** Frames that arrived while handleInitialState was in progress. */
    pendingUpdates = new Map();
    /** Docs currently being initialized (guards against concurrent handleInitialState). */
    initializing = new Set();
    constructor(client, pool) {
        this.client = client;
        this.pool = pool;
        // Wire incoming binary frames from server
        client.on('onSyncData', (docId, data) => {
            this.handleRemoteUpdate(docId, data);
        });
        // React to new workbooks added to the pool while in a room
        const unsub = pool.onWorkbookAdded((name, wb) => {
            if (this.destroyed)
                return;
            // Only sync workbooks we don't already have a bridge for
            if (!this.bridges.has(name)) {
                void this.shareWorkbook(name, wb);
            }
        });
        this.unsubscribers.push(unsub);
    }
    /**
     * Start syncing all current workbooks in the pool.
     * Called after creating/joining a room.
     */
    async startSyncing() {
        const names = this.pool.listWorkbooks();
        for (const name of names) {
            const wb = this.pool.getWorkbook(name);
            await this.shareWorkbook(name, wb);
        }
    }
    /**
     * Handle initial state for a doc received from server (late joiner).
     * Creates a new engine from raw Yrs state bytes, registers it in the pool,
     * and starts syncing.
     */
    async handleInitialState(docId, state) {
        if (this.bridges.has(docId))
            return;
        if (this.initializing.has(docId))
            return; // already creating — caller will buffer
        this.initializing.add(docId);
        try {
            // Create engine from raw Yrs bytes — preserves exact Yrs item identities
            // so that subsequent syncApply diffs merge correctly.
            const addon = SyncBridge.getNativeAddon();
            const engine = await createHeadlessEngine({
                computeAddon: addon,
                yrsState: state
            });
            const wb = engine.workbook;
            const bridge = new SyncBridge(wb);
            this.bridges.set(docId, bridge);
            this.pool.registerWorkbook(docId, wb);
            this.subscribeToWorkbookEvents(docId, wb);
            // Drain any frames that arrived while we were creating the engine
            const buffered = this.pendingUpdates.get(docId);
            if (buffered) {
                this.pendingUpdates.delete(docId);
                for (const update of buffered) {
                    try {
                        await bridge.applyUpdate(update);
                    }
                    catch {
                        /* best effort */
                    }
                }
            }
        }
        catch (err) {
            console.error(`[CollabSync] Failed to create workbook from Yrs state for "${docId}":`, err);
        }
        finally {
            this.initializing.delete(docId);
        }
    }
    /**
     * Handle incoming binary from server — apply remote update to the correct bridge.
     */
    async handleRemoteUpdate(docId, data) {
        if (this.initializing.has(docId)) {
            // Engine is being created — buffer this frame for later
            let buf = this.pendingUpdates.get(docId);
            if (!buf) {
                buf = [];
                this.pendingUpdates.set(docId, buf);
            }
            buf.push(data);
            return;
        }
        if (!this.bridges.has(docId)) {
            await this.handleInitialState(docId, data);
            return;
        }
        // Incremental update — syncApply merges the Yrs diff into the engine
        try {
            const bridge = this.bridges.get(docId);
            if (!bridge)
                return;
            await bridge.applyUpdate(data);
        }
        catch (err) {
            console.error(`[CollabSync] Failed to apply remote update for "${docId}":`, err);
        }
    }
    destroy() {
        this.destroyed = true;
        for (const unsub of this.unsubscribers)
            unsub();
        this.unsubscribers = [];
        for (const [, unsubs] of this.workbookEventUnsubs) {
            for (const unsub of unsubs)
                unsub();
        }
        this.workbookEventUnsubs.clear();
        for (const [, timer] of this.debounceTimers)
            clearTimeout(timer);
        this.debounceTimers.clear();
        this.bridges.clear();
        this.pendingUpdates.clear();
        this.initializing.clear();
    }
    // ── Private ────────────────────────────────────────────────────────────
    /**
     * Share a local workbook with the room — create a bridge, send full state,
     * subscribe to mutation events.
     */
    async shareWorkbook(name, wb) {
        try {
            const bridge = new SyncBridge(wb);
            this.bridges.set(name, bridge);
            // Subscribe to events FIRST (synchronously) so no cell changes are
            // missed between sending the initial state and subscribing.
            this.subscribeToWorkbookEvents(name, wb);
            // Then send full Yrs state to server
            const fullState = await bridge.getFullState();
            this.client.sendSyncData(name, fullState);
        }
        catch (err) {
            console.error(`[CollabSync] Failed to share workbook "${name}":`, err);
        }
    }
    /**
     * Subscribe to workbook mutation events for outbound sync.
     * Synchronous — uses activeSheet property to avoid async gaps where
     * cell changes could be missed.
     */
    subscribeToWorkbookEvents(docId, wb) {
        const unsubs = [];
        const pushDebounced = () => {
            if (this.destroyed)
                return;
            const existing = this.debounceTimers.get(docId);
            if (existing)
                clearTimeout(existing);
            this.debounceTimers.set(docId, setTimeout(() => {
                void this.pushLocalChanges(docId);
            }, 50));
        };
        // Subscribe to active sheet events (synchronous — no missed events)
        const activeSheet = wb.activeSheet;
        if (activeSheet) {
            unsubs.push(activeSheet.on('cellChanged', pushDebounced));
            unsubs.push(activeSheet.on('structureChanged', pushDebounced));
        }
        // Workbook-level events (also synchronous)
        unsubs.push(wb.on('sheetAdded', pushDebounced));
        unsubs.push(wb.on('sheetRemoved', pushDebounced));
        unsubs.push(wb.on('sheetRenamed', pushDebounced));
        this.workbookEventUnsubs.set(docId, unsubs);
    }
    /**
     * Push local Yrs changes to the server for a specific doc.
     */
    async pushLocalChanges(docId) {
        const bridge = this.bridges.get(docId);
        if (!bridge || this.destroyed)
            return;
        try {
            const state = await bridge.getFullState();
            this.client.sendSyncData(docId, state);
        }
        catch (err) {
            console.error(`[CollabSync] Failed to push changes for "${docId}":`, err);
        }
    }
}
//# sourceMappingURL=collab-sync.js.map