/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  AsyncObservable,
  BasicSet,
  Bytes,
  Construction,
  Crypto,
  Diagnostic,
  Environmental,
  ImplementationError,
  Logger,
  MatterError,
  MatterFlowError,
  MaybePromise,
  Observable,
  StorageManager
} from "@matter/general";
import { FabricIndex, GlobalFabricId, NodeId } from "@matter/types";
import { Fabric } from "./Fabric.js";
const logger = Logger.get("FabricManager");
class FabricNotFoundError extends MatterError {
}
class FabricTableFullError extends MatterError {
}
class FabricManager {
  #crypto;
  #nextFabricIndex = 1;
  #fabrics = new BasicSet();
  #initializationDone = false;
  #storage;
  #events = {
    added: Observable(),
    replaced: Observable(unhandled("replacing")),
    leaving: Observable(unhandled("leaving")),
    deleting: AsyncObservable(unhandled("deleting")),
    deleted: AsyncObservable(unhandled("deleted")),
    failsafeClosed: Observable()
  };
  #construction;
  constructor(crypto, storage) {
    this.#crypto = crypto;
    this.#storage = storage;
    let construct;
    if (this.#storage === void 0) {
      construct = () => {
      };
    } else {
      construct = async () => {
        if (this.#storage === void 0) {
          return;
        }
        const fabrics = await this.#storage.get("fabrics", []);
        for (const fabricConfig of fabrics) {
          this.#addNewFabric(await Fabric.create(crypto, fabricConfig));
        }
        this.#nextFabricIndex = await this.#storage.get("nextFabricIndex", this.#nextFabricIndex);
        this.#initializationDone = true;
      };
    }
    this.#construction = Construction(this, construct);
  }
  get crypto() {
    return this.#crypto;
  }
  get construction() {
    return this.#construction;
  }
  async [Construction.construct]() {
    await this.construction;
  }
  static [Environmental.create](env) {
    const instance = new FabricManager(env.get(Crypto), env.get(StorageManager).createContext("fabrics"));
    env.set(FabricManager, instance);
    return instance;
  }
  get events() {
    return this.#events;
  }
  async clear() {
    await this.#construction;
    this.#nextFabricIndex = 1;
    this.#fabrics.clear();
    await this.#storage?.clearAll();
  }
  /**
   * Test whether the fabric identified by {@link identifier} is present.
   */
  has(address) {
    let fabric;
    if (typeof address === "bigint") {
      fabric = this.#fabrics.get("globalId", address);
    } else {
      if (typeof address === "object") {
        address = address.fabricIndex;
      }
      fabric = this.#fabrics.get("fabricIndex", address);
    }
    return fabric !== void 0 && !fabric.isDeleting;
  }
  /**
   * Obtain the fabric identified by {@link identifier}.
   *
   * Throws if the fabric is not found.
   */
  for(identifier) {
    let fabric;
    if (typeof identifier === "bigint") {
      fabric = this.#fabrics.get("globalId", identifier);
    } else {
      if (typeof identifier === "object") {
        identifier = identifier.fabricIndex;
      }
      fabric = this.#fabrics.get("fabricIndex", identifier);
    }
    if (fabric === void 0 || fabric.isDeleting) {
      const str = typeof identifier === "bigint" ? GlobalFabricId.strOf(identifier) : `#${identifier}`;
      throw new FabricNotFoundError(
        `Fabric index ${str} ${fabric === void 0 ? "does not exist" : "is deleted"}`
      );
    }
    return fabric;
  }
  /**
   * Get the fabric identified by {@link identifier} if present.
   */
  maybeFor(identifier) {
    if (this.has(identifier)) {
      return this.for(identifier);
    }
  }
  allocateFabricIndex() {
    this.#construction.assert();
    for (let i = 0; i < 254; i++) {
      const fabricIndex = this.#nextFabricIndex++;
      if (this.#nextFabricIndex > 254) this.#nextFabricIndex = 1;
      if (!this.#fabrics.get("fabricIndex", FabricIndex(fabricIndex))) {
        return FabricIndex(fabricIndex);
      }
    }
    throw new FabricTableFullError("No free fabric index available.");
  }
  persistFabrics() {
    if (this.#storage === void 0) {
      throw new ImplementationError(
        "Fabric persistence is disabled because FabricManager constructed without storage"
      );
    }
    this.#construction.assert();
    const storeResult = this.#storage.set(
      "fabrics",
      this.fabrics.map((fabric) => fabric.config)
    );
    if (MaybePromise.is(storeResult)) {
      return storeResult.then(() => this.#storage.set("nextFabricIndex", this.#nextFabricIndex));
    }
    return this.#storage.set("nextFabricIndex", this.#nextFabricIndex);
  }
  addFabric(fabric) {
    this.#construction.assert();
    this.#addNewFabric(fabric);
  }
  #addNewFabric(fabric) {
    const { fabricIndex } = fabric;
    if (this.#fabrics.get("fabricIndex", fabricIndex)) {
      throw new MatterFlowError(`Fabric with index ${fabricIndex} already exists.`);
    }
    this.#addOrUpdateFabricEntry(fabric);
    if (this.#initializationDone) {
      this.#events.added.emit(fabric);
    }
  }
  /** Insert Fabric into the manager without emitting events */
  #addOrUpdateFabricEntry(fabric) {
    const existing = this.#fabrics.get("fabricIndex", fabric.fabricIndex);
    if (existing) {
      this.#fabrics.delete(existing);
    }
    this.#fabrics.add(fabric);
    fabric.leaving.on(() => this.events.leaving.emit(fabric));
    fabric.deleting.on(() => this.events.deleting.emit(fabric));
    fabric.deleted.on(() => this.#handleFabricDeleted(fabric));
    fabric.persistCallback = (isUpdate = true) => {
      if (!this.#storage) {
        if (isUpdate) {
          logger.warn(`Fabric ${fabric.fabricIndex} cannot persist because FabricManager has no storage`);
        }
        return;
      }
      const persistResult = this.persistFabrics();
      return MaybePromise.then(persistResult, () => {
        if (isUpdate) {
          this.#events.replaced.emit(fabric);
        }
      });
    };
    if (this.#storage !== void 0 && fabric.storage === void 0) {
      fabric.storage = this.#storage.createContext(`fabric-${fabric.fabricIndex}`);
    }
  }
  async #handleFabricDeleted(fabric) {
    await this.#construction;
    this.#fabrics.delete(fabric);
    if (this.#storage) {
      await this.persistFabrics();
    }
    await fabric.storage?.clearAll();
  }
  [Symbol.iterator]() {
    this.#construction.assert();
    return this.fabrics[Symbol.iterator]();
  }
  get fabrics() {
    this.#construction.assert();
    return this.#fabrics.filter((fabric) => !fabric.isDeleting);
  }
  get length() {
    return this.fabrics.length;
  }
  find(predicate) {
    return this.fabrics.find(predicate);
  }
  map(translator) {
    return this.fabrics.map(translator);
  }
  async findFabricFromDestinationId(destinationId, initiatorRandom) {
    this.#construction.assert();
    for (const fabric of this.#fabrics) {
      const candidateDestinationIds = await fabric.destinationIdsFor(fabric.nodeId, initiatorRandom);
      if (candidateDestinationIds.some((candidate) => Bytes.areEqual(candidate, destinationId))) {
        if (fabric.isDeleting) {
          throw new FabricNotFoundError("Fabric is deleting for CASE sigma2");
        }
        return fabric;
      }
    }
    const fabrics = this.#fabrics.map(
      (fabric) => `#${fabric.fabricIndex} (node ID ${NodeId.strOf(fabric.nodeId)}) keys ${fabric.groups.keySets.allKeysForId(0).map(({ key }) => Bytes.toHex(key)).join(" & ")}`
    );
    logger.debug(
      `No match for destination ID`,
      Diagnostic.dict({ destId: destinationId, random: initiatorRandom, ...fabrics })
    );
    throw new FabricNotFoundError("Fabric not found for CASE sigma2");
  }
  findByKeypair(keypair) {
    this.#construction.assert();
    for (const fabric of this.fabrics) {
      if (fabric.matchesKeyPair(keypair)) {
        return fabric;
      }
    }
    return void 0;
  }
  forDescriptor(descriptor) {
    this.#construction.assert();
    return this.find(
      (fabric) => Bytes.areEqual(fabric.rootPublicKey, descriptor.rootPublicKey) && fabric.fabricId === descriptor.fabricId
    );
  }
  async replaceFabric(fabric) {
    await this.#construction;
    const { fabricIndex } = fabric;
    const existingFabric = this.for(fabricIndex);
    if (existingFabric === void 0) {
      throw new FabricNotFoundError(
        `Fabric with index ${fabricIndex} cannot be replaced because it does not exist.`
      );
    }
    if (existingFabric === fabric) {
      return;
    }
    this.#addOrUpdateFabricEntry(fabric);
    if (this.#storage) {
      await this.persistFabrics();
    }
    this.#events.replaced.emit(fabric);
  }
}
function unhandled(what) {
  return (e, observer) => {
    logger.error(`Unhandled error in fabric ${what} observer ${observer.name || "(anon)"}`, e);
  };
}
export {
  FabricManager,
  FabricNotFoundError,
  FabricTableFullError
};
//# sourceMappingURL=FabricManager.js.map
