/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  anyPromise,
  Diagnostic,
  Logger,
  NoResponseTimeoutError,
  Seconds,
  ServerAddress
} from "#general";
import { RetransmissionLimitReachedError } from "#protocol/errors.js";
import { CommissioningError } from "./CommissioningError.js";
const logger = Logger.get("ControllerDiscovery");
class CommissionableDeviceDiscoveryFailedError extends CommissioningError {
}
class DiscoveryError extends RetransmissionLimitReachedError {
}
class PairRetransmissionLimitReachedError extends RetransmissionLimitReachedError {
}
class ControllerDiscovery {
  /**
   * Discovers devices by a provided identifier and a list of scanners (e.g. IP and BLE in parallel).
   * It returns after the timeout or if at least one device was found.
   * The method returns a list of addresses of the discovered devices.
   */
  static async discoverDeviceAddressesByIdentifier(scanners, identifier, timeout = Seconds(30)) {
    logger.info(`Start Discovering devices using identifier ${Diagnostic.json(identifier)} ...`);
    const scanResults = scanners.map(async (scanner) => {
      const foundDevices = await scanner.findCommissionableDevices(
        identifier,
        timeout,
        scanner.type === "ble"
        // Force rediscovery for BLE
      );
      logger.info(`Found ${foundDevices.length} devices using identifier ${Diagnostic.json(identifier)}`);
      if (foundDevices.length === 0) {
        throw new CommissionableDeviceDiscoveryFailedError(
          `No device discovered using identifier ${Diagnostic.json(
            identifier
          )}! Please check that the relevant device is online.`
        );
      }
      const devices = foundDevices.filter((device) => device.addresses.length > 0);
      if (devices.length === 0) {
        throw new CommissionableDeviceDiscoveryFailedError(
          `Device discovered using identifier ${Diagnostic.json(
            identifier
          )}, but no Network addresses discovered.`
        );
      }
      return devices;
    });
    return await anyPromise(scanResults);
  }
  static async discoverCommissionableDevices(scanners, timeout, identifier = {}, discoveredCallback) {
    const discoveredDevices = /* @__PURE__ */ new Map();
    const results = await Promise.all(
      scanners.map(
        async (scanner) => scanner.findCommissionableDevicesContinuously(
          identifier,
          (device) => {
            const { deviceIdentifier } = device;
            if (!discoveredDevices.has(deviceIdentifier)) {
              discoveredDevices.set(deviceIdentifier, device);
              discoveredCallback?.(device);
            }
          },
          timeout
        )
      )
    );
    const finalDiscoveredDevices = /* @__PURE__ */ new Map();
    results.forEach((devices) => {
      devices.forEach((device) => {
        const { deviceIdentifier } = device;
        if (!discoveredDevices.has(deviceIdentifier)) {
          discoveredDevices.set(deviceIdentifier, device);
          discoveredCallback?.(device);
        }
        if (!finalDiscoveredDevices.has(deviceIdentifier)) {
          finalDiscoveredDevices.set(deviceIdentifier, device);
        }
      });
    });
    return Array.from(finalDiscoveredDevices.values());
  }
  static async discoverOperationalDevice(fabric, peerNodeId, scanner, timeout, ignoreExistingRecords) {
    const foundDevice = await scanner.findOperationalDevice(fabric, peerNodeId, timeout, ignoreExistingRecords);
    if (foundDevice === void 0) {
      throw new DiscoveryError(
        "The operational device cannot be found on the network. Please make sure it is online."
      );
    }
    return foundDevice;
  }
  static cancelOperationalDeviceDiscovery(fabric, peerNodeId, scanner) {
    scanner.cancelOperationalDeviceDiscovery(fabric, peerNodeId);
  }
  static cancelCommissionableDeviceDiscovery(scanner, identifier = {}) {
    scanner.cancelCommissionableDeviceDiscovery(identifier);
  }
  /**
   * Helper method to iterate through a list of server addresses and try to execute a method on each of them. If the
   * method throws a configurable error (or EHOSTUNREACH), the server address list is updated (to also add later
   * discovered addresses or devices) and then next server address is tried.The result of the first successful method
   * call is returned. The logic makes sure to only try each unique address (IP/port) once.
   */
  static async iterateServerAddresses(devices, errorType, updateDevicesFunc, func, lastKnownAddress) {
    const processOneAddress = async (address, device) => {
      const serverKey = ServerAddress.urlFor(address);
      logger.debug(`Try to communicate with ${serverKey} ...`);
      try {
        return { result: await func(address, device), resultAddress: address, resultDevice: device };
      } catch (error) {
        if (error instanceof errorType || error instanceof NoResponseTimeoutError) {
          logger.debug(`Failed to communicate with ${serverKey}, try other servers ...`, error);
        } else {
          throw error;
        }
      }
    };
    const addresses = /* @__PURE__ */ new Map();
    devices.forEach(
      (device) => device.addresses.forEach((address) => addresses.set(ServerAddress.urlFor(address), { address, device }))
    );
    const triedAddresses = /* @__PURE__ */ new Set();
    if (lastKnownAddress !== void 0) {
      const knownKey = ServerAddress.urlFor(lastKnownAddress);
      const knownDevice = addresses.has(ServerAddress.urlFor(lastKnownAddress)) ? addresses.get(knownKey)?.device : void 0;
      addresses.delete(knownKey);
      const result = await processOneAddress(lastKnownAddress, knownDevice);
      if (result !== void 0) {
        return result;
      }
      triedAddresses.add(knownKey);
    }
    while (true) {
      logger.debug(
        `Server addresses to try: ${Array.from(addresses).map(([addressString, { device }]) => `${addressString}${device?.DN ? ` (${device.DN})` : ""}`).join(",")}`
      );
      let triedOne = false;
      for (const { address, device } of addresses.values()) {
        const serverKey = ServerAddress.urlFor(address);
        if (triedAddresses.has(serverKey)) continue;
        triedAddresses.add(serverKey);
        try {
          triedOne = true;
          const result = await processOneAddress(address, device);
          if (result !== void 0) {
            return result;
          }
        } catch (error) {
          if (error instanceof errorType || error instanceof NoResponseTimeoutError) {
            logger.debug(`Failed to communicate with ${serverKey}, try next server ...`, error);
          } else {
            throw error;
          }
        }
      }
      if (triedOne) {
        (await updateDevicesFunc()).forEach(
          (device) => device.addresses.forEach(
            (address) => addresses.set(ServerAddress.urlFor(address), { address, device })
          )
        );
      } else {
        throw new PairRetransmissionLimitReachedError(`Failed to connect on any discovered server`);
      }
    }
  }
}
export {
  CommissionableDeviceDiscoveryFailedError,
  ControllerDiscovery,
  DiscoveryError,
  PairRetransmissionLimitReachedError
};
//# sourceMappingURL=ControllerDiscovery.js.map
