import { assert } from "../../../shared/src/asserts.js";
import { deepEqual } from "../../../shared/src/json.js";
import { compareCookies } from "../cookies.js";
import { isErrorResponse } from "../error-responses.js";
import { assertPullerResultV1 } from "../get-default-puller.js";
import { deepFreeze } from "../frozen-json.js";
import { emptyHash } from "../hash.js";
import { BTreeRead } from "../btree/read.js";
import { DEFAULT_HEAD_NAME, assertSnapshotMetaDD31, baseSnapshotFromHash, commitFromHash, commitIsLocalDD31, localMutations, snapshotMetaParts } from "../db/commit.js";
import { diff } from "../btree/diff.js";
import { DiffsMap, addDiffsForIndexes } from "./diff.js";
import { newWriteSnapshotDD31 } from "../db/write.js";
import { withRead, withWriteNoImplicitCommit } from "../with-transactions.js";
import { ReportError } from "../report-error.js";
import { toError } from "../to-error.js";
import { apply } from "./patch.js";
import { PullError } from "./pull-error.js";
import { SYNC_HEAD_NAME } from "./sync-head-name.js";
async function beginPullV1(profileID, clientID, clientGroupID, schemaVersion, puller, requestID, store, formatVersion, lc, createSyncBranch = true) {
	const baseCookie = await withRead(store, async (dagRead) => {
		const mainHeadHash = await dagRead.getHead(DEFAULT_HEAD_NAME);
		if (!mainHeadHash) throw new Error("Internal no main head found");
		const baseSnapshotMeta = (await baseSnapshotFromHash(mainHeadHash, dagRead)).meta;
		assertSnapshotMetaDD31(baseSnapshotMeta);
		return baseSnapshotMeta.cookieJSON;
	});
	const { response, httpRequestInfo } = await callPuller(lc, puller, {
		profileID,
		clientGroupID,
		cookie: baseCookie,
		pullVersion: 1,
		schemaVersion
	}, requestID);
	if (!response) return {
		httpRequestInfo,
		syncHead: emptyHash
	};
	if (!createSyncBranch || isErrorResponse(response)) return {
		httpRequestInfo,
		pullResponse: response,
		syncHead: emptyHash
	};
	const result = await handlePullResponseV1(lc, store, baseCookie, response, clientID, formatVersion);
	return {
		httpRequestInfo,
		pullResponse: response,
		syncHead: result.type === 0 ? result.syncHead : emptyHash
	};
}
async function callPuller(lc, puller, pullReq, requestID) {
	lc.debug?.("Starting pull...");
	const pullStart = Date.now();
	let pullerResult;
	try {
		pullerResult = await puller(pullReq, requestID);
		lc.debug?.(`...Pull ${pullerResult.response ? "complete" : "failed"} in `, Date.now() - pullStart, "ms");
	} catch (e) {
		throw new PullError(toError(e));
	}
	try {
		assertPullerResultV1(pullerResult);
		return pullerResult;
	} catch (e) {
		throw new ReportError("Invalid puller result", toError(e));
	}
}
function badOrderMessage(name, receivedValue, lastSnapshotValue) {
	return `Received ${name} ${receivedValue} is < than last snapshot ${name} ${lastSnapshotValue}; ignoring client view`;
}
function handlePullResponseV1(lc, store, expectedBaseCookie, response, clientID, formatVersion) {
	return withWriteNoImplicitCommit(store, async (dagWrite) => {
		const dagRead = dagWrite;
		const mainHead = await dagRead.getHead(DEFAULT_HEAD_NAME);
		if (mainHead === void 0) throw new Error("Main head disappeared");
		const baseSnapshot = await baseSnapshotFromHash(mainHead, dagRead);
		const baseSnapshotMeta = baseSnapshot.meta;
		assertSnapshotMetaDD31(baseSnapshotMeta);
		const baseCookie = baseSnapshotMeta.cookieJSON;
		if (!deepEqual(expectedBaseCookie, baseCookie)) {
			lc.debug?.("handlePullResponse: cookie mismatch, response is not applicable");
			return { type: 2 };
		}
		for (const [clientID, lmidChange] of Object.entries(response.lastMutationIDChanges)) {
			const lastMutationID = baseSnapshotMeta.lastMutationIDs[clientID];
			if (lastMutationID !== void 0 && lmidChange < lastMutationID) throw new Error(badOrderMessage(`${clientID} lastMutationID`, String(lmidChange), String(lastMutationID)));
		}
		const frozenResponseCookie = deepFreeze(response.cookie);
		if (compareCookies(frozenResponseCookie, baseCookie) < 0) throw new Error(badOrderMessage("cookie", JSON.stringify(frozenResponseCookie), JSON.stringify(baseCookie)));
		if (deepEqual(frozenResponseCookie, baseCookie)) {
			if (response.patch.length > 0) lc.error?.(`handlePullResponse: cookie ${JSON.stringify(baseCookie)} did not change, but patch is not empty`);
			if (Object.keys(response.lastMutationIDChanges).length > 0) lc.error?.(`handlePullResponse: cookie ${JSON.stringify(baseCookie)} did not change, but lastMutationIDChanges is not empty`);
			return { type: 1 };
		}
		const dbWrite = await newWriteSnapshotDD31(baseSnapshot.chunk.hash, {
			...baseSnapshotMeta.lastMutationIDs,
			...response.lastMutationIDChanges
		}, frozenResponseCookie, dagWrite, clientID, formatVersion);
		await apply(lc, dbWrite, response.patch);
		return {
			type: 0,
			syncHead: await dbWrite.commit(SYNC_HEAD_NAME)
		};
	});
}
function maybeEndPull(store, lc, expectedSyncHead, clientID, diffConfig, formatVersion) {
	return withWriteNoImplicitCommit(store, async (dagWrite) => {
		const dagRead = dagWrite;
		const syncHeadHash = await dagRead.getHead(SYNC_HEAD_NAME);
		if (syncHeadHash === void 0) throw new Error("Missing sync head");
		if (syncHeadHash !== expectedSyncHead) {
			lc.error?.("maybeEndPull, Wrong sync head. Expecting:", expectedSyncHead, "got:", syncHeadHash);
			throw new Error("Wrong sync head");
		}
		const syncSnapshot = await baseSnapshotFromHash(syncHeadHash, dagRead);
		const mainHeadHash = await dagRead.getHead(DEFAULT_HEAD_NAME);
		if (mainHeadHash === void 0) throw new Error("Missing main head");
		const mainSnapshot = await baseSnapshotFromHash(mainHeadHash, dagRead);
		const { meta } = syncSnapshot;
		const syncSnapshotBasis = meta.basisHash;
		if (syncSnapshot === null) throw new Error("Sync snapshot with no basis");
		if (syncSnapshotBasis !== mainSnapshot.chunk.hash) throw new Error("Overlapping syncs");
		const syncHead = await commitFromHash(syncHeadHash, dagRead);
		const pending = [];
		const localMutations$1 = await localMutations(mainHeadHash, dagRead);
		for (const commit of localMutations$1) {
			let cid = clientID;
			assert(commitIsLocalDD31(commit), "Expected commit to be a local DD31 commit");
			cid = commit.meta.clientID;
			if (await commit.getMutationID(cid, dagRead) > await syncHead.getMutationID(cid, dagRead)) pending.push(commit);
		}
		pending.reverse();
		const diffsMap = new DiffsMap();
		if (pending.length > 0) return {
			syncHead: syncHeadHash,
			oldMainHead: mainHeadHash,
			mainHead: mainHeadHash,
			replayMutations: pending,
			diffs: diffsMap
		};
		const mainHead = await commitFromHash(mainHeadHash, dagRead);
		if (diffConfig.shouldComputeDiffs()) {
			const valueDiff = await diff(new BTreeRead(dagRead, formatVersion, mainHead.valueHash), new BTreeRead(dagRead, formatVersion, syncHead.valueHash));
			diffsMap.set("", valueDiff);
			await addDiffsForIndexes(mainHead, syncHead, dagRead, diffsMap, diffConfig, formatVersion);
		}
		await Promise.all([dagWrite.setHead(DEFAULT_HEAD_NAME, syncHeadHash), dagWrite.removeHead(SYNC_HEAD_NAME)]);
		await dagWrite.commit();
		const newMainHeadHash = syncHeadHash;
		if (lc.debug) {
			const [oldLastMutationID, oldCookie] = snapshotMetaParts(mainSnapshot, clientID);
			const [newLastMutationID, newCookie] = snapshotMetaParts(syncSnapshot, clientID);
			lc.debug(`Successfully pulled new snapshot with lastMutationID:`, newLastMutationID, `(prev:`, oldLastMutationID, `), cookie: `, newCookie, `(prev:`, oldCookie, `), sync head hash:`, syncHeadHash, ", main head hash:", mainHeadHash, `, valueHash:`, syncHead.valueHash, `(prev:`, mainSnapshot.valueHash);
		}
		return {
			syncHead: syncHeadHash,
			oldMainHead: mainHeadHash,
			mainHead: newMainHeadHash,
			replayMutations: [],
			diffs: diffsMap
		};
	});
}
//#endregion
export { beginPullV1, handlePullResponseV1, maybeEndPull };

//# sourceMappingURL=pull.js.map