"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
var __typeError = (msg) => {
  throw TypeError(msg);
};
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 __using = (stack, value, async) => {
  if (value != null) {
    if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
    var dispose, inner;
    if (async) dispose = value[__knownSymbol("asyncDispose")];
    if (dispose === void 0) {
      dispose = value[__knownSymbol("dispose")];
      if (async) inner = dispose;
    }
    if (typeof dispose !== "function") __typeError("Object not disposable");
    if (inner) dispose = function() {
      try {
        inner.call(this);
      } catch (e) {
        return Promise.reject(e);
      }
    };
    stack.push([async, dispose, value]);
  } else if (async) {
    stack.push([async]);
  }
  return value;
};
var __callDispose = (stack, error, hasError) => {
  var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
    return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
  };
  var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
  var next = (it) => {
    while (it = stack.pop()) {
      try {
        var result = it[1] && it[1].call(it[2]);
        if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
      } catch (e) {
        fail(e);
      }
    }
    if (hasError) throw error;
  };
  return next();
};
var ControllerCommissioner_exports = {};
__export(ControllerCommissioner_exports, {
  ControllerCommissioner: () => ControllerCommissioner
});
module.exports = __toCommonJS(ControllerCommissioner_exports);
var import_ClientInteraction = require("#action/client/ClientInteraction.js");
var import_CertificateAuthority = require("#certificate/CertificateAuthority.js");
var import_Scanner = require("#common/Scanner.js");
var import_CommissioningConnection = require("#peer/CommissioningConnection.js");
var import_CommissioningError = require("#peer/CommissioningError.js");
var import_ControllerCommissioningFlow = require("#peer/ControllerCommissioningFlow.js");
var import_ExchangeManager = require("#protocol/ExchangeManager.js");
var import_ExchangeProvider = require("#protocol/ExchangeProvider.js");
var import_SecureChannelMessenger = require("#securechannel/SecureChannelMessenger.js");
var import_PaseClient = require("#session/pase/PaseClient.js");
var import_SessionManager = require("#session/SessionManager.js");
var import_general = require("@matter/general");
var import_types = require("@matter/types");
var import_general_commissioning = require("@matter/types/clusters/general-commissioning");
var import_PeerCommunicationError = require("./PeerCommunicationError.js");
var import_PeerSet = require("./PeerSet.js");
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_general.Logger.get("ControllerCommissioner");
class ControllerCommissioner {
  #context;
  #paseClient;
  constructor(context) {
    this.#context = context;
    this.#paseClient = new import_PaseClient.PaseClient(context.sessions);
  }
  static [import_general.Environmental.create](env) {
    const instance = new ControllerCommissioner({
      peers: env.get(import_PeerSet.PeerSet),
      transports: env.get(import_general.ConnectionlessTransportSet),
      sessions: env.get(import_SessionManager.SessionManager),
      exchanges: env.get(import_ExchangeManager.ExchangeManager),
      ca: env.get(import_CertificateAuthority.CertificateAuthority),
      environment: env
    });
    env.set(ControllerCommissioner, instance);
    return instance;
  }
  /**
   * Commission a previously discovered node.
   */
  async commission(options) {
    const {
      passcode,
      addresses,
      discoveryData,
      fabric,
      nodeId,
      abort,
      continueCommissioningAfterPase,
      timeout = (0, import_general.Seconds)(30)
    } = options;
    this.#assertRequestedNodeIdAvailable(fabric, nodeId);
    this.#validateCommissioningOptions(options);
    const addressCandidates = this.#addressesToCandidates(addresses, discoveryData);
    const { session } = await this.#establishPaseFromCandidates({
      devices: addressCandidates,
      timeout,
      passcode,
      retryFailureAsPeerCommunication: "Could not connect to device",
      abort
    });
    if (continueCommissioningAfterPase !== void 0 && !continueCommissioningAfterPase()) {
      await session.initiateForceClose({
        cause: new import_CommissioningError.CommissioningError("PASE established but other device connected faster")
      });
      throw new import_CommissioningError.CommissioningError("Commissioning cancelled: another device was already successfully connected");
    }
    return await this.#commissionConnectedNode(session, options, discoveryData);
  }
  /**
   * Establishes a PASE session with a known device without running a commissioning flow.
   *
   * All provided addresses are tried in parallel.  The first to complete PASE wins; the rest are cancelled
   * via an abort signal.  A credential failure (wrong passcode) on any address immediately cancels all
   * other in-flight attempts for this device.
   */
  async establishPase(options) {
    const { addresses, discoveryData, passcode, timeout = (0, import_general.Seconds)(30), abort, continueAfterPase } = options;
    const candidates = this.#addressesToCandidates(addresses, discoveryData);
    const { session } = await this.#establishPaseFromCandidates({
      devices: candidates,
      timeout,
      passcode,
      retryFailureAsPeerCommunication: "Could not connect to device",
      abort
    });
    if (continueAfterPase !== void 0 && !continueAfterPase()) {
      await session.initiateForceClose({
        cause: new import_CommissioningError.CommissioningError("PASE established but another candidate already won")
      });
      throw new import_CommissioningError.CommissioningError("PASE cancelled: another candidate was already selected");
    }
    return { paseSession: session, discoveryData };
  }
  async #establishPaseFromCandidates(options) {
    try {
      return await (0, import_CommissioningConnection.CommissioningConnection)({
        devices: options.devices,
        timeout: options.timeout,
        externalAbort: options.abort,
        establishSession: (address, device, signal) => this.#establishEphemeralNodeSession(address, options.passcode, device, signal)
      });
    } catch (error) {
      if (options.retryFailureAsPeerCommunication !== void 0 && (0, import_general.causedBy)(error, import_CommissioningError.PairRetransmissionLimitReachedError)) {
        throw new import_PeerCommunicationError.PeerCommunicationError(options.retryFailureAsPeerCommunication);
      }
      throw error;
    }
  }
  /**
   * Method to start commission process with a PASE pairing.
   * If this not successful and throws an RetransmissionLimitReachedError the address is invalid or the passcode
   * is wrong.
   */
  async #establishEphemeralNodeSession(address, passcode, device, signal) {
    let paseChannel;
    if (device !== void 0) {
      logger.info(`Establish PASE to device`, (0, import_Scanner.DiscoveryDataDiagnostics)(device));
    }
    switch (address.type) {
      case "udp":
        const { ip } = address;
        const isIpv6Address = (0, import_general.isIPv6)(ip);
        const paseInterface = this.#context.transports.interfaceFor(
          import_general.ChannelType.UDP,
          isIpv6Address ? "::" : "0.0.0.0"
        );
        if (paseInterface === void 0) {
          throw new import_CommissioningError.PairRetransmissionLimitReachedError(
            `IPv${isIpv6Address ? "6" : "4"} interface not initialized. Cannot use ${ip} for commissioning.`
          );
        }
        paseChannel = await import_general.Abort.attempt(signal, paseInterface.openChannel(address));
        break;
      case "ble":
        const ble = this.#context.transports.interfaceFor(import_general.ChannelType.BLE);
        if (!ble) {
          throw new import_CommissioningError.PairRetransmissionLimitReachedError(
            `BLE interface not initialized. Cannot use ${address.peripheralAddress} for commissioning.`
          );
        }
        paseChannel = await import_general.Abort.attempt(signal, ble.openChannel(address));
        break;
      default:
        throw new import_general.ImplementationError(
          `Unsupported address type ${address.type} for Matter protocol`
        );
    }
    const unsecuredSession = this.#context.sessions.createUnsecuredSession({
      channel: paseChannel,
      // Use the session parameters from MDNS announcements when available and rest is assumed to be fallbacks
      sessionParameters: {
        idleInterval: (0, import_general.Millis)(device?.SII),
        activeInterval: (0, import_general.Millis)(device?.SAI),
        activeThreshold: (0, import_general.Millis)(device?.SAT)
      },
      isInitiator: true
    });
    const paseExchange = this.#context.exchanges.initiateExchangeForSession(
      unsecuredSession,
      import_types.SECURE_CHANNEL_PROTOCOL_ID
    );
    try {
      const caseSession = await this.#paseClient.pair(
        this.#context.sessions.sessionParameters,
        paseExchange,
        paseChannel,
        passcode,
        { abort: signal }
      );
      unsecuredSession.detachChannel();
      return caseSession;
    } catch (e) {
      if ((0, import_general.causedBy)(e, import_SecureChannelMessenger.ChannelStatusResponseError)) {
        throw new import_general.NoResponseTimeoutError(
          `Establishing PASE channel failed with channel status response error ${(0, import_general.asError)(e).message}`
        );
      }
      throw e;
    } finally {
      await unsecuredSession.initiateForceClose({
        cause: new import_PeerCommunicationError.CommissioningTransitionError("PASE session has transitioned to CASE")
      });
    }
  }
  /** Validate if a Peer Address is already known and commissioned */
  #assertPeerAddress(address) {
    if (this.#context.peers.has(address)) {
      throw new import_ControllerCommissioningFlow.NodeIdConflictError(`Node ID ${address.nodeId} is already commissioned and can not be reused`);
    }
  }
  #assertRequestedNodeIdAvailable(fabric, nodeId) {
    if (nodeId !== void 0) {
      this.#assertPeerAddress(fabric.addressOf(nodeId));
    }
  }
  #validateCommissioningOptions(options) {
    if (options.threadNetwork !== void 0) {
      const { operationalDataset } = options.threadNetwork;
      if (operationalDataset.length === 0) {
        throw new import_CommissioningError.CommissioningError("Thread operational dataset must not be empty");
      }
      if (operationalDataset.length % 2 !== 0) {
        throw new import_CommissioningError.CommissioningError("Thread operational dataset must have an even number of hex characters");
      }
      if (!/^[0-9a-fA-F]+$/.test(operationalDataset)) {
        throw new import_CommissioningError.CommissioningError(
          "Thread operational dataset must only contain valid hexadecimal characters"
        );
      }
    }
    if (options.wifiNetwork !== void 0) {
      const { wifiSsid } = options.wifiNetwork;
      if (wifiSsid.length === 0) {
        throw new import_CommissioningError.CommissioningError("Wi-Fi SSID must not be empty");
      }
    }
  }
  /**
   * Maps a sorted list of addresses to synthetic `CommissionableDevice` candidates for use with
   * {@link CommissioningConnection}.  Each address becomes its own candidate so that a credential failure
   * on one does not cancel attempts on others, and the per-device abort logic works correctly even in the
   * single-address case.
   */
  #addressesToCandidates(addresses, discoveryData) {
    const sorted = [...addresses].sort((a, b) => a.type === "udp" ? -1 : b.type === "udp" ? 1 : 0);
    return sorted.map((address, index) => ({
      ...discoveryData ?? {},
      addresses: [address],
      deviceIdentifier: `known-address-${index}-${import_general.ServerAddress.urlFor(address)}`,
      D: 0,
      CM: 1
    }));
  }
  /** Finds an unused random Node-ID to use for commissioning if not already provided. */
  #determineAddress(fabric, nodeId) {
    while (true) {
      const address = fabric.addressOf(nodeId ?? import_types.NodeId.randomOperationalNodeId(fabric.crypto));
      try {
        this.#assertPeerAddress(address);
      } catch (error) {
        if (error instanceof import_CommissioningError.CommissioningError && nodeId !== void 0) {
          throw error;
        }
        continue;
      }
      return address;
    }
  }
  /**
   * Method to commission a device with a PASE secure channel. It returns the NodeId of the commissioned device on
   * success.
   */
  async #commissionConnectedNode(ephemeralSession, options, discoveryData) {
    var _stack = [];
    try {
      const {
        fabric,
        finalizeCommissioning: performCaseCommissioning,
        commissioningFlowImpl = import_ControllerCommissioningFlow.ControllerCommissioningFlow,
        caseConnectionTiming
      } = options;
      const commissioningOptions = {
        regulatoryLocation: import_general_commissioning.GeneralCommissioning.RegulatoryLocationType.Outdoor,
        // Most restrictive default if not specified
        regulatoryCountryCode: "XX",
        ...options
      };
      discoveryData = discoveryData ?? {};
      discoveryData.SII = ephemeralSession.parameters.idleInterval;
      discoveryData.SAI = ephemeralSession.parameters.activeInterval;
      discoveryData.SAT = ephemeralSession.parameters.activeThreshold;
      const address = this.#determineAddress(fabric, commissioningOptions.nodeId);
      logger.info(`Start commissioning of node ${address.toString()} into fabric ${fabric.fabricId}`);
      const exchangeProvider = new import_ExchangeProvider.DedicatedChannelExchangeProvider(this.#context.exchanges, ephemeralSession);
      const commissioner = __using(_stack, new commissioningFlowImpl(
        new import_ClientInteraction.ClientInteraction({
          environment: this.#context.environment,
          exchangeProvider,
          address
        }),
        this.#context.ca,
        fabric,
        commissioningOptions,
        async (address2, supportsConcurrentConnections) => {
          if (!supportsConcurrentConnections) {
            await ephemeralSession.initiateForceClose({
              cause: new import_PeerCommunicationError.CommissioningTransitionError(
                "Commissioning session closed because node has now joined fabric"
              )
            });
          }
          if (performCaseCommissioning !== void 0) {
            await performCaseCommissioning(address2, discoveryData);
            return;
          }
          const peer = this.#context.peers.for(address2);
          peer.descriptor.discoveryData = discoveryData;
          await peer.connect({
            connectionTimeout: (0, import_general.Minutes)(4),
            timing: caseConnectionTiming,
            handleError: (error) => {
              const csre = import_SecureChannelMessenger.ChannelStatusResponseError.of(error);
              if (csre?.protocolStatusCode === import_types.SecureChannelStatusCode.NoSharedTrustRoots) {
                logger.warn("Peer reports no shared trust roots during commissioning, retrying quickly");
                return (0, import_general.Seconds)(15);
              }
            }
          });
          return new import_ClientInteraction.ClientInteraction({
            environment: this.#context.environment,
            exchangeProvider: peer.exchangeProvider,
            address: address2
          });
        }
      ), true);
      try {
        await commissioner.executeCommissioning();
      } catch (error) {
        await this.#context.peers.get(address)?.delete();
        throw error;
      } finally {
        commissioner.close();
        await ephemeralSession.initiateForceClose({
          cause: new import_PeerCommunicationError.CommissioningTransitionError(
            "Commissioning session closed because node has now joined fabric"
          )
        });
      }
      return address;
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      var _promise = __callDispose(_stack, _error, _hasError);
      _promise && await _promise;
    }
  }
}
//# sourceMappingURL=ControllerCommissioner.js.map
