/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { SubscriptionsServer } from "#behavior/system/subscriptions/SubscriptionsServer.js";
import { EndpointType } from "#endpoint/type/EndpointType.js";
import {
  AsyncObservable,
  Crypto,
  Diagnostic,
  EventEmitter,
  ImplementationError,
  Lifecycle,
  Logger,
  MatterFlowError,
  Mutex,
  MutexClosedError,
  Observable
} from "@matter/general";
import { DatatypeModel, FieldElement } from "@matter/model";
import {
  CommissioningConfigProvider,
  DeviceAdvertiser,
  DeviceCommissioner,
  FabricManager,
  FailsafeContext,
  PaseClient,
  Val
} from "@matter/protocol";
import {
  CommissioningFlowType,
  CommissioningOptions,
  DiscoveryCapabilitiesSchema,
  ManualPairingCodeCodec,
  QrCode,
  QrPairingCodeCodec
} from "@matter/types";
import { BasicInformationBehavior } from "../../../behaviors/basic-information/BasicInformationBehavior.js";
import { OperationalCredentialsBehavior } from "../../../behaviors/operational-credentials/OperationalCredentialsBehavior.js";
import { Behavior } from "../../Behavior.js";
import { NetworkServer } from "../network/NetworkServer.js";
import { ProductDescriptionServer } from "../product-description/ProductDescriptionServer.js";
import { SessionsBehavior } from "../sessions/SessionsBehavior.js";
const logger = Logger.get("Commissioning");
class CommissioningServer extends Behavior {
  static id = "commissioning";
  static early = true;
  initialize() {
    this.internal.name = this.toString();
    if (this.state.passcode === -1) {
      this.state.passcode = PaseClient.generateRandomPasscode(this.env.get(Crypto));
    } else if (CommissioningOptions.FORBIDDEN_PASSCODES.includes(this.state.passcode)) {
      throw new ImplementationError(`Passcode ${this.state.passcode} is not allowed`);
    }
    if (this.state.discriminator === -1) {
      this.state.discriminator = PaseClient.generateRandomDiscriminator(this.env.get(Crypto));
    }
    this.reactTo(this.endpoint.lifecycle.partsReady, this.#initializeNode);
    if (!this.env.has(CommissioningConfigProvider)) {
      this.env.set(
        CommissioningConfigProvider,
        new CommissioningServerConfigProvider(this.endpoint)
      );
    }
    this.reactTo(this.endpoint.lifecycle.online, this.#enterOnlineMode);
    this.reactTo(this.endpoint.lifecycle.goingOffline, this.#enterOfflineMode);
  }
  async [Symbol.asyncDispose]() {
    this.#enterOfflineMode();
    await this.internal.mutex;
  }
  handleFabricChange(fabricIndex, fabricAction) {
    if (this.env.has(FailsafeContext)) {
      const failsafe = this.env.get(FailsafeContext);
      if (fabricAction === "added" || fabricAction === "updated") {
        if (failsafe.construction.status !== Lifecycle.Status.Destroyed) {
          if (failsafe.fabricIndex === fabricIndex) {
            this.#monitorFailsafe(failsafe);
            return;
          } else {
            throw new MatterFlowError(
              `Failsafe owns a different fabricIndex then ${failsafe.forUpdateNoc ? "updated" : "added"}: ${failsafe.fabricIndex} vs. ${fabricIndex}`
            );
          }
        }
      } else if (fabricAction === "deleted") {
        if (failsafe.fabricIndex !== fabricIndex) {
          throw new MatterFlowError(
            `Failsafe owns a different fabricIndex then removed: ${failsafe.fabricIndex} vs. ${fabricIndex}`
          );
        }
      }
    }
    const commissioned = !!this.env.get(FabricManager).fabrics.length;
    let doFactoryReset = false;
    if (commissioned !== this.state.commissioned) {
      this.state.commissioned = commissioned;
      if (commissioned) {
        this.events.commissioned.emit(this.context);
        this.endpoint.lifecycle.commissioned.emit(this.context);
      } else {
        this.events.decommissioned.emit(this.context);
        this.endpoint.lifecycle.decommissioned.emit(this.context);
        doFactoryReset = true;
      }
    }
    this.events.fabricsChanged.emit(fabricIndex, fabricAction);
    if (doFactoryReset) {
      const sessions = this.agent.get(SessionsBehavior);
      if (Object.keys(sessions.state.sessions).length > 0) {
        this.reactTo(sessions.events.closed, this.#resetAfterSessionsClear);
      } else {
        this.#triggerFactoryReset();
      }
    }
  }
  #resetAfterSessionsClear() {
    const sessions = this.agent.get(SessionsBehavior);
    if (Object.keys(sessions.state.sessions).length === 0) {
      this.#triggerFactoryReset();
    }
  }
  #triggerFactoryReset() {
    this.env.runtime.add(this.endpoint.erase().catch((e) => MutexClosedError.accept(e)));
  }
  #monitorFailsafe(failsafe) {
    if (this.internal.unregisterFailsafeListener) {
      return;
    }
    const listener = this.callback(function(status) {
      if (status === Lifecycle.Status.Destroyed) {
        if (failsafe.fabricIndex !== void 0) {
          this.handleFabricChange(failsafe.fabricIndex, failsafe.forUpdateNoc ? "updated" : "added");
        }
        this.internal.unregisterFailsafeListener?.();
      }
    });
    this.internal.unregisterFailsafeListener = this.callback(function() {
      failsafe.construction.change.off(listener);
      this.internal.unregisterFailsafeListener = void 0;
    });
    failsafe.construction.change.on(listener);
  }
  async #enterOnlineMode() {
    this.reactTo(this.env.get(FabricManager).events.added, this.enterOperationalMode);
    if (this.endpoint.lifecycle.isCommissioned) {
      await this.endpoint.act((agent) => agent.get(SubscriptionsServer).reestablishFormerSubscriptions());
      this.enterOperationalMode();
      return;
    }
    if (this.state.enabled === false) {
      return;
    }
    if (this.env.get(FabricManager).fabrics.length) {
      return;
    }
    if (!this.#hasAdvertisableDeviceType) {
      return;
    }
    await this.enterCommissionableMode();
  }
  #enterOfflineMode() {
    this.internal.mutex.run(async () => {
      await this.env.close(DeviceCommissioner);
      this.env.delete(CommissioningConfigProvider);
      this.internal.unregisterFailsafeListener?.();
      await this.env.close(FailsafeContext);
    });
  }
  /**
   * An uncommissioned node is not yet associated with fabrics.  It cannot be used until commissioned by a controller.
   *
   * The server normally invokes this method when the node starts and is not yet commissioned.  You can disable by
   * setting {@link CommissioningServer.State#enabled} to false.  Then you must invoke yourself.
   */
  async enterCommissionableMode() {
    if (!this.#hasAdvertisableDeviceType) {
      throw new ImplementationError(
        `Node ${this.endpoint} has no endpoints with advertisable device types; you must add an endpoint or set the device type`
      );
    }
    await this.env.get(DeviceCommissioner).allowBasicCommissioning();
    this.initiateCommissioning();
  }
  /**
   * The server invokes this method when the node starts and is already commissioned, or immediately after
   * commissioning.
   */
  enterOperationalMode() {
    if (!this.endpoint.lifecycle.isOnline) {
      throw new ImplementationError("Cannot advertise offline server");
    }
    this.env.get(DeviceAdvertiser).enterOperationalMode();
  }
  /**
   * Display instructions on commissioning the device.
   *
   * The default implementation logs the QR code and credentials.
   */
  initiateCommissioning() {
    const { passcode, discriminator } = this.state;
    const { qrPairingCode, manualPairingCode } = this.state.pairingCodes;
    logger.notice(
      Diagnostic.strong(this.endpoint.toString()),
      "is uncommissioned",
      Diagnostic.dict({
        passcode,
        discriminator,
        "manual pairing code": manualPairingCode
      }),
      Diagnostic.list([
        QrCode.get(qrPairingCode).trim(),
        `QR code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}
`
      ])
    );
  }
  /**
   * Define logical schema to make passcode and discriminator persistent.
   */
  static schema = new DatatypeModel({
    name: "CommissioningState",
    type: "struct",
    children: [
      FieldElement({ name: "passcode", type: "uint32", quality: "N" }),
      FieldElement({ name: "discriminator", type: "uint16", quality: "N" })
    ]
  });
  #initializeNode() {
    this.state.commissioned = !!this.agent.get(OperationalCredentialsBehavior).state.commissionedFabrics;
    this.endpoint.lifecycle.initialized.emit(this.state.commissioned);
  }
  get #hasAdvertisableDeviceType() {
    return this.agent.get(ProductDescriptionServer).state.deviceType !== EndpointType.UNKNOWN_DEVICE_TYPE;
  }
}
((CommissioningServer2) => {
  class Internal {
    unregisterFailsafeListener = void 0;
    /**
     * We use this to synchronize internal state transitions that would otherwise have race conditions due to the
     * large number of asynchronous calls.
     */
    mutex = new Mutex({
      toString: () => this.name
    });
    /**
     * Passed to the mutex for error handling purposes
     */
    name = "?";
  }
  CommissioningServer2.Internal = Internal;
  class State {
    enabled;
    commissioned = false;
    fabrics = {};
    passcode = -1;
    discriminator = -1;
    flowType = CommissioningFlowType.Standard;
    additionalBleAdvertisementData = void 0;
    pairingCodes = {};
    mdns;
    ble;
    [Val.properties](endpoint) {
      const comm = this;
      return {
        get pairingCodes() {
          if (comm.passcode < 0 || comm.discriminator < 0) {
            throw new ImplementationError("pairingCodes read before commissioning was initialized");
          }
          const bi = endpoint.stateOf(BasicInformationBehavior);
          const net = endpoint.stateOf(NetworkServer);
          const qrPairingCode = QrPairingCodeCodec.encode([
            {
              version: 0,
              vendorId: bi.vendorId,
              productId: bi.productId,
              flowType: comm.flowType,
              discriminator: comm.discriminator,
              passcode: comm.passcode,
              discoveryCapabilities: DiscoveryCapabilitiesSchema.encode(net.discoveryCapabilities)
            }
          ]);
          return {
            manualPairingCode: ManualPairingCodeCodec.encode({
              discriminator: comm.discriminator,
              passcode: comm.passcode
            }),
            qrPairingCode
          };
        },
        get fabrics() {
          const exposedFabrics = {};
          endpoint.env.get(FabricManager).fabrics.forEach(
            ({ fabricIndex, externalInformation }) => exposedFabrics[fabricIndex] = externalInformation
          );
          return exposedFabrics;
        }
      };
    }
  }
  CommissioningServer2.State = State;
  class Events extends EventEmitter {
    commissioned = Observable();
    decommissioned = Observable();
    fabricsChanged = Observable();
    enabled$Changed = AsyncObservable();
  }
  CommissioningServer2.Events = Events;
})(CommissioningServer || (CommissioningServer = {}));
class CommissioningServerConfigProvider extends CommissioningConfigProvider {
  #node;
  constructor(node) {
    super();
    this.#node = node;
  }
  get values() {
    const { commissioning, productDescription, network } = this.#node.state;
    const config = {
      ...commissioning,
      productDescription,
      ble: !!network.ble
    };
    return config;
  }
}
export {
  CommissioningServer
};
//# sourceMappingURL=CommissioningServer.js.map
