/**
 * @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 { 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,
  Logger,
  MatterError,
  Minutes,
  Mutex,
  Seconds,
  Time,
  Timestamp,
  UninitializedDependencyError
} from "@matter/general";
import {
  ClientSubscriptionHandler,
  ClientSubscriptions,
  FabricManager,
  PeerAddress,
  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;
  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) {
        continue;
      }
      try {
        await peer.start();
      } catch (e) {
        MatterError.accept(e);
        logger.error(`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);
  }
  /**
   * 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");
    }
    let node = this.get(peerAddress);
    if (!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;
  }
  /**
   * 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.error("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;
        }
        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));
  }
  #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);
      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);
    }
  }
}
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) {
      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
