import "../../../shared/src/dotenv.js";
import { writeFile } from "node:fs/promises";
import { ident, literal } from "pg-format";
import { createLogContext, colorConsole } from "../../../shared/src/logging.js";
import { parseOptions } from "../../../shared/src/options.js";
import { difference } from "../../../shared/src/set-utils.js";
import { mapCondition } from "../../../zero-protocol/src/ast.js";
import "../../../zero-schema/src/compiled-permissions.js";
import { validator } from "../../../zero-schema/src/name-mapper.js";
import { ZERO_ENV_VAR_PREFIX } from "../config/zero-config.js";
import { runTx } from "../db/run-transaction.js";
import { getPublicationInfo } from "../services/change-source/pg/schema/published.js";
import { ensureGlobalTables, SHARD_CONFIG_TABLE } from "../services/change-source/pg/schema/shard.js";
import { liteTableName } from "../types/names.js";
import { pgClient } from "../types/pg.js";
import { getShardID, appSchema, upstreamSchema } from "../types/shards.js";
import { deployPermissionsOptions, loadSchemaAndPermissions } from "./permissions.js";
const config = parseOptions(deployPermissionsOptions, {
  argv: process.argv.slice(2),
  envNamePrefix: ZERO_ENV_VAR_PREFIX
});
const shard = getShardID(config);
const app = appSchema(shard);
const lc = createLogContext(config);
async function validatePermissions(db, permissions) {
  const schema = upstreamSchema(shard);
  const result = await db`
    SELECT relname FROM pg_class
      JOIN pg_namespace ON relnamespace = pg_namespace.oid
      WHERE nspname = ${schema} AND relname = ${SHARD_CONFIG_TABLE}`;
  if (result.length === 0) {
    colorConsole.warn(
      `zero-cache has not yet initialized the upstream database.
Deploying ${app} permissions without validating against published tables/columns.`
    );
    return;
  }
  const config2 = await db`
    SELECT publications FROM ${db(schema + "." + SHARD_CONFIG_TABLE)}
  `;
  if (config2.length === 0) {
    colorConsole.warn(
      `zero-cache has not yet initialized the upstream database.
Deploying ${app} permissions without validating against published tables/columns.`
    );
    return;
  }
  colorConsole.info(
    `Validating permissions against tables and columns published for "${app}".`
  );
  const [{ publications: shardPublications }] = config2;
  const { tables, publications } = await getPublicationInfo(
    db,
    shardPublications
  );
  const pubnames = publications.map((p) => p.pubname);
  const missing = difference(new Set(shardPublications), new Set(pubnames));
  if (missing.size) {
    colorConsole.warn(
      `Upstream is missing expected publications "${[...missing]}".
You may need to re-initialize your replica.
Deploying ${app} permissions without validating against published tables/columns.`
    );
    return;
  }
  const tablesToColumns = new Map(
    tables.map((t) => [liteTableName(t), Object.keys(t.columns)])
  );
  const validate = validator(tablesToColumns);
  try {
    for (const [table, perms] of Object.entries(permissions?.tables ?? {})) {
      const validateRule = ([_, cond]) => {
        mapCondition(cond, table, validate);
      };
      const validateAsset = (asset) => {
        asset?.select?.forEach(validateRule);
        asset?.delete?.forEach(validateRule);
        asset?.insert?.forEach(validateRule);
        asset?.update?.preMutation?.forEach(validateRule);
        asset?.update?.postMutation?.forEach(validateRule);
      };
      validateAsset(perms.row);
      if (perms.cell) {
        Object.values(perms.cell).forEach(validateAsset);
      }
    }
  } catch (e) {
    failWithMessage(String(e));
  }
}
function failWithMessage(msg) {
  colorConsole.error(msg);
  colorConsole.info("\nUse --force to deploy at your own risk.\n");
  process.exit(-1);
}
async function deployPermissions(upstreamURI, permissions, force) {
  const db = pgClient(lc, upstreamURI);
  const { host, port } = db.options;
  colorConsole.debug(`Connecting to upstream@${host}:${port}`);
  try {
    await ensureGlobalTables(db, shard);
    const { hash, changed } = await runTx(db, async (tx) => {
      if (force) {
        colorConsole.warn(`--force specified. Skipping validation.`);
      } else {
        await validatePermissions(tx, permissions);
      }
      const { appID } = shard;
      colorConsole.info(
        `Deploying permissions for --app-id "${appID}" to upstream@${db.options.host}`
      );
      const [{ hash: beforeHash }] = await tx`
        SELECT hash from ${tx(app)}.permissions`;
      const [{ hash: hash2 }] = await tx`
        UPDATE ${tx(app)}.permissions SET ${db({ permissions })} RETURNING hash`;
      return { hash: hash2.substring(0, 7), changed: beforeHash !== hash2 };
    });
    if (changed) {
      colorConsole.info(`Deployed new permissions (hash=${hash})`);
    } else {
      colorConsole.info(`Permissions unchanged (hash=${hash})`);
    }
  } finally {
    await db.end();
  }
}
async function writePermissionsFile(perms, file, format) {
  const contents = format === "sql" ? `UPDATE ${ident(app)}.permissions SET permissions = ${literal(
    JSON.stringify(perms)
  )};` : JSON.stringify(perms, null, format === "pretty" ? 2 : 0);
  await writeFile(file, contents);
  colorConsole.info(`Wrote ${format} permissions to ${config.output.file}`);
}
const ret = await loadSchemaAndPermissions(config.schema.path, true);
if (!ret || Object.keys(ret?.permissions ?? {}).length === 0) {
  colorConsole.warn(
    `No permissions found at ${config.schema.path}, so could not deploy permissions. Replicating data, but no tables will be syncable. Create a schema file with permissions to be able to sync data.`
  );
} else {
  colorConsole.warn(
    `Permissions are deprecated and will be removed in an upcoming release. See: https://zero.rocicorp.dev/docs/auth.`
  );
  const { permissions } = ret;
  if (config.output.file) {
    await writePermissionsFile(
      permissions,
      config.output.file,
      config.output.format
    );
  } else if (config.upstream.type !== "pg") {
    colorConsole.warn(
      `Permissions deployment is not supported for ${config.upstream.type} upstreams`
    );
    process.exit(-1);
  } else if (config.upstream.db) {
    await deployPermissions(config.upstream.db, permissions, config.force);
  } else {
    colorConsole.error(`No --output-file or --upstream-db specified`);
    parseOptions(deployPermissionsOptions, {
      argv: ["--help"],
      envNamePrefix: ZERO_ENV_VAR_PREFIX
    });
  }
}
//# sourceMappingURL=deploy-permissions.js.map
