import { randomId, } from '@syncular/core';
import { Kysely, } from 'kysely';
import { BaseSqliteDialect, BaseSqliteDriver } from 'kysely-generic-sqlite';
import { createMutationsApi, } from './mutations.js';
import { assertSyncularReadonlySql } from './sql-safety.js';
export function createSyncularBridgeDialect(bridge) {
    return new BaseSqliteDialect(() => new SyncularBridgeDriver(bridge));
}
export async function createSyncularBridgeClient(options) {
    await options.bridge.open?.();
    const dialect = createSyncularBridgeDialect(options.bridge);
    const db = new Kysely({ dialect });
    let closed = false;
    const syncAfterMutation = options.sync?.autoSyncAfterMutation !== false;
    const client = {
        db,
        dialect,
        mutations: createSyncularBridgeMutations(options, async () => {
            if (syncAfterMutation)
                await client.sync().catch(() => undefined);
        }),
        leasedMutations: createSyncularBridgeMutations(options, async () => {
            if (syncAfterMutation)
                await client.sync().catch(() => undefined);
        }, { leased: true }),
        blobs: createBridgeBlobs(options.bridge),
        on: (event, listener) => options.bridge.on?.(event, listener) ?? noop,
        getStatus: () => resolveBridgeStatusSnapshot(options.bridge),
        setSubscriptions: async (subscriptions) => {
            await options.bridge.setSubscriptions?.(subscriptions);
        },
        resumeFromBackground: (syncOptions) => requireBridgeMethod(options.bridge.resumeFromBackground, 'resumeFromBackground')(syncOptions),
        issueAuthLease: (request) => requireBridgeMethod(options.bridge.issueAuthLease, 'issueAuthLease')(request),
        upsertAuthLease: (lease) => requireBridgeMethod(options.bridge.upsertAuthLease, 'upsertAuthLease')(lease),
        authLease: (leaseId) => requireBridgeMethod(options.bridge.authLease, 'authLease')(leaseId),
        activeAuthLeases: (actorId, nowMs) => requireBridgeMethod(options.bridge.activeAuthLeases, 'activeAuthLeases')(actorId, nowMs),
        diagnosticSnapshot: () => requireBridgeMethod(options.bridge.diagnosticSnapshot, 'diagnosticSnapshot')(),
        presence: createBridgePresence(options.bridge),
        conflicts: createBridgeConflicts(options.bridge),
        start: async () => {
            await options.bridge.start?.();
        },
        stop: async () => {
            await options.bridge.stop?.();
        },
        sync: async () => {
            if (!options.bridge.sync) {
                throw new Error('Syncular bridge does not implement sync().');
            }
            return await options.bridge.sync();
        },
        async close() {
            if (closed)
                return;
            closed = true;
            await options.bridge.stop?.();
            await db.destroy();
            await options.bridge.close?.();
        },
    };
    return client;
}
function createSyncularBridgeMutations(options, afterCommit, mode = {}) {
    const commit = createSyncularBridgeCommit(options, afterCommit, mode);
    return createMutationsApi(commit);
}
function createSyncularBridgeCommit(options, afterCommit, mode = {}) {
    const idColumn = options.idColumn ?? 'id';
    const versionColumn = options.versionColumn ?? 'server_version';
    const omitColumns = options.omitColumns ?? [];
    return async (fn) => {
        const operations = [];
        const makeTxTable = (table) => ({
            async insert(values) {
                const raw = objectRecord(values);
                const id = typeof raw[idColumn] === 'string' && raw[idColumn]
                    ? raw[idColumn]
                    : randomId();
                operations.push({
                    table,
                    row_id: id,
                    op: 'upsert',
                    payload: sanitizeOperationPayload({ ...raw, [idColumn]: id }, [
                        idColumn,
                        ...(versionColumn ? [versionColumn] : []),
                        ...omitColumns,
                    ]),
                    base_version: null,
                });
                return id;
            },
            async insertMany(rows) {
                const ids = [];
                for (const row of rows)
                    ids.push(await this.insert(row));
                return ids;
            },
            async update(id, patch, opts) {
                operations.push(upsertOperation(table, id, patch, opts));
            },
            async delete(id, opts) {
                operations.push({
                    table,
                    row_id: id,
                    op: 'delete',
                    payload: null,
                    base_version: opts?.baseVersion ?? null,
                });
            },
            async upsert(id, patch, opts) {
                operations.push(upsertOperation(table, id, patch, opts));
            },
        });
        const tx = new Proxy({}, {
            get(_target, prop) {
                if (prop === 'then')
                    return undefined;
                return typeof prop === 'string' ? makeTxTable(prop) : undefined;
            },
        });
        const result = await fn(tx);
        if (operations.length === 0)
            throw new Error('No mutations were enqueued');
        const commitFn = mode.leased
            ? requireBridgeMethod(options.bridge.applyLeasedMutationsCommit, 'applyLeasedMutationsCommit')
            : options.bridge.applyMutationsCommit.bind(options.bridge);
        const receipt = normalizeMutationReceipt(await commitFn({ operations }));
        await afterCommit();
        return { result, receipt, meta: { operations } };
    };
    function upsertOperation(table, id, patch, opts) {
        return {
            table,
            row_id: id,
            op: 'upsert',
            payload: sanitizeOperationPayload(objectRecord(patch), [
                idColumn,
                ...(versionColumn ? [versionColumn] : []),
                ...omitColumns,
            ]),
            base_version: opts?.baseVersion ?? null,
        };
    }
}
class SyncularBridgeDriver extends BaseSqliteDriver {
    constructor(bridge) {
        super(async () => {
            this.conn = new SyncularBridgeConnection(bridge);
        });
    }
    async destroy() {
        this.conn = undefined;
    }
}
class SyncularBridgeConnection {
    bridge;
    constructor(bridge) {
        this.bridge = bridge;
    }
    async executeQuery(compiledQuery) {
        assertSyncularReadonlySql(compiledQuery.sql);
        const result = await this.bridge.executeSql({
            sql: compiledQuery.sql,
            parameters: compiledQuery.parameters,
        });
        return {
            rows: result.rows,
            numAffectedRows: result.numAffectedRows == null
                ? undefined
                : BigInt(result.numAffectedRows),
            insertId: result.insertId == null ? undefined : BigInt(result.insertId),
        };
    }
    streamQuery(_compiledQuery, _chunkSize) {
        throw new Error('Syncular bridge dialect does not support streaming.');
    }
}
function createBridgePresence(bridge) {
    return (bridge.presence ?? {
        get: () => [],
        join: unsupportedFn('presence.join'),
        leave: unsupportedFn('presence.leave'),
        updateMetadata: unsupportedFn('presence.updateMetadata'),
        onChange: () => noop,
    });
}
function createBridgeConflicts(bridge) {
    return (bridge.conflicts ?? {
        list: async () => [],
        retryKeepLocal: unsupportedFn('conflicts.retryKeepLocal'),
        resolve: unsupportedFn('conflicts.resolve'),
    });
}
function createBridgeBlobs(bridge) {
    return {
        getUploadQueueStats: async () => bridge.blobs?.getUploadQueueStats?.() ?? {
            pending: 0,
            uploading: 0,
            failed: 0,
        },
        processUploadQueue: async (options) => bridge.blobs?.processUploadQueue?.(options) ?? { uploaded: 0, failed: 0 },
        retrieve: bridge.blobs?.retrieve ??
            (async () => {
                throw new Error('Syncular bridge does not implement blob retrieval.');
            }),
    };
}
function resolveBridgeStatusSnapshot(bridge) {
    const snapshot = readSynchronousStatus(bridge);
    const connection = snapshot.connection ?? {
        closed: false,
        pendingRequests: 0,
        realtime: 'disconnected',
    };
    const lifecycle = snapshot.lifecycle ?? {
        phase: connection.closed ? 'closed' : 'offline',
        realtime: connection.realtime,
        online: connection.realtime === 'connected',
        requiresAction: false,
        pendingRequests: connection.pendingRequests,
        ...(snapshot.outbox ? { outbox: snapshot.outbox } : {}),
        ...(snapshot.conflicts ? { conflicts: snapshot.conflicts } : {}),
    };
    const outbox = snapshot.outbox ?? lifecycle.outbox ?? null;
    const conflicts = snapshot.conflicts ?? lifecycle.conflicts ?? null;
    return {
        lifecycle,
        connection,
        outbox,
        conflicts,
        isConnected: connection.realtime === 'connected' && !connection.closed,
        isSyncing: lifecycle.phase === 'syncing' || lifecycle.phase === 'recovering',
        hasPendingMutations: (outbox?.pending ?? 0) + (outbox?.sending ?? 0) > 0,
        hasConflicts: (conflicts?.unresolved ?? 0) > 0,
        requiresAction: lifecycle.requiresAction,
    };
}
function readSynchronousStatus(bridge) {
    if (!bridge.getStatus)
        return {};
    const snapshot = bridge.getStatus();
    if (isPromiseLike(snapshot)) {
        throw new Error('Syncular bridge getStatus() must be synchronous for React snapshots.');
    }
    return snapshot;
}
function normalizeMutationReceipt(value) {
    if (typeof value === 'string')
        return { commitId: value, clientCommitId: value };
    return value;
}
function sanitizeOperationPayload(payload, omitColumns) {
    const omit = new Set(omitColumns);
    const out = {};
    for (const [key, value] of Object.entries(payload)) {
        if (!omit.has(key))
            out[key] = value;
    }
    return out;
}
function objectRecord(value) {
    return value && typeof value === 'object' && !Array.isArray(value)
        ? { ...value }
        : {};
}
function unsupportedFn(method) {
    return () => {
        throw new Error(`Syncular bridge does not implement ${method}.`);
    };
}
function requireBridgeMethod(fn, method) {
    if (typeof fn === 'function')
        return fn;
    return unsupportedFn(method);
}
function isPromiseLike(value) {
    return (value != null &&
        typeof value === 'object' &&
        typeof value.then === 'function');
}
function noop() { }
//# sourceMappingURL=bridge-client.js.map