/**
 * @since 4.0.0
 */
import * as Effect from "../../Effect.js";
import * as Layer from "../../Layer.js";
import * as PubSub from "../../PubSub.js";
import * as RcMap from "../../RcMap.js";
import * as Schema from "../../Schema.js";
import * as Stream from "../../Stream.js";
import * as SqlClient from "../sql/SqlClient.js";
import { EntryId, makeRemoteIdUnsafe } from "./EventJournal.js";
import * as EventLogEncryption from "./EventLogEncryption.js";
import * as EventLogServerEncrypted from "./EventLogServerEncrypted.js";
/**
 * @since 4.0.0
 * @category constructors
 */
export const makeStorage = options => Effect.gen(function* () {
  const encryptions = yield* EventLogEncryption.EventLogEncryption;
  const sql = (yield* SqlClient.SqlClient).withoutTransforms();
  const tablePrefix = options?.entryTablePrefix ?? "effect_events";
  const remoteIdTable = options?.remoteIdTable ?? "effect_remote_id";
  const sessionAuthBindingsTable = `${tablePrefix}_session_auth_bindings`;
  const insertBatchSize = options?.insertBatchSize ?? 200;
  const remoteIdTableSql = sql(remoteIdTable);
  const sessionAuthBindingsTableSql = sql(sessionAuthBindingsTable);
  yield* sql.onDialectOrElse({
    pg: () => sql`
          CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} (
            remote_id BYTEA PRIMARY KEY
          )`,
    mysql: () => sql`
          CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} (
            remote_id BINARY(16) PRIMARY KEY
          )`,
    mssql: () => sql`
          CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} (
            remote_id VARBINARY(16) PRIMARY KEY
          )`,
    orElse: () => sql`
          CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} (
            remote_id BLOB PRIMARY KEY
          )`
  });
  yield* sql.onDialectOrElse({
    pg: () => sql`
          CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} (
            public_key TEXT PRIMARY KEY,
            signing_public_key BYTEA NOT NULL
          )`,
    mysql: () => sql`
          CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} (
            public_key VARCHAR(191) PRIMARY KEY,
            signing_public_key BINARY(32) NOT NULL
          )`,
    mssql: () => sql`
          CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} (
            public_key NVARCHAR(191) PRIMARY KEY,
            signing_public_key VARBINARY(32) NOT NULL
          )`,
    orElse: () => sql`
          CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} (
            public_key TEXT PRIMARY KEY,
            signing_public_key BLOB NOT NULL
          )`
  });
  const remoteId = yield* sql`SELECT remote_id FROM ${remoteIdTableSql}`.pipe(Effect.flatMap(results => {
    if (results.length > 0) {
      return Effect.succeed(results[0].remote_id);
    }
    const created = makeRemoteIdUnsafe();
    return Effect.as(sql`INSERT INTO ${remoteIdTableSql} (remote_id) VALUES (${created})`, created);
  }));
  const resources = yield* RcMap.make({
    lookup: Effect.fnUntraced(function* (scopeKey) {
      const scopeHash = (yield* encryptions.sha256String(new TextEncoder().encode(scopeKey))).slice(0, 16);
      const table = `${tablePrefix}_${scopeHash}`;
      const tableSql = sql(table);
      yield* sql.onDialectOrElse({
        pg: () => sql`
                CREATE TABLE IF NOT EXISTS ${tableSql} (
                  sequence SERIAL PRIMARY KEY,
                  iv BYTEA NOT NULL,
                  entry_id BYTEA UNIQUE NOT NULL,
                  encrypted_entry BYTEA NOT NULL
                )`,
        mysql: () => sql`
                CREATE TABLE IF NOT EXISTS ${tableSql} (
                  sequence INT AUTO_INCREMENT PRIMARY KEY,
                  iv BINARY(12) NOT NULL,
                  entry_id BINARY(16) UNIQUE NOT NULL,
                  encrypted_entry BLOB NOT NULL
                )`,
        mssql: () => sql`
                CREATE TABLE IF NOT EXISTS ${tableSql} (
                  sequence INT IDENTITY(1,1) PRIMARY KEY,
                  iv VARBINARY(12) NOT NULL,
                  entry_id VARBINARY(16) UNIQUE NOT NULL,
                  encrypted_entry VARBINARY(MAX) NOT NULL
                )`,
        orElse: () => sql`
                CREATE TABLE IF NOT EXISTS ${tableSql} (
                  sequence INTEGER PRIMARY KEY AUTOINCREMENT,
                  iv BLOB NOT NULL,
                  entry_id BLOB UNIQUE NOT NULL,
                  encrypted_entry BLOB NOT NULL
                )`
      });
      const pubsub = yield* Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown);
      return {
        pubsub,
        table
      };
    }, withTracerDisabled),
    idleTimeToLive: "5 minutes"
  });
  const getSessionAuthBinding = publicKey => sql`
          SELECT public_key, signing_public_key
          FROM ${sessionAuthBindingsTableSql}
          WHERE public_key = ${publicKey}
        `.pipe(Effect.flatMap(decodeSessionAuthBindings), Effect.map(rows => {
    const row = rows[0];
    return row === undefined ? undefined : row.signing_public_key;
  }), Effect.orDie);
  return EventLogServerEncrypted.Storage.of({
    getId: Effect.succeed(remoteId),
    getOrCreateSessionAuthBinding: Effect.fnUntraced(function* (publicKey, signingPublicKey) {
      const existing = yield* getSessionAuthBinding(publicKey);
      if (existing !== undefined) {
        return existing;
      }
      return yield* sql`
            INSERT INTO ${sessionAuthBindingsTableSql} (public_key, signing_public_key)
            VALUES (${publicKey}, ${signingPublicKey})
          `.pipe(Effect.as(signingPublicKey));
    }, sql.withTransaction, withTracerDisabled, Effect.orDie),
    write: Effect.fnUntraced(function* (publicKey, storeId, entries) {
      if (entries.length === 0) return [];
      const scopeKey = makeEncryptedScopeKey(publicKey, storeId);
      const {
        pubsub,
        table
      } = yield* RcMap.get(resources, scopeKey);
      const forInsert = [{
        ids: [],
        entries: []
      }];
      let currentBatch = forInsert[0];
      for (const entry of entries) {
        currentBatch.ids.push(entry.entryId);
        currentBatch.entries.push({
          iv: entry.iv,
          entry_id: entry.entryId,
          encrypted_entry: entry.encryptedEntry
        });
        if (currentBatch.entries.length === insertBatchSize) {
          currentBatch = {
            ids: [],
            entries: []
          };
          forInsert.push(currentBatch);
        }
      }
      const allEntries = [];
      for (const batch of forInsert) {
        if (batch.entries.length === 0) continue;
        const encryptedEntries = yield* sql`
              INSERT INTO ${sql(table)} ${sql.insert(batch.entries)} ON CONFLICT DO NOTHING
            `.pipe(Effect.andThen(sql`SELECT * FROM ${sql(table)} WHERE ${sql.in("entry_id", batch.ids)} ORDER BY sequence ASC`), Effect.flatMap(decodeEntries));
        yield* PubSub.publishAll(pubsub, encryptedEntries);
        allEntries.push(...encryptedEntries);
      }
      return allEntries;
    }, Effect.orDie, Effect.scoped, withTracerDisabled),
    changes: Effect.fnUntraced(function* (publicKey, storeId, startSequence) {
      const scopeKey = makeEncryptedScopeKey(publicKey, storeId);
      const {
        pubsub,
        table
      } = yield* RcMap.get(resources, scopeKey);
      const subscription = yield* PubSub.subscribe(pubsub);
      const initial = yield* sql`
            SELECT * FROM ${sql(table)} WHERE sequence >= ${startSequence} ORDER BY sequence ASC
          `.pipe(Effect.flatMap(decodeEntries));
      return Stream.fromArray(initial).pipe(Stream.concat(Stream.fromSubscription(subscription)));
    }, Effect.orDie, withTracerDisabled, Stream.unwrap)
  });
}).pipe(withTracerDisabled);
const EncryptedRemoteEntrySql = /*#__PURE__*/Schema.Struct({
  sequence: Schema.Number,
  iv: Schema.Uint8Array,
  entry_id: EntryId,
  encrypted_entry: Schema.Uint8Array
});
const SessionAuthBindingSql = /*#__PURE__*/Schema.Struct({
  public_key: Schema.String,
  signing_public_key: Schema.Uint8Array
});
const decodeEntryRows = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Array(EncryptedRemoteEntrySql));
const decodeSessionAuthBindingRows = /*#__PURE__*/Schema.decodeUnknownEffect(/*#__PURE__*/Schema.Array(SessionAuthBindingSql));
const toEncryptedRemoteEntry = row => ({
  sequence: row.sequence,
  iv: row.iv,
  entryId: row.entry_id,
  encryptedEntry: row.encrypted_entry
});
const decodeEntries = rows => decodeEntryRows(rows).pipe(Effect.map(entries => entries.map(toEncryptedRemoteEntry)));
const decodeSessionAuthBindings = rows => decodeSessionAuthBindingRows(rows);
/**
 * @since 4.0.0
 * @category layers
 */
export const layerStorage = options => Layer.effect(EventLogServerEncrypted.Storage)(makeStorage(options));
/**
 * @since 4.0.0
 * @category layers
 */
export const layerStorageSubtle = options => layerStorage(options).pipe(Layer.provide(EventLogEncryption.layerSubtle));
const makeEncryptedScopeKey = (publicKey, storeId) => `${publicKey}/${storeId}`;
const withTracerDisabled = /*#__PURE__*/Effect.withTracerEnabled(false);
//# sourceMappingURL=SqlEventLogServerEncrypted.js.map