import { assert } from "../../../../shared/src/asserts.js";
import { must } from "../../../../shared/src/must.js";
import "../../types/processes.js";
import { HttpService } from "../http-service.js";
import { pgClient } from "../../types/pg.js";
import { PROTOCOL_ERROR, closeWithError } from "../../types/ws.js";
import { installWebSocketReceiver } from "../../types/websocket-handoff.js";
import { discoverChangeStreamerAddress } from "./schema/tables.js";
import { streamIn, streamOut } from "../../types/streams.js";
import { URLParams } from "../../types/url-params.js";
import { downstreamSchema } from "./change-streamer.js";
import { snapshotMessageSchema } from "./snapshot.js";
import WebSocket$1 from "ws";
import websocket from "@fastify/websocket";
//#region ../zero-cache/src/services/change-streamer/change-streamer-http.ts
var MIN_SUPPORTED_PROTOCOL_VERSION = 1;
var SNAPSHOT_PATH_PATTERN = "/replication/:version/snapshot";
var CHANGES_PATH_PATTERN = "/replication/:version/changes";
var PATH_REGEX = /\/replication\/v(?<version>\d+)\/(changes|snapshot)$/;
var SNAPSHOT_PATH = `/replication/v6/snapshot`;
var CHANGES_PATH = `/replication/v6/changes`;
var ChangeStreamerHttpServer = class extends HttpService {
	id = "change-streamer-http-server";
	#lc;
	#opts;
	#changeStreamer;
	#backupMonitor;
	constructor(lc, config, opts, parent, changeStreamer, backupMonitor) {
		super("change-streamer-http-server", lc, opts, async (fastify) => {
			const websocketOptions = {};
			if (config.websocketCompression) if (config.websocketCompressionOptions) try {
				websocketOptions.perMessageDeflate = JSON.parse(config.websocketCompressionOptions);
			} catch (e) {
				throw new Error(`Failed to parse ZERO_WEBSOCKET_COMPRESSION_OPTIONS: ${String(e)}. Expected valid JSON.`);
			}
			else websocketOptions.perMessageDeflate = true;
			await fastify.register(websocket, { options: websocketOptions });
			fastify.get(CHANGES_PATH_PATTERN, { websocket: true }, this.#subscribe);
			fastify.get(SNAPSHOT_PATH_PATTERN, { websocket: true }, this.#reserveSnapshot);
			installWebSocketReceiver(lc, fastify.websocketServer, this.#receiveWebsocket, parent);
		});
		this.#lc = lc;
		this.#opts = opts;
		this.#changeStreamer = changeStreamer;
		this.#backupMonitor = backupMonitor;
	}
	#getBackupMonitor() {
		return must(this.#backupMonitor, "replication-manager is not configured with a ZERO_LITESTREAM_BACKUP_URL");
	}
	#receiveWebsocket = (ws, action, msg) => {
		switch (action) {
			case "snapshot": return this.#reserveSnapshot(ws, msg);
			case "changes": return this.#subscribe(ws, msg);
			default:
				closeWithError(this._lc, ws, `invalid action "${action}" received in handoff`);
				return;
		}
	};
	#reserveSnapshot = (ws, req) => {
		try {
			const url = new URL(req.url ?? "", req.headers.origin ?? "http://localhost");
			checkProtocolVersion(url.pathname);
			const taskID = url.searchParams.get("taskID");
			if (!taskID) throw new Error("Missing taskID in snapshot request");
			const downstream = this.#getBackupMonitor().startSnapshotReservation(taskID);
			streamOut(this._lc, downstream, ws);
		} catch (err) {
			closeWithError(this._lc, ws, err, PROTOCOL_ERROR);
		}
	};
	#subscribe = async (ws, req) => {
		try {
			const ctx = getSubscriberContext(req);
			if (ctx.mode === "serving") this.#ensureChangeStreamerStarted("incoming subscription");
			const downstream = await this.#changeStreamer.subscribe(ctx);
			if (ctx.initial && ctx.taskID && this.#backupMonitor) this.#backupMonitor.endReservation(ctx.taskID);
			streamOut(this._lc, downstream, ws);
		} catch (err) {
			closeWithError(this._lc, ws, err, PROTOCOL_ERROR);
		}
	};
	#changeStreamerStarted = false;
	#ensureChangeStreamerStarted(reason) {
		if (!this.#changeStreamerStarted && this._state.shouldRun()) {
			this.#lc.info?.(`starting ChangeStreamerService: ${reason}`);
			this.#changeStreamer.run().catch((e) => this.#lc.warn?.(`ChangeStreamerService ended with error`, e)).finally(() => this.stop());
			this.#changeStreamerStarted = true;
		}
	}
	_onStart() {
		const { startupDelayMs } = this.#opts;
		this._state.setTimeout(() => this.#ensureChangeStreamerStarted(`startup delay elapsed (${startupDelayMs} ms)`), startupDelayMs);
	}
	async _onStop() {
		if (this.#changeStreamerStarted) await this.#changeStreamer.stop();
	}
};
var ChangeStreamerHttpClient = class {
	#lc;
	#shardID;
	#changeDB;
	#changeStreamerURI;
	constructor(lc, shardID, changeDB, changeStreamerURI) {
		this.#lc = lc;
		this.#shardID = shardID;
		this.#changeDB = pgClient(lc, changeDB, {
			max: 1,
			["idle_timeout"]: 15,
			connection: { ["application_name"]: "change-streamer-discovery" }
		});
		this.#changeStreamerURI = changeStreamerURI;
	}
	async #resolveChangeStreamer(path) {
		let baseURL = this.#changeStreamerURI;
		if (!baseURL) {
			const address = await discoverChangeStreamerAddress(this.#shardID, this.#changeDB);
			if (!address) throw new Error(`no change-streamer is running`);
			baseURL = address.includes("://") ? `${address}/` : `ws://${address}/`;
		}
		const uri = new URL(path, baseURL);
		this.#lc.info?.(`connecting to change-streamer@${uri}`);
		return uri;
	}
	async reserveSnapshot(taskID) {
		const ws = new WebSocket$1(await this.#resolveChangeStreamer(SNAPSHOT_PATH) + `?${new URLSearchParams({ taskID }).toString()}`);
		return streamIn(this.#lc, ws, snapshotMessageSchema);
	}
	async subscribe(ctx) {
		const ws = new WebSocket$1(await this.#resolveChangeStreamer(CHANGES_PATH) + `?${getParams(ctx).toString()}`);
		return streamIn(this.#lc, ws, downstreamSchema);
	}
};
function getSubscriberContext(req) {
	const url = new URL(req.url ?? "", req.headers.origin ?? "http://localhost");
	const protocolVersion = checkProtocolVersion(url.pathname);
	const params = new URLParams(url);
	return {
		protocolVersion,
		id: params.get("id", true),
		taskID: params.get("taskID", false),
		mode: params.get("mode", false) === "backup" ? "backup" : "serving",
		replicaVersion: params.get("replicaVersion", true),
		watermark: params.get("watermark", true),
		initial: params.getBoolean("initial")
	};
}
function checkProtocolVersion(pathname) {
	const match = PATH_REGEX.exec(pathname);
	if (!match) throw new Error(`invalid path: ${pathname}`);
	const v = Number(match.groups?.version);
	if (Number.isNaN(v) || v > 6 || v < MIN_SUPPORTED_PROTOCOL_VERSION) throw new Error(`Cannot service client at protocol v${v}. Supported protocols: [v${MIN_SUPPORTED_PROTOCOL_VERSION} ... v6]`);
	return v;
}
function getParams(ctx) {
	const { protocolVersion, ...stringParams } = ctx;
	assert(protocolVersion === 6, `replicator should be setting protocolVersion to 6`);
	return new URLSearchParams({
		...stringParams,
		taskID: ctx.taskID ? ctx.taskID : "",
		initial: ctx.initial ? "true" : "false"
	});
}
//#endregion
export { ChangeStreamerHttpClient, ChangeStreamerHttpServer };

//# sourceMappingURL=change-streamer-http.js.map