import { assert, unreachable } from "../../../../shared/src/asserts.js";
import { parse } from "../../../../shared/src/valita.js";
import { InvalidPush, MutationFailed, MutationRateLimited } from "../../../../zero-protocol/src/error-kind-enum.js";
import { ZeroCache } from "../../../../zero-protocol/src/error-origin-enum.js";
import { ProtocolError, isProtocolError } from "../../../../zero-protocol/src/error.js";
import { CRUD } from "../../../../zero-protocol/src/mutation-type-enum.js";
import { primaryKeyValueSchema } from "../../../../zero-protocol/src/primary-key.js";
import "../../../../zero-protocol/src/push.js";
import { MutationAlreadyProcessedError } from "./error.js";
import { Database } from "../../../../zqlite/src/db.js";
import { upstreamSchema } from "../../types/shards.js";
import "../../config/zero-config.js";
import { SERIALIZABLE } from "../../db/mode-enum.js";
import { runTx } from "../../db/run-transaction.js";
import { getOrCreateCounter } from "../../observability/metrics.js";
import { recordMutation } from "../../server/anonymous-otel-start.js";
import { WriteAuthorizerImpl } from "../../auth/write-authorizer.js";
import { SlidingWindowLimiter } from "../limiter/sliding-window-limiter.js";
import { resolver } from "@rocicorp/resolver";
import postgres from "postgres";
import { PG_SERIALIZATION_FAILURE } from "@drdgvhbh/postgres-error-codes";
//#region ../zero-cache/src/services/mutagen/mutagen.ts
var MutagenService = class {
	id;
	#lc;
	#upstream;
	#shard;
	#stopped = resolver();
	#replica;
	#writeAuthorizer;
	#limiter;
	#refCount = 0;
	#isStopped = false;
	#crudMutations = getOrCreateCounter("mutation", "crud", "Number of CRUD mutations processed");
	constructor(lc, shard, clientGroupID, upstream, config, writeAuthzStorage) {
		this.id = clientGroupID;
		this.#lc = lc;
		this.#upstream = upstream;
		this.#shard = shard;
		this.#replica = new Database(this.#lc, config.replica.file, { fileMustExist: true });
		this.#writeAuthorizer = new WriteAuthorizerImpl(this.#lc, config, this.#replica, shard.appID, clientGroupID, writeAuthzStorage);
		if (config.perUserMutationLimit.max !== void 0) this.#limiter = new SlidingWindowLimiter(config.perUserMutationLimit.windowMs, config.perUserMutationLimit.max);
	}
	ref() {
		assert(!this.#isStopped, "MutagenService is already stopped");
		++this.#refCount;
	}
	unref() {
		assert(!this.#isStopped, "MutagenService is already stopped");
		--this.#refCount;
		if (this.#refCount <= 0) this.stop();
	}
	hasRefs() {
		return this.#refCount > 0;
	}
	processMutation(mutation, authData, customMutatorsEnabled = false) {
		if (this.#limiter?.canDo() === false) return Promise.resolve([MutationRateLimited, "Rate limit exceeded"]);
		this.#crudMutations.add(1, { clientGroupID: this.id });
		return processMutation(this.#lc, authData, this.#upstream, this.#shard, this.id, mutation, this.#writeAuthorizer, void 0, customMutatorsEnabled);
	}
	run() {
		return this.#stopped.promise;
	}
	stop() {
		if (this.#isStopped) return this.#stopped.promise;
		this.#writeAuthorizer.destroy();
		this.#isStopped = true;
		this.#stopped.resolve();
		return this.#stopped.promise;
	}
};
var MAX_SERIALIZATION_ATTEMPTS = 10;
async function processMutation(lc, authData, db, shard, clientGroupID, mutation, writeAuthorizer, onTxStart, customMutatorsEnabled = false) {
	assert(mutation.type === CRUD, "Only CRUD mutations are supported");
	lc = lc.withContext("mutationID", mutation.id);
	lc = lc.withContext("processMutation");
	lc.debug?.("Process mutation start", mutation);
	recordMutation("crud");
	let result;
	const start = Date.now();
	try {
		let errorMode = false;
		for (let i = 0; i < MAX_SERIALIZATION_ATTEMPTS; i++) try {
			await runTx(db, async (tx) => {
				const done = onTxStart?.();
				try {
					return await processMutationWithTx(lc, tx, authData, shard, clientGroupID, mutation, errorMode, writeAuthorizer);
				} finally {
					await done;
				}
			}, { mode: SERIALIZABLE });
			if (errorMode) lc.debug?.("Ran mutation successfully in error mode");
			break;
		} catch (e) {
			if (e instanceof MutationAlreadyProcessedError) {
				lc.debug?.(e.message);
				return;
			}
			if (isProtocolError(e) && !errorMode && e.kind === "InvalidPush" && customMutatorsEnabled && i < 2) {
				lc.info?.("Both CRUD and Custom mutators are being used at once. This is supported for now but IS NOT RECOMMENDED. Migrate completely to custom mutators.", e);
				await new Promise((resolve) => setTimeout(resolve, 100));
				continue;
			}
			if (isProtocolError(e) || errorMode) {
				lc.error?.("Process mutation error", e);
				throw e;
			}
			if (e instanceof postgres.PostgresError && e.code === PG_SERIALIZATION_FAILURE) {
				lc.info?.(`attempt ${i + 1}: ${String(e)}`, e);
				continue;
			}
			result = [MutationFailed, String(e)];
			if (errorMode) break;
			lc.error?.("Got error running mutation, re-running in error mode", e);
			errorMode = true;
			i--;
		}
	} finally {
		lc.debug?.("Process mutation complete in", Date.now() - start);
	}
	return result;
}
async function processMutationWithTx(lc, tx, authData, shard, clientGroupID, mutation, errorMode, authorizer) {
	const tasks = [];
	async function execute(stmt) {
		try {
			return await stmt.execute();
		} finally {
			const q = stmt;
			lc.debug?.(`${q.string}: ${JSON.stringify(q.parameters)}`);
		}
	}
	authorizer.reloadPermissions();
	if (!errorMode) {
		const { ops } = mutation.args[0];
		authorizer.validateTableNames(ops);
		const normalizedOps = authorizer.normalizeOps(ops);
		const [canPre, canPost] = await Promise.all([authorizer.canPreMutation(authData, normalizedOps), authorizer.canPostMutation(authData, normalizedOps)]);
		if (canPre && canPost) for (const op of ops) switch (op.op) {
			case "insert":
				tasks.push(() => execute(getInsertSQL(tx, op)));
				break;
			case "upsert":
				tasks.push(() => execute(getUpsertSQL(tx, op)));
				break;
			case "update":
				tasks.push(() => execute(getUpdateSQL(tx, op)));
				break;
			case "delete":
				tasks.push(() => execute(getDeleteSQL(tx, op)));
				break;
			default: unreachable(op);
		}
	}
	tasks.unshift(() => checkSchemaVersionAndIncrementLastMutationID(tx, shard, clientGroupID, mutation.clientID, mutation.id));
	await Promise.all(tasks.map((task) => task()));
}
function getInsertSQL(tx, create) {
	return tx`INSERT INTO ${tx(create.tableName)} ${tx(create.value)}`;
}
function getUpsertSQL(tx, set) {
	const { tableName, primaryKey, value } = set;
	return tx`
    INSERT INTO ${tx(tableName)} ${tx(value)}
    ON CONFLICT (${tx(primaryKey)})
    DO UPDATE SET ${tx(value)}
  `;
}
function getUpdateSQL(tx, update) {
	const table = update.tableName;
	const { primaryKey, value } = update;
	const id = {};
	for (const key of primaryKey) id[key] = parse(value[key], primaryKeyValueSchema);
	return tx`UPDATE ${tx(table)} SET ${tx(value)} WHERE ${Object.entries(id).flatMap(([key, value], i) => i ? [tx`AND`, tx`${tx(key)} = ${value}`] : tx`${tx(key)} = ${value}`)}`;
}
function getDeleteSQL(tx, deleteOp) {
	const { tableName, primaryKey, value } = deleteOp;
	const conditions = [];
	for (const key of primaryKey) {
		if (conditions.length > 0) conditions.push(tx`AND`);
		conditions.push(tx`${tx(key)} = ${value[key]}`);
	}
	return tx`DELETE FROM ${tx(tableName)} WHERE ${conditions}`;
}
async function checkSchemaVersionAndIncrementLastMutationID(tx, shard, clientGroupID, clientID, receivedMutationID) {
	const [{ lastMutationID }] = await tx`
    INSERT INTO ${tx(upstreamSchema(shard))}.clients 
      as current ("clientGroupID", "clientID", "lastMutationID")
          VALUES (${clientGroupID}, ${clientID}, ${1})
      ON CONFLICT ("clientGroupID", "clientID")
      DO UPDATE SET "lastMutationID" = current."lastMutationID" + 1
      RETURNING "lastMutationID"
  `;
	if (receivedMutationID < lastMutationID) throw new MutationAlreadyProcessedError(clientID, receivedMutationID, lastMutationID);
	else if (receivedMutationID > lastMutationID) throw new ProtocolError({
		kind: InvalidPush,
		message: `Push contains unexpected mutation id ${receivedMutationID} for client ${clientID}. Expected mutation id ${lastMutationID.toString()}.`,
		origin: ZeroCache
	});
}
//#endregion
export { MutagenService };

//# sourceMappingURL=mutagen.js.map