import { assert } from "../../shared/src/asserts.js";
import { mapValues } from "../../shared/src/objects.js";
import { recordProxy } from "../../shared/src/record-proxy.js";
import { formatPgInternalConvert, sql, sqlConvertColumnArg } from "../../z2s/src/sql.js";
import { makeTransactionMutate, makeCRUDMutate } from "../../zql/src/mutate/crud.js";
import { createRunnableBuilder } from "../../zql/src/query/create-builder.js";
import { QueryDelegateBase } from "../../zql/src/query/query-delegate-base.js";
import { asQueryInternals } from "../../zql/src/query/query-internals.js";
import { getServerSchema } from "./schema.js";
class ServerTransactionQueryDelegate extends QueryDelegateBase {
  #dbTransaction;
  #schema;
  #serverSchema;
  defaultQueryComplete = true;
  constructor(dbTransaction, schema, serverSchema) {
    super();
    this.#dbTransaction = dbTransaction;
    this.#schema = schema;
    this.#serverSchema = serverSchema;
  }
  getSource() {
    throw new Error("not implemented");
  }
  run(query, _options) {
    const queryInternals = asQueryInternals(query);
    return this.#dbTransaction.runQuery(
      queryInternals.ast,
      queryInternals.format,
      this.#schema,
      this.#serverSchema
    );
  }
  preload() {
    throw new Error("preload() is not supported in server transactions");
  }
  materialize() {
    throw new Error("materialize() is not supported in server transactions");
  }
}
class TransactionImpl {
  location = "server";
  reason = "authoritative";
  dbTransaction;
  clientID;
  mutationID;
  mutate;
  /**
   * @deprecated Use {@linkcode createBuilder} with `tx.run(zql.table.where(...))` instead.
   */
  query;
  #schema;
  #serverSchema;
  constructor(dbTransaction, clientID, mutationID, mutate, schema, serverSchema) {
    this.dbTransaction = dbTransaction;
    this.clientID = clientID;
    this.mutationID = mutationID;
    this.mutate = mutate;
    this.#schema = schema;
    this.#serverSchema = serverSchema;
    const delegate = new ServerTransactionQueryDelegate(
      dbTransaction,
      schema,
      serverSchema
    );
    this.query = createRunnableBuilder(delegate, schema);
  }
  run(query, _options) {
    const queryInternals = asQueryInternals(query);
    return this.dbTransaction.runQuery(
      queryInternals.ast,
      queryInternals.format,
      this.#schema,
      this.#serverSchema
    );
  }
}
const dbTxSymbol = Symbol();
const serverSchemaSymbol = Symbol();
class CRUDMutatorFactory {
  #schema;
  #tableCRUDs;
  #serverSchema;
  constructor(schema) {
    this.#schema = schema;
    this.#tableCRUDs = {};
    for (const tableSchema of Object.values(schema.tables)) {
      this.#tableCRUDs[tableSchema.name] = makeServerTableCRUD(tableSchema);
    }
  }
  /**
   * Gets the cached serverSchema, or fetches and caches it on first call.
   */
  async #getOrFetchServerSchema(dbTransaction) {
    if (!this.#serverSchema) {
      this.#serverSchema = await getServerSchema(dbTransaction, this.#schema);
    }
    return this.#serverSchema;
  }
  /**
   * Creates a CRUDExecutor bound to the given transaction and serverSchema.
   * Uses the pre-created TableCRUD methods from construction time.
   */
  createExecutor(dbTransaction, serverSchema) {
    const txHolder = {
      [dbTxSymbol]: dbTransaction,
      [serverSchemaSymbol]: serverSchema
    };
    const boundCRUDs = recordProxy(
      this.#tableCRUDs,
      (tableCRUD) => mapValues(tableCRUD, (method) => method.bind(txHolder))
    );
    return (table, kind, args) => {
      const tableCRUD = boundCRUDs[table];
      return tableCRUD[kind](args);
    };
  }
  /**
   * Creates a full ServerTransaction.
   * Fetches/caches serverSchema automatically.
   */
  async createTransaction(dbTransaction, clientID, mutationID) {
    const serverSchema = await this.#getOrFetchServerSchema(dbTransaction);
    const executor = this.createExecutor(dbTransaction, serverSchema);
    const mutate = makeTransactionMutate(this.#schema, executor);
    return new TransactionImpl(
      dbTransaction,
      clientID,
      mutationID,
      mutate,
      this.#schema,
      serverSchema
    );
  }
}
function makeSchemaCRUD(schema) {
  return (dbTransaction, serverSchema) => makeCRUDMutate(
    schema,
    true,
    makeServerCRUDExecutor(schema, dbTransaction, serverSchema)
  );
}
function removeUndefined(value) {
  const valueWithoutUndefined = {};
  for (const [key, val] of Object.entries(value)) {
    if (val !== void 0) {
      valueWithoutUndefined[key] = val;
    }
  }
  return valueWithoutUndefined;
}
function makeServerCRUDExecutor(schema, dbTransaction, serverSchema) {
  const tableCRUDs = {};
  for (const tableSchema of Object.values(schema.tables)) {
    tableCRUDs[tableSchema.name] = makeServerTableCRUD(tableSchema);
  }
  const txHolder = {
    [dbTxSymbol]: dbTransaction,
    [serverSchemaSymbol]: serverSchema
  };
  const boundCRUDs = recordProxy(
    tableCRUDs,
    (tableCRUD) => mapValues(tableCRUD, (method) => method.bind(txHolder))
  );
  return (table, kind, args) => {
    const tableCRUD = boundCRUDs[table];
    return tableCRUD[kind](args);
  };
}
function makeServerTableCRUD(schema) {
  return {
    async insert(value) {
      value = removeUndefined(value);
      const serverTableSchema = this[serverSchemaSymbol][serverName(schema)];
      const targetedColumns = origAndServerNamesFor(Object.keys(value), schema);
      const stmt = formatPgInternalConvert(
        sql`INSERT INTO ${sql.ident(serverName(schema))} (${sql.join(
          targetedColumns.map(([, serverName2]) => sql.ident(serverName2)),
          ","
        )}) VALUES (${sql.join(
          Object.entries(value).map(
            ([col, v]) => sqlInsertValue(v, serverTableSchema[serverNameFor(col, schema)])
          ),
          ", "
        )})`
      );
      const tx = this[dbTxSymbol];
      await tx.query(stmt.text, stmt.values);
    },
    async upsert(value) {
      value = removeUndefined(value);
      const serverTableSchema = this[serverSchemaSymbol][serverName(schema)];
      const targetedColumns = origAndServerNamesFor(Object.keys(value), schema);
      const primaryKeyColumns = origAndServerNamesFor(
        schema.primaryKey,
        schema
      );
      const stmt = formatPgInternalConvert(
        sql`INSERT INTO ${sql.ident(serverName(schema))} (${sql.join(
          targetedColumns.map(([, serverName2]) => sql.ident(serverName2)),
          ","
        )}) VALUES (${sql.join(
          Object.entries(value).map(
            ([col, val]) => sqlInsertValue(val, serverTableSchema[serverNameFor(col, schema)])
          ),
          ", "
        )}) ON CONFLICT (${sql.join(
          primaryKeyColumns.map(([, serverName2]) => sql.ident(serverName2)),
          ", "
        )}) DO UPDATE SET ${sql.join(
          Object.entries(value).map(
            ([col, val]) => sql`${sql.ident(
              schema.columns[col].serverName ?? col
            )} = ${sqlInsertValue(val, serverTableSchema[serverNameFor(col, schema)])}`
          ),
          ", "
        )}`
      );
      const tx = this[dbTxSymbol];
      await tx.query(stmt.text, stmt.values);
    },
    async update(value) {
      value = removeUndefined(value);
      const serverTableSchema = this[serverSchemaSymbol][serverName(schema)];
      const targetedColumns = origAndServerNamesFor(Object.keys(value), schema);
      const stmt = formatPgInternalConvert(
        sql`UPDATE ${sql.ident(serverName(schema))} SET ${sql.join(
          targetedColumns.map(
            ([origName, serverName2]) => sql`${sql.ident(serverName2)} = ${sqlInsertValue(value[origName], serverTableSchema[serverName2])}`
          ),
          ", "
        )} WHERE ${primaryKeyClause(schema, serverTableSchema, value)}`
      );
      const tx = this[dbTxSymbol];
      await tx.query(stmt.text, stmt.values);
    },
    async delete(value) {
      value = removeUndefined(value);
      const serverTableSchema = this[serverSchemaSymbol][serverName(schema)];
      const stmt = formatPgInternalConvert(
        sql`DELETE FROM ${sql.ident(
          serverName(schema)
        )} WHERE ${primaryKeyClause(schema, serverTableSchema, value)}`
      );
      const tx = this[dbTxSymbol];
      await tx.query(stmt.text, stmt.values);
    }
  };
}
function serverName(x) {
  return x.serverName ?? x.name;
}
function primaryKeyClause(schema, serverTableSchema, row) {
  const primaryKey = origAndServerNamesFor(schema.primaryKey, schema);
  return sql`${sql.join(
    primaryKey.map(
      ([origName, serverName2]) => sql`${sql.ident(serverName2)}${maybeCastColumn(serverTableSchema[serverName2])} = ${sqlValue(row[origName], serverTableSchema[serverName2])}`
    ),
    " AND "
  )}`;
}
function maybeCastColumn(col) {
  if (col.type === "uuid" || col.isEnum) {
    return sql`::text`;
  }
  return sql``;
}
function origAndServerNamesFor(originalNames, schema) {
  return originalNames.map(
    (name) => [name, serverNameFor(name, schema)]
  );
}
function serverNameFor(originalName, schema) {
  const col = schema.columns[originalName];
  assert(
    col,
    `Column ${originalName} was not found in the Zero schema for the table ${schema.name}`
  );
  return col.serverName ?? originalName;
}
function sqlValue(value, serverColumnSchema) {
  return sqlConvertColumnArg(serverColumnSchema, value, false, true);
}
function sqlInsertValue(value, serverColumnSchema) {
  return sqlConvertColumnArg(serverColumnSchema, value, false, false);
}
export {
  CRUDMutatorFactory,
  TransactionImpl,
  makeSchemaCRUD
};
//# sourceMappingURL=custom.js.map
