import { BigIntJSON } from "../../../shared/src/bigint-json.js";
import { Database } from "../../../zqlite/src/db.js";
import { getShardID, upstreamSchema } from "../types/shards.js";
import { isAdminPasswordValid } from "../config/zero-config.js";
import { StatementRunner } from "../db/statements.js";
import { pgClient } from "../types/pg.js";
import { fromStateVersionString } from "./change-source/pg/lsn.js";
import { getReplicationState } from "./replicator/schema/replication-state.js";
import os from "os";
import auth from "basic-auth";
import fs from "fs";
//#region ../zero-cache/src/services/statz.ts
async function upstreamStats(lc, config) {
	const schema = upstreamSchema(getShardID(config));
	const sql = pgClient(lc, config.upstream.db);
	try {
		return await getPgStats([
			["numReplicas", sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."replicas"`],
			["numClientsWithMutations", sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."clients"`],
			["numMutationsProcessed", sql`SELECT SUM("lastMutationID") as "c" FROM ${sql(schema)}."clients"`]
		]);
	} finally {
		await sql.end();
	}
}
async function cvrStats(lc, config) {
	const schema = upstreamSchema(getShardID(config)) + "/cvr";
	const sql = pgClient(lc, config.cvr.db);
	function numQueriesPerClientGroup(active) {
		const filter = active ? sql`WHERE "inactivatedAt" IS NULL AND deleted = false` : sql`WHERE "inactivatedAt" IS NOT NULL AND ("inactivatedAt" + "ttl") > NOW()`;
		return sql`WITH
    group_counts AS (
      SELECT
        "clientGroupID",
        COUNT(*) AS num_queries
      FROM ${sql(schema)}."desires"
      ${filter}
      GROUP BY "clientGroupID"
    ),
    -- Count distinct clientIDs per clientGroupID
    client_per_group_counts AS (
      SELECT
        "clientGroupID",
        COUNT(DISTINCT "clientID") AS num_clients
      FROM ${sql(schema)}."desires"
      ${filter}
      GROUP BY "clientGroupID"
    )
    -- Combine all the information
    SELECT
      g."clientGroupID",
      cpg.num_clients,
      g.num_queries
    FROM group_counts g
    JOIN client_per_group_counts cpg ON g."clientGroupID" = cpg."clientGroupID"
    ORDER BY g.num_queries DESC;`;
	}
	try {
		return await getPgStats([
			["totalNumQueries", sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires"`],
			["numUniqueQueryHashes", sql`SELECT COUNT(DISTINCT "queryHash") as "c" FROM ${sql(schema)}."desires"`],
			["numActiveQueries", sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "inactivatedAt" IS NULL AND "deleted" = false`],
			["numInactiveQueries", sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "inactivatedAt" IS NOT NULL AND ("inactivatedAt" + "ttl") > NOW()`],
			["numDeletedQueries", sql`SELECT COUNT(*) as "c" FROM ${sql(schema)}."desires" WHERE "deleted" = true`],
			["freshQueriesPercentiles", sql`WITH client_group_counts AS (
        -- Count inactive desires per clientGroupID
        SELECT
          "clientGroupID",
          COUNT(*) AS fresh_count
        FROM ${sql(schema)}."desires"
        WHERE
          ("inactivatedAt" IS NOT NULL
          AND ("inactivatedAt" + "ttl") > NOW()) OR ("inactivatedAt" IS NULL
          AND deleted = false)
        GROUP BY "clientGroupID"
      )

      SELECT
        percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS "p50",
        percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS "p75",
        percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS "p90",
        percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS "p95",
        percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS "p99",
        MIN(fresh_count) AS "min",
        MAX(fresh_count) AS "max",
        AVG(fresh_count) AS "avg"
      FROM client_group_counts;`],
			["rowsPerClientGroupPercentiles", sql`WITH client_group_counts AS (
        -- Count inactive desires per clientGroupID
        SELECT
          "clientGroupID",
          COUNT(*) AS row_count
        FROM ${sql(schema)}."rows"
        GROUP BY "clientGroupID"
      )
      SELECT
        percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS "p50",
        percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS "p75",
        percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS "p90",
        percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS "p95",
        percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS "p99",
        MIN(row_count) AS "min",
        MAX(row_count) AS "max",
        AVG(row_count) AS "avg"
      FROM client_group_counts;`],
			["astSizes", sql`SELECT
        percentile_cont(0.25) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "25th_percentile",
        percentile_cont(0.5) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "50th_percentile",
        percentile_cont(0.75) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "75th_percentile",
        percentile_cont(0.9) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "90th_percentile",
        percentile_cont(0.95) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "95th_percentile",
        percentile_cont(0.99) WITHIN GROUP (ORDER BY length("clientAST"::text)) AS "99th_percentile",
        MIN(length("clientAST"::text)) AS "minimum_length",
        MAX(length("clientAST"::text)) AS "maximum_length",
        AVG(length("clientAST"::text))::integer AS "average_length",
        COUNT(*) AS "total_records"
      FROM ${sql(schema)}."queries";`],
			["biggestAstHash", sql`SELECT "queryHash", length("clientAST"::text) AS "ast_length"
      FROM ${sql(schema)}."queries"
      ORDER BY length("clientAST"::text) DESC
      LIMIT 1;`],
			["totalActiveQueriesPerClientAndClientGroup", numQueriesPerClientGroup(true)],
			["totalInactiveQueriesPerClientAndClientGroup", numQueriesPerClientGroup(false)],
			["totalRowsPerClientGroup", sql`SELECT "clientGroupID", COUNT(*) as "c" FROM ${sql(schema)}."rows" GROUP BY "clientGroupID" ORDER BY "c" DESC`],
			["numRowsPerQuery", sql`SELECT
        k.key AS "queryHash",
        COUNT(*) AS row_count
      FROM ${sql(schema)}."rows" r,
      LATERAL jsonb_each(r."refCounts") k
      GROUP BY k.key
      ORDER BY row_count DESC;`]
		]);
	} finally {
		await sql.end();
	}
}
async function changeLogStats(lc, config) {
	const schema = upstreamSchema(getShardID(config)) + "/cdc";
	const sql = pgClient(lc, config.change.db);
	try {
		return await getPgStats([["changeLogSize", sql`SELECT COUNT(*) as "change_log_size" FROM ${sql(schema)}."changeLog"`]]);
	} finally {
		await sql.end();
	}
}
function replicaStats(lc, config) {
	const db = new Database(lc, config.replica.file);
	try {
		return Object.fromEntries([
			["wal checkpoint", pick(first(db.pragma("WAL_CHECKPOINT")))],
			["page count", pick(first(db.pragma("PAGE_COUNT")))],
			["page size", pick(first(db.pragma("PAGE_SIZE")))],
			["journal mode", pick(first(db.pragma("JOURNAL_MODE")))],
			["synchronous", pick(first(db.pragma("SYNCHRONOUS")))],
			["cache size", pick(first(db.pragma("CACHE_SIZE")))],
			["auto vacuum", pick(first(db.pragma("AUTO_VACUUM")))],
			["freelist count", pick(first(db.pragma("FREELIST_COUNT")))],
			["wal autocheckpoint", pick(first(db.pragma("WAL_AUTOCHECKPOINT")))],
			["db file stats", fs.statSync(config.replica.file)]
		]);
	} finally {
		db.close();
	}
}
function replicationStats(lc, config) {
	const db = new Database(lc, config.replica.file);
	try {
		return getReplicationStats(db);
	} finally {
		db.close();
	}
}
function getReplicationStats(db) {
	const { stateVersion } = getReplicationState(new StatementRunner(db));
	return { lsn: fromStateVersionString(stateVersion) };
}
function osStats() {
	return Object.fromEntries([
		["load avg", os.loadavg()],
		["uptime", os.uptime()],
		["total mem", os.totalmem()],
		["free mem", os.freemem()],
		["cpus", os.cpus().length],
		["available parallelism", os.availableParallelism()],
		["platform", os.platform()],
		["arch", os.arch()],
		["release", os.release()]
	]);
}
async function getPgStats(pendingQueries) {
	const results = await Promise.all(pendingQueries.map(async ([name, query]) => [name, await query]));
	return Object.fromEntries(results);
}
function printStats(group, statsObject) {
	const lines = ["\n" + header(group)];
	for (const [name, result] of Object.entries(statsObject)) lines.push("\n" + name + ": " + BigIntJSON.stringify(result, null, 2));
	lines.push("\n");
	return lines.join("");
}
/**
* HTTP query parameters:
* * `group`: restricts the groups for which stats are computed
* * `format=json`: returns the stats as a JSON object
* * `pretty`: formats the JSON object with indentation
*/
async function handleStatzRequest(lc, config, req, res) {
	if (!isAdminPasswordValid(lc, config, auth(req)?.pass)) {
		res.code(401).header("WWW-Authenticate", "Basic realm=\"Statz Protected Area\"").send("Unauthorized");
		return;
	}
	const statsFns = {
		upstream: () => upstreamStats(lc, config),
		cvr: () => cvrStats(lc, config),
		changeLog: () => changeLogStats(lc, config),
		replica: () => replicaStats(lc, config),
		replication: () => replicationStats(lc, config),
		os: () => osStats()
	};
	async function computeStats(group) {
		try {
			return [group, await statsFns[group]()];
		} catch (e) {
			lc.error?.(`error computing ${group} stats`, e);
			return [group, { error: String(e) }];
		}
	}
	const query = req.query;
	const groups = typeof query.group === "string" ? query.group.split(",") : Array.isArray(query.group) ? query.group : void 0;
	const stats = await Promise.all(groups ? groups.filter((g) => g in statsFns).map(computeStats) : Object.keys(statsFns).map(computeStats));
	if (query.format === "json") {
		const indent = query.pretty !== void 0 ? 2 : void 0;
		await res.header("Content-Type", "application/json").send(BigIntJSON.stringify(Object.fromEntries(stats), null, indent));
		return;
	} else {
		const body = stats.map(([group, statsObject]) => printStats(group, statsObject)).join("");
		await res.header("Content-Type", "text/plain; charset=utf-8").send(body);
	}
}
function first(x) {
	return x[0];
}
function pick(x) {
	return Object.values(x)[0];
}
function header(name) {
	return `=== ${name} ===\n`;
}
//#endregion
export { handleStatzRequest };

//# sourceMappingURL=statz.js.map