/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { CommissioningClient } from "#behavior/system/commissioning/CommissioningClient.js";
import { NetworkClient } from "#behavior/system/network/NetworkClient.js";
import { BasicInformationClient } from "#behaviors/basic-information";
import { OtaSoftwareUpdateRequestorClient } from "#behaviors/ota-software-update-requestor";
import { Node } from "#node/Node.js";
import {
  Duration,
  Hours,
  isDeepEqual,
  Logger,
  MatterError,
  Millis,
  Minutes,
  Seconds,
  Time
} from "@matter/general";
import { PeerAddress, Write, WriteResult } from "@matter/protocol";
import { OtaSoftwareUpdateRequestor } from "@matter/types/clusters/ota-software-update-requestor";
const logger = new Logger("OTAAnnouncements");
class OtaAnnouncements {
  #announcementQueue = new Array();
  #announcementTimer;
  #announcementDelayTimer;
  #ownNodeId;
  #ownFabricIndex;
  #ownVendorId;
  #node;
  #otaProviderEndpoint;
  #announcementInterval;
  #currentAnnouncementPromise;
  constructor(endpoint, ownFabric) {
    this.#node = Node.forEndpoint(endpoint);
    this.#ownNodeId = ownFabric.rootNodeId;
    this.#ownFabricIndex = ownFabric.fabricIndex;
    this.#ownVendorId = ownFabric.rootVendorId;
    this.#otaProviderEndpoint = endpoint.number;
    this.#announcementDelayTimer = Time.getTimer(
      "OTA Node announcement delay",
      Seconds(10),
      () => this.#processQueueEntry()
    );
  }
  /**
   * Set the interval to a time value or undefined to disable announcements
   */
  set interval(interval) {
    if (interval === void 0) {
      this.#announcementInterval = void 0;
      this.#announcementTimer?.stop();
      this.#announcementTimer = void 0;
      return;
    }
    if (interval < Hours(24)) {
      logger.warn("Announcements interval is too short, consider increasing it to at least 24 hours.");
      interval = Hours(24);
    }
    if (interval === this.#announcementInterval) {
      return;
    }
    this.#announcementInterval = Millis(interval + Seconds(Math.floor(Math.random() * 120) + 60));
    this.#announcementTimer?.stop();
    const initialDelay = Millis(Seconds(Math.floor(Math.random() * 300)) + Minutes(10));
    logger.debug(`Initial OTA announcement delay is ${Duration.format(initialDelay)}`);
    this.#announcementTimer = Time.getTimer(
      "Delay for initial OTA announcement",
      initialDelay,
      () => this.#initializeAnnouncements()
    ).start();
  }
  #initializeAnnouncements() {
    if (this.#announcementInterval === void 0) {
      return;
    }
    this.#announcementTimer?.stop();
    this.#announcementTimer = Time.getTimer(
      "OTA All Nodes announcement timer",
      this.#announcementInterval,
      () => this.#queueAllPeers()
    );
    this.#queueAllPeers();
  }
  #queueAllPeers() {
    if (this.#announcementTimer === void 0) {
      return;
    }
    this.#announcementTimer.stop();
    for (const peer of this.#node.peers) {
      if (!peer.lifecycle.isCommissioned || !peer.lifecycle.isOnline) {
        continue;
      }
      const peerAddress = PeerAddress(peer.maybeStateOf(CommissioningClient)?.peerAddress);
      if (peerAddress === void 0 || this.#announcementQueue.some((p) => PeerAddress.is(p, peerAddress))) {
        continue;
      }
      this.#queuePeer(peerAddress);
    }
    this.#announcementTimer.start();
  }
  // Queue a peer because processing is delayed and better to check /get peer anew when we process it
  #queuePeer(peerAddress) {
    if (this.#announcementTimer === void 0) {
      return;
    }
    this.#announcementQueue.push(peerAddress);
    logger.debug("Queued peer", peerAddress, "for OTA announcement;", this.#announcementQueue.length, "queued");
    if (this.#announcementQueue.length > 0 && !this.#announcementTimer.isRunning) {
      this.#announcementDelayTimer.start();
    }
  }
  async #processQueueEntry() {
    if (this.#currentAnnouncementPromise !== void 0) {
      return;
    }
    if (this.#announcementQueue.length === 0) {
      this.#announcementDelayTimer.stop();
      return;
    }
    const peerAddress = this.#announcementQueue[0];
    try {
      this.#currentAnnouncementPromise = this.#announceOtaProvider(peerAddress);
      await this.#currentAnnouncementPromise;
    } catch (error) {
      logger.error(`Error announcing OTA provider to ${peerAddress}`, error);
    } finally {
      this.#currentAnnouncementPromise = void 0;
    }
    this.#announcementQueue.shift();
    if (this.#announcementQueue.length > 0) {
      this.#announcementDelayTimer.start();
    }
  }
  async #announceOtaProvider(peerAddress) {
    const peer = this.#node.peers.get(peerAddress);
    if (peer === void 0 || !peer.lifecycle.isCommissioned || !peer.lifecycle.isOnline) {
      return;
    }
    const { otaEndpoint } = this.peerApplicableForUpdate(peer) ?? {};
    if (otaEndpoint === void 0) {
      return;
    }
    const consideredOtaProviderRecord = {
      providerNodeId: this.#ownNodeId,
      endpoint: this.#otaProviderEndpoint,
      fabricIndex: this.#ownFabricIndex
    };
    const existingOtaProviderRecord = otaEndpoint.stateOf(OtaSoftwareUpdateRequestorClient).defaultOtaProviders.filter(({ fabricIndex }) => fabricIndex === this.#ownFabricIndex)[0];
    if (existingOtaProviderRecord === void 0 || !isDeepEqual(consideredOtaProviderRecord, consideredOtaProviderRecord)) {
      try {
        WriteResult.assertSuccess(
          await peer.interaction.write(
            Write(
              Write.Attribute({
                endpoint: otaEndpoint.number,
                cluster: OtaSoftwareUpdateRequestor.Complete,
                attributes: ["defaultOtaProviders"],
                value: [consideredOtaProviderRecord]
              })
            )
          )
        );
        logger.debug(
          `${existingOtaProviderRecord === void 0 ? "Added" : "Updated"} default OTA provider for`,
          peerAddress,
          `on endpoint ${otaEndpoint.number}`
        );
      } catch (error) {
        logger.info("Could not set default OTA provider", error);
      }
    }
  }
  /**
   * Determine if we can request an update for the given node and return node meta-data needed for the process.
   * The Node needs to be commissioned (have a peer address), being not disabled, and having an active subscription
   * (to not work with outdated data).
   * When a node is applicable for updates, it also subscribes to softwareVersion changes to be able to react
   */
  peerApplicableForUpdate(peer) {
    if (peer.isGroup || !peer.behaviors.has(BasicInformationClient)) {
      return;
    }
    const peerAddress = PeerAddress(peer.stateOf(CommissioningClient).peerAddress);
    if (!peer.behaviors.has(NetworkClient) || peerAddress === void 0 || peer.stateOf(NetworkClient).isDisabled || peer.behaviors.internalsOf(NetworkClient).activeSubscription === void 0) {
      logger.debug(`Node`, (peerAddress ?? peer.id).toString(), ` is currently not applicable for OTA updates`);
      return;
    }
    const otaEndpoint = this.#findOtaRequestorEndpointOn(peer);
    if (otaEndpoint === void 0) {
      logger.debug(`Node`, (peerAddress ?? peer.id).toString(), ` does not support OTA updates`);
      return;
    }
    return { otaEndpoint, peerAddress };
  }
  /** Searches all endpoints of a peer for the OtaSoftwareUpdateRequestor cluster and returns it if found */
  #findOtaRequestorEndpointOn(peer) {
    for (const ep of peer.endpoints) {
      if (ep.behaviors.has(OtaSoftwareUpdateRequestorClient)) {
        return ep;
      }
    }
  }
  /** Announce ourselves as OTA Provider to the given node's endpoint */
  async announceOtaProvider(endpoint, peerAddress, announcementReason = OtaSoftwareUpdateRequestor.AnnouncementReason.SimpleAnnouncement) {
    try {
      await endpoint.commandsOf(OtaSoftwareUpdateRequestorClient).announceOtaProvider({
        providerNodeId: this.#ownNodeId,
        vendorId: this.#ownVendorId,
        fabricIndex: this.#ownFabricIndex,
        announcementReason,
        endpoint: this.#otaProviderEndpoint
      });
    } catch (error) {
      MatterError.accept(error);
      logger.error(
        `Failed to notify node ${peerAddress.toString()}/ep${endpoint.number} about available OTA update:`,
        error
      );
    }
  }
  async close() {
    this.#announcementTimer?.stop();
    this.#announcementDelayTimer.stop();
    if (this.#currentAnnouncementPromise !== void 0) {
      await this.#currentAnnouncementPromise;
    }
  }
}
export {
  OtaAnnouncements
};
//# sourceMappingURL=OtaAnnouncements.js.map
