/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { ClientInteraction } from "#action/client/ClientInteraction.js";
import { CertificateAuthority } from "#certificate/CertificateAuthority.js";
import { GeneralCommissioning } from "#clusters/general-commissioning";
import { ScannerSet } from "#common/Scanner.js";
import {
  ChannelType,
  ConnectionlessTransportSet,
  Diagnostic,
  Environmental,
  ImplementationError,
  isIPv6,
  Logger,
  Millis,
  Minutes,
  NoResponseTimeoutError,
  Seconds,
  ServerAddress
} from "#general";
import { MdnsClient } from "#mdns/MdnsClient.js";
import { CommissioningError } from "#peer/CommissioningError.js";
import {
  ControllerCommissioningFlow,
  NodeIdConflictError
} from "#peer/ControllerCommissioningFlow.js";
import { ControllerDiscovery, PairRetransmissionLimitReachedError } from "#peer/ControllerDiscovery.js";
import { ExchangeManager } from "#protocol/ExchangeManager.js";
import { DedicatedChannelExchangeProvider } from "#protocol/ExchangeProvider.js";
import { ChannelStatusResponseError } from "#securechannel/SecureChannelMessenger.js";
import { PaseClient } from "#session/pase/PaseClient.js";
import { SessionManager } from "#session/SessionManager.js";
import { NodeId, SECURE_CHANNEL_PROTOCOL_ID } from "#types";
import { NodeDiscoveryType, PeerSet } from "./PeerSet.js";
const logger = Logger.get("PeerCommissioner");
class ControllerCommissioner {
  #context;
  #paseClient;
  constructor(context) {
    this.#context = context;
    this.#paseClient = new PaseClient(context.sessions);
  }
  static [Environmental.create](env) {
    const instance = new ControllerCommissioner({
      peers: env.get(PeerSet),
      scanners: env.get(ScannerSet),
      transports: env.get(ConnectionlessTransportSet),
      sessions: env.get(SessionManager),
      exchanges: env.get(ExchangeManager),
      ca: env.get(CertificateAuthority),
      environment: env
    });
    env.set(ControllerCommissioner, instance);
    return instance;
  }
  /**
   * Commission a previously discovered node.
   */
  async commission(options) {
    const { passcode, addresses, discoveryData, fabric, nodeId } = options;
    if (nodeId !== void 0) {
      this.#assertPeerAddress(fabric.addressOf(nodeId));
    }
    addresses.sort((a) => a.type === "udp" ? -1 : 1);
    let session;
    for (const address of addresses) {
      try {
        session = await this.#initializePaseSecureChannel(address, passcode, discoveryData);
      } catch (e) {
        NoResponseTimeoutError.accept(e);
        logger.warn(`Could not connect to ${ServerAddress.urlFor(address)}: ${e.message}`);
      }
    }
    if (session === void 0) {
      throw new NoResponseTimeoutError("Could not connect to device");
    }
    return await this.#commissionConnectedNode(session, options, discoveryData);
  }
  /**
   * Discover and establish a PASE channel with a device.
   */
  async discoverAndEstablishPase(options) {
    const {
      discovery: { timeout = Seconds(30) },
      passcode
    } = options;
    const commissionableDevice = "commissionableDevice" in options.discovery ? options.discovery.commissionableDevice : void 0;
    let {
      discovery: { discoveryCapabilities = {}, knownAddress }
    } = options;
    let identifierData = "identifierData" in options.discovery ? options.discovery.identifierData : {};
    if (this.#context.scanners.hasScannerFor(ChannelType.UDP) && this.#context.transports.hasInterfaceFor(ChannelType.UDP, "::") !== void 0) {
      discoveryCapabilities.onIpNetwork = true;
    }
    if (commissionableDevice !== void 0) {
      let { addresses } = commissionableDevice;
      if (discoveryCapabilities.ble === true) {
        discoveryCapabilities = { onIpNetwork: true, ble: addresses.some((address) => address.type === "ble") };
      } else if (discoveryCapabilities.onIpNetwork === true) {
        addresses = addresses.filter((address) => address.type !== "ble");
      }
      addresses.sort((a) => a.type === "udp" ? -1 : 1);
      knownAddress = addresses[0];
      if ("instanceId" in commissionableDevice && commissionableDevice.instanceId !== void 0) {
        identifierData = { instanceId: commissionableDevice.instanceId };
      } else {
        identifierData = { longDiscriminator: commissionableDevice.D };
      }
    }
    const scannersToUse = this.#context.scanners.select(discoveryCapabilities);
    logger.info(
      `Connecting to device with identifier ${Diagnostic.json(identifierData)} and ${scannersToUse.length} scanners and knownAddress ${Diagnostic.json(knownAddress)}`
    );
    let paseSession;
    let discoveryData;
    if (knownAddress !== void 0) {
      try {
        paseSession = await this.#initializePaseSecureChannel(knownAddress, passcode);
      } catch (error) {
        NoResponseTimeoutError.accept(error);
      }
    }
    if (paseSession === void 0) {
      const discoveredDevices = await ControllerDiscovery.discoverDeviceAddressesByIdentifier(
        scannersToUse,
        identifierData,
        timeout
      );
      const { result } = await ControllerDiscovery.iterateServerAddresses(
        discoveredDevices,
        NoResponseTimeoutError,
        async () => scannersToUse.flatMap((scanner) => scanner.getDiscoveredCommissionableDevices(identifierData)),
        async (address, device) => {
          const channel = await this.#initializePaseSecureChannel(address, passcode, device);
          discoveryData = device;
          return channel;
        }
      );
      paseSession = result;
    }
    return { paseSession, discoveryData };
  }
  /**
   * Commission a node with discovery.
   */
  async commissionWithDiscovery(options) {
    const { fabric, nodeId } = options;
    if (nodeId !== void 0) {
      this.#assertPeerAddress(fabric.addressOf(nodeId));
    }
    const { paseSession, discoveryData } = await this.discoverAndEstablishPase(options);
    return await this.#commissionConnectedNode(paseSession, options, discoveryData);
  }
  /**
   * 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 #initializePaseSecureChannel(address, passcode, device) {
    let paseChannel;
    if (device !== void 0) {
      logger.info(`Establish PASE to device`, MdnsClient.discoveryDataDiagnostics(device));
    }
    switch (address.type) {
      case "udp":
        const { ip } = address;
        const isIpv6Address = isIPv6(ip);
        const paseInterface = this.#context.transports.interfaceFor(
          ChannelType.UDP,
          isIpv6Address ? "::" : "0.0.0.0"
        );
        if (paseInterface === void 0) {
          throw new PairRetransmissionLimitReachedError(
            `IPv${isIpv6Address ? "6" : "4"} interface not initialized. Cannot use ${ip} for commissioning.`
          );
        }
        paseChannel = await paseInterface.openChannel(address);
        break;
      case "ble":
        const ble = this.#context.transports.interfaceFor(ChannelType.BLE);
        if (!ble) {
          throw new PairRetransmissionLimitReachedError(
            `BLE interface not initialized. Cannot use ${address.peripheralAddress} for commissioning.`
          );
        }
        paseChannel = await ble.openChannel(address);
        break;
      default:
        throw new 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: Millis(device?.SII),
        activeInterval: Millis(device?.SAI),
        activeThreshold: Millis(device?.SAT)
      },
      isInitiator: true
    });
    const paseExchange = this.#context.exchanges.initiateExchangeForSession(
      unsecuredSession,
      SECURE_CHANNEL_PROTOCOL_ID
    );
    try {
      const caseSession = await this.#paseClient.pair(
        this.#context.sessions.sessionParameters,
        paseExchange,
        paseChannel,
        passcode
      );
      unsecuredSession.detachChannel();
      return caseSession;
    } catch (e) {
      if (e instanceof ChannelStatusResponseError) {
        throw new NoResponseTimeoutError(
          `Establishing PASE channel failed with channel status response error ${e.message}`
        );
      }
      throw e;
    } finally {
      await unsecuredSession.initiateForceClose();
    }
  }
  /** Validate if a Peer Address is already known and commissioned */
  #assertPeerAddress(address) {
    if (this.#context.peers.has(address)) {
      throw new NodeIdConflictError(`Node ID ${address.nodeId} is already commissioned and can not be reused`);
    }
  }
  /** Finds an unused random Node-ID to use for commissioning if not already provided. */
  #determineAddress(fabric, nodeId) {
    while (true) {
      const address = fabric.addressOf(nodeId ?? NodeId.randomOperationalNodeId(fabric.crypto));
      try {
        this.#assertPeerAddress(address);
      } catch (error) {
        if (error instanceof 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(paseSession, options, discoveryData) {
    const {
      fabric,
      finalizeCommissioning: performCaseCommissioning,
      commissioningFlowImpl = ControllerCommissioningFlow
    } = options;
    const commissioningOptions = {
      regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.Outdoor,
      // Set to the most restrictive if relevant
      regulatoryCountryCode: "XX",
      ...options
    };
    discoveryData = discoveryData ?? {};
    discoveryData.SII = paseSession.parameters.idleInterval;
    discoveryData.SAI = paseSession.parameters.activeInterval;
    discoveryData.SAT = paseSession.parameters.activeThreshold;
    const address = this.#determineAddress(fabric, commissioningOptions.nodeId);
    logger.info(`Start commissioning of node ${address.toString()} into fabric ${fabric.fabricId}`);
    const exchangeProvider = new DedicatedChannelExchangeProvider(this.#context.exchanges, paseSession);
    const commissioningManager = new commissioningFlowImpl(
      // Use the created secure session to do the commissioning
      new ClientInteraction({
        environment: this.#context.environment,
        exchangeProvider,
        address
      }),
      this.#context.ca,
      fabric,
      commissioningOptions,
      async (address2, supportsConcurrentConnections) => {
        if (!supportsConcurrentConnections) {
          await paseSession.initiateClose();
        }
        if (performCaseCommissioning !== void 0) {
          await performCaseCommissioning(address2, discoveryData);
          return;
        }
        await this.#context.peers.connect(address2, {
          discoveryOptions: {
            discoveryType: NodeDiscoveryType.TimedDiscovery,
            timeout: Minutes(4),
            discoveryData
          }
        });
        const exchangeProvider2 = await this.#context.peers.exchangeProviderFor(address2);
        return new ClientInteraction({
          environment: this.#context.environment,
          exchangeProvider: exchangeProvider2,
          address: address2
        });
      }
    );
    try {
      await commissioningManager.executeCommissioning();
    } catch (error) {
      await this.#context.peers.get(address)?.delete();
      throw error;
    } finally {
      commissioningManager.close();
      await paseSession.initiateClose();
    }
    return address;
  }
}
export {
  ControllerCommissioner
};
//# sourceMappingURL=ControllerCommissioner.js.map
