/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { ClientNodeFactory } from "#node/client/ClientNodeFactory.js";
import { CancelablePromise, Diagnostic, Logger, MaybePromise, withTimeout } from "@matter/general";
import { ScannerSet } from "@matter/protocol";
import { ControllerBehavior } from "../ControllerBehavior.js";
import { ActiveDiscoveries } from "./ActiveDiscoveries.js";
import { DiscoveryAggregateError } from "./DiscoveryError.js";
const logger = Logger.get("Discovery");
class Discovery extends CancelablePromise {
  #abortReason;
  #isStopped = false;
  #stopDiscovery;
  #owner;
  #options;
  #resolve;
  #reject;
  #settled;
  #resolveSettled;
  constructor(owner, options) {
    let resolve, reject;
    super((resolver, rejecter) => {
      resolve = resolver;
      reject = rejecter;
    });
    this.#resolve = (result) => {
      this.#isStopped = true;
      this.#owner.env.get(ActiveDiscoveries).delete(this);
      resolve(result);
      this.#resolveSettled?.();
    };
    this.#reject = (cause) => {
      this.#isStopped = true;
      this.#owner.env.get(ActiveDiscoveries).delete(this);
      reject(cause);
      this.#resolveSettled?.();
    };
    owner.env.get(ActiveDiscoveries).add(this);
    this.#owner = owner;
    this.#options = options ?? {};
    queueMicrotask(this.#initializeController.bind(this));
  }
  get settled() {
    if (this.#settled === void 0) {
      this.#settled = new Promise((resolve) => this.#resolveSettled = resolve);
    }
    return this.#settled;
  }
  get owner() {
    return this.#owner;
  }
  /**
   * Terminate discovery.
   *
   * This will not abort node initialization but it will terminate any active discoveries.  The discovery result will
   * be the same as if the discovery had timed out.
   *
   * To abort the operation due to error use {@link cancel}.
   */
  stop() {
    if (this.#isStopped) {
      return;
    }
    this.#isStopped = true;
    this.#stopDiscovery?.();
  }
  toString() {
    const description = this.#description;
    if (description === void 0) {
      return "node discovery";
    }
    return `discovery of ${description}`;
  }
  get #description() {
    if ("instanceId" in this.#options) {
      return `node instance ${this.#options.instanceId}`;
    }
    if ("longDiscriminator" in this.#options) {
      return `node with discriminator ${this.#options.longDiscriminator}`;
    }
    if ("shortDiscriminator" in this.#options) {
      return `node with discriminator ${this.#options.shortDiscriminator}`;
    }
    if ("productId" in this.#options && this.#options.productId !== void 0) {
      if ("vendorId" in this.#options) {
        return `product ${this.#options.productId} from vendor ${this.#options.vendorId}`;
      }
      return `product ${this.#options.productId}`;
    }
    if ("vendorId" in this.#options) {
      return `node from vendor ${this.#options.vendorId}`;
    }
    if ("deviceType" in this.#options) {
      return `node with device type ${this.#options.deviceType}`;
    }
    return "node discovery";
  }
  onCancel(reason) {
    this.#abortReason = reason;
    this.stop();
  }
  /**
   * Step 1 - ensure node is initialized as a controller
   */
  #initializeController() {
    let controllerInitialized;
    try {
      this.#owner.behaviors.require(ControllerBehavior);
      controllerInitialized = this.#owner.act((agent) => agent.load(ControllerBehavior));
    } catch (e) {
      this.#reject(e);
      return;
    }
    if (MaybePromise.is(controllerInitialized)) {
      controllerInitialized.then(this.#startNode.bind(this), this.#reject);
      return;
    }
    this.#startNode();
  }
  /**
   * Step 2 - ensure node is online
   */
  #startNode() {
    if (this.#isStopped) {
      this.#afterDiscovery();
      return;
    }
    if (this.#owner.lifecycle.isOnline) {
      this.#performDiscovery();
      return;
    }
    this.#owner.start().then(this.#performDiscovery.bind(this), this.#reject);
  }
  /**
   * Step 3 - perform actual discovery
   */
  #performDiscovery() {
    if (this.#isStopped) {
      this.#afterDiscovery();
      return;
    }
    const description = this.#description;
    if (description === void 0) {
      logger.info("Initiating", Diagnostic.strong("node discovery"));
    } else {
      logger.info("Initiating discovery of", Diagnostic.strong(description));
    }
    const scanners = this.#owner.env.get(ScannerSet);
    const { timeout: _, scannerFilter, id, ...identifier } = this.#options;
    const factory = this.#owner.env.get(ClientNodeFactory);
    const promises = new Array();
    const cancelSignal = new Promise((resolve) => this.#stopDiscovery = resolve);
    for (const scanner of scanners) {
      if (scannerFilter && !scannerFilter(scanner)) continue;
      promises.push(
        scanner.findCommissionableDevicesContinuously(
          identifier,
          (descriptor) => {
            let node = factory.find(descriptor);
            if (node?.lifecycle.isCommissioned) {
              node = void 0;
            }
            if (node) {
              const reusedNode = node;
              MaybePromise.then(
                node.act(async (agent) => {
                  const { transaction } = agent.context;
                  await transaction.addResources(agent.commissioning);
                  await transaction.begin();
                  agent.commissioning.descriptor = descriptor;
                }),
                () => this.onDiscovered(reusedNode),
                () => {
                }
              );
            } else {
              node = factory.create({
                id,
                environment: this.#owner.env,
                commissioning: { descriptor }
              });
              const newNode = node;
              Promise.resolve(newNode.construction.ready).then(
                () => this.onDiscovered(newNode),
                () => {
                }
              );
            }
          },
          void 0,
          cancelSignal
        )
      );
    }
    let promise = DiscoveryAggregateError.allSettled(promises, `${this} failed`);
    if (this.#options.timeout !== void 0) {
      promise = withTimeout(this.#options.timeout, promise, this.stop.bind(this));
    }
    promise.then(this.#afterDiscovery.bind(this)).catch(this.#reject);
  }
  /**
   * Step 4 - invoke completion callback
   */
  #afterDiscovery() {
    let result;
    if (this.#abortReason) {
      this.#reject(this.#abortReason);
      return;
    }
    try {
      result = this.onComplete();
    } catch (e) {
      this.#reject(e);
      return;
    }
    if (MaybePromise.is(result)) {
      result.then(this.#resolve.bind(this), this.#reject);
      return;
    }
    this.#resolve(result);
  }
}
export {
  Discovery
};
//# sourceMappingURL=Discovery.js.map
