/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { LocalActorContext } from "#behavior/context/server/LocalActorContext.js";
import { CommissioningClient } from "#behavior/system/commissioning/CommissioningClient.js";
import { RemoteDescriptor } from "#behavior/system/commissioning/RemoteDescriptor.js";
import { CommissioningDiscovery } from "#behavior/system/controller/discovery/CommissioningDiscovery.js";
import { ContinuousDiscovery } from "#behavior/system/controller/discovery/ContinuousDiscovery.js";
import { InstanceDiscovery } from "#behavior/system/controller/discovery/InstanceDiscovery.js";
import { PaseDiscovery } from "#behavior/system/controller/discovery/PaseDiscovery.js";
import { NetworkClient } from "#behavior/system/network/NetworkClient.js";
import { BasicInformationClient } from "#behaviors/basic-information";
import { OperationalCredentialsClient } from "#behaviors/operational-credentials";
import { EndpointContainer } from "#endpoint/properties/EndpointContainer.js";
import { ClientGroup } from "#node/ClientGroup.js";
import { InteractionServer } from "#node/server/InteractionServer.js";
import { ServerNodeStore } from "#storage/server/ServerNodeStore.js";
import {
  Diagnostic,
  ImplementationError,
  Lifecycle,
  Logger,
  MatterError,
  Minutes,
  Mutex,
  Seconds,
  Time,
  Timestamp,
  UninitializedDependencyError
} from "@matter/general";
import {
  ClientSubscriptionHandler,
  ClientSubscriptions,
  CommissioningError,
  FabricManager,
  Peer,
  PeerAddress,
  PeerLeftError,
  SessionManager
} from "@matter/protocol";
import { ClientNode } from "../ClientNode.js";
import { ClientNodeFactory } from "./ClientNodeFactory.js";
import { ClientStructureEvents } from "./ClientStructureEvents.js";
const logger = Logger.get("Peers");
const DEFAULT_TTL = Minutes(15);
const EXPIRATION_INTERVAL = Minutes.one;
class Peers extends EndpointContainer {
  #expirationInterval;
  #installedSubscriptionHandler;
  #mutex = new Mutex(this);
  #closed = false;
  #commissioning = /* @__PURE__ */ new Set();
  constructor(owner) {
    super(owner);
    if (!owner.env.has(ClientNodeFactory)) {
      owner.env.set(ClientNodeFactory, new Factory(this));
    }
    owner.env.applyTo(InteractionServer, this.#configureInteractionServer.bind(this));
    this.added.on(this.#handlePeerAdded.bind(this));
    this.deleted.on(this.#manageExpiration.bind(this));
    this.clusterInstalled(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(ClientNodeFactory);
    const clientStores = this.owner.env.maybeGet(ServerNodeStore)?.clientStores;
    if (clientStores === void 0) {
      throw new 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) {
        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 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 ContinuousDiscovery(this.owner, options);
  }
  /**
   * Find a specific commissionable node and commission.
   */
  commission(options) {
    return new 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 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(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(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(ClientStructureEvents).clusterInstalled(type);
  }
  /**
   * Emits when fixed attributes
   */
  get(id) {
    if (typeof id !== "string" && typeof id !== "number") {
      const address = 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 && 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(FabricManager).has(peerAddress)) {
      throw new 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(ServerNodeStore).clientStores.storeForNode(options.id);
        await store.construction;
      }
      const factory = this.owner.env.get(ClientNodeFactory);
      node = factory.create(options, peerAddress);
      await node.construction;
      this.add(node);
      await node.set({
        commissioning: { peerAddress: 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(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(InteractionServer)) {
      return;
    }
    const subscriptions = this.owner.env.get(ClientSubscriptions);
    const interactionServer = this.owner.env.get(InteractionServer);
    this.#installedSubscriptionHandler = new 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 === Lifecycle.Status.Destroying || status === Lifecycle.Status.Destroyed || status === Lifecycle.Status.Crashed) {
        throw new CommissioningError(`Cannot commission ${node.toString()} because the node is ${status}`);
      }
      if (this.#commissioning.has(node)) {
        throw new 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 = 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 = Time.nowMs;
      for (const node of this) {
        if (!node.lifecycle.isReady) {
          continue;
        }
        if (this.#commissioning.has(node)) {
          continue;
        }
        const state = node.maybeStateOf(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 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(OperationalCredentialsClient) ?? {};
      const peerFabric = peerFabrics?.find((fabric) => fabric.fabricIndex === fabricIndex);
      if (!peerFabric) {
        return;
      }
      const peerAddress = node.maybeStateOf(CommissioningClient)?.peerAddress;
      if (!peerAddress) {
        return;
      }
      const localFabrics = this.owner.env.get(FabricManager);
      const localFabric = localFabrics.forDescriptor(peerFabric);
      if (!localFabric || localFabric.fabricIndex !== peerAddress.fabricIndex) {
        return;
      }
      if (!node.act((agent) => agent.get(NetworkClient).subscriptionActive)) {
        logger.info(
          "Leave event for peer",
          Diagnostic.strong(node.id),
          " received without active subscription. Ignoring."
        );
        return;
      }
      logger.notice("Peer", Diagnostic.strong(node.id), "has left the fabric");
      node.lifecycle.decommissioned.emit(LocalActorContext.ReadOnly);
      try {
        await node.env.maybeGet(Peer)?.disconnect(new 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(NetworkClient).subscriptionActive)) {
      logger.debug(
        "Shutdown event for peer",
        Diagnostic.strong(node.id),
        "received without active subscription. Ignoring."
      );
      return;
    }
    const peerAddress = node.maybeStateOf(CommissioningClient)?.peerAddress;
    if (peerAddress !== void 0) {
      await this.owner.env.get(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(NetworkClient).subscriptionActive)) {
      logger.debug(
        "Startup event for peer",
        Diagnostic.strong(node.id),
        "received without active subscription. Ignoring."
      );
      return;
    }
    const peerAddress = node.maybeStateOf(CommissioningClient)?.peerAddress;
    if (peerAddress === void 0) {
      return;
    }
    const sessionManager = this.owner.env.get(SessionManager);
    await sessionManager.handlePeerShutdown(peerAddress, sessionManager.maybeSessionFor(peerAddress)?.createdAt);
  }
}
class Factory extends ClientNodeFactory {
  #owner;
  #groupIdCounter = 0;
  constructor(owner) {
    super();
    this.#owner = owner;
  }
  create(options, peerAddress) {
    let node;
    if (peerAddress !== void 0 && PeerAddress.isGroup(peerAddress)) {
      if (options.id === void 0) {
        options.id = `group${++this.#groupIdCounter}`;
      }
      node = new ClientGroup({
        ...options,
        owner: this.#owner.owner
      });
    } else {
      if (options.id === void 0) {
        options.id = this.#owner.owner.env.get(ServerNodeStore).clientStores.allocateId();
      }
      node = new 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 === Lifecycle.Status.Destroying || status === Lifecycle.Status.Destroyed || status === Lifecycle.Status.Crashed) {
        continue;
      }
      if (RemoteDescriptor.is(node.state.commissioning, descriptor)) {
        return node;
      }
    }
  }
  get nodes() {
    return this.#owner;
  }
}
function expirationOf(lifespan) {
  if (lifespan.discoveredAt !== void 0) {
    return Timestamp(lifespan.discoveredAt + (Seconds(lifespan.ttl) ?? DEFAULT_TTL));
  }
  return void 0;
}
export {
  Peers
};
//# sourceMappingURL=Peers.js.map
