/**
 * @license
 * Copyright 2025-2026 Open Home Foundation
 * SPDX-License-Identifier: Apache-2.0
 */
import { Logger, Mutex, StorageService } from "@matter/main";
const logger = new Logger("ConfigStorage");
function incrementNodeId(value) {
  return typeof value === "bigint" ? value + 1n : value + 1;
}
const SENSITIVE_KEYS = /* @__PURE__ */ new Set(["wifiCredentials", "threadDataset"]);
function sanitizeForLog(key, value) {
  if (SENSITIVE_KEYS.has(key)) {
    return "<redacted>";
  }
  return String(value);
}
class ConfigStorage {
  #env;
  #storageService;
  #storage;
  #configStore;
  #nodeIdMutex = new Mutex(this);
  #data = {
    nextNodeId: 1,
    fabricLabel: "HomeAssistant",
    wifiSsid: void 0,
    wifiCredentials: void 0,
    threadDataset: void 0
  };
  static async create(env) {
    const instance = new ConfigStorage(env);
    await instance.open();
    return instance;
  }
  constructor(env) {
    this.#env = env;
  }
  get service() {
    if (this.#storageService === void 0) {
      throw new Error("Storage not open");
    }
    return this.#storageService;
  }
  async open() {
    this.#storageService = this.#env.get(StorageService);
    logger.info(`Storage location: ${this.#storageService.location} (Directory)`);
    this.#storage = await this.#storageService.open("config");
    this.#configStore = this.#storage.createContext("values");
    const fabricLabel = await this.#configStore.has("fabricLabel") ? await this.#configStore.get("fabricLabel") : this.#env.vars.string("fabricLabel") ?? this.#data.fabricLabel;
    const nextNodeId = await this.#configStore.get("nextNodeId", this.#data.nextNodeId);
    const wifiSsid = await this.#configStore.has("wifiSsid") ? await this.#configStore.get("wifiSsid", "") : void 0;
    const wifiCredentials = await this.#configStore.has("wifiCredentials") ? await this.#configStore.get("wifiCredentials", "") : void 0;
    const threadDataset = await this.#configStore.has("threadDataset") ? await this.#configStore.get("threadDataset", "") : void 0;
    await this.set({ fabricLabel, nextNodeId, wifiSsid, wifiCredentials, threadDataset });
  }
  get fabricLabel() {
    return this.#data.fabricLabel;
  }
  get nextNodeId() {
    return this.#data.nextNodeId;
  }
  /**
   * Atomically allocate the next free node id and persist the advanced counter.
   *
   * The mutex serializes concurrent commissioning requests so they cannot read the same counter and collide.
   * `isInUse` lets a drifted counter skip ids already assigned on the fabric instead of colliding with them.
   */
  async allocateNodeId(isInUse) {
    return this.#nodeIdMutex.produce(async () => {
      const start = this.#data.nextNodeId;
      let candidate = start;
      let skipped = 0;
      while (isInUse(candidate)) {
        candidate = incrementNodeId(candidate);
        skipped++;
      }
      if (skipped > 0) {
        logger.notice(
          `Skipped ${skipped} node id(s) from ${start} already in use on the fabric, allocated ${candidate} instead`
        );
      }
      await this.set({ nextNodeId: incrementNodeId(candidate) });
      return candidate;
    });
  }
  get wifiSsid() {
    return this.#data.wifiSsid;
  }
  get wifiCredentials() {
    return this.#data.wifiCredentials;
  }
  get threadDataset() {
    return this.#data.threadDataset;
  }
  async set(data) {
    if (!this.#configStore) {
      throw new Error("Storage not open");
    }
    for (const key of Object.keys(data)) {
      if (!(key in this.#data)) {
        throw new Error(`Invalid key: ${key}`);
      }
      this.#data[key] = data[key];
      logger.info(`Set config key ${key} to ${sanitizeForLog(key, data[key])}`);
      await this.#configStore.set(key, data[key]);
    }
  }
  async removeWifiCredentials() {
    if (!this.#configStore) {
      throw new Error("Storage not open");
    }
    this.#data.wifiSsid = void 0;
    this.#data.wifiCredentials = void 0;
    await this.#configStore.delete("wifiSsid");
    await this.#configStore.delete("wifiCredentials");
    logger.info("Removed WiFi credentials");
  }
  async removeThreadDataset() {
    if (!this.#configStore) {
      throw new Error("Storage not open");
    }
    this.#data.threadDataset = void 0;
    await this.#configStore.delete("threadDataset");
    logger.info("Removed Thread dataset");
  }
  async close() {
    if (this.#storage) {
      await this.#storage.close();
    }
  }
}
export {
  ConfigStorage
};
//# sourceMappingURL=ConfigStorage.js.map
