"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 Peers_exports = {};
__export(Peers_exports, {
  Peers: () => Peers
});
module.exports = __toCommonJS(Peers_exports);
var import_LocalActorContext = require("#behavior/context/server/LocalActorContext.js");
var import_CommissioningClient = require("#behavior/system/commissioning/CommissioningClient.js");
var import_RemoteDescriptor = require("#behavior/system/commissioning/RemoteDescriptor.js");
var import_CommissioningDiscovery = require("#behavior/system/controller/discovery/CommissioningDiscovery.js");
var import_ContinuousDiscovery = require("#behavior/system/controller/discovery/ContinuousDiscovery.js");
var import_InstanceDiscovery = require("#behavior/system/controller/discovery/InstanceDiscovery.js");
var import_PaseDiscovery = require("#behavior/system/controller/discovery/PaseDiscovery.js");
var import_NetworkClient = require("#behavior/system/network/NetworkClient.js");
var import_basic_information = require("#behaviors/basic-information");
var import_operational_credentials = require("#behaviors/operational-credentials");
var import_EndpointContainer = require("#endpoint/properties/EndpointContainer.js");
var import_ClientGroup = require("#node/ClientGroup.js");
var import_InteractionServer = require("#node/server/InteractionServer.js");
var import_ServerNodeStore = require("#storage/server/ServerNodeStore.js");
var import_general = require("@matter/general");
var import_protocol = require("@matter/protocol");
var import_ClientNode = require("../ClientNode.js");
var import_ClientNodeFactory = require("./ClientNodeFactory.js");
var import_ClientStructureEvents = require("./ClientStructureEvents.js");
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_general.Logger.get("Peers");
const DEFAULT_TTL = (0, import_general.Minutes)(15);
const EXPIRATION_INTERVAL = import_general.Minutes.one;
class Peers extends import_EndpointContainer.EndpointContainer {
  #expirationInterval;
  #installedSubscriptionHandler;
  #mutex = new import_general.Mutex(this);
  #closed = false;
  #commissioning = /* @__PURE__ */ new Set();
  constructor(owner) {
    super(owner);
    if (!owner.env.has(import_ClientNodeFactory.ClientNodeFactory)) {
      owner.env.set(import_ClientNodeFactory.ClientNodeFactory, new Factory(this));
    }
    owner.env.applyTo(import_InteractionServer.InteractionServer, this.#configureInteractionServer.bind(this));
    this.added.on(this.#handlePeerAdded.bind(this));
    this.deleted.on(this.#manageExpiration.bind(this));
    this.clusterInstalled(import_basic_information.BasicInformationClient).on(this.#instrumentBasicInformation.bind(this));
    const lifecycle = owner.lifecycle;
    lifecycle.online.on(this.#nodeOnline.bind(this));
    lifecycle.offline.on(this.#nodeOffline.bind(this));
  }
  /**
   * Load nodes.  Invoked automatically by owner.
   */
  initialize() {
    const factory = this.owner.env.get(import_ClientNodeFactory.ClientNodeFactory);
    const clientStores = this.owner.env.maybeGet(import_ServerNodeStore.ServerNodeStore)?.clientStores;
    if (clientStores === void 0) {
      throw new import_general.UninitializedDependencyError(
        "Peers",
        "are not available because ServerNode initialization is incomplete"
      );
    }
    for (const id of clientStores.knownIds) {
      this.add(
        factory.create({
          id,
          owner: this.owner
        })
      );
    }
  }
  async #nodeOnline() {
    for (const peer of this) {
      if (!peer.lifecycle.isCommissioned || peer.maybeStateOf("network")?.isDisabled) {
        continue;
      }
      try {
        await peer.start();
      } catch (e) {
        import_general.MatterError.accept(e);
        logger.warn(`Error starting peer ${peer}:`, e);
      }
    }
    this.#manageExpiration();
  }
  async #nodeOffline() {
    this.#cancelExpiration();
    for (const peer of this) {
      await peer.stop();
    }
  }
  /**
   * Find a specific commissionable node, or, if no discovery options are provided, returns the first discovered node.
   * TODO: Allow to provide an array of options for multiple discoveries (e.g. from a Multi QR code).
   */
  locate(options) {
    return new import_InstanceDiscovery.InstanceDiscovery(this.owner, options);
  }
  /**
   * Employ discovery to find a set of commissionable nodes, the options can be used to limit the discovered devices
   * (e.g. just a specific vendor).
   * TODO: Allow to provide multiple identifiers for multiple discoveries (e.g. from a Multi QR code).
   *
   * If you do not provide a timeout value, will search until canceled, and you need to add a listener to
   * {@link Discovery#discovered} or {@link added} to receive discovered nodes.
   */
  discover(options) {
    return new import_ContinuousDiscovery.ContinuousDiscovery(this.owner, options);
  }
  /**
   * Find a specific commissionable node and commission.
   */
  commission(options) {
    return new import_CommissioningDiscovery.CommissioningDiscovery(this.owner, options);
  }
  /**
   * Find a specific commissionable node and establish a PASE session without commissioning.
   *
   * Useful for split-commissioning scenarios where one controller establishes PASE and another
   * performs the commissioning flow, or for raw PASE channel establishment.
   */
  pase(options) {
    return new import_PaseDiscovery.PaseDiscovery(this.owner, options);
  }
  /**
   * Find or create a {@link ClientNode} for a device described by {@link descriptor}.
   *
   * If a matching node already exists in the peer collection, returns it after refreshing its addresses and
   * discovery data from the supplied descriptor.  Otherwise creates a new node using the descriptor.
   *
   * After calling {@link forDescriptor}, commission the returned node via {@link ClientNode.commission}.
   */
  async forDescriptor(descriptor) {
    const factory = this.owner.env.get(import_ClientNodeFactory.ClientNodeFactory);
    let node = factory.find(descriptor);
    if (node !== void 0) {
      await node.act((agent) => {
        agent.commissioning.descriptor = descriptor;
      });
      return node;
    }
    return factory.create({ commissioning: { descriptor } });
  }
  /**
   * Obtain an {@link Observable} that emits when a specific type of endpoint initializes for a peer.
   *
   * This is useful for initializing general behavior on any peer endpoint of the specified type.
   */
  endpointInstalled(type) {
    return this.owner.env.get(import_ClientStructureEvents.ClientStructureEvents).endpointInstalled(type);
  }
  /**
   * Obtain a {@link Observable} that emits when a specific type of cluster initializes for a peer.
   *
   * This is useful for initializing general behavior on any peer endpoint with the specified cluster.
   */
  clusterInstalled(type) {
    return this.owner.env.get(import_ClientStructureEvents.ClientStructureEvents).clusterInstalled(type);
  }
  /**
   * Emits when fixed attributes
   */
  get(id) {
    if (typeof id !== "string" && typeof id !== "number") {
      const address = (0, import_protocol.PeerAddress)(id);
      for (const node of this) {
        if (node.behaviors.active.some(({ id: id2 }) => id2 === "commissioning")) {
          const nodeAddress = node.maybeStateOf("commissioning")?.peerAddress;
          if (nodeAddress !== void 0 && import_protocol.PeerAddress.is(nodeAddress, address)) {
            return node;
          }
        }
      }
      return void 0;
    }
    return super.get(id);
  }
  get owner() {
    return super.owner;
  }
  add(node) {
    node.owner = this.owner;
    super.add(node);
  }
  /**
   * Get or create a client node for the given peer address.
   *
   * This is mainly used to communicate to other known nodes on the fabric without having a formal commissioning
   * process.
   */
  async forAddress(peerAddress, options = {}) {
    if (!this.owner.env.get(import_protocol.FabricManager).has(peerAddress)) {
      throw new import_general.ImplementationError("Cannot register a peer address for a fabric we do not belong to");
    }
    const existing = this.get(peerAddress);
    if (existing) {
      return existing;
    }
    return this.#mutex.produce(async () => {
      let node = this.get(peerAddress);
      if (node) {
        return node;
      }
      if (options.id !== void 0) {
        const store = this.owner.env.get(import_ServerNodeStore.ServerNodeStore).clientStores.storeForNode(options.id);
        await store.construction;
      }
      const factory = this.owner.env.get(import_ClientNodeFactory.ClientNodeFactory);
      node = factory.create(options, peerAddress);
      await node.construction;
      this.add(node);
      await node.set({
        commissioning: { peerAddress: (0, import_protocol.PeerAddress)(peerAddress) }
      });
      return node;
    });
  }
  async close() {
    this.#closed = true;
    await this.#installedSubscriptionHandler?.close();
    this.#cancelExpiration();
    await this.#mutex;
    await super.close();
  }
  #cancelExpiration() {
    if (this.#expirationInterval) {
      this.#expirationInterval.cancel();
      this.#expirationInterval = void 0;
    }
  }
  #handlePeerAdded() {
    if (this.owner.env.has(import_InteractionServer.InteractionServer)) {
      this.#configureInteractionServer();
    }
    this.#manageExpiration();
  }
  /**
   * If required, installs a listener in the environment's {@link InteractionServer} to handle subscription responses.
   */
  #configureInteractionServer() {
    if (this.#closed || this.#installedSubscriptionHandler !== void 0 || !this.owner.env.has(import_InteractionServer.InteractionServer)) {
      return;
    }
    const subscriptions = this.owner.env.get(import_protocol.ClientSubscriptions);
    const interactionServer = this.owner.env.get(import_InteractionServer.InteractionServer);
    this.#installedSubscriptionHandler = new import_protocol.ClientSubscriptionHandler(subscriptions);
    interactionServer.clientHandler = this.#installedSubscriptionHandler;
  }
  /**
   * Run a commission attempt on {@link node} while protecting the node from the expired-node cull.
   *
   * Serialization is done through the same {@link #mutex} the cull uses: the busy registration runs as a mutex task,
   * which guarantees no cull is in flight when we register and that any subsequent cull observes the busy flag.
   *
   * Rejects with {@link CommissioningError} if the node is already mid-commission (parallel attempts on the same
   * {@link ClientNode} would race on device-side state) or if a cull queued ahead of us already destroyed/crashed
   * the node.  Either way the attempt fails fast instead of crashing later when the closed backing is accessed.
   */
  async runCommissioning(node, fn) {
    await this.#mutex.produce(async () => {
      const status = node.construction.status;
      if (status === import_general.Lifecycle.Status.Destroying || status === import_general.Lifecycle.Status.Destroyed || status === import_general.Lifecycle.Status.Crashed) {
        throw new import_protocol.CommissioningError(`Cannot commission ${node.toString()} because the node is ${status}`);
      }
      if (this.#commissioning.has(node)) {
        throw new import_protocol.CommissioningError(
          `Cannot commission ${node.toString()} because a commission attempt is already in progress`
        );
      }
      this.#commissioning.add(node);
    });
    try {
      return await fn();
    } finally {
      this.#commissioning.delete(node);
    }
  }
  /**
   * Enables or disables the expiration timer that culls expired uncommissioned nodes.
   */
  #manageExpiration() {
    if (this.#closed) {
      return;
    }
    if (this.#expirationInterval) {
      if (!this.size) {
        this.#cancelExpiration();
      }
      return;
    }
    if (!this.size) {
      return;
    }
    this.#expirationInterval = import_general.Time.sleep("client node expiration", EXPIRATION_INTERVAL).then(
      this.#onExpirationIntervalElapsed.bind(this)
    );
  }
  #onExpirationIntervalElapsed() {
    if (this.#closed) {
      return;
    }
    this.#mutex.run(
      () => this.#cullExpiredNodesAndAddresses().catch((error) => {
        logger.warn("Error culling expired nodes", error);
      }).finally(() => {
        this.#manageExpiration();
      })
    );
  }
  async #cullExpiredNodesAndAddresses() {
    try {
      const now = import_general.Time.nowMs;
      for (const node of this) {
        if (!node.lifecycle.isReady) {
          continue;
        }
        if (this.#commissioning.has(node)) {
          continue;
        }
        const state = node.maybeStateOf(import_CommissioningClient.CommissioningClient);
        if (state === void 0) {
          continue;
        }
        const { addresses } = state;
        const isCommissioned = state.peerAddress !== void 0;
        if (addresses === void 0 || isCommissioned && addresses.length === 1) {
          continue;
        }
        let newAddresses = addresses.filter((addr) => {
          const exp = expirationOf(addr);
          if (exp === void 0) {
            return true;
          }
          return exp > now;
        });
        if (!isCommissioned) {
          if (!newAddresses?.length || (expirationOf(state) ?? 0) <= now) {
            await node.delete();
            continue;
          }
        }
        if (isCommissioned && addresses.length && !newAddresses.length) {
          if (addresses.length === 1) {
            continue;
          }
          const freshestExp = addresses.reduce((freshestExp2, addr) => {
            return Math.max(freshestExp2, expirationOf(addr));
          }, 0);
          newAddresses = addresses.filter((addr) => expirationOf(addr) === freshestExp);
        }
        if (addresses.length !== newAddresses.length) {
          await node.set({ commissioning: { addresses: newAddresses } });
        }
      }
    } finally {
      this.#expirationInterval = void 0;
    }
  }
  #instrumentBasicInformation(node, type) {
    if (!(node instanceof import_ClientNode.ClientNode)) {
      return;
    }
    node.eventsOf(type).leave?.on(({ fabricIndex }) => this.#onLeave(node, fabricIndex));
    node.eventsOf(type).shutDown?.on(() => this.#onShutdown(node));
    node.eventsOf(type).startUp?.on(() => this.#onStartUp(node));
  }
  #onLeave(node, fabricIndex) {
    if (!node.lifecycle.isReady) {
      return;
    }
    this.#mutex.run(async () => {
      if (!node.lifecycle.isReady) {
        return;
      }
      const { fabrics: peerFabrics } = node.maybeStateOf(import_operational_credentials.OperationalCredentialsClient) ?? {};
      const peerFabric = peerFabrics?.find((fabric) => fabric.fabricIndex === fabricIndex);
      if (!peerFabric) {
        return;
      }
      const peerAddress = node.maybeStateOf(import_CommissioningClient.CommissioningClient)?.peerAddress;
      if (!peerAddress) {
        return;
      }
      const localFabrics = this.owner.env.get(import_protocol.FabricManager);
      const localFabric = localFabrics.forDescriptor(peerFabric);
      if (!localFabric || localFabric.fabricIndex !== peerAddress.fabricIndex) {
        return;
      }
      if (!node.act((agent) => agent.get(import_NetworkClient.NetworkClient).subscriptionActive)) {
        logger.info(
          "Leave event for peer",
          import_general.Diagnostic.strong(node.id),
          " received without active subscription. Ignoring."
        );
        return;
      }
      logger.notice("Peer", import_general.Diagnostic.strong(node.id), "has left the fabric");
      node.lifecycle.decommissioned.emit(import_LocalActorContext.LocalActorContext.ReadOnly);
      try {
        await node.env.maybeGet(import_protocol.Peer)?.disconnect(new import_protocol.PeerLeftError());
      } catch (error) {
        logger.warn(`Error force-closing sessions for ${node.id} on Leave:`, error);
      }
      await node.delete();
    });
  }
  async #onShutdown(node) {
    if (!node.lifecycle.isReady || !node.lifecycle.isOnline) {
      return;
    }
    if (!node.act((agent) => agent.get(import_NetworkClient.NetworkClient).subscriptionActive)) {
      logger.debug(
        "Shutdown event for peer",
        import_general.Diagnostic.strong(node.id),
        "received without active subscription. Ignoring."
      );
      return;
    }
    const peerAddress = node.maybeStateOf(import_CommissioningClient.CommissioningClient)?.peerAddress;
    if (peerAddress !== void 0) {
      await this.owner.env.get(import_protocol.SessionManager).handlePeerShutdown(peerAddress);
    }
  }
  /**
   * Process startup events to detect node reboots in real-time. This allows us to terminate sessions
   * that would otherwise linger until timed out and cause interaction delays.
   */
  async #onStartUp(node) {
    if (!node.lifecycle.isReady || !node.lifecycle.isOnline) {
      return;
    }
    if (!node.act((agent) => agent.get(import_NetworkClient.NetworkClient).subscriptionActive)) {
      logger.debug(
        "Startup event for peer",
        import_general.Diagnostic.strong(node.id),
        "received without active subscription. Ignoring."
      );
      return;
    }
    const peerAddress = node.maybeStateOf(import_CommissioningClient.CommissioningClient)?.peerAddress;
    if (peerAddress === void 0) {
      return;
    }
    const sessionManager = this.owner.env.get(import_protocol.SessionManager);
    await sessionManager.handlePeerShutdown(peerAddress, sessionManager.maybeSessionFor(peerAddress)?.createdAt);
  }
}
class Factory extends import_ClientNodeFactory.ClientNodeFactory {
  #owner;
  #groupIdCounter = 0;
  constructor(owner) {
    super();
    this.#owner = owner;
  }
  create(options, peerAddress) {
    let node;
    if (peerAddress !== void 0 && import_protocol.PeerAddress.isGroup(peerAddress)) {
      if (options.id === void 0) {
        options.id = `group${++this.#groupIdCounter}`;
      }
      node = new import_ClientGroup.ClientGroup({
        ...options,
        owner: this.#owner.owner
      });
    } else {
      if (options.id === void 0) {
        options.id = this.#owner.owner.env.get(import_ServerNodeStore.ServerNodeStore).clientStores.allocateId();
      }
      node = new import_ClientNode.ClientNode({
        ...options,
        owner: this.#owner.owner
      });
    }
    node.construction.start();
    return node;
  }
  find(descriptor) {
    for (const node of this.#owner) {
      const status = node.construction.status;
      if (status === import_general.Lifecycle.Status.Destroying || status === import_general.Lifecycle.Status.Destroyed || status === import_general.Lifecycle.Status.Crashed) {
        continue;
      }
      if (import_RemoteDescriptor.RemoteDescriptor.is(node.state.commissioning, descriptor)) {
        return node;
      }
    }
  }
  get nodes() {
    return this.#owner;
  }
}
function expirationOf(lifespan) {
  if (lifespan.discoveredAt !== void 0) {
    return (0, import_general.Timestamp)(lifespan.discoveredAt + ((0, import_general.Seconds)(lifespan.ttl) ?? DEFAULT_TTL));
  }
  return void 0;
}
//# sourceMappingURL=Peers.js.map
