import { valita_exports } from "../../shared/src/valita.js";
import { must } from "../../shared/src/must.js";
import "../../zero-protocol/src/ast.js";
import { clientSchemaFrom } from "../../zero-schema/src/builder/schema-builder.js";
import { asQueryInternals } from "../../zql/src/query/query-internals.js";
import { newQuery } from "../../zql/src/query/query-impl.js";
import { clientToServer } from "../../zero-schema/src/name-mapper.js";
import { QueryDelegateBase } from "../../zql/src/query/query-delegate-base.js";
import { Database } from "../../zqlite/src/db.js";
import { TableSource } from "../../zqlite/src/table-source.js";
import "../../shared/src/dotenv.js";
import { logLevel, logOptions } from "../../otel/src/log-options.js";
import { parseOptions } from "../../shared/src/options.js";
import { Debug, runtimeDebugFlags } from "../../zql/src/builder/debug-delegate.js";
import { getShardID, upstreamSchema } from "../../zero-cache/src/types/shards.js";
import { ZERO_ENV_VAR_PREFIX, appOptions, shardOptions, zeroOptions } from "../../zero-cache/src/config/zero-config.js";
import { colorConsole, createLogContext } from "../../shared/src/logging.js";
import { pgClient } from "../../zero-cache/src/types/pg.js";
import { deployPermissionsOptions, loadSchemaAndPermissions } from "../../zero-cache/src/scripts/permissions.js";
import { computeZqlSpecs, mustGetTableSpec } from "../../zero-cache/src/db/lite-tables.js";
import { astToZQL } from "../../ast-to-zql/src/ast-to-zql.js";
import { formatOutput } from "../../ast-to-zql/src/format.js";
import { testLogConfig } from "../../otel/src/test-log-config.js";
import { runAst } from "../../zero-cache/src/services/run-ast.js";
import { explainQueries } from "./explain-queries.js";
import { styleText } from "node:util";
import fs from "node:fs";
//#region ../analyze-query/src/bin-analyze.ts
var cfg = parseOptions({
	schema: deployPermissionsOptions.schema,
	replicaFile: {
		...zeroOptions.replica.file,
		desc: [`File path to the SQLite replica to test queries against.`]
	},
	ast: {
		type: valita_exports.string().optional(),
		desc: ["AST for the query to be analyzed.  Only one of ast/query/hash should be provided."]
	},
	query: {
		type: valita_exports.string().optional(),
		desc: [`Query to be analyzed in the form of: table.where(...).related(...).etc. `, `Only one of ast/query/hash should be provided.`]
	},
	hash: {
		type: valita_exports.string().optional(),
		desc: [
			`Hash of the query to be analyzed. This is used to look up the query in the database. `,
			`Only one of ast/query/hash should be provided.`,
			`You should run this script from the directory containing your .env file to reduce the amount of`,
			`configuration required. The .env file should contain the connection URL to the CVR database.`
		]
	},
	applyPermissions: {
		type: valita_exports.boolean().default(false),
		desc: ["Whether to apply permissions (from your schema file) to the provided query."]
	},
	authData: {
		type: valita_exports.string().optional(),
		desc: ["JSON encoded payload of the auth data.", "This will be used to fill permission variables if the \"applyPermissions\" option is set"]
	},
	outputVendedRows: {
		type: valita_exports.boolean().default(false),
		desc: ["Whether to output the rows which were read from the replica in order to execute the analyzed query. ", "If the same row is read more than once it will be logged once for each time it was read."]
	},
	outputSyncedRows: {
		type: valita_exports.boolean().default(false),
		desc: ["Whether to output the rows which would be synced to the client for the analyzed query."]
	},
	cvr: { db: {
		type: valita_exports.string().optional(),
		desc: ["Connection URL to the CVR database. If using --hash, either this or --upstream-db", "must be specified."]
	} },
	upstream: {
		db: {
			desc: [`Connection URL to the "upstream" authoritative postgres database. If using --hash, `, "either this or --cvr-db must be specified."],
			type: valita_exports.string().optional()
		},
		type: zeroOptions.upstream.type
	},
	app: appOptions,
	shard: shardOptions,
	log: {
		...logOptions,
		level: logLevel.default("error")
	}
}, {
	argv: process.argv.slice(2).map((s) => s.replaceAll("\n", " ")),
	envNamePrefix: ZERO_ENV_VAR_PREFIX,
	description: [{
		header: "analyze-query",
		content: `Analyze a ZQL query and show information about how it runs against a SQLite replica.

  analyze-query uses the same environment variables and flags as zero-cache-dev. If run from your development environment, it will pick up your ZERO_REPLICA_FILE, ZERO_SCHEMA_PATH, and other env vars automatically.

  If run in another environment (e.g., production) you will have to specify these flags. In particular, you must have a copy of the appropriate Zero schema file to give to the --schema-path flag.`
	}, {
		header: "Examples",
		content: `# In development
  npx analyze-query --query='issue.related("comments").limit(10)'
  npx analyze-query --ast='\\{"table": "artist","limit": 10\\}'
  npx analyze-query --hash=1234567890

  # In production
  # First copy schema.ts to your production environment, then run:
  npx analyze-query \\
    --schema-path='./schema.ts' \\
    --replica-file='/path/to/replica.db' \\
    --query='issue.related("comments").limit(10)'

  npx analyze-query \\
    --schema-path='./schema.ts' \\
    --replica-file='/path/to/replica.db' \\
    --ast='\\{"table": "artist","limit": 10\\}'

  # cvr-db is required when using the hash option.
  # It is typically the same as your upstream db.
  npx analyze-query \\
    --schema-path='./schema.ts' \\
    --replica-file='/path/to/replica.db' \\
    --cvr-db='postgres://user:pass@host:port/db' \\
    --hash=1234567890
  `
	}]
});
var config = {
	...cfg,
	cvr: {
		...cfg.cvr,
		db: cfg.cvr.db ?? cfg.upstream.db
	}
};
runtimeDebugFlags.trackRowCountsVended = true;
runtimeDebugFlags.trackRowsVended = config.outputVendedRows;
var lc = createLogContext({ log: config.log });
if (!fs.existsSync(config.replicaFile)) {
	colorConsole.error(`Replica file ${config.replicaFile} does not exist`);
	process.exit(1);
}
var db = new Database(lc, config.replicaFile);
var { schema, permissions } = await loadSchemaAndPermissions(config.schema.path);
var clientSchema = clientSchemaFrom(schema).clientSchema;
var sources = /* @__PURE__ */ new Map();
var clientToServerMapper = clientToServer(schema.tables);
var debug = new Debug();
var tableSpecs = computeZqlSpecs(lc, db, { includeBackfillingColumns: false });
var AnalyzeQueryDelegate = class extends QueryDelegateBase {
	debug = debug;
	defaultQueryComplete = true;
	getSource(serverTableName) {
		let source = sources.get(serverTableName);
		if (source) return source;
		const tableSpec = mustGetTableSpec(tableSpecs, serverTableName);
		const { primaryKey } = tableSpec.tableSpec;
		source = new TableSource(lc, testLogConfig, db, serverTableName, tableSpec.zqlSpec, primaryKey);
		sources.set(serverTableName, source);
		return source;
	}
};
var host = new AnalyzeQueryDelegate();
var result;
if (config.ast) result = await runAst(lc, clientSchema, JSON.parse(config.ast), true, {
	applyPermissions: config.applyPermissions,
	auth: config.authData ? {
		type: "jwt",
		raw: "",
		decoded: JSON.parse(config.authData)
	} : void 0,
	clientToServerMapper,
	permissions,
	syncedRows: config.outputSyncedRows,
	db,
	tableSpecs,
	host
}, async () => {});
else if (config.query) result = await runQuery(config.query);
else if (config.hash) result = await runHash(config.hash);
else {
	colorConsole.error("No query or AST or hash provided");
	process.exit(1);
}
function runQuery(queryString) {
	const z = { query: Object.fromEntries(Object.entries(schema.tables).map(([name]) => [name, newQuery(schema, name)])) };
	const ast = asQueryInternals(new Function("z", `return z.query.${queryString};`)(z)).ast;
	return runAst(lc, clientSchema, ast, false, {
		applyPermissions: config.applyPermissions,
		auth: config.authData ? {
			type: "jwt",
			raw: "",
			decoded: JSON.parse(config.authData)
		} : void 0,
		clientToServerMapper,
		permissions,
		syncedRows: config.outputSyncedRows,
		db,
		tableSpecs,
		host
	}, async () => {});
}
async function runHash(hash) {
	const cvrDB = pgClient(lc, must(config.cvr.db, "CVR DB must be provided when using the hash option"));
	const rows = await cvrDB`select "clientAST", "internal" from ${cvrDB(upstreamSchema(getShardID(config)) + "/cvr")}."queries" where "queryHash" = ${must(hash)} limit 1;`;
	await cvrDB.end();
	colorConsole.log("ZQL from Hash:");
	const ast = rows[0].clientAST;
	colorConsole.log(await formatOutput(ast.table + astToZQL(ast)));
	return runAst(lc, clientSchema, ast, true, {
		applyPermissions: config.applyPermissions,
		auth: config.authData ? {
			type: "jwt",
			raw: "",
			decoded: JSON.parse(config.authData)
		} : void 0,
		clientToServerMapper,
		permissions,
		syncedRows: config.outputSyncedRows,
		db,
		tableSpecs,
		host
	}, async () => {});
}
if (config.outputSyncedRows) {
	colorConsole.log(styleText(["blue", "bold"], "=== Synced Rows: ===\n"));
	for (const [table, rows] of Object.entries(result.syncedRows ?? {})) colorConsole.log(styleText("bold", table + ":"), rows);
}
colorConsole.log(styleText(["blue", "bold"], "=== Query Stats: ===\n"));
colorConsole.log(styleText("bold", "total synced rows:"), result.syncedRowCount);
showStats();
if (config.outputVendedRows) {
	colorConsole.log(styleText(["blue", "bold"], "=== JS Row Scan Values: ===\n"));
	for (const source of sources.values()) colorConsole.log(styleText("bold", `${source.tableSchema.name}:`), debug.getVendedRows()?.[source.tableSchema.name] ?? {});
}
colorConsole.log(styleText(["blue", "bold"], "\n=== Rows Scanned (by SQLite): ===\n"));
var nvisitCounts = debug.getNVisitCounts();
var totalNVisit = 0;
for (const [table, queries] of Object.entries(nvisitCounts)) {
	colorConsole.log(styleText("bold", `${table}:`), queries);
	for (const count of Object.values(queries)) totalNVisit += count;
}
colorConsole.log(styleText("bold", "total rows scanned:"), colorRowsConsidered(totalNVisit));
colorConsole.log(styleText(["blue", "bold"], "\n\n=== Query Plans: ===\n"));
var plans = explainQueries(debug.getVendedRowCounts() ?? {}, db);
for (const [query, plan] of Object.entries(plans)) {
	colorConsole.log(styleText("bold", "query"), query);
	colorConsole.log(plan.map((row, i) => colorPlanRow(row, i)).join("\n"));
	colorConsole.log("\n");
}
function showStats() {
	let totalRowsConsidered = 0;
	for (const source of sources.values()) {
		const values = Object.values(debug.getVendedRowCounts()?.[source.tableSchema.name] ?? {});
		for (const v of values) totalRowsConsidered += v;
		colorConsole.log(styleText("bold", source.tableSchema.name + " vended:"), debug.getVendedRowCounts()?.[source.tableSchema.name] ?? {});
	}
	colorConsole.log(styleText("bold", "Rows Read (into JS):"), colorRowsConsidered(totalRowsConsidered));
	colorConsole.log(styleText("bold", "time:"), colorTime(result.end - result.start), "ms");
}
function colorTime(duration) {
	if (duration < 100) return styleText("green", duration.toFixed(2) + "ms");
	else if (duration < 1e3) return styleText("yellow", duration.toFixed(2) + "ms");
	return styleText("red", duration.toFixed(2) + "ms");
}
function colorRowsConsidered(n) {
	if (n < 1e3) return styleText("green", n.toString());
	else if (n < 1e4) return styleText("yellow", n.toString());
	return styleText("red", n.toString());
}
function colorPlanRow(row, i) {
	if (row.includes("SCAN")) {
		if (i === 0) return styleText("yellow", row);
		return styleText("red", row);
	}
	return styleText("green", row);
}
//#endregion
export {};

//# sourceMappingURL=bin-analyze.js.map