/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { ClusterClient } from "#cluster/client/ClusterClient.js";
import { UnknownNodeError } from "#cluster/client/InteractionClient.js";
import {
  AsyncObservable,
  camelize,
  Construction,
  Diagnostic,
  ImplementationError,
  InternalError,
  Logger,
  MatterError,
  MaybePromise,
  Observable,
  ObserverGroup,
  Seconds,
  Time
} from "@matter/general";
import {
  AcceptedCommandList,
  AggregatorDt,
  AttributeList,
  ClusterModel,
  ClusterRevision,
  FeatureMap,
  Matter
} from "@matter/model";
import {
  ClusterBehavior,
  EndpointLifecycle,
  NetworkClient
} from "@matter/node";
import { DescriptorClient } from "@matter/node/behaviors/descriptor";
import {
  PaseClient,
  Read,
  SustainedSubscription
} from "@matter/protocol";
import {
  ClusterType,
  CommissioningFlowType,
  DiscoveryCapabilitiesSchema,
  EndpointNumber,
  ManualPairingCodeCodec,
  QrPairingCodeCodec,
  StatusCode,
  StatusResponseError
} from "@matter/types";
import { AdministratorCommissioning } from "@matter/types/clusters/administrator-commissioning";
import { BasicInformation } from "@matter/types/clusters/basic-information";
import { Descriptor } from "@matter/types/clusters/descriptor";
import { Aggregator } from "./Aggregator.js";
import { ComposedDevice } from "./ComposedDevice.js";
import { PairedDevice, RootEndpoint } from "./Device.js";
import { DeviceInformation } from "./DeviceInformation.js";
import { getDeviceTypeDefinitionFromModelByCode, UnknownDeviceType } from "./DeviceTypes.js";
import { asClusterClientInternal } from "./TypeHelpers.js";
const logger = Logger.get("PairedNode");
const STRUCTURE_UPDATE_TIMEOUT = Seconds(5);
var NodeStates = /* @__PURE__ */ ((NodeStates2) => {
  NodeStates2[NodeStates2["Connected"] = 0] = "Connected";
  NodeStates2[NodeStates2["Disconnected"] = 1] = "Disconnected";
  NodeStates2[NodeStates2["Reconnecting"] = 2] = "Reconnecting";
  NodeStates2[NodeStates2["WaitingForDeviceDiscovery"] = 3] = "WaitingForDeviceDiscovery";
  return NodeStates2;
})(NodeStates || {});
var NodeStateInformation = /* @__PURE__ */ ((NodeStateInformation2) => {
  NodeStateInformation2[NodeStateInformation2["Connected"] = 0] = "Connected";
  NodeStateInformation2[NodeStateInformation2["Disconnected"] = 1] = "Disconnected";
  NodeStateInformation2[NodeStateInformation2["Reconnecting"] = 2] = "Reconnecting";
  NodeStateInformation2[NodeStateInformation2["WaitingForDeviceDiscovery"] = 3] = "WaitingForDeviceDiscovery";
  NodeStateInformation2[NodeStateInformation2["StructureChanged"] = 4] = "StructureChanged";
  NodeStateInformation2[NodeStateInformation2["Decommissioned"] = 5] = "Decommissioned";
  return NodeStateInformation2;
})(NodeStateInformation || {});
class NodeNotConnectedError extends MatterError {
}
function areNumberListsSame(list1, list2) {
  const set1 = new Set(list1);
  const set2 = new Set(list2);
  if (set1.size !== set2.size) return false;
  for (const entry of set1.values()) {
    if (!set2.has(entry)) return false;
  }
  return true;
}
class PairedNode {
  constructor(nodeId, commissioningController, options = {}, clientNode, interactionClient, peer, crypto, changes) {
    this.nodeId = nodeId;
    this.#commissioningController = commissioningController;
    this.#peerAddress = commissioningController.fabric.addressOf(nodeId);
    this.#options = options;
    this.#crypto = crypto;
    this.#clientNode = clientNode;
    this.#interactionClient = interactionClient;
    this.#observers.on(changes, this.#handleNodeChange.bind(this));
    this.#observers.on(this.#clientNode.lifecycle.changed, (type, endpoint) => {
      if (this.#endpoints !== void 0) {
        return;
      }
      switch (type) {
        case EndpointLifecycle.Change.PartsReady:
          this.#pendingNodeChangeEvents.set(endpoint.number, "nodeEndpointAdded");
          break;
      }
    });
    this.#observers.on(this.#clientNode.lifecycle.decommissioned, () => this.#handleNodeDecommissioning());
    this.#observers.on(
      this.#clientNode.eventsOf(NetworkClient).subscriptionStatusChanged,
      this.#handleSubscriptionStatusChanged.bind(this)
    );
    this.#observers.on(
      this.#clientNode.eventsOf(NetworkClient).subscriptionAlive,
      this.#handleSubscriptionAlive.bind(this)
    );
    this.#observers.on(peer.service.changed, () => {
      if (!peer.service.addresses.size && this.#connectionState === 2 /* Reconnecting */) {
        this.#setConnectionState(3 /* WaitingForDeviceDiscovery */);
      }
    });
    this.#observers.on(peer.sessions.deleted, () => {
      if (peer.sessions.size) {
        return;
      }
      if (peer.service.addresses.size) {
        if (this.#connectionState === 0 /* Connected */) {
          this.#setConnectionState(2 /* Reconnecting */);
        }
      } else if (this.#connectionState === 2 /* Reconnecting */) {
        this.#setConnectionState(3 /* WaitingForDeviceDiscovery */);
      }
    });
    this.#nodeDetails = new DeviceInformation(clientNode);
    logger.info(this.#peerAddress, `Created paired node with device data`, this.#nodeDetails.meta);
    this.#construction = Construction(this, async () => {
      await this.#initializeFromStoredData();
      let state = 2 /* Reconnecting */;
      const subscription = this.#clientNode.behaviors.internalsOf(NetworkClient).activeSubscription;
      if (subscription instanceof SustainedSubscription) {
        if (subscription.active.value) {
          state = 0 /* Connected */;
        } else if (!peer.service.addresses.size) {
          state = 3 /* WaitingForDeviceDiscovery */;
        }
      }
      this.#setConnectionState(state);
      if (this.#options.autoConnect !== false) {
        if (this.#options.autoSubscribe === false) {
          this.#initializeWithRead().catch((error) => {
            logger.info(this.#peerAddress, `Error during read-only initialization`, error);
          });
        } else {
          this.#activateSubscription();
        }
      }
    });
  }
  #clientNode;
  /** Legacy Endpoint structure, only initialized when needed */
  #endpoints;
  #interactionClient;
  #updateEndpointStructureTimer = Time.getTimer(
    "Endpoint structure update",
    STRUCTURE_UPDATE_TIMEOUT,
    () => this.#updateEndpointStructure()
  );
  #connectionState = 1 /* Disconnected */;
  #localInitializationDone = false;
  #remoteInitializationDone = false;
  #nodeDetails;
  #construction;
  /**
   * When true, attribute/event changes are forwarded to callbacks and events. Set after initial data is processed.
   */
  #changesActive = false;
  #commissioningController;
  #options;
  #crypto;
  #deviceInformationUpdateNeeded = false;
  #observers = new ObserverGroup();
  #attributeIdToNameMap = /* @__PURE__ */ new Map();
  /** Collected Node change events from the node level. Only filled when the legacy endpoint structure is NOT used. */
  #pendingNodeChangeEvents = /* @__PURE__ */ new Map();
  #decommissioned = false;
  #peerAddress;
  #closing = false;
  /**
   * Endpoint structure change information that are checked when updating structure
   * - null means that the endpoint itself changed, so will be regenerated completely any case
   * - array of ClusterIds means that only these clusters changed and will be updated
   */
  #registeredEndpointStructureChanges = /* @__PURE__ */ new Map();
  events = {
    initialized: AsyncObservable(),
    initializedFromRemote: AsyncObservable(),
    deviceInformationChanged: AsyncObservable(),
    stateChanged: Observable(),
    attributeChanged: Observable(),
    eventTriggered: Observable(),
    structureChanged: Observable(),
    nodeEndpointAdded: Observable(),
    nodeEndpointRemoved: Observable(),
    nodeEndpointChanged: Observable(),
    decommissioned: Observable(),
    connectionAlive: Observable()
  };
  static async create(nodeId, commissioningController, options = {}, clientNode, interactionClient, peer, crypto, changes) {
    const node = new PairedNode(
      nodeId,
      commissioningController,
      options,
      clientNode,
      interactionClient,
      peer,
      crypto,
      changes
    );
    await node.construction;
    return node;
  }
  get construction() {
    return this.#construction;
  }
  get isConnected() {
    return this.#connectionState === 0 /* Connected */;
  }
  /** Returns the Node connection state. */
  get connectionState() {
    return this.#connectionState;
  }
  /** Returns the BasicInformation cluster metadata collected from the device. */
  get basicInformation() {
    return this.#nodeDetails.basicInformation;
  }
  /** Returns the general capability metadata collected from the device. */
  get deviceInformation() {
    return this.#nodeDetails.meta;
  }
  /** Is the Node fully initialized with formerly stored subscription data? False when the node was never connected so far. */
  get localInitializationDone() {
    return this.#localInitializationDone;
  }
  /** Is the Node fully initialized with remote subscription or read data? */
  get remoteInitializationDone() {
    return this.#remoteInitializationDone;
  }
  /** Is the Node initialized - locally or remotely? */
  get initialized() {
    return this.#remoteInitializationDone || this.#localInitializationDone;
  }
  get id() {
    return this.#clientNode.id;
  }
  get node() {
    return this.#clientNode;
  }
  /** If a subscription is established, then this is the interval in seconds, otherwise undefined */
  get currentSubscriptionIntervalSeconds() {
    const sub = this.#clientNode.behaviors.internalsOf(NetworkClient).activeSubscription;
    return sub?.maxInterval !== void 0 ? sub.maxInterval : void 0;
  }
  #setConnectionState(state) {
    if (this.#connectionState === state || this.#connectionState === 3 /* WaitingForDeviceDiscovery */ && state === 2 /* Reconnecting */) {
      return;
    }
    this.#connectionState = state;
    this.#options.stateInformationCallback?.(this.nodeId, state);
    this.events.stateChanged.emit(state);
  }
  /**
   * Activate the sustained subscription on NetworkClient. This triggers the Read and Subscribe flow.
   */
  #activateSubscription() {
    if (this.#options.autoSubscribe === false) {
      return;
    }
    const networkState = this.#clientNode.stateOf(NetworkClient);
    if (!networkState.autoSubscribe) {
      this.#clientNode.act((agent) => agent.get(NetworkClient).state.autoSubscribe = true);
    }
  }
  /**
   * Perform a one-time wildcard read (no subscription) and complete remote initialization.
   * Used when autoSubscribe is false to still populate the node state once.
   */
  async #initializeWithRead() {
    const read = Read({
      fabricFilter: true,
      attributes: [{}]
    });
    for await (const _chunk of this.#clientNode.interaction.read(read)) ;
    if (this.#endpoints !== void 0) {
      this.#initializeEndpointStructure();
    }
    this.#remoteInitializationDone = true;
    this.#changesActive = true;
    this.#setConnectionState(0 /* Connected */);
    await this.events.initializedFromRemote.emit(this.#nodeDetails.details);
    if (!this.#localInitializationDone) {
      this.#localInitializationDone = true;
      await this.events.initialized.emit(this.#nodeDetails.details);
    }
    try {
      await this.#commissioningController.validateAndUpdateFabricLabel(this.nodeId);
    } catch (error) {
      logger.info(this.#peerAddress, `Error updating fabric label`, error);
    }
  }
  /**
   * Schedule a connection to the device. This method is non-blocking and will return immediately.
   * The connection happens in the background. Please monitor the state events of the node to see if the
   * connection was successful.
   * The provided connection options will be set and used internally if the node reconnects successfully.
   */
  connect(connectOptions) {
    if (this.#decommissioned) {
      throw new UnknownNodeError("This node is decommissioned and cannot be connected to.");
    }
    if (connectOptions !== void 0) {
      this.#options = connectOptions;
    }
    if (this.#options.autoSubscribe === false) {
      this.#initializeWithRead().catch((error) => {
        logger.info(this.#peerAddress, `Error during read-only initialization`, error);
      });
    } else {
      this.#activateSubscription();
    }
  }
  /**
   * Trigger a reconnection to the device. This method is non-blocking and will return immediately.
   * The reconnection happens in the background. Please monitor the state events of the node to see if the
   * reconnection was successful.
   */
  triggerReconnect() {
    this.reconnect().catch((error) => logger.error(this.#peerAddress, `Failed to reconnect to node`, error));
  }
  /**
   * Force a reconnection by tearing down and re-establishing the sustained subscription.
   */
  async reconnect(connectOptions) {
    if (this.#decommissioned) {
      logger.debug(this.#peerAddress, "Ignoring reconnect request because node is decommissioned.");
      return;
    }
    if (connectOptions !== void 0) {
      this.#options = connectOptions;
    }
    await this.#clientNode.set({ network: { autoSubscribe: false } });
    this.#activateSubscription();
  }
  async #initializeFromStoredData() {
    const { autoSubscribe } = this.#options;
    if (this.#remoteInitializationDone || this.#localInitializationDone || autoSubscribe === false) return;
    let rootEndpointIncluded = false;
    let otherEndpointIncluded = false;
    for (const ep of this.#clientNode.endpoints) {
      const epClusters = ep.behaviors.active.filter((behavior) => ClusterBehavior.is(behavior));
      if (epClusters.length === 0) {
        continue;
      }
      if (ep.number === 0) {
        rootEndpointIncluded = true;
      } else {
        otherEndpointIncluded = true;
      }
      if (rootEndpointIncluded && otherEndpointIncluded) {
        break;
      }
    }
    if (!rootEndpointIncluded || !otherEndpointIncluded) {
      return;
    }
    await this.events.initialized.emit(this.#nodeDetails.details);
    this.#localInitializationDone = true;
  }
  #handleNodeChange(changes) {
    const { kind } = changes;
    switch (kind) {
      case "update": {
        if (!this.#changesActive) {
          return;
        }
        const { behavior, endpoint, properties, version } = changes;
        if (!ClusterBehavior.is(behavior)) {
          return;
        }
        const endpointId = endpoint.number;
        const clusterId = behavior.cluster.id;
        if (!endpoint.behaviors.supported[behavior.id]) {
          logger.info(
            `Ignoring attribute changes for ${endpointId}.${behavior.cluster.name} for fields`,
            properties
          );
          return;
        }
        const state = endpoint.stateOf(behavior);
        const attributes = behavior.cluster.attributes ?? {};
        for (const attribute of properties ?? Object.keys(attributes)) {
          let attributeId = parseInt(attribute, 10);
          if (isNaN(attributeId)) {
            attributeId = attributes[attribute]?.id;
            if (attributeId === void 0) {
              continue;
            }
          }
          const attrKey = `${clusterId}.${attributeId}`;
          let attributeName = this.#attributeIdToNameMap.get(attrKey);
          if (attributeName === void 0) {
            const clusterModel = Matter.clusters(clusterId);
            const attrModel = clusterModel?.attributes(attributeId);
            attributeName = attrModel ? attrModel.propertyName : `Unknown (${Diagnostic.hex(attributeId)})`;
            this.#attributeIdToNameMap.set(attrKey, attributeName);
          }
          const value = state[attribute];
          const data = {
            path: {
              endpointId,
              clusterId,
              attributeId,
              attributeName
            },
            value,
            version
          };
          const cluster = this.#endpoints?.get(endpointId)?.getClusterClientById(clusterId);
          if (this.#endpoints !== void 0 && cluster === void 0) {
            continue;
          }
          logger.debug(
            this.#peerAddress,
            `Trigger attribute update for ${endpointId}.${cluster?.name ?? Matter.clusters(clusterId)?.name ?? Diagnostic.hex(clusterId)}.${attributeId} to ${Diagnostic.json(value)}`
          );
          if (cluster !== void 0) {
            asClusterClientInternal(cluster)._triggerAttributeUpdate(attributeId, value);
          }
          this.#options.attributeChangedCallback?.(this.nodeId, data);
          this.events.attributeChanged.emit(data);
          this.#checkAttributesForNeededUpdates(endpointId, clusterId, attributeId, value);
        }
        break;
      }
      case "event": {
        if (!this.#changesActive) {
          return;
        }
        const { endpoint, behavior, event, number, timestamp: epochTimestamp, priority, payload } = changes;
        if (!ClusterBehavior.is(behavior)) {
          return;
        }
        const endpointId = endpoint.number;
        const clusterId = behavior.cluster.id;
        const eventId = event.id;
        const data = {
          path: {
            endpointId,
            clusterId,
            eventId,
            eventName: event.propertyName
          },
          events: [{ eventNumber: number, epochTimestamp, priority, data: payload }]
        };
        const cluster = this.#endpoints?.get(endpointId)?.getClusterClientById(clusterId);
        if (this.#endpoints !== void 0 && cluster === void 0) {
          break;
        }
        logger.debug(
          this.#peerAddress,
          `Trigger event update for ${endpointId}.${cluster?.name ?? Matter.clusters(clusterId)?.name ?? Diagnostic.hex(clusterId)}.${eventId} for ${data.events.length} events`
        );
        if (cluster !== void 0) {
          asClusterClientInternal(cluster)._triggerEventUpdate(eventId, data.events);
        }
        this.#options.eventTriggeredCallback?.(this.nodeId, data);
        this.events.eventTriggered.emit(data);
        this.#checkEventsForNeededStructureUpdate(endpointId, clusterId, eventId);
        break;
      }
      case "delete": {
        if (this.#endpoints !== void 0) {
          return;
        }
        const { endpoint } = changes;
        if (endpoint === this.#clientNode) {
          return;
        }
        this.#pendingNodeChangeEvents.set(endpoint.number, "nodeEndpointRemoved");
        break;
      }
    }
  }
  /**
   * Called when NetworkClient reports, the subscription is alive (data received).
   * Handles structure update scheduling, device info updates, and connectionAlive events.
   */
  #handleSubscriptionAlive() {
    if (this.#remoteInitializationDone && (this.#registeredEndpointStructureChanges.size > 0 || this.#pendingNodeChangeEvents.size > 0) && !this.#updateEndpointStructureTimer.isRunning) {
      if (!this.#closing && !this.#decommissioned) {
        logger.info(this.#peerAddress, `Endpoint structure needs to be updated ...`);
        this.#updateEndpointStructureTimer.stop().start();
      }
    } else if (this.#deviceInformationUpdateNeeded) {
      this.events.deviceInformationChanged.emit(this.#nodeDetails.details);
    }
    this.#deviceInformationUpdateNeeded = false;
    this.events.connectionAlive.emit();
  }
  /**
   * Called when NetworkClient reports the subscription status changed.
   * On first activation after startup, triggers the remote initialization completion flow.
   */
  async #handleSubscriptionStatusChanged(isActive) {
    if (isActive) {
      this.#setConnectionState(0 /* Connected */);
      if (!this.#remoteInitializationDone) {
        if (this.#endpoints !== void 0) {
          this.#initializeEndpointStructure();
        }
        this.#remoteInitializationDone = true;
        this.#changesActive = true;
        await this.events.initializedFromRemote.emit(this.#nodeDetails.details);
        if (!this.#localInitializationDone) {
          this.#localInitializationDone = true;
          await this.events.initialized.emit(this.#nodeDetails.details);
        }
        try {
          await this.#commissioningController.validateAndUpdateFabricLabel(this.nodeId);
        } catch (error) {
          logger.info(this.#peerAddress, `Error updating fabric label`, error);
        }
      }
    } else if (this.#connectionState === 0 /* Connected */) {
      this.#setConnectionState(2 /* Reconnecting */);
    }
  }
  /**
   * Request the current InteractionClient for custom special interactions with the device. Usually the
   * ClusterClients of the Devices of the node should be used instead. An own InteractionClient is only needed
   * when you want to read or write multiple attributes or events in a single request or send batch invokes.
   */
  getInteractionClient() {
    return this.#interactionClient;
  }
  /** Method to log the structure of this node with all endpoints and clusters. */
  logStructure() {
    logger.info(this.#clientNode);
  }
  /**
   * Subscribe to all attributes and events of the device. Unless setting the Controller property autoSubscribe to
   * false, this is executed automatically. Alternatively, you can manually subscribe by calling this method.
   *
   * @deprecated Subscription is now managed by NetworkClient on the ClientNode. Use connect() to activate.
   */
  async subscribeAllAttributesAndEvents(_options) {
    this.#activateSubscription();
    return this.currentSubscriptionIntervalSeconds;
  }
  #checkAttributesForNeededUpdates(endpointId, clusterId, attributeId, _value) {
    if (clusterId === Descriptor.id) {
      switch (attributeId) {
        case Descriptor.attributes.partsList.id:
        case Descriptor.attributes.serverList.id:
        case Descriptor.attributes.clientList.id:
        case Descriptor.attributes.deviceTypeList.id:
          this.#registeredEndpointStructureChanges.set(endpointId, null);
          return;
      }
    } else if (clusterId === BasicInformation.id) {
      this.#deviceInformationUpdateNeeded = true;
    }
    switch (attributeId) {
      case FeatureMap.id:
      case AttributeList.id:
      case AcceptedCommandList.id:
      case ClusterRevision.id:
        let knownForUpdate = this.#registeredEndpointStructureChanges.get(endpointId);
        if (knownForUpdate !== null) {
          knownForUpdate = knownForUpdate ?? [];
          if (!knownForUpdate.includes(clusterId)) {
            knownForUpdate.push(clusterId);
            this.#registeredEndpointStructureChanges.set(endpointId, knownForUpdate);
          }
        }
        break;
    }
  }
  #checkEventsForNeededStructureUpdate(_endpointId, clusterId, eventId) {
    if (clusterId === BasicInformation.id && eventId === BasicInformation.events.shutDown.id) {
      this.#handleNodeShutdown();
    }
  }
  /** Handles a node shutDown event (if supported by the node and received). */
  #handleNodeShutdown() {
    logger.info(this.#peerAddress, `Node shutdown detected. SustainedSubscription will handle reconnection.`);
  }
  #updateEndpointStructure() {
    if (this.#decommissioned || this.#closing) {
      return;
    }
    if (this.#endpoints === void 0) {
      for (const endpointId of this.#registeredEndpointStructureChanges.keys()) {
        if (!this.#pendingNodeChangeEvents.has(endpointId)) {
          this.#pendingNodeChangeEvents.set(endpointId, "nodeEndpointChanged");
        }
      }
      const eventsToEmit = new Map(this.#pendingNodeChangeEvents);
      this.#registeredEndpointStructureChanges.clear();
      this.#pendingNodeChangeEvents.clear();
      this.#triggerNodeStructureChanges(eventsToEmit);
    } else {
      this.#initializeEndpointStructure(true);
    }
    MaybePromise.then(
      this.events.deviceInformationChanged.emit(this.#nodeDetails.details),
      () => {
      },
      (error) => logger.warn(this.#peerAddress, `Error updating endpoint structure`, error)
    );
  }
  /**
   * Traverse the structure data and collect the endpoints for the given endpointId.
   * If data was found, it is added to the collectedData map.
   */
  #collectEndpoints(endpointId, collectedData) {
    if (collectedData.has(endpointId)) {
      return;
    }
    if (!this.#clientNode.endpoints.has(endpointId)) {
      logger.info(this.#peerAddress, `Endpoint ${endpointId} not found on node. Ignoring endpoint ...`);
      return;
    }
    const endpoint = this.#clientNode.endpoints.for(endpointId);
    const descriptorData = endpoint.maybeStateOf(DescriptorClient);
    if (descriptorData === void 0) {
      logger.info(`Descriptor data for endpoint ${endpointId} not found in structure! Ignoring endpoint ...`);
      return;
    }
    collectedData.set(endpointId, endpoint);
    if (descriptorData.partsList.length) {
      for (const partEndpointId of descriptorData.partsList) {
        this.#collectEndpoints(partEndpointId, collectedData);
      }
    }
  }
  #hasEndpointChanged(device, endpoint) {
    const descriptorData = endpoint?.maybeStateOf(DescriptorClient);
    if (!descriptorData) {
      return true;
    }
    return !(areNumberListsSame(
      device.getDeviceTypes().map(({ code }) => code),
      descriptorData.deviceTypeList.map(({ deviceType }) => deviceType)
    ) && // Check if the cluster clients are the same - they map to the serverList attribute
    areNumberListsSame(
      device.getAllClusterClients().map(({ id }) => id),
      descriptorData.serverList
    ));
  }
  /** Trigger collected node and endpoint changes */
  #triggerNodeStructureChanges(eventsToEmit) {
    const emitChangeEvents = () => {
      for (const [endpointId, eventName] of eventsToEmit.entries()) {
        logger.debug(this.#peerAddress, `Emitting event ${eventName} for endpoint ${endpointId}`);
        this.events[eventName].emit(endpointId);
      }
      this.#options.stateInformationCallback?.(this.nodeId, 4 /* StructureChanged */);
      this.events.structureChanged.emit();
    };
    if (this.#connectionState === 0 /* Connected */) {
      emitChangeEvents();
    } else {
      this.events.stateChanged.once((state) => {
        if (state === 0 /* Connected */) {
          emitChangeEvents();
        }
      });
    }
  }
  /**
   * This method initializes the legacy ClusterClient-based structure.
   * Reads all data from the device and create a device object structure out of it.
   *
   */
  #initializeEndpointStructure(updateStructure = this.#localInitializationDone || this.#remoteInitializationDone) {
    if (this.#endpoints === void 0) {
      this.#endpoints = /* @__PURE__ */ new Map();
    }
    if (this.#updateEndpointStructureTimer.isRunning) {
      this.#updateEndpointStructureTimer.stop();
    }
    const eventsToEmit = /* @__PURE__ */ new Map();
    const structureUpdateDetails = new Map(this.#registeredEndpointStructureChanges);
    this.#registeredEndpointStructureChanges.clear();
    const endpoints = /* @__PURE__ */ new Map();
    this.#collectEndpoints(EndpointNumber(0), endpoints);
    if (updateStructure) {
      const endpointsToRemove = new Set(this.#endpoints.keys());
      for (const endpointId of endpoints.keys()) {
        const device = this.#endpoints.get(endpointId);
        if (device !== void 0) {
          const hasChanged = structureUpdateDetails.has(endpointId);
          if (!hasChanged || !this.#hasEndpointChanged(device, endpoints.get(endpointId))) {
            logger.debug(
              this.#peerAddress,
              `Retaining endpoint`,
              endpointId,
              hasChanged ? "(with only structure changes)" : "(unchanged)"
            );
            endpointsToRemove.delete(endpointId);
            if (hasChanged) {
              eventsToEmit.set(endpointId, "nodeEndpointChanged");
            }
          } else {
            logger.debug(this.#peerAddress, `Recreating endpoint`, endpointId);
            eventsToEmit.set(endpointId, "nodeEndpointChanged");
          }
        }
      }
      for (const endpoint of endpointsToRemove.values()) {
        const endpointId = EndpointNumber(endpoint);
        const device = this.#endpoints.get(endpointId);
        if (device !== void 0) {
          if (eventsToEmit.get(endpointId) !== "nodeEndpointChanged") {
            logger.debug(this.#peerAddress, `Removing endpoint`, endpointId);
            eventsToEmit.set(endpointId, "nodeEndpointRemoved");
          }
          device.removeFromStructure();
          this.#endpoints.delete(endpointId);
        }
      }
    } else {
      this.#endpoints.clear();
    }
    for (const [endpointId, endpoint] of endpoints.entries()) {
      if (this.#endpoints.has(endpointId)) {
        continue;
      }
      const isRecreation = eventsToEmit.get(endpointId) === "nodeEndpointChanged";
      logger.debug(() => [
        this.#peerAddress,
        `${isRecreation ? "Recreating" : "Creating"} endpoint`,
        endpointId,
        Diagnostic.json(endpoint.state)
      ]);
      this.#endpoints.set(endpointId, this.#createDevice(endpointId, endpoint, this.#interactionClient));
      if (!isRecreation) {
        eventsToEmit.set(endpointId, "nodeEndpointAdded");
      }
    }
    for (const [endpointId, clientEndpoint] of endpoints.entries()) {
      const partsList = clientEndpoint.stateOf(DescriptorClient).partsList ?? [];
      const endpoint = this.#endpoints.get(endpointId);
      if (endpoint === void 0) {
        continue;
      }
      endpoint.getChildEndpoints().forEach((child) => {
        if (child.number !== void 0 && !partsList.includes(child.number)) {
          endpoint.removeChildEndpoint(child);
          if (!eventsToEmit.has(endpointId)) {
            eventsToEmit.set(endpointId, "nodeEndpointChanged");
          }
        }
      });
    }
    this.#structureEndpoints(endpoints);
    if (updateStructure && eventsToEmit.size) {
      this.#triggerNodeStructureChanges(eventsToEmit);
    }
  }
  /**
   * Bring the endpoints in a structure based on their partsList attribute. This method only adds endpoints into the
   * right place as children, Cleanup is not happening here
   */
  #structureEndpoints(descriptors) {
    if (this.#endpoints === void 0) {
      throw new InternalError("Endpoints not initialized yet! Should never happen");
    }
    const partLists = Array.from(descriptors.entries()).map(
      ([epNo, ep]) => [epNo, ep.stateOf(DescriptorClient).partsList]
      // else Typescript gets confused
    );
    logger.debug(() => [this.#peerAddress, `Endpoints from PartsLists`, Diagnostic.json(partLists)]);
    const endpointUsages = {};
    partLists.forEach(
      ([parent, partsList]) => partsList.forEach((endPoint) => {
        if (endPoint === parent) {
          logger.warn(this.#peerAddress, `Endpoint ${endPoint} is referencing itself!`);
          return;
        }
        endpointUsages[endPoint] = endpointUsages[endPoint] || [];
        endpointUsages[endPoint].push(parent);
      })
    );
    while (true) {
      const singleUsageEndpoints = Object.entries(endpointUsages).filter(([_, usages]) => usages.length === 1);
      if (singleUsageEndpoints.length === 0) {
        if (Object.entries(endpointUsages).length)
          throw new InternalError(`Endpoint structure for Node ${this.nodeId} could not be parsed!`);
        break;
      }
      const idsToCleanup = {};
      for (const [childId, usages] of singleUsageEndpoints) {
        const childEndpointId = EndpointNumber(parseInt(childId));
        const childEndpoint = this.#endpoints.get(childEndpointId);
        const parentEndpoint = this.#endpoints.get(usages[0]);
        const existingChildEndpoint = parentEndpoint?.getChildEndpoint(childEndpointId);
        if (childEndpoint === void 0 || parentEndpoint === void 0) {
          logger.warn(
            this.#peerAddress,
            `Endpoint ${usages[0]} not found in the data received from the device!`
          );
        } else if (existingChildEndpoint !== childEndpoint) {
          if (existingChildEndpoint !== void 0) {
            parentEndpoint.removeChildEndpoint(existingChildEndpoint);
          }
          parentEndpoint.addChildEndpoint(childEndpoint);
        }
        delete endpointUsages[EndpointNumber(parseInt(childId))];
        idsToCleanup[usages[0]] = true;
      }
      logger.debug(() => [this.#peerAddress, `Endpoint data Cleanup`, Diagnostic.json(idsToCleanup)]);
      Object.keys(idsToCleanup).forEach((idToCleanup) => {
        Object.keys(endpointUsages).forEach((id) => {
          const usageId = EndpointNumber(parseInt(id));
          endpointUsages[usageId] = endpointUsages[usageId].filter(
            (endpointId) => endpointId !== parseInt(idToCleanup)
          );
          if (!endpointUsages[usageId].length) {
            delete endpointUsages[usageId];
          }
        });
      });
    }
  }
  /** Create a device object from the data read from the device. */
  #createDevice(endpointId, endpoint, interactionClient) {
    const descriptorData = endpoint.stateOf(DescriptorClient);
    const deviceTypes = descriptorData.deviceTypeList.flatMap(({ deviceType, revision }) => {
      const deviceTypeDefinition = getDeviceTypeDefinitionFromModelByCode(deviceType);
      if (deviceTypeDefinition === void 0) {
        logger.info(
          this.#peerAddress,
          `Device type with code ${deviceType} not known, use generic replacement.`
        );
        return UnknownDeviceType(deviceType, revision);
      }
      if (deviceTypeDefinition.revision < revision) {
        logger.debug(
          this.#peerAddress,
          `Device type with code ${deviceType} and revision ${revision} not supported, some data might be unknown.`
        );
      }
      return deviceTypeDefinition;
    });
    if (deviceTypes.length === 0) {
      logger.info(this.#peerAddress, `No device type found for endpoint ${endpointId}, ignore`);
      throw new MatterError(`NodeId ${this.nodeId}: No device type found for endpoint`);
    }
    const endpointClusters = Array();
    for (const clusterId of descriptorData.serverList) {
      const clusterModel = Matter.clusters(clusterId);
      const cluster = clusterModel !== void 0 ? ClusterType(clusterModel) : ClusterType(new ClusterModel({ id: clusterId, name: `Cluster$${clusterId.toString(16)}` }));
      const data = endpoint.state[camelize(cluster.name)];
      endpointClusters.push(ClusterClient(cluster, endpointId, interactionClient, data));
    }
    if (endpointId === 0) {
      const rootEndpoint = new RootEndpoint(endpoint);
      rootEndpoint.setDeviceTypes(deviceTypes);
      endpointClusters.forEach((cluster) => rootEndpoint.addClusterClient(cluster));
      return rootEndpoint;
    } else if (deviceTypes.find((deviceType) => deviceType.code === AggregatorDt.id) !== void 0) {
      const aggregator = new Aggregator(endpoint, [], { endpointId });
      aggregator.setDeviceTypes(deviceTypes);
      endpointClusters.forEach((cluster) => aggregator.addClusterClient(cluster));
      return aggregator;
    } else {
      if (descriptorData.partsList.length > 0) {
        const composedDevice = new ComposedDevice(endpoint, deviceTypes[0], [], { endpointId });
        composedDevice.setDeviceTypes(deviceTypes);
        endpointClusters.forEach((cluster) => composedDevice.addClusterClient(cluster));
        return composedDevice;
      } else {
        return new PairedDevice(
          endpoint,
          deviceTypes,
          endpointClusters,
          endpointId
        );
      }
    }
  }
  /** Returns all parts (endpoints) known for the Root Endpoint of this node. */
  get parts() {
    return this.getRootEndpoint()?.parts ?? /* @__PURE__ */ new Map();
  }
  /** Ensures that the legacy Endpoint structure is initialized when needed. */
  #ensureLegacyEndpointStructure() {
    if (this.#endpoints === void 0) {
      this.#initializeEndpointStructure(false);
    }
    return this.#endpoints;
  }
  /** Returns the functional devices/endpoints (the "children" of the Root Endpoint) known for this node. */
  getDevices() {
    return this.#ensureLegacyEndpointStructure().get(EndpointNumber(0))?.getChildEndpoints() ?? [];
  }
  /** Returns the device/endpoint with the given endpoint ID. */
  getDeviceById(endpointId) {
    return this.#ensureLegacyEndpointStructure().get(EndpointNumber(endpointId));
  }
  /** Returns the Root Endpoint of the device. */
  getRootEndpoint() {
    return this.getDeviceById(0);
  }
  /** De-Commission (unpair) the device from this controller by removing the fabric from the device. */
  async decommission() {
    if (this.#connectionState === 2 /* Reconnecting */ || this.#connectionState === 3 /* WaitingForDeviceDiscovery */) {
      throw new ImplementationError(
        `This Node ${this.nodeId} is currently in a reconnect state, decommissioning is not possible.`
      );
    }
    await this.#clientNode.decommission();
    await this.#handleNodeDecommissioning();
  }
  async #handleNodeDecommissioning() {
    if (this.#decommissioned) {
      return;
    }
    this.#decommissioned = true;
    this.#updateEndpointStructureTimer?.stop();
    this.#options.stateInformationCallback?.(this.nodeId, 5 /* Decommissioned */);
    this.#setConnectionState(1 /* Disconnected */);
    this.events.decommissioned.emit();
  }
  /**
   * Opens a Basic Commissioning Window (uses the original Passcode printed on the device) with the device.
   * This is an optional method, so it might not be supported by all devices and could be rejected with an error in
   * this case! Better use openEnhancedCommissioningWindow() instead.
   */
  async openBasicCommissioningWindow(commissioningTimeout = 900) {
    const adminCommissioningCluster = this.getRootClusterClient(AdministratorCommissioning);
    if (adminCommissioningCluster === void 0) {
      throw new ImplementationError(`AdministratorCommissioningCluster for node ${this.nodeId} not found.`);
    }
    if (adminCommissioningCluster.supportedFeatures?.basic === false) {
      throw new ImplementationError(
        `AdministratorCommissioningCluster for node ${this.nodeId} does not support basic commissioning.`
      );
    }
    try {
      await adminCommissioningCluster.commands.revokeCommissioning();
    } catch (error) {
      if (!StatusResponseError.is(error, StatusCode.Failure) || StatusResponseError.of(error)?.clusterCode !== AdministratorCommissioning.StatusCode.WindowNotOpen) {
        throw error;
      }
    }
    await adminCommissioningCluster.commands.openBasicCommissioningWindow({ commissioningTimeout });
  }
  /** Opens an Enhanced Commissioning Window (uses a generated random Passcode) with the device. */
  async openEnhancedCommissioningWindow(commissioningTimeout = 900) {
    const adminCommissioningCluster = this.getRootClusterClient(AdministratorCommissioning);
    if (adminCommissioningCluster === void 0) {
      throw new ImplementationError(`AdministratorCommissioningCluster for node ${this.nodeId} not found.`);
    }
    try {
      await adminCommissioningCluster.commands.revokeCommissioning();
    } catch (error) {
      if (!StatusResponseError.is(error, StatusCode.Failure) || StatusResponseError.of(error)?.clusterCode !== AdministratorCommissioning.StatusCode.WindowNotOpen) {
        throw error;
      }
    }
    const basicInformationCluster = this.getRootClusterClient(BasicInformation);
    if (basicInformationCluster === void 0) {
      throw new ImplementationError(`BasicInformationCluster for node ${this.nodeId} not found.`);
    }
    const vendorId = await basicInformationCluster.getVendorIdAttribute();
    const productId = await basicInformationCluster.getProductIdAttribute();
    const discriminator = PaseClient.generateRandomDiscriminator(this.#crypto);
    const passcode = PaseClient.generateRandomPasscode(this.#crypto);
    const salt = this.#crypto.randomBytes(32);
    const iterations = 1e3;
    const pakePasscodeVerifier = await PaseClient.generatePakePasscodeVerifier(this.#crypto, passcode, {
      iterations,
      salt
    });
    await adminCommissioningCluster.commands.openCommissioningWindow({
      commissioningTimeout,
      pakePasscodeVerifier,
      salt,
      iterations,
      discriminator
    });
    const qrPairingCode = QrPairingCodeCodec.encode([
      {
        version: 0,
        vendorId,
        productId,
        flowType: CommissioningFlowType.Standard,
        discriminator,
        passcode,
        discoveryCapabilities: DiscoveryCapabilitiesSchema.encode({
          onIpNetwork: true
        })
      }
    ]);
    return {
      manualPairingCode: ManualPairingCodeCodec.encode({
        discriminator,
        passcode
      }),
      qrPairingCode
    };
  }
  /** Closes the current session, ends the subscription and disconnects the device. */
  async disconnect() {
    this.close();
    await this.#commissioningController.disconnectNode(this.nodeId);
  }
  /** Closes the subscription and ends all timers used by this PairedNode instance. */
  close(sendDecommissionedStatus = false) {
    this.#closing = true;
    this.#observers.close();
    this.#updateEndpointStructureTimer.stop();
    if (sendDecommissionedStatus) {
      this.#decommissioned = true;
      this.#options.stateInformationCallback?.(this.nodeId, 5 /* Decommissioned */);
      this.events.decommissioned.emit();
    }
    this.#setConnectionState(1 /* Disconnected */);
    this.#construction.close();
  }
  getRootClusterClient(cluster) {
    return this.#ensureLegacyEndpointStructure().get(EndpointNumber(0))?.getClusterClient(cluster);
  }
  getClusterClientForDevice(endpointId, cluster) {
    return this.getDeviceById(endpointId)?.getClusterClient(cluster);
  }
  get [Diagnostic.value]() {
    const root = this.getRootEndpoint();
    let statusIcon = "\u2717";
    switch (this.#connectionState) {
      case 2 /* Reconnecting */:
        statusIcon = "\u231B";
        break;
      case 3 /* WaitingForDeviceDiscovery */:
        statusIcon = "\u{1F4A4}";
        break;
      case 0 /* Connected */:
        statusIcon = "\u2713";
        break;
    }
    return Diagnostic.node(statusIcon, this.nodeId, {
      children: [
        Diagnostic.strong("Information"),
        Diagnostic.list([Diagnostic.dict(this.deviceInformation)]),
        Diagnostic.strong("Structure"),
        root ? Diagnostic.list([root]) : "Unknown"
      ]
    });
  }
  /**
   * Access to cached cluster state values of the root endpoint using node.state.clusterNameOrId.attributeNameOrId
   * Returns immutable cached attribute values from cluster clients
   */
  get state() {
    return this.#clientNode.state;
  }
  /**
   * Access to cluster commands of the root endpoint using node.commands.clusterNameOrId.commandName
   * Returns async functions that can be called to invoke commands on cluster clients
   */
  get commands() {
    return this.#clientNode.commands;
  }
  /**
   * Access to typed cached cluster state values of the root endpoint
   * Returns immutable cached attribute values from cluster clients
   */
  stateOf(type) {
    return this.#clientNode.stateOf(type);
  }
  /**
   * Access to typed cluster commands of the root endpoint
   * Returns async functions that can be called to invoke commands on cluster clients
   */
  commandsOf(type) {
    return this.#clientNode.commandsOf(type);
  }
}
export {
  NodeNotConnectedError,
  NodeStateInformation,
  NodeStates,
  PairedNode
};
//# sourceMappingURL=PairedNode.js.map
