import { assert } from "../../../shared/src/asserts.js";
import { deepEqual } from "../../../shared/src/json.js";
import { diff } from "../btree/diff.js";
import { BTreeRead } from "../btree/read.js";
import { compareCookies } from "../cookies.js";
import { DEFAULT_HEAD_NAME, baseSnapshotFromHash, assertSnapshotMetaDD31, commitFromHash, localMutations, commitIsLocalDD31, snapshotMetaParts } from "../db/commit.js";
import { newWriteSnapshotDD31 } from "../db/write.js";
import { isErrorResponse } from "../error-responses.js";
import { deepFreeze } from "../frozen-json.js";
import { assertPullerResultV1 } from "../get-default-puller.js";
import { emptyHash } from "../hash.js";
import { ReportError } from "../report-error.js";
import { toError } from "../to-error.js";
import { withWriteNoImplicitCommit, withRead } from "../with-transactions.js";
import { DiffsMap, addDiffsForIndexes } from "./diff.js";
import { Applied, CookieMismatch, NoOp } from "./handle-pull-response-result-type-enum.js";
import { apply } from "./patch.js";
import { PullError } from "./pull-error.js";
import { SYNC_HEAD_NAME } from "./sync-head-name.js";
const PULL_VERSION_DD31 = 1;
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 baseSnapshot = await baseSnapshotFromHash(mainHeadHash, dagRead);
    const baseSnapshotMeta = baseSnapshot.meta;
    assertSnapshotMetaDD31(baseSnapshotMeta);
    return baseSnapshotMeta.cookieJSON;
  });
  const pullReq = {
    profileID,
    clientGroupID,
    cookie: baseCookie,
    pullVersion: PULL_VERSION_DD31,
    schemaVersion
  };
  const { response, httpRequestInfo } = await callPuller(
    lc,
    puller,
    pullReq,
    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 === Applied ? 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: CookieMismatch
      };
    }
    for (const [clientID2, lmidChange] of Object.entries(
      response.lastMutationIDChanges
    )) {
      const lastMutationID = baseSnapshotMeta.lastMutationIDs[clientID2];
      if (lastMutationID !== void 0 && lmidChange < lastMutationID) {
        throw new Error(
          badOrderMessage(
            `${clientID2} 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: NoOp
      };
    }
    const dbWrite = await newWriteSnapshotDD31(
      baseSnapshot.chunk.hash,
      { ...baseSnapshotMeta.lastMutationIDs, ...response.lastMutationIDChanges },
      frozenResponseCookie,
      dagWrite,
      clientID,
      formatVersion
    );
    await apply(lc, dbWrite, response.patch);
    return {
      type: Applied,
      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));
      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,
        // The changed keys are not reported when further replays are
        // needed. The diffs will be reported at the end when there
        // are no more mutations to be replay and then it will be reported
        // relative to DEFAULT_HEAD_NAME.
        diffs: diffsMap
      };
    }
    const mainHead = await commitFromHash(mainHeadHash, dagRead);
    if (diffConfig.shouldComputeDiffs()) {
      const mainHeadMap = new BTreeRead(
        dagRead,
        formatVersion,
        mainHead.valueHash
      );
      const syncHeadMap = new BTreeRead(
        dagRead,
        formatVersion,
        syncHead.valueHash
      );
      const valueDiff = await diff(mainHeadMap, syncHeadMap);
      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
    };
  });
}
export {
  PULL_VERSION_DD31,
  beginPullV1,
  handlePullResponseV1,
  maybeEndPull
};
//# sourceMappingURL=pull.js.map
