import { assert, assertObject } from "../../../shared/src/asserts.js";
import { hasOwn } from "../../../shared/src/has-own.js";
import { readonlyObject, readonlyArray, assert as assert$1 } from "../../../shared/src/valita.js";
import { emptyDataNode } from "../btree/node.js";
import { BTreeRead } from "../btree/read.js";
import { compareCookies } from "../cookies.js";
import { toRefs } from "../dag/chunk.js";
import { toChunkIndexDefinition, newSnapshotCommitDataDD31, getRefs, chunkIndexDefinitionEqualIgnoreName, baseSnapshotFromHash, assertSnapshotCommitDD31 } from "../db/commit.js";
import { createIndexBTree } from "../db/write.js";
import { deepFreeze } from "../frozen-json.js";
import { hashSchema } from "../hash.js";
import { indexDefinitionsEqual } from "../index-defs.js";
import { clientGroupIDSchema } from "../sync/ids.js";
import { withWrite } from "../with-transactions.js";
import { setClientGroup, getClientGroups, mutatorNamesEqual, getClientGroup } from "./client-groups.js";
import { makeClientID } from "./make-client-id.js";
import { number, union } from "@badrap/valita";
const clientV5Schema = readonlyObject({
  heartbeatTimestampMs: number(),
  headHash: hashSchema,
  /**
   * The hash of a commit we are in the middle of refreshing into this client's
   * memdag.
   */
  tempRefreshHash: hashSchema.nullable(),
  /**
   * ID of this client's perdag client group. This needs to be sent in pull
   * request (to enable syncing all last mutation ids in the client group).
   */
  clientGroupID: clientGroupIDSchema
});
const clientV6Schema = readonlyObject({
  heartbeatTimestampMs: number(),
  /**
   * A set of hashes, which contains:
   * 1. The hash of the last commit this client refreshed from its client group
   *    (this is the commit it bootstrapped from until it completes its first
   *    refresh).
   * 2. One or more hashes that were added to retain chunks of a commit while it
   *    was being refreshed into this client's memdag. (This can be one or more
   *    because refresh's cleanup step is a separate transaction and can fail).
   * Upon refresh completing and successfully running its clean up step, this
   * set will contain a single hash: the hash of the last commit this client
   * refreshed.
   */
  refreshHashes: readonlyArray(hashSchema),
  /**
   * The hash of the last snapshot commit persisted by this client to this
   * client's client group, or null if has never persisted a snapshot.
   */
  persistHash: hashSchema.nullable(),
  /**
   * ID of this client's perdag client group. This needs to be sent in pull
   * request (to enable syncing all last mutation ids in the client group).
   */
  clientGroupID: clientGroupIDSchema
});
function isClientV6(client) {
  return client.refreshHashes !== void 0;
}
const CLIENTS_HEAD_NAME = "clients";
const clientSchema = union(clientV5Schema, clientV6Schema);
function assertClient(value) {
  assert$1(value, clientSchema);
}
function assertClientV6(value) {
  assert$1(value, clientV6Schema);
}
function chunkDataToClientMap(chunkData) {
  assertObject(chunkData);
  const clients = /* @__PURE__ */ new Map();
  for (const key in chunkData) {
    if (hasOwn(chunkData, key)) {
      const value = chunkData[key];
      if (value !== void 0) {
        assertClient(value);
        clients.set(key, value);
      }
    }
  }
  return clients;
}
function clientMapToChunkData(clients, dagWrite) {
  for (const client of clients.values()) {
    if (isClientV6(client)) {
      client.refreshHashes.forEach(dagWrite.assertValidHash);
      if (client.persistHash) {
        dagWrite.assertValidHash(client.persistHash);
      }
    } else {
      dagWrite.assertValidHash(client.headHash);
      if (client.tempRefreshHash) {
        dagWrite.assertValidHash(client.tempRefreshHash);
      }
    }
  }
  return deepFreeze(Object.fromEntries(clients));
}
async function getClients(dagRead) {
  const hash = await dagRead.getHead(CLIENTS_HEAD_NAME);
  return getClientsAtHash(hash, dagRead);
}
async function getClientsAtHash(hash, dagRead) {
  if (!hash) {
    return /* @__PURE__ */ new Map();
  }
  const chunk = await dagRead.getChunk(hash);
  return chunkDataToClientMap(chunk?.data);
}
class ClientStateNotFoundError extends Error {
  name = "ClientStateNotFoundError";
  id;
  constructor(id) {
    super(`Client state not found, id: ${id}`);
    this.id = id;
  }
}
async function assertHasClientState(id, dagRead) {
  if (!await hasClientState(id, dagRead)) {
    throw new ClientStateNotFoundError(id);
  }
}
async function hasClientState(id, dagRead) {
  return !!await getClient(id, dagRead);
}
async function getClient(id, dagRead) {
  const clients = await getClients(dagRead);
  return clients.get(id);
}
async function mustGetClient(id, dagRead) {
  const client = await getClient(id, dagRead);
  if (!client) {
    throw new ClientStateNotFoundError(id);
  }
  return client;
}
function initClientV6(newClientID, lc, perdag, mutatorNames, indexes, formatVersion, enableClientGroupForking) {
  return withWrite(perdag, async (dagWrite) => {
    async function setClientsAndClientGroupAndCommit(basisHash, cookieJSON, valueHash2, indexRecords2) {
      const newSnapshotData = newSnapshotCommitDataDD31(
        basisHash,
        {},
        cookieJSON,
        valueHash2,
        indexRecords2
      );
      const chunk = dagWrite.createChunk(
        newSnapshotData,
        getRefs(newSnapshotData)
      );
      const newClientGroupID = makeClientID();
      const newClient = {
        heartbeatTimestampMs: Date.now(),
        refreshHashes: [chunk.hash],
        persistHash: null,
        clientGroupID: newClientGroupID
      };
      const newClients = new Map(clients).set(newClientID, newClient);
      const clientGroup = {
        headHash: chunk.hash,
        mutatorNames,
        indexes,
        mutationIDs: {},
        lastServerAckdMutationIDs: {},
        disabled: false
      };
      await Promise.all([
        dagWrite.putChunk(chunk),
        setClients(newClients, dagWrite),
        setClientGroup(newClientGroupID, clientGroup, dagWrite)
      ]);
      return [newClient, chunk.hash, newClients, true];
    }
    const clients = await getClients(dagWrite);
    const res = await findMatchingClient(dagWrite, mutatorNames, indexes);
    if (res.type === FIND_MATCHING_CLIENT_TYPE_HEAD) {
      const { clientGroupID, headHash } = res;
      const newClient = {
        clientGroupID,
        refreshHashes: [headHash],
        heartbeatTimestampMs: Date.now(),
        persistHash: null
      };
      const newClients = new Map(clients).set(newClientID, newClient);
      await setClients(newClients, dagWrite);
      return [newClient, headHash, newClients, false];
    }
    if (!enableClientGroupForking || res.type === FIND_MATCHING_CLIENT_TYPE_NEW) {
      const emptyBTreeChunk = dagWrite.createChunk(emptyDataNode, []);
      await dagWrite.putChunk(emptyBTreeChunk);
      const indexRecords2 = [];
      for (const [name, indexDefinition] of Object.entries(indexes)) {
        const chunkIndexDefinition = toChunkIndexDefinition(
          name,
          indexDefinition
        );
        indexRecords2.push({
          definition: chunkIndexDefinition,
          valueHash: emptyBTreeChunk.hash
        });
      }
      return setClientsAndClientGroupAndCommit(
        null,
        null,
        emptyBTreeChunk.hash,
        indexRecords2
      );
    }
    assert(res.type === FIND_MATCHING_CLIENT_TYPE_FORK);
    const { snapshot } = res;
    const indexRecords = [];
    const { valueHash, indexes: oldIndexes } = snapshot;
    const map = new BTreeRead(dagWrite, formatVersion, valueHash);
    for (const [name, indexDefinition] of Object.entries(indexes)) {
      const { prefix = "", jsonPointer, allowEmpty = false } = indexDefinition;
      const chunkIndexDefinition = {
        name,
        keyPrefix: prefix,
        jsonPointer,
        allowEmpty
      };
      const oldIndex = findMatchingOldIndex(oldIndexes, chunkIndexDefinition);
      if (oldIndex) {
        indexRecords.push({
          definition: chunkIndexDefinition,
          valueHash: oldIndex.valueHash
        });
      } else {
        const indexBTree = await createIndexBTree(
          lc,
          dagWrite,
          map,
          prefix,
          jsonPointer,
          allowEmpty,
          formatVersion
        );
        indexRecords.push({
          definition: chunkIndexDefinition,
          valueHash: await indexBTree.flush()
        });
      }
    }
    return setClientsAndClientGroupAndCommit(
      snapshot.meta.basisHash,
      snapshot.meta.cookieJSON,
      snapshot.valueHash,
      indexRecords
    );
  });
}
function findMatchingOldIndex(oldIndexes, chunkIndexDefinition) {
  return oldIndexes.find(
    (index) => chunkIndexDefinitionEqualIgnoreName(index.definition, chunkIndexDefinition)
  );
}
const FIND_MATCHING_CLIENT_TYPE_NEW = 0;
const FIND_MATCHING_CLIENT_TYPE_FORK = 1;
const FIND_MATCHING_CLIENT_TYPE_HEAD = 2;
async function findMatchingClient(dagRead, mutatorNames, indexes) {
  let newestCookie;
  let bestSnapshot;
  const mutatorNamesSet = new Set(mutatorNames);
  const clientGroups = await getClientGroups(dagRead);
  for (const [clientGroupID, clientGroup] of clientGroups) {
    if (!clientGroup.disabled && mutatorNamesEqual(mutatorNamesSet, clientGroup.mutatorNames) && indexDefinitionsEqual(indexes, clientGroup.indexes)) {
      return {
        type: FIND_MATCHING_CLIENT_TYPE_HEAD,
        clientGroupID,
        headHash: clientGroup.headHash
      };
    }
    const clientGroupSnapshotCommit = await baseSnapshotFromHash(
      clientGroup.headHash,
      dagRead
    );
    assertSnapshotCommitDD31(clientGroupSnapshotCommit);
    const { cookieJSON } = clientGroupSnapshotCommit.meta;
    if (newestCookie === void 0 || compareCookies(cookieJSON, newestCookie) > 0) {
      newestCookie = cookieJSON;
      bestSnapshot = clientGroupSnapshotCommit;
    }
  }
  if (bestSnapshot) {
    return {
      type: FIND_MATCHING_CLIENT_TYPE_FORK,
      snapshot: bestSnapshot
    };
  }
  return { type: FIND_MATCHING_CLIENT_TYPE_NEW };
}
function getRefsForClients(clients) {
  const refs = /* @__PURE__ */ new Set();
  for (const client of clients.values()) {
    if (isClientV6(client)) {
      for (const hash of client.refreshHashes) {
        refs.add(hash);
      }
      if (client.persistHash) {
        refs.add(client.persistHash);
      }
    } else {
      refs.add(client.headHash);
      if (client.tempRefreshHash) {
        refs.add(client.tempRefreshHash);
      }
    }
  }
  return toRefs(refs);
}
async function getClientGroupForClient(clientID, read) {
  const clientGroupID = await getClientGroupIDForClient(clientID, read);
  if (!clientGroupID) {
    return void 0;
  }
  return getClientGroup(clientGroupID, read);
}
async function getClientGroupIDForClient(clientID, read) {
  const client = await getClient(clientID, read);
  return client?.clientGroupID;
}
async function setClient(clientID, client, dagWrite) {
  const clients = await getClients(dagWrite);
  const newClients = new Map(clients).set(clientID, client);
  return setClients(newClients, dagWrite);
}
async function setClients(clients, dagWrite) {
  const chunkData = clientMapToChunkData(clients, dagWrite);
  const chunk = dagWrite.createChunk(chunkData, getRefsForClients(clients));
  await dagWrite.putChunk(chunk);
  await dagWrite.setHead(CLIENTS_HEAD_NAME, chunk.hash);
  return chunk.hash;
}
export {
  CLIENTS_HEAD_NAME,
  ClientStateNotFoundError,
  FIND_MATCHING_CLIENT_TYPE_FORK,
  FIND_MATCHING_CLIENT_TYPE_HEAD,
  FIND_MATCHING_CLIENT_TYPE_NEW,
  assertClientV6,
  assertHasClientState,
  findMatchingClient,
  getClient,
  getClientGroupForClient,
  getClientGroupIDForClient,
  getClients,
  hasClientState,
  initClientV6,
  mustGetClient,
  setClient,
  setClients
};
//# sourceMappingURL=clients.js.map
