"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var StorageService_exports = {};
__export(StorageService_exports, {
  StorageService: () => StorageService
});
module.exports = __toCommonJS(StorageService_exports);
var import_MatterError = require("../MatterError.js");
var import_Environmental = require("../environment/Environmental.js");
var import_Filesystem = require("../fs/Filesystem.js");
var import_Diagnostic = require("../log/Diagnostic.js");
var import_Logger = require("../log/Logger.js");
var import_DataNamespace = require("./DataNamespace.js");
var import_DatafileRoot = require("./DatafileRoot.js");
var import_StorageDriver = require("./StorageDriver.js");
var import_StorageDriverHandle = require("./StorageDriverHandle.js");
var import_StorageManager = require("./StorageManager.js");
var import_StorageMigration = require("./StorageMigration.js");
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_Logger.Logger.get("StorageService");
class StorageService {
  #drivers = /* @__PURE__ */ new Map();
  #defaultDriver = "wal";
  #configuredDriver;
  #environment;
  #openDrivers = /* @__PURE__ */ new Map();
  #blobDrivers = /* @__PURE__ */ new Map();
  #defaultBlobDriver = "dir";
  #configuredBlobDriver;
  #openBlobDrivers = /* @__PURE__ */ new Map();
  constructor(environment) {
    environment.set(StorageService, this);
    this.#environment = environment;
  }
  static [import_Environmental.Environmental.create](environment) {
    return new this(environment);
  }
  /**
   * Register a driver implementation so that it can be resolved by id.
   */
  registerDriver(impl) {
    this.#drivers.set(impl.id, impl);
  }
  /**
   * Register a blob driver implementation so that it can be resolved by id.
   */
  registerBlobDriver(impl) {
    this.#blobDrivers.set(impl.id, impl);
  }
  /**
   * Set the default driver id used when no existing storage is detected.
   */
  set defaultDriver(id) {
    this.#defaultDriver = id;
  }
  get defaultDriver() {
    return this.#defaultDriver;
  }
  /**
   * Set the explicitly configured driver id (e.g. from user configuration).  When set, triggers migration if the
   * existing storage uses a different driver.
   */
  set configuredDriver(id) {
    this.#configuredDriver = id;
  }
  get configuredDriver() {
    return this.#configuredDriver;
  }
  /**
   * Set the default blob driver id used when no existing blob storage is detected.
   */
  set defaultBlobDriver(id) {
    this.#defaultBlobDriver = id;
  }
  get defaultBlobDriver() {
    return this.#defaultBlobDriver;
  }
  /**
   * Set the explicitly configured blob driver id (e.g. from user configuration).  When set, takes precedence over
   * detected or default blob driver.
   */
  set configuredBlobDriver(id) {
    this.#configuredBlobDriver = id;
  }
  get configuredBlobDriver() {
    return this.#configuredBlobDriver;
  }
  /**
   * Whether a {@link Filesystem} service is installed in the environment.  Callers can check this before calling
   * {@link open} to avoid errors in memory-only environments.
   */
  get hasFilesystem() {
    return this.#environment.has(import_Filesystem.Filesystem);
  }
  /**
   * Whether drivers are registered.
   */
  get isConfigured() {
    return this.#drivers.size > 0;
  }
  /**
   * Open storage.  The storage is initialized but the caller must take ownership.
   *
   * @param namespace a unique namespace identifier (string) or a pre-built {@link DataNamespace}/{@link DatafileRoot}
   */
  async open(namespace) {
    if (this.#drivers.size === 0) {
      throw new import_MatterError.NoProviderError("Storage is unavailable because no drivers are registered");
    }
    const dataNs = this.#resolveNamespace(namespace);
    const cacheKey = dataNs.namespace;
    if (this.#openBlobDrivers.has(`blob:${cacheKey}`)) {
      throw new import_StorageDriver.StorageError(`Namespace "${cacheKey}" is already open as blob storage`);
    }
    const cached = this.#openDrivers.get(cacheKey);
    if (cached) {
      cached.refs++;
      const manager = new import_StorageManager.StorageManager(new import_StorageDriverHandle.StorageDriverHandle(cached.driver, () => this.#release(cacheKey)));
      await manager.initialize();
      return manager;
    }
    if (dataNs instanceof import_DatafileRoot.DatafileRoot) {
      return this.#openFilesystem(cacheKey, dataNs);
    }
    return this.#openSimple(cacheKey, dataNs);
  }
  /**
   * Resolve a namespace string or object into a {@link DataNamespace}.  When a string is given,
   * creates a {@link DatafileRoot} if a {@link Filesystem} is available, otherwise a plain
   * {@link DataNamespace}.
   */
  #resolveNamespace(namespace) {
    if (typeof namespace === "string") {
      if (this.#environment.has(import_Filesystem.Filesystem)) {
        const fs = this.#environment.get(import_Filesystem.Filesystem);
        return new import_DatafileRoot.DatafileRoot(fs.directory(namespace));
      }
      return new import_DataNamespace.DataNamespace(namespace);
    }
    return namespace;
  }
  async #openFilesystem(cacheKey, root) {
    const fs = this.#environment.get(import_Filesystem.Filesystem);
    const dir = root.directory;
    const namespace = root.namespace;
    let descriptor = await this.#readDescriptor(dir);
    let detectedKind;
    if (descriptor) {
      if (descriptor.type === "blob") {
        throw new import_StorageDriver.StorageError(`Namespace "${namespace}" contains blob storage, not KV storage`);
      }
      detectedKind = descriptor.kind;
    } else if (await this.#hasLegacyFileData(dir)) {
      detectedKind = "file";
    } else {
      const dbFile = fs.file(`${namespace}.db`);
      if (await dbFile.exists()) {
        detectedKind = "sqlite";
      }
    }
    const targetKind = this.#configuredDriver ?? detectedKind ?? this.#defaultDriver;
    if (detectedKind !== void 0 && detectedKind !== targetKind) {
      await this.#migrate(fs, namespace, dir, detectedKind, targetKind);
      descriptor = await this.#readDescriptor(dir);
    }
    if (!descriptor) {
      descriptor = { kind: targetKind, type: "kv" };
    } else if (descriptor.type === void 0) {
      descriptor = { ...descriptor, type: "kv" };
    }
    const impl = this.#drivers.get(targetKind);
    if (!impl) {
      throw new import_MatterError.NoProviderError(`No storage driver registered for "${targetKind}"`);
    }
    if (impl.preinitialize) {
      await impl.preinitialize(fs, descriptor);
    }
    const storage = await impl.create(root, descriptor);
    try {
      if (await dir.exists()) {
        await this.#writeDescriptor(dir, descriptor);
      }
      this.#openDrivers.set(cacheKey, { driver: storage, refs: 1 });
      const manager = new import_StorageManager.StorageManager(new import_StorageDriverHandle.StorageDriverHandle(storage, () => this.#release(cacheKey)));
      await manager.initialize();
      return manager;
    } catch (e) {
      try {
        await storage.close();
      } catch (closeError) {
        logger.warn("Error closing storage after failed initialization:", closeError);
      }
      throw e;
    }
  }
  async #openSimple(cacheKey, dataNs) {
    const targetKind = this.#configuredDriver ?? this.#defaultDriver;
    const descriptor = { kind: targetKind, type: "kv" };
    const impl = this.#drivers.get(targetKind);
    if (!impl) {
      throw new import_MatterError.NoProviderError(`No storage driver registered for "${targetKind}"`);
    }
    const storage = await impl.create(dataNs, descriptor);
    try {
      this.#openDrivers.set(cacheKey, { driver: storage, refs: 1 });
      const manager = new import_StorageManager.StorageManager(new import_StorageDriverHandle.StorageDriverHandle(storage, () => this.#release(cacheKey)));
      await manager.initialize();
      return manager;
    } catch (e) {
      try {
        await storage.close();
      } catch (closeError) {
        logger.warn("Error closing storage after failed initialization:", closeError);
      }
      throw e;
    }
  }
  async #release(cacheKey) {
    const cached = this.#openDrivers.get(cacheKey);
    if (!cached) {
      return;
    }
    cached.refs--;
    if (cached.refs <= 0) {
      this.#openDrivers.delete(cacheKey);
      await cached.driver.close();
    }
  }
  /**
   * Close storage for a namespace previously opened with {@link open}.
   *
   * Locking is now managed by the drivers themselves via ref-counted {@link DatafileRoot.Lock}s, so this method is a
   * no-op retained for backward compatibility.
   */
  async close(_namespace) {
  }
  /**
   * Open blob storage.  The storage is initialized and returned directly (no StorageManager wrapper).
   *
   * For filesystem namespaces, the blob driver type is persisted in `driver.json` with `type: "blob"`.
   * Detection logic:
   * - `driver.json` exists with `kind` → use that driver (explicit)
   * - No `driver.json` but directory exists → legacy implicit "file" (data from WAL or old file driver)
   * - No `driver.json`, no directory → fresh namespace, use default blob driver
   *
   * @param namespace a unique namespace identifier (string) or a pre-built {@link DataNamespace}/{@link DatafileRoot}
   */
  async openBlobStorage(namespace) {
    if (this.#blobDrivers.size === 0) {
      throw new import_MatterError.NoProviderError("Blob storage is unavailable because no blob drivers are registered");
    }
    const dataNs = this.#resolveNamespace(namespace);
    const nsKey = dataNs.namespace;
    const cacheKey = `blob:${nsKey}`;
    if (this.#openDrivers.has(nsKey)) {
      throw new import_StorageDriver.StorageError(`Namespace "${nsKey}" is already open as KV storage`);
    }
    const cached = this.#openBlobDrivers.get(cacheKey);
    if (cached) {
      cached.refs++;
      return { driver: cached.driver, close: () => this.#releaseBlobDriver(cacheKey) };
    }
    if (dataNs instanceof import_DatafileRoot.DatafileRoot) {
      return this.#openBlobFilesystem(cacheKey, dataNs);
    }
    return this.#openBlobSimple(cacheKey, dataNs);
  }
  async #openBlobFilesystem(cacheKey, root) {
    const dir = root.directory;
    let descriptor = await this.#readDescriptor(dir);
    let detectedKind;
    if (descriptor) {
      if (descriptor.type === "kv") {
        throw new import_StorageDriver.StorageError(`Namespace "${root.namespace}" contains KV storage, not blob storage`);
      }
      detectedKind = descriptor.kind;
    } else if (await dir.exists()) {
      const blobsSubDir = dir.directory("blobs");
      if (await blobsSubDir.exists()) {
        detectedKind = "wal";
      } else {
        detectedKind = "file";
      }
    }
    const targetKind = this.#configuredBlobDriver ?? detectedKind ?? this.#defaultBlobDriver;
    if (detectedKind !== void 0 && detectedKind !== targetKind) {
      await this.#migrateBlob(root, dir, detectedKind, targetKind);
      descriptor = await this.#readDescriptor(dir);
    }
    const impl = this.#blobDrivers.get(targetKind);
    if (!impl) {
      throw new import_MatterError.NoProviderError(`No blob storage driver registered for "${targetKind}"`);
    }
    if (!descriptor) {
      descriptor = { kind: targetKind, type: "blob" };
    }
    if (impl.preinitialize) {
      const fs = this.#environment.get(import_Filesystem.Filesystem);
      await impl.preinitialize(fs, descriptor);
    }
    const storage = await impl.create(root, descriptor);
    try {
      await storage.initialize();
      if (await dir.exists()) {
        await this.#writeDescriptor(dir, { kind: targetKind, type: "blob" });
      }
      this.#openBlobDrivers.set(cacheKey, { driver: storage, refs: 1 });
      return { driver: storage, close: () => this.#releaseBlobDriver(cacheKey) };
    } catch (e) {
      try {
        await storage.close();
      } catch (closeError) {
        logger.warn("Error closing blob storage after failed initialization:", closeError);
      }
      throw e;
    }
  }
  async #openBlobSimple(cacheKey, dataNs) {
    const targetBlobKind = this.#configuredBlobDriver ?? this.#defaultBlobDriver;
    const impl = this.#blobDrivers.get(targetBlobKind);
    if (!impl) {
      throw new import_MatterError.NoProviderError(`No blob storage driver registered for "${targetBlobKind}"`);
    }
    const descriptor = { kind: targetBlobKind, type: "blob" };
    const storage = await impl.create(dataNs, descriptor);
    try {
      await storage.initialize();
      this.#openBlobDrivers.set(cacheKey, { driver: storage, refs: 1 });
      return { driver: storage, close: () => this.#releaseBlobDriver(cacheKey) };
    } catch (e) {
      try {
        await storage.close();
      } catch (closeError) {
        logger.warn("Error closing blob storage after failed initialization:", closeError);
      }
      throw e;
    }
  }
  async #releaseBlobDriver(cacheKey) {
    const cached = this.#openBlobDrivers.get(cacheKey);
    if (!cached) {
      return;
    }
    cached.refs--;
    if (cached.refs <= 0) {
      this.#openBlobDrivers.delete(cacheKey);
      await cached.driver.close();
    }
  }
  /**
   * The root filesystem path for storage, or a placeholder if no filesystem is available.
   */
  get location() {
    if (this.#environment.has(import_Filesystem.Filesystem)) {
      return this.#environment.get(import_Filesystem.Filesystem).path;
    }
    return "(off filesystem)";
  }
  [import_Diagnostic.Diagnostic.value]() {
    return [
      "Persistence",
      import_Diagnostic.Diagnostic.dict({
        location: this.location,
        available: this.#drivers.size > 0
      })
    ];
  }
  async #hasLegacyFileData(dir) {
    if (!await dir.exists()) {
      return false;
    }
    const ignoredFiles = import_StorageDriver.StorageDriver.RESERVED_FILENAMES;
    for await (const entry of dir.entries()) {
      if (entry.kind === "file" && !ignoredFiles.has(entry.name)) {
        return true;
      }
    }
    return false;
  }
  async #readDescriptor(dir) {
    const file = dir.file("driver.json");
    try {
      if (!await file.exists()) {
        return void 0;
      }
      const text = await file.readAllText();
      return JSON.parse(text);
    } catch {
      return void 0;
    }
  }
  async #writeDescriptor(dir, descriptor) {
    const file = dir.file("driver.json");
    await file.write(JSON.stringify(descriptor, void 0, 2));
  }
  async #migrate(fs, namespace, sourceDir, fromKind, toKind) {
    const fromImpl = this.#drivers.get(fromKind);
    const toImpl = this.#drivers.get(toKind);
    if (!fromImpl || !toImpl) {
      logger.warn(`Cannot migrate from "${fromKind}" to "${toKind}": driver not registered`);
      return;
    }
    await this.#migrateStorage({
      label: "storage",
      storageType: "kv",
      fs,
      namespace,
      sourceDir,
      fromKind,
      toKind,
      createSource: async (root, descriptor) => fromImpl.create(root, descriptor),
      createTarget: async (root, descriptor) => toImpl.create(root, descriptor),
      preinitializeSource: fromImpl.preinitialize?.bind(fromImpl),
      preinitializeTarget: toImpl.preinitialize?.bind(toImpl)
    });
  }
  async #migrateBlob(root, sourceDir, fromKind, toKind) {
    const fromImpl = this.#blobDrivers.get(fromKind);
    const toImpl = this.#blobDrivers.get(toKind);
    if (!fromImpl || !toImpl) {
      logger.warn(`Cannot migrate blob storage from "${fromKind}" to "${toKind}": driver not registered`);
      return;
    }
    await this.#migrateStorage({
      label: "blob storage",
      storageType: "blob",
      fs: this.#environment.get(import_Filesystem.Filesystem),
      namespace: root.namespace,
      sourceDir,
      fromKind,
      toKind,
      createSource: async (r, descriptor) => {
        const storage = await fromImpl.create(r, descriptor);
        await storage.initialize();
        return storage;
      },
      createTarget: async (r, descriptor) => {
        const storage = await toImpl.create(r, descriptor);
        await storage.initialize();
        return storage;
      },
      preinitializeSource: fromImpl.preinitialize?.bind(fromImpl),
      preinitializeTarget: toImpl.preinitialize?.bind(toImpl)
    });
  }
  async #migrateStorage(args) {
    const { label, storageType, fs, namespace, sourceDir, fromKind, toKind } = args;
    logger.notice(
      `Migrating ${label} "${namespace}" from "${fromKind}" to "${toKind}". Be patient, this may take a while...`
    );
    const migrationsDir = fs.directory(".migrations");
    await migrationsDir.mkdir();
    const tempSuffix = storageType === "blob" ? "-blob-new" : "-new";
    const tempDir = migrationsDir.directory(`${namespace}${tempSuffix}`);
    if (await tempDir.exists()) {
      await tempDir.delete();
    }
    await tempDir.mkdir();
    const fromDescriptor = { kind: fromKind, type: storageType };
    const toDescriptor = { kind: toKind, type: storageType };
    try {
      let sourceStorage;
      let targetStorage;
      try {
        if (args.preinitializeSource) {
          await args.preinitializeSource(fs, fromDescriptor);
        }
        sourceStorage = await args.createSource(new import_DatafileRoot.DatafileRoot(sourceDir), fromDescriptor);
        if (args.preinitializeTarget) {
          await args.preinitializeTarget(fs, toDescriptor);
        }
        targetStorage = await args.createTarget(new import_DatafileRoot.DatafileRoot(tempDir), toDescriptor);
        const result = await import_StorageMigration.StorageMigration.migrate(sourceStorage, targetStorage);
        const skipNote = result.otherTypeKeysSkipped > 0 ? `, ${result.otherTypeKeysSkipped} non-matching keys skipped` : "";
        if (result.success) {
          logger.notice(
            `${label} migration complete: ${result.migratedCount} migrated, ${result.skippedCount} skipped${skipNote}`
          );
        } else {
          logger.warn(
            `${label} migration had issues: ${result.migratedCount} migrated, ${result.skippedCount} skipped${skipNote}`
          );
        }
      } finally {
        if (targetStorage) {
          try {
            await targetStorage.close();
          } catch (e) {
            logger.warn(`Error closing target ${label} during migration:`, e);
          }
        }
        if (sourceStorage) {
          try {
            await sourceStorage.close();
          } catch (e) {
            logger.warn(`Error closing source ${label} during migration:`, e);
          }
        }
      }
      await this.#writeDescriptor(tempDir, toDescriptor);
      const sourcePath = sourceDir.path;
      const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
      const backupSuffix = storageType === "blob" ? `-old-blob-${fromKind}-${ts}` : `-old-${fromKind}-${ts}`;
      const backupDir = migrationsDir.directory(`${namespace}${backupSuffix}`);
      await fs.directory(sourcePath).rename(backupDir.path);
      await tempDir.rename(sourcePath);
    } catch (e) {
      try {
        await tempDir.delete();
      } catch (cleanupError) {
        logger.warn(`Error cleaning up ${label} migration temp directory:`, cleanupError);
      }
      throw e;
    }
  }
}
//# sourceMappingURL=StorageService.js.map
