import { assert } from "../../shared/src/asserts.js";
import * as v from "../../shared/src/valita.js";
import { MutationAlreadyProcessedError } from "../../zero-cache/src/services/mutagen/error.js";
import { pushBodySchema, pushParamsSchema, } from "../../zero-protocol/src/push.js";
import { createLogContext } from "./logging.js";
export async function handleMutationRequest(cb, queryOrQueryString, body, logLevel) {
    if (logLevel === undefined) {
        if (queryOrQueryString instanceof Request && typeof body === 'string') {
            logLevel = body;
        }
        else {
            logLevel = 'info';
        }
    }
    let queryString;
    if (queryOrQueryString instanceof Request) {
        const url = new URL(queryOrQueryString.url);
        queryString = url.searchParams;
        body = await queryOrQueryString.json();
    }
    else {
        queryString = queryOrQueryString;
    }
    const req = v.parse(body, pushBodySchema);
    if (queryString instanceof URLSearchParams) {
        queryString = Object.fromEntries(queryString);
    }
    const queryParams = v.parse(queryString, pushParamsSchema, 'passthrough');
    if (req.pushVersion !== 1) {
        return {
            error: 'unsupportedPushVersion',
        };
    }
    const transactor = new Transactor(req, queryParams, logLevel);
    const responses = [];
    for (const m of req.mutations) {
        assert(m.type === 'custom', 'Expected custom mutation');
        const res = await cb((dbProvider, innerCb) => transactor.transact(dbProvider, m, innerCb), m);
        responses.push(res);
        // We only stop processing if the mutation is out of order.
        // If the mutation has already been processed or if it returns an application error,
        // we continue processing the next mutation.
        if ('error' in res.result && res.result.error === 'oooMutation') {
            break;
        }
    }
    return {
        mutations: responses,
    };
}
class Transactor {
    #req;
    #params;
    #lc;
    constructor(req, params, logLevel) {
        this.#req = req;
        this.#params = params;
        this.#lc = createLogContext(logLevel).withContext('PushProcessor');
    }
    transact = async (dbProvider, mutation, cb) => {
        let caughtError = undefined;
        for (;;) {
            try {
                const ret = await this.#transactImpl(dbProvider, mutation, cb, caughtError);
                // The first time through we caught an error.
                // We want to report that error as it was an application
                // level error.
                if (caughtError !== undefined) {
                    this.#lc.warn?.(`Mutation ${mutation.id} for client ${mutation.clientID} was retried after an error: ${caughtError}`);
                    return makeAppErrorResponse(mutation, caughtError);
                }
                return ret;
            }
            catch (e) {
                if (e instanceof OutOfOrderMutation) {
                    this.#lc.error?.(e);
                    return {
                        id: {
                            clientID: mutation.clientID,
                            id: mutation.id,
                        },
                        result: {
                            error: 'oooMutation',
                            details: e.message,
                        },
                    };
                }
                if (e instanceof MutationAlreadyProcessedError) {
                    this.#lc.warn?.(e);
                    return {
                        id: {
                            clientID: mutation.clientID,
                            id: mutation.id,
                        },
                        result: {
                            error: 'alreadyProcessed',
                            details: e.message,
                        },
                    };
                }
                // We threw an error while running in error mode.
                // Re-throw the error and stop processing any further
                // mutations as all subsequent mutations will fail by being
                // out of order.
                if (caughtError !== undefined) {
                    throw e;
                }
                caughtError = e;
                this.#lc.error?.(`Unexpected error processing mutation ${mutation.id} for client ${mutation.clientID}`, e);
            }
        }
    };
    #transactImpl(dbProvider, mutation, cb, caughtError) {
        return dbProvider.transaction(async (dbTx, transactionHooks) => {
            await this.#checkAndIncrementLastMutationID(transactionHooks, mutation.clientID, mutation.id);
            if (caughtError === undefined) {
                await cb(dbTx, mutation.name, mutation.args[0]);
            }
            else {
                const appError = makeAppErrorResponse(mutation, caughtError);
                await transactionHooks.writeMutationResult(appError);
            }
            return {
                id: {
                    clientID: mutation.clientID,
                    id: mutation.id,
                },
                result: {},
            };
        }, {
            upstreamSchema: this.#params.schema,
            clientGroupID: this.#req.clientGroupID,
            clientID: mutation.clientID,
            mutationID: mutation.id,
        });
    }
    async #checkAndIncrementLastMutationID(transactionHooks, clientID, receivedMutationID) {
        const { lastMutationID } = await transactionHooks.updateClientMutationID();
        if (receivedMutationID < lastMutationID) {
            throw new MutationAlreadyProcessedError(clientID, receivedMutationID, lastMutationID);
        }
        else if (receivedMutationID > lastMutationID) {
            throw new OutOfOrderMutation(clientID, receivedMutationID, lastMutationID);
        }
    }
}
export class OutOfOrderMutation extends Error {
    constructor(clientID, receivedMutationID, lastMutationID) {
        super(`Client ${clientID} sent mutation ID ${receivedMutationID} but expected ${lastMutationID}`);
    }
}
function makeAppErrorResponse(m, e) {
    return {
        id: {
            clientID: m.clientID,
            id: m.id,
        },
        result: {
            error: 'app',
            details: e instanceof Error ? e.message : 'exception was not of type `Error`',
        },
    };
}
export function getMutation(
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
mutators, name) {
    let path;
    if (name.includes('|')) {
        path = name.split('|');
    }
    else {
        path = name.split('.');
    }
    // oxlint-disable-next-line @typescript-eslint/no-explicit-any
    let mutator;
    if (path.length === 1) {
        mutator = mutators[path[0]];
    }
    else {
        const nextMap = mutators[path[0]];
        assert(typeof nextMap === 'object' && nextMap !== undefined, `could not find mutator map for ${name}`);
        mutator = nextMap[path[1]];
    }
    assert(typeof mutator === 'function', () => `could not find mutator ${name}`);
    return mutator;
}
//# sourceMappingURL=process-mutations.js.map