import { readFromHash } from "../../../../replicache/src/db/read.js";
import { Latest } from "../../../../replicache/src/format-version-enum.js";
import { getClientGroup } from "../../../../replicache/src/persist/client-groups.js";
import { getClients, getClient } from "../../../../replicache/src/persist/clients.js";
import { withRead } from "../../../../replicache/src/with-transactions.js";
import { assert } from "../../../../shared/src/asserts.js";
import { mapValues } from "../../../../shared/src/objects.js";
import { TDigest } from "../../../../shared/src/tdigest.js";
import { test } from "../../../../shared/src/valita.js";
import { inspectMetricsDownSchema, inspectVersionDownSchema, inspectAnalyzeQueryDownSchema, inspectQueriesDownSchema, inspectAuthenticatedDownSchema } from "../../../../zero-protocol/src/inspect-down.js";
import { asQueryInternals } from "../../../../zql/src/query/query-internals.js";
import { nanoid } from "../../util/nanoid.js";
import { ENTITIES_KEY_PREFIX } from "../keys.js";
import { Client } from "./client.js";
import { createHTMLPasswordPrompt } from "./html-dialog-prompt.js";
import { Query } from "./query.js";
async function rpc(socket, arg, downSchema) {
  try {
    return await rpcNoAuthTry(socket, arg, downSchema);
  } catch (e) {
    if (e instanceof UnauthenticatedError) {
      const password = await createHTMLPasswordPrompt("Enter password:");
      if (password) {
        const authRes = await rpcNoAuthTry(
          socket,
          { op: "authenticate", value: password },
          inspectAuthenticatedDownSchema
        );
        if (authRes) {
          return rpcNoAuthTry(socket, arg, downSchema);
        }
      }
      throw new Error("Authentication failed");
    }
    throw e;
  }
}
function rpcNoAuthTry(socket, arg, downSchema) {
  return new Promise((resolve, reject) => {
    const id = nanoid();
    const f = (ev) => {
      const msg = JSON.parse(ev.data);
      if (msg[0] === "inspect") {
        const body = msg[1];
        if (body.id !== id) {
          return;
        }
        const res = test(body, downSchema);
        if (res.ok) {
          if (res.value.op === "error") {
            reject(new Error(res.value.value));
          } else {
            resolve(res.value.value);
          }
        } else {
          const authRes = test(body, inspectAuthenticatedDownSchema);
          if (authRes.ok) {
            assert(
              authRes.value.value === false,
              "Expected unauthenticated response"
            );
            reject(new UnauthenticatedError());
          }
          reject(res.error);
        }
        socket.removeEventListener("message", f);
      }
    };
    socket.addEventListener("message", f);
    socket.send(JSON.stringify(["inspect", { ...arg, id }]));
  });
}
function mergeMetrics(clientMetrics, serverMetrics) {
  return {
    ...clientMetrics ?? newClientMetrics(),
    ...serverMetrics ? convertServerMetrics(serverMetrics) : newServerMetrics()
  };
}
function newClientMetrics() {
  return {
    "query-materialization-client": new TDigest(),
    "query-materialization-end-to-end": new TDigest(),
    "query-update-client": new TDigest()
  };
}
function newServerMetrics() {
  return {
    "query-materialization-server": new TDigest(),
    "query-update-server": new TDigest()
  };
}
function convertServerMetrics(metrics) {
  return mapValues(metrics, (v) => TDigest.fromJSON(v));
}
async function inspectorMetrics(delegate) {
  const clientMetrics = delegate.metrics;
  const serverMetricsJSON = await rpc(
    await delegate.getSocket(),
    { op: "metrics" },
    inspectMetricsDownSchema
  );
  return mergeMetrics(clientMetrics, serverMetricsJSON);
}
function inspectorClients(delegate) {
  return withDagRead(delegate, (dagRead) => clients(delegate, dagRead));
}
function inspectorClientsWithQueries(delegate) {
  return withDagRead(
    delegate,
    (dagRead) => clientsWithQueries(delegate, dagRead)
  );
}
async function withDagRead(delegate, f) {
  const { rep } = delegate;
  await rep.refresh();
  await rep.persist();
  return withRead(rep.perdag, f);
}
async function getBTree(dagRead, clientID) {
  const client = await getClient(clientID, dagRead);
  assert(client, `Client not found: ${clientID}`);
  const { clientGroupID } = client;
  const clientGroup = await getClientGroup(clientGroupID, dagRead);
  assert(clientGroup, `Client group not found: ${clientGroupID}`);
  const dbRead = await readFromHash(
    clientGroup.headHash,
    dagRead,
    Latest
  );
  return dbRead.map;
}
async function clients(delegate, dagRead, predicate = () => true) {
  const clients2 = await getClients(dagRead);
  return [...clients2.entries()].filter(predicate).map(
    ([clientID, { clientGroupID }]) => new Client(delegate, clientID, clientGroupID)
  );
}
async function clientsWithQueries(delegate, dagRead, predicate = () => true) {
  const allClients = await clients(delegate, dagRead, predicate);
  const clientsWithQueries2 = [];
  await Promise.all(
    allClients.map(async (client) => {
      const queries2 = await client.queries();
      if (queries2.length > 0) {
        clientsWithQueries2.push(client);
      }
    })
  );
  return clientsWithQueries2;
}
async function clientGroupClients(delegate, clientGroupID) {
  const id = await clientGroupID;
  return withDagRead(
    delegate,
    (dagRead) => clients(delegate, dagRead, ([_, v]) => v.clientGroupID === id)
  );
}
async function clientGroupClientsWithQueries(delegate, clientGroupID) {
  const id = await clientGroupID;
  return withDagRead(
    delegate,
    (dagRead) => clientsWithQueries(delegate, dagRead, ([_, v]) => v.clientGroupID === id)
  );
}
function clientGroupQueries(delegate) {
  return queries(delegate, { op: "queries" });
}
function clientMap(delegate, clientID) {
  return withDagRead(delegate, async (dagRead) => {
    const tree = await getBTree(dagRead, clientID);
    const map = /* @__PURE__ */ new Map();
    for await (const [key, value] of tree.scan("")) {
      map.set(key, value);
    }
    return map;
  });
}
function clientRows(delegate, clientID, tableName) {
  return withDagRead(delegate, async (dagRead) => {
    const prefix = ENTITIES_KEY_PREFIX + tableName + "/";
    const tree = await getBTree(dagRead, clientID);
    const rows = [];
    for await (const [key, value] of tree.scan(prefix)) {
      if (!key.startsWith(prefix)) {
        break;
      }
      rows.push(value);
    }
    return rows;
  });
}
async function serverVersion(delegate) {
  return rpc(
    await delegate.getSocket(),
    { op: "version" },
    inspectVersionDownSchema
  );
}
function clientQueries(delegate, clientID) {
  return queries(delegate, { op: "queries", clientID });
}
async function queries(delegate, arg) {
  const rows = await rpc(
    await delegate.getSocket(),
    arg,
    inspectQueriesDownSchema
  );
  const queries2 = rows.map((row) => new Query(row, delegate, delegate.getSocket));
  queries2.sort((a, b) => (b.hydrateServer ?? 0) - (a.hydrateServer ?? 0));
  return queries2;
}
async function analyzeQuery(delegate, query, options) {
  const qi = asQueryInternals(query);
  const { customQueryID } = qi;
  const queryParameters = customQueryID ? { name: customQueryID.name, args: customQueryID.args } : { ast: delegate.mapClientASTToServer(qi.ast) };
  return rpc(
    await delegate.getSocket(),
    {
      op: "analyze-query",
      ...queryParameters,
      options
    },
    inspectAnalyzeQueryDownSchema
  );
}
class UnauthenticatedError extends Error {
}
export {
  analyzeQuery,
  clientGroupClients,
  clientGroupClientsWithQueries,
  clientGroupQueries,
  clientMap,
  clientQueries,
  clientRows,
  inspectorClients,
  inspectorClientsWithQueries,
  inspectorMetrics,
  mergeMetrics,
  rpc,
  serverVersion
};
//# sourceMappingURL=lazy-inspector.js.map
