import { must } from "../../../../../shared/src/must.js";
import { equals } from "../../../../../shared/src/set-utils.js";
import { ALLOWED_APP_ID_CHARACTERS } from "../../../types/shards.js";
import { pgClient } from "../../../types/pg.js";
import { toStateVersionString } from "./lsn.js";
import { liteValue } from "../../../types/lite.js";
import { liteTableName } from "../../../types/names.js";
import { mapPostgresToLite, mapPostgresToLiteIndex } from "../../../db/pg-to-lite.js";
import { ColumnMetadataStore } from "../../replicator/schema/column-metadata.js";
import { initReplicationState } from "../../replicator/schema/replication-state.js";
import { READONLY } from "../../../db/mode-enum.js";
import { runTx } from "../../../db/run-transaction.js";
import { getPublicationInfo } from "./schema/published.js";
import { id } from "../../../types/sql.js";
import { addReplica, dropShard, getInternalShardConfig, newReplicationSlot, replicationSlotExpression, validatePublications } from "./schema/shard.js";
import { createLiteIndexStatement, createLiteTableStatement } from "../../../db/create.js";
import { ReplicationStatusPublisher } from "../../replicator/replication-status.js";
import "../../../types/pg-versions.js";
import { TsvParser } from "../../../db/pg-copy.js";
import { getTypeParsers } from "../../../db/pg-type-parser.js";
import { TransactionPool, importSnapshot } from "../../../db/transaction-pool.js";
import { BinaryCopyParser, hasBinaryDecoder, makeBinaryDecoder, textCastDecoder } from "../../../db/pg-copy-binary.js";
import { CpuProfiler } from "../../../types/profiler.js";
import { ensureShardSchema } from "./schema/init.js";
import postgres from "postgres";
import { platform } from "node:os";
import { PG_CONFIGURATION_LIMIT_EXCEEDED, PG_INSUFFICIENT_PRIVILEGE } from "@drdgvhbh/postgres-error-codes";
import { Writable } from "node:stream";
import { pipeline as pipeline$1 } from "node:stream/promises";
//#region ../zero-cache/src/services/change-source/pg/initial-sync.ts
async function initialSync(lc, shard, tx, upstreamURI, syncOptions, context) {
	if (!ALLOWED_APP_ID_CHARACTERS.test(shard.appID)) throw new Error("The App ID may only consist of lower-case letters, numbers, and the underscore character");
	const { tableCopyWorkers, profileCopy, textCopy = false, replicationSlotFailover = false } = syncOptions;
	const copyProfiler = profileCopy ? await CpuProfiler.connect() : null;
	const sql = pgClient(lc, upstreamURI);
	const replicationSession = pgClient(lc, upstreamURI, {
		["fetch_types"]: false,
		connection: { replication: "database" }
	});
	const slotName = newReplicationSlot(shard);
	const statusPublisher = ReplicationStatusPublisher.forRunningTransaction(tx).publish(lc, "Initializing");
	try {
		const pgVersion = await checkUpstreamConfig(sql);
		const { publications } = await ensurePublishedTables(lc, sql, shard);
		lc.info?.(`Upstream is setup with publications [${publications}]`);
		const { database, host } = sql.options;
		lc.info?.(`opening replication session to ${database}@${host}`);
		let slot;
		for (let first = true;; first = false) try {
			slot = await createReplicationSlot(lc, replicationSession, slotName, replicationSlotFailover && pgVersion >= 17e4);
			break;
		} catch (e) {
			if (first && e instanceof postgres.PostgresError) {
				if (e.code === PG_INSUFFICIENT_PRIVILEGE) {
					await sql`ALTER ROLE current_user WITH REPLICATION`;
					lc.info?.(`Added the REPLICATION role to database user`);
					continue;
				}
				if (e.code === PG_CONFIGURATION_LIMIT_EXCEEDED) {
					const dropped = await sql`
              SELECT slot_name as slot, pg_drop_replication_slot(slot_name) 
                FROM pg_replication_slots
                WHERE slot_name LIKE ${replicationSlotExpression(shard)} AND NOT active`;
					if (dropped.length) {
						lc.warn?.(`Dropped inactive replication slots: ${dropped.map(({ slot }) => slot)}`, e);
						continue;
					}
					lc.error?.(`Unable to drop replication slots`, e);
				}
			}
			throw e;
		}
		const { snapshot_name: snapshot, consistent_point: lsn } = slot;
		const initialVersion = toStateVersionString(lsn);
		initReplicationState(tx, publications, initialVersion, context);
		const start = performance.now();
		const published = await runTx(sql, async (tx) => {
			await tx.unsafe(`SET TRANSACTION SNAPSHOT '${snapshot}'`);
			return getPublicationInfo(tx, publications);
		}, { mode: READONLY });
		validatePublications(lc, published);
		const { tables, indexes } = published;
		const numTables = tables.length;
		if (platform() === "win32" && tableCopyWorkers < numTables) lc.warn?.(`Increasing the number of copy workers from ${tableCopyWorkers} to ${numTables} to work around a Node/Postgres connection bug`);
		const numWorkers = platform() === "win32" ? numTables : Math.min(tableCopyWorkers, numTables);
		const copyPool = pgClient(lc, upstreamURI, {
			max: numWorkers,
			connection: { ["application_name"]: "initial-sync-copy-worker" },
			["max_lifetime"]: 7200
		});
		const copiers = startTableCopyWorkers(lc, copyPool, snapshot, numWorkers, numTables);
		try {
			createLiteTables(tx, tables, initialVersion);
			const downloads = await Promise.all(tables.map((spec) => copiers.processReadTask((db, lc) => getInitialDownloadState(lc, db, spec))));
			statusPublisher.publish(lc, "Initializing", `Copying ${numTables} upstream tables at version ${initialVersion}`, 5e3, () => ({ downloadStatus: downloads.map(({ status }) => status) }));
			copyProfiler?.start();
			const rowCounts = await Promise.all(downloads.map((table) => copiers.processReadTask((db, lc) => copy(lc, table, copyPool, db, tx, textCopy))));
			copyProfiler?.stopAndDispose(lc, "initial-copy");
			copiers.setDone();
			const total = rowCounts.reduce((acc, curr) => ({
				rows: acc.rows + curr.rows,
				flushTime: acc.flushTime + curr.flushTime
			}), {
				rows: 0,
				flushTime: 0
			});
			statusPublisher.publish(lc, "Indexing", `Creating ${indexes.length} indexes`, 5e3);
			const indexStart = performance.now();
			createLiteIndices(tx, indexes);
			const index = performance.now() - indexStart;
			lc.info?.(`Created indexes (${index.toFixed(3)} ms)`);
			await addReplica(sql, shard, slotName, initialVersion, published, context);
			const elapsed = performance.now() - start;
			lc.info?.(`Synced ${total.rows.toLocaleString()} rows of ${numTables} tables in ${publications} up to ${lsn} (flush: ${total.flushTime.toFixed(3)}, index: ${index.toFixed(3)}, total: ${elapsed.toFixed(3)} ms)`);
		} finally {
			copyPool.end().catch((e) => lc.warn?.(`Error closing copyPool`, e));
		}
	} catch (e) {
		lc.warn?.(`dropping replication slot ${slotName}`, e);
		await sql`
      SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots
        WHERE slot_name = ${slotName};
    `.catch((e) => lc.warn?.(`Unable to drop replication slot ${slotName}`, e));
		await statusPublisher.publishAndThrowError(lc, "Initializing", e);
	} finally {
		statusPublisher.stop();
		await replicationSession.end();
		await sql.end();
	}
}
async function checkUpstreamConfig(sql) {
	const { walLevel, version } = (await sql`
      SELECT current_setting('wal_level') as "walLevel", 
             current_setting('server_version_num') as "version";
  `)[0];
	if (walLevel !== "logical") throw new Error(`Postgres must be configured with "wal_level = logical" (currently: "${walLevel})`);
	if (version < 15e4) throw new Error(`Must be running Postgres 15 or higher (currently: "${version}")`);
	return version;
}
async function ensurePublishedTables(lc, sql, shard, validate = true) {
	const { database, host } = sql.options;
	lc.info?.(`Ensuring upstream PUBLICATION on ${database}@${host}`);
	await ensureShardSchema(lc, sql, shard);
	const { publications } = await getInternalShardConfig(sql, shard);
	if (validate) {
		let valid = false;
		const nonInternalPublications = publications.filter((p) => !p.startsWith("_"));
		const exists = await sql`
      SELECT pubname FROM pg_publication WHERE pubname IN ${sql(publications)}
      `.values();
		if (exists.length !== publications.length) lc.warn?.(`some configured publications [${publications}] are missing: [${exists.flat()}]. resyncing`);
		else if (!equals(new Set(shard.publications), new Set(nonInternalPublications))) lc.warn?.(`requested publications [${shard.publications}] differ from previouspublications [${nonInternalPublications}]. resyncing`);
		else valid = true;
		if (!valid) {
			await sql.unsafe(dropShard(shard.appID, shard.shardNum));
			return ensurePublishedTables(lc, sql, shard, false);
		}
	}
	return { publications };
}
function startTableCopyWorkers(lc, db, snapshot, numWorkers, numTables) {
	const { init } = importSnapshot(snapshot);
	const tableCopiers = new TransactionPool(lc, READONLY, init, void 0, numWorkers);
	tableCopiers.run(db);
	lc.info?.(`Started ${numWorkers} workers to copy ${numTables} tables`);
	if (parseInt(process.versions.node) < 22) lc.warn?.("\n\n\nOlder versions of Node have a bug that results in an unresponsive\nPostgres connection after running certain combinations of COPY commands.\nIf initial sync hangs, run zero-cache with Node v22+. This has the additional\nbenefit of being consistent with the Node version run in the production container image.\n\n\n");
	return tableCopiers;
}
async function createReplicationSlot(lc, session, slotName, failover = false) {
	const [slot] = failover ? await session.unsafe(`CREATE_REPLICATION_SLOT "${slotName}" LOGICAL pgoutput (FAILOVER)`) : await session.unsafe(`CREATE_REPLICATION_SLOT "${slotName}" LOGICAL pgoutput`);
	lc.info?.(`Created replication slot ${slotName}`, slot);
	return slot;
}
function createLiteTables(tx, tables, initialVersion) {
	const columnMetadata = must(ColumnMetadataStore.getInstance(tx));
	for (const t of tables) {
		tx.exec(createLiteTableStatement(mapPostgresToLite(t, initialVersion)));
		const tableName = liteTableName(t);
		for (const [colName, colSpec] of Object.entries(t.columns)) columnMetadata.insert(tableName, colName, colSpec);
	}
}
function createLiteIndices(tx, indices) {
	for (const index of indices) tx.exec(createLiteIndexStatement(mapPostgresToLiteIndex(index)));
}
var MB = 1024 * 1024;
var MAX_BUFFERED_ROWS = 1e4;
var BUFFERED_SIZE_THRESHOLD = 8 * MB;
function makeDownloadStatements(table, cols) {
	const filterConditions = Object.values(table.publications).map(({ rowFilter }) => rowFilter).filter((f) => !!f);
	const where = filterConditions.length === 0 ? "" : `WHERE ${filterConditions.join(" OR ")}`;
	const fromTable = `FROM ${id(table.schema)}.${id(table.name)} ${where}`;
	const totalBytes = `(${cols.map((col) => `SUM(COALESCE(pg_column_size(${id(col)}), 0))`).join(" + ")})`;
	return {
		select: `SELECT ${cols.map(id).join(",")} ${fromTable}`,
		getTotalRows: `SELECT COUNT(*) AS "totalRows" ${fromTable}`,
		getTotalBytes: `SELECT ${totalBytes} AS "totalBytes" ${fromTable}`
	};
}
async function getInitialDownloadState(lc, sql, spec) {
	const start = performance.now();
	const table = liteTableName(spec);
	const columns = Object.keys(spec.columns);
	const stmts = makeDownloadStatements(spec, columns);
	const rowsResult = sql.unsafe(stmts.getTotalRows).execute();
	const bytesResult = sql.unsafe(stmts.getTotalBytes).execute();
	const state = {
		spec,
		status: {
			table,
			columns,
			rows: 0,
			totalRows: Number((await rowsResult)[0].totalRows),
			totalBytes: Number((await bytesResult)[0].totalBytes)
		}
	};
	const elapsed = (performance.now() - start).toFixed(3);
	lc.info?.(`Computed initial download state for ${table} (${elapsed} ms)`, { state: state.status });
	return state;
}
function copy(lc, { spec: table, status }, dbClient, from, to, textCopy) {
	if (textCopy) return copyText(lc, table, status, dbClient, from, to);
	return copyBinary(lc, table, status, from, to);
}
async function copyBinary(lc, table, status, from, to) {
	const start = performance.now();
	let flushTime = 0;
	const tableName = liteTableName(table);
	const orderedColumns = Object.entries(table.columns);
	const columnNames = orderedColumns.map(([c]) => c);
	const columnSpecs = orderedColumns.map(([_name, spec]) => spec);
	const insertColumnList = columnNames.map((c) => id(c)).join(",");
	const valuesSql = columnNames.length > 0 ? `(${"?,".repeat(columnNames.length - 1)}?)` : "()";
	const insertSql = `
    INSERT INTO "${tableName}" (${insertColumnList}) VALUES ${valuesSql}`;
	const insertStmt = to.prepare(insertSql);
	const insertBatchStmt = to.prepare(insertSql + `,${valuesSql}`.repeat(49));
	const filterConditions = Object.values(table.publications).map(({ rowFilter }) => rowFilter).filter((f) => !!f);
	const where = filterConditions.length === 0 ? "" : `WHERE ${filterConditions.join(" OR ")}`;
	const fromTable = `FROM ${id(table.schema)}.${id(table.name)} ${where}`;
	const select = `SELECT ${orderedColumns.map(([name, spec]) => hasBinaryDecoder(spec) ? id(name) : `${id(name)}::text`).join(",")} ${fromTable}`;
	const decoders = orderedColumns.map(([, spec]) => hasBinaryDecoder(spec) ? makeBinaryDecoder(spec) : textCastDecoder);
	const valuesPerRow = columnSpecs.length;
	const valuesPerBatch = valuesPerRow * 50;
	const pendingValues = Array.from({ length: MAX_BUFFERED_ROWS * valuesPerRow });
	let pendingRows = 0;
	let pendingSize = 0;
	function flush() {
		const start = performance.now();
		const flushedRows = pendingRows;
		const flushedSize = pendingSize;
		let l = 0;
		for (; pendingRows > 50; pendingRows -= 50) insertBatchStmt.run(pendingValues.slice(l, l += valuesPerBatch));
		for (; pendingRows > 0; pendingRows--) insertStmt.run(pendingValues.slice(l, l += valuesPerRow));
		const flushedValues = flushedRows * valuesPerRow;
		for (let i = 0; i < flushedValues; i++) pendingValues[i] = void 0;
		pendingSize = 0;
		status.rows += flushedRows;
		const elapsed = performance.now() - start;
		flushTime += elapsed;
		lc.debug?.(`flushed ${flushedRows} ${tableName} rows (${flushedSize} bytes) in ${elapsed.toFixed(3)} ms`);
	}
	const binaryParser = new BinaryCopyParser();
	let col = 0;
	lc.info?.(`Starting binary copy stream of ${tableName}:`, select);
	await pipeline$1(await from.unsafe(`COPY (${select}) TO STDOUT WITH (FORMAT binary)`).readable(), new Writable({
		highWaterMark: BUFFERED_SIZE_THRESHOLD,
		write(chunk, _encoding, callback) {
			try {
				for (const fieldBuf of binaryParser.parse(chunk)) {
					pendingSize += fieldBuf === null ? 4 : fieldBuf.length;
					pendingValues[pendingRows * valuesPerRow + col] = fieldBuf === null ? null : decoders[col](fieldBuf);
					if (++col === decoders.length) {
						col = 0;
						if (++pendingRows >= MAX_BUFFERED_ROWS - valuesPerRow || pendingSize >= BUFFERED_SIZE_THRESHOLD) flush();
					}
				}
				callback();
			} catch (e) {
				callback(e instanceof Error ? e : new Error(String(e)));
			}
		},
		final: (callback) => {
			try {
				flush();
				callback();
			} catch (e) {
				callback(e instanceof Error ? e : new Error(String(e)));
			}
		}
	}));
	const elapsed = performance.now() - start;
	lc.info?.(`Finished copying ${status.rows} rows into ${tableName} (flush: ${flushTime.toFixed(3)} ms) (total: ${elapsed.toFixed(3)} ms) `);
	return {
		rows: status.rows,
		flushTime
	};
}
async function copyText(lc, table, status, dbClient, from, to) {
	const start = performance.now();
	let flushTime = 0;
	const tableName = liteTableName(table);
	const orderedColumns = Object.entries(table.columns);
	const columnNames = orderedColumns.map(([c]) => c);
	const columnSpecs = orderedColumns.map(([_name, spec]) => spec);
	const insertColumnList = columnNames.map((c) => id(c)).join(",");
	const valuesSql = columnNames.length > 0 ? `(${"?,".repeat(columnNames.length - 1)}?)` : "()";
	const insertSql = `
    INSERT INTO "${tableName}" (${insertColumnList}) VALUES ${valuesSql}`;
	const insertStmt = to.prepare(insertSql);
	const insertBatchStmt = to.prepare(insertSql + `,${valuesSql}`.repeat(49));
	const { select } = makeDownloadStatements(table, columnNames);
	const valuesPerRow = columnSpecs.length;
	const valuesPerBatch = valuesPerRow * 50;
	const pendingValues = Array.from({ length: MAX_BUFFERED_ROWS * valuesPerRow });
	let pendingRows = 0;
	let pendingSize = 0;
	function flush() {
		const start = performance.now();
		const flushedRows = pendingRows;
		const flushedSize = pendingSize;
		let l = 0;
		for (; pendingRows > 50; pendingRows -= 50) insertBatchStmt.run(pendingValues.slice(l, l += valuesPerBatch));
		for (; pendingRows > 0; pendingRows--) insertStmt.run(pendingValues.slice(l, l += valuesPerRow));
		const flushedValues = flushedRows * valuesPerRow;
		for (let i = 0; i < flushedValues; i++) pendingValues[i] = void 0;
		pendingSize = 0;
		status.rows += flushedRows;
		const elapsed = performance.now() - start;
		flushTime += elapsed;
		lc.debug?.(`flushed ${flushedRows} ${tableName} rows (${flushedSize} bytes) in ${elapsed.toFixed(3)} ms`);
	}
	lc.info?.(`Starting text copy stream of ${tableName}:`, select);
	const pgParsers = await getTypeParsers(dbClient, { returnJsonAsString: true });
	const parsers = columnSpecs.map((c) => {
		const pgParse = pgParsers.getTypeParser(c.typeOID);
		return (val) => liteValue(pgParse(val), c.dataType, "s");
	});
	const tsvParser = new TsvParser();
	let col = 0;
	await pipeline$1(await from.unsafe(`COPY (${select}) TO STDOUT`).readable(), new Writable({
		highWaterMark: BUFFERED_SIZE_THRESHOLD,
		write(chunk, _encoding, callback) {
			try {
				for (const text of tsvParser.parse(chunk)) {
					pendingSize += text === null ? 4 : text.length;
					pendingValues[pendingRows * valuesPerRow + col] = text === null ? null : parsers[col](text);
					if (++col === parsers.length) {
						col = 0;
						if (++pendingRows >= MAX_BUFFERED_ROWS - valuesPerRow || pendingSize >= BUFFERED_SIZE_THRESHOLD) flush();
					}
				}
				callback();
			} catch (e) {
				callback(e instanceof Error ? e : new Error(String(e)));
			}
		},
		final: (callback) => {
			try {
				flush();
				callback();
			} catch (e) {
				callback(e instanceof Error ? e : new Error(String(e)));
			}
		}
	}));
	const elapsed = performance.now() - start;
	lc.info?.(`Finished copying ${status.rows} rows into ${tableName} (flush: ${flushTime.toFixed(3)} ms) (total: ${elapsed.toFixed(3)} ms) `);
	return {
		rows: status.rows,
		flushTime
	};
}
//#endregion
export { createReplicationSlot, initialSync, makeDownloadStatements };

//# sourceMappingURL=initial-sync.js.map