/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  FilesystemStorageDriver,
  fromJson,
  NoProviderError,
  StorageTransaction,
  toJson
} from "@matter/general";
import { resolve } from "node:path";
import { isBunjs, supportsSqlite } from "#util/runtimeChecks.js";
import { SqliteStorageDriverError } from "./SqliteStorageDriverError.js";
import { SqliteTransaction as Transaction } from "./SqliteTypes.js";
import { buildContextKeyLog, buildContextKeyPair, buildContextPath, escapeGlob } from "./SqliteUtil.js";
class SqliteStorageDriver extends FilesystemStorageDriver {
  static id = "sqlite";
  static memoryPath = ":memory:";
  static defaultTableName = "kvstore";
  /**
   * Create a SqliteStorageDriver for the given namespace using the platform-appropriate database.
   */
  static async create(namespace) {
    const storage = new SqliteStorageDriver({ namespaceOrPath: namespace });
    await storage.initialize();
    return storage;
  }
  isInitialized = false;
  #inTransaction = false;
  // internal values
  #database;
  #databaseCreator;
  dbPath;
  tableName;
  clearOnInit;
  // queries
  #queryGet;
  #queryGetRaw;
  #querySet;
  #querySetRaw;
  #queryDelete;
  #queryKeys;
  #queryValues;
  #queryContextSub;
  #queryClearAll;
  #queryHas;
  #queryOpenBlob;
  #queryWriteBlob;
  /**
   * Create sqlite-based disk storage.
   *
   * @param args.databaseCreator Optional database instance creator.  If omitted, resolved automatically during
   *   {@link initialize} via platform detection.
   * @param args.namespaceOrPath DataNamespace (derives path from root directory), string (direct path), or
   *   null/undefined for in-memory database
   * @param args.clear Clear on init
   * @param args.tableName table name
   */
  constructor(args) {
    const namespaceOrPath = args?.namespaceOrPath;
    super(typeof namespaceOrPath === "string" || namespaceOrPath == null ? void 0 : namespaceOrPath);
    this.dbPath = typeof namespaceOrPath === "string" ? namespaceOrPath : namespaceOrPath != null ? resolve(this.root.directory.path, "storage.db") : SqliteStorageDriver.memoryPath;
    this.tableName = args?.tableName ?? SqliteStorageDriver.defaultTableName;
    this.clearOnInit = args?.clear ?? false;
    if (args?.databaseCreator) {
      this.#openDatabase(args.databaseCreator);
    }
  }
  #openDatabase(databaseCreator) {
    this.#databaseCreator = databaseCreator;
    this.#database = databaseCreator(this.dbPath);
    const initQuery = this.#database.prepare(`
      CREATE TABLE IF NOT EXISTS ${this.tableName} (
        context TEXT NOT NULL,
        key TEXT NOT NULL,
        value_type TEXT CHECK(value_type IN ('json', 'blob')),
        value_json TEXT,
        value_blob BLOB,
        CONSTRAINT PKPair PRIMARY KEY (context, key)
      ) STRICT
    `);
    initQuery.run();
    this.#queryGet = this.#database.prepare(`
      SELECT value_json FROM ${this.tableName} WHERE
        context=$context AND
        key=$key AND
        value_type='json'
    `);
    this.#queryGetRaw = this.#database.prepare(`
      SELECT * FROM ${this.tableName}
    `);
    this.#queryHas = this.#database.prepare(`
      SELECT EXISTS(
        SELECT 1 FROM ${this.tableName}
        WHERE context=$context AND key=$key
      ) as has_record
    `);
    this.#querySet = this.#database.prepare(`
      INSERT INTO ${this.tableName}
        (context, key, value_type, value_json, value_blob)
      VALUES($context, $key, 'json', $value_json, NULL)
      ON CONFLICT(context, key)
      DO UPDATE SET
        value_type = 'json',
        value_json = excluded.value_json,
        value_blob = NULL
    `);
    this.#querySetRaw = this.#database.prepare(`
      INSERT INTO ${this.tableName}
        (context, key, value_type, value_json, value_blob)
      VALUES($context, $key, $value_type, $value_json, $value_blob)
      ON CONFLICT(context, key)
      DO UPDATE SET
        value_type = excluded.value_type,
        value_json = excluded.value_json,
        value_blob = excluded.value_blob
    `);
    this.#queryDelete = this.#database.prepare(`
      DELETE FROM ${this.tableName} WHERE
        context=$context AND
        key=$key
    `);
    this.#queryClearAll = this.#database.prepare(`
      DELETE FROM ${this.tableName} WHERE
        context=$context OR context GLOB $contextGlob
    `);
    this.#queryKeys = this.#database.prepare(`
      SELECT DISTINCT key FROM ${this.tableName} WHERE
        context=$context
    `);
    this.#queryValues = this.#database.prepare(`
      SELECT key, value_json FROM ${this.tableName} WHERE
        context=$context AND
        value_type='json'
    `);
    this.#queryContextSub = this.#database.prepare(`
      SELECT DISTINCT context FROM ${this.tableName} WHERE
        context GLOB $contextGlob
    `);
    this.#queryOpenBlob = this.#database.prepare(`
      SELECT value_type, value_json, value_blob FROM ${this.tableName} WHERE
        context=$context AND
        key=$key
    `);
    this.#queryWriteBlob = this.#database.prepare(`
      INSERT INTO ${this.tableName}
        (context, key, value_type, value_json, value_blob)
      VALUES($context, $key, 'blob', NULL, $value_blob)
      ON CONFLICT(context, key)
      DO UPDATE SET
        value_type = 'blob',
        value_json = NULL,
        value_blob = excluded.value_blob
    `);
  }
  /**
   * Manual transaction control
   *
   * Use this for explicit transaction management across multiple operations.
   * Internal methods like `set()` will automatically detect and use external transactions.
   *
   * TODO: Sync transaction to native matter.js API
   */
  transaction(mode) {
    switch (mode) {
      case Transaction.BEGIN:
        if (this.#inTransaction) {
          throw new SqliteStorageDriverError("transaction", "BEGIN", "Transaction is in progress.");
        }
        this.#database.exec("BEGIN IMMEDIATE TRANSACTION");
        this.#inTransaction = true;
        break;
      case Transaction.COMMIT:
        if (!this.#inTransaction) {
          throw new SqliteStorageDriverError("transaction", "COMMIT", "No transaction in progress.");
        }
        this.#database.exec("COMMIT");
        this.#inTransaction = false;
        break;
      case Transaction.ROLLBACK:
        if (!this.#inTransaction) {
          return;
        }
        this.#database.exec("ROLLBACK");
        this.#inTransaction = false;
        break;
    }
  }
  withAnyTransaction(callback) {
    if (this.#inTransaction) {
      return callback();
    }
    this.transaction(Transaction.BEGIN);
    try {
      const result = callback();
      this.transaction(Transaction.COMMIT);
      return result;
    } catch (err) {
      this.transaction(Transaction.ROLLBACK);
      throw err;
    }
  }
  get initialized() {
    return this.isInitialized;
  }
  async initialize() {
    if (this.isInitialized) {
      throw new SqliteStorageDriverError("initialize", this.tableName, "Storage already initialized!");
    }
    if (!this.#databaseCreator) {
      this.#openDatabase(await platformDatabaseCreator());
    }
    await super.initialize();
    if (this.clearOnInit) {
      this.#database.prepare(`DELETE FROM ${this.tableName}`).run();
    }
    this.isInitialized = true;
  }
  clone() {
    const clonedStorage = new SqliteStorageDriver({
      databaseCreator: this.#databaseCreator,
      namespaceOrPath: null,
      tableName: this.tableName,
      clear: false
    });
    const rawData = this.getRawAll();
    clonedStorage.setRaw(rawData);
    clonedStorage.isInitialized = true;
    return clonedStorage;
  }
  async close() {
    this.isInitialized = false;
    this.#database.close();
    await super.close();
  }
  get(contexts, key) {
    const queryResult = this.#queryGet.get(buildContextKeyPair(contexts, key));
    if (queryResult == null) {
      return void 0;
    }
    if (queryResult.value_json === null) {
      this.delete(contexts, key);
      throw new SqliteStorageDriverError(
        "get",
        buildContextKeyLog(contexts, key),
        "path has null json-value! (expected non-null value)"
      );
    }
    return fromJson(queryResult.value_json);
  }
  getRawAll() {
    return this.#queryGetRaw.all().filter((v) => v != null);
  }
  set(contexts, keyOrValues, value) {
    if (typeof keyOrValues === "string") {
      if (value === void 0) {
        throw new SqliteStorageDriverError(
          "set",
          buildContextKeyLog(contexts, keyOrValues),
          "Use null instead of undefined if you want to store null value!"
        );
      }
      this.setValue(contexts, keyOrValues, toJson(value));
    } else {
      this.withAnyTransaction(() => {
        for (const [key, value2] of Object.entries(keyOrValues)) {
          this.setValue(contexts, key, toJson(value2 ?? null));
        }
      });
    }
  }
  /**
   * Set [contexts, key] to value
   * @param contexts Context
   * @param key Key
   * @param value Value
   * @returns
   */
  setValue(contexts, key, value) {
    const { changes } = this.#querySet.run({
      ...buildContextKeyPair(contexts, key),
      value_json: value
    });
    if (Number(changes) <= 0) {
      throw new SqliteStorageDriverError(
        "set",
        buildContextKeyLog(contexts, key),
        `Something went wrong! Value wasn't changed.`
      );
    }
  }
  /**
   * Set Raw data. (for copy)
   */
  setRaw(rawData) {
    if (rawData.length <= 0) {
      return;
    }
    if (rawData.length === 1) {
      const raw = rawData[0];
      const { changes } = this.#querySetRaw.run({
        context: raw.context,
        key: raw.key,
        value_type: raw.value_type,
        value_json: raw.value_json,
        value_blob: raw.value_blob
      });
      if (Number(changes) <= 0) {
        throw new SqliteStorageDriverError(
          "setraw",
          `${raw.context}$${raw.key}`,
          `Something went wrong! Value wasn't changed.`
        );
      }
      return;
    }
    this.withAnyTransaction(() => {
      for (const raw of rawData) {
        const { changes } = this.#querySetRaw.run({
          context: raw.context,
          key: raw.key,
          value_type: raw.value_type,
          value_json: raw.value_json,
          value_blob: raw.value_blob
        });
        if (Number(changes) <= 0) {
          throw new SqliteStorageDriverError(
            "setraw",
            `${raw.context}$${raw.key}`,
            `Something went wrong! Value wasn't changed.`
          );
        }
      }
    });
  }
  delete(contexts, key) {
    this.#queryDelete.run(buildContextKeyPair(contexts, key));
  }
  keys(contexts) {
    const queryResults = this.#queryKeys.all({
      context: buildContextPath(contexts)
    }).filter((v) => v != null);
    return queryResults.map((v) => v.key);
  }
  values(contexts) {
    const queryResults = this.#queryValues.all({
      context: buildContextPath(contexts)
    }).filter((v) => v != null);
    const record = /* @__PURE__ */ Object.create(null);
    for (const element of queryResults) {
      record[element.key] = fromJson(element.value_json);
    }
    return record;
  }
  /**
   * Return sub contexts of context
   * (search nested depth, return 1 depth of them)
   * @param contexts context path
   * @returns sub contexts
   */
  contexts(contexts) {
    const parentCtx = buildContextPath(contexts);
    let subContexts;
    if (contexts.length === 0) {
      const allContexts = this.#queryContextSub.all({ contextGlob: "*" }).filter((v) => v != null);
      subContexts = allContexts.map((v) => {
        const firstDotIndex = v.context.indexOf(".");
        if (firstDotIndex < 0) {
          return v.context;
        }
        return v.context.substring(0, firstDotIndex);
      });
    } else {
      const allSubContexts = this.#queryContextSub.all({
        contextGlob: escapeGlob(parentCtx) + ".*"
      }).filter((v) => v != null);
      subContexts = allSubContexts.map((v) => {
        const subKey = v.context.substring(parentCtx.length + 1);
        const dotIndex = subKey.indexOf(".");
        if (dotIndex < 0) {
          return subKey;
        }
        return subKey.substring(0, dotIndex);
      });
    }
    return [...new Set(subContexts.filter((c) => c != null && c.trim().length > 0))];
  }
  clearAll(contexts) {
    if (contexts.length === 0) {
      return;
    }
    const contextPath = buildContextPath(contexts);
    this.#queryClearAll.run({
      context: contextPath,
      contextGlob: escapeGlob(contextPath) + ".*"
    });
  }
  has(contexts, key) {
    const result = this.#queryHas.get(buildContextKeyPair(contexts, key));
    return result?.has_record === 1;
  }
  openBlob(contexts, key) {
    const queryResult = this.#queryOpenBlob.get(buildContextKeyPair(contexts, key));
    if (queryResult == null) {
      return new Blob();
    }
    if (queryResult.value_type === "blob" && queryResult.value_blob != null) {
      return new Blob([new Uint8Array(queryResult.value_blob)]);
    }
    if (queryResult.value_type === "json" && queryResult.value_json != null) {
      return new Blob([queryResult.value_json]);
    }
    this.delete(contexts, key);
    return new Blob();
  }
  async writeBlobFromStream(contexts, key, stream) {
    const arrayBuffer = await new Response(stream).arrayBuffer();
    const bytes = new Uint8Array(arrayBuffer);
    const queryResult = this.#queryWriteBlob.run({
      ...buildContextKeyPair(contexts, key),
      value_blob: bytes
    });
    if (Number(queryResult.changes) <= 0) {
      throw new SqliteStorageDriverError(
        "writeBlob",
        buildContextKeyLog(contexts, key),
        `Something went wrong! Value wasn't changed.`
      );
    }
  }
  begin() {
    return new SqliteStorageDriverTransaction(this);
  }
}
async function platformDatabaseCreator() {
  if (!supportsSqlite()) {
    throw new NoProviderError("SQLite requires Node.js 22+ or Bun");
  }
  if (isBunjs()) {
    const module2 = await import("./platform/BunSqlite.js");
    return findDefaultExport(module2, "createBunDatabase");
  }
  const module = await import("./platform/NodeJsSqlite.js");
  return findDefaultExport(module, "createNodeJsDatabase");
}
function findDefaultExport(moduleLike, name) {
  return moduleLike[name] || moduleLike.default?.[name] || moduleLike.default;
}
class SqliteStorageDriverTransaction extends StorageTransaction {
  constructor(sqliteStorage) {
    super(sqliteStorage);
    this.sqliteStorage = sqliteStorage;
    sqliteStorage.transaction(Transaction.BEGIN);
  }
  commit() {
    this.assertActive();
    this.sqliteStorage.transaction(Transaction.COMMIT);
    super.commit();
  }
  rollback() {
    this.sqliteStorage.transaction(Transaction.ROLLBACK);
  }
}
export {
  SqliteStorageDriver
};
//# sourceMappingURL=SqliteStorageDriver.js.map
