/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Behavior } from "#behavior/Behavior.js";
import { OtaAnnouncements } from "#behavior/system/software-update/OtaAnnouncements.js";
import { BasicInformationClient } from "#behaviors/basic-information";
import { OtaSoftwareUpdateRequestor } from "#clusters/ota-software-update-requestor";
import {
  Diagnostic,
  Duration,
  EventEmitter,
  Hours,
  ImplementationError,
  InternalError,
  Logger,
  MatterError,
  Millis,
  Minutes,
  Observable,
  ObserverGroup,
  Seconds,
  Time
} from "#general";
import { Node } from "#node/Node.js";
import {
  BdxProtocol,
  DclOtaUpdateService,
  FabricAuthority,
  FileDesignator,
  OtaUpdateError,
  PeerAddress
} from "#protocol";
const logger = Logger.get("SoftwareUpdateManager");
var OtaUpdateStatus = /* @__PURE__ */ ((OtaUpdateStatus2) => {
  OtaUpdateStatus2[OtaUpdateStatus2["Unknown"] = 0] = "Unknown";
  OtaUpdateStatus2[OtaUpdateStatus2["WaitForConsent"] = 1] = "WaitForConsent";
  OtaUpdateStatus2[OtaUpdateStatus2["Querying"] = 2] = "Querying";
  OtaUpdateStatus2[OtaUpdateStatus2["Downloading"] = 3] = "Downloading";
  OtaUpdateStatus2[OtaUpdateStatus2["WaitForApply"] = 4] = "WaitForApply";
  OtaUpdateStatus2[OtaUpdateStatus2["Applying"] = 5] = "Applying";
  OtaUpdateStatus2[OtaUpdateStatus2["Done"] = 6] = "Done";
  OtaUpdateStatus2[OtaUpdateStatus2["Cancelled"] = 7] = "Cancelled";
  return OtaUpdateStatus2;
})(OtaUpdateStatus || {});
class SoftwareUpdateManager extends Behavior {
  static id = "softwareupdates";
  async initialize() {
    this.internal.services = this.env.asDependent();
    this.internal.otaService = this.internal.services.get(DclOtaUpdateService);
    const node = Node.forEndpoint(this.endpoint);
    this.reactTo(node.lifecycle.online, this.#nodeOnline);
    if (node.lifecycle.isOnline) {
      await this.#nodeOnline();
    }
    this.reactTo(this.events.announceAsDefaultProvider$Changed, this.#updateAnnouncementSettings);
    this.reactTo(this.events.announcementInterval$Changed, this.#updateAnnouncementSettings);
  }
  async #nodeOnline() {
    if (this.internal.announcements !== void 0) {
      await this.internal.announcements.close();
      this.internal.announcements = void 0;
    }
    const fabricAuthority = this.env.get(FabricAuthority);
    const ownFabric = fabricAuthority.fabrics[0];
    if (!ownFabric) {
      logger.info(`No commissioned peers yet, cannot check for OTA updates. Wait for Fabric being added.`);
      fabricAuthority.fabricAdded.once(this.callback(this.#nodeOnline));
      return;
    }
    this.internal.announcements = new OtaAnnouncements(this.endpoint, ownFabric);
    this.#updateAnnouncementSettings();
    const delay = Millis(Seconds(Math.floor(Math.random() * 300)) + Minutes(5));
    logger.info(`Scheduling first OTA update check in ${Duration.format(delay)}`);
    this.internal.checkForUpdateTimer?.stop();
    this.internal.checkForUpdateTimer = Time.getTimer(
      "initializeUpdateCheck",
      delay,
      this.callback(this.#initializeUpdateCheck)
    ).start();
  }
  #updateAnnouncementSettings() {
    if (this.internal.announcements === void 0) {
      return;
    }
    if (this.state.announceAsDefaultProvider) {
      this.internal.announcements.interval = this.state.announcementInterval;
    } else {
      this.internal.announcements.interval = void 0;
    }
  }
  /**
   * Trigger the first check for updates some time after the node went online, and register a periodic interval
   * afterward.
   */
  async #initializeUpdateCheck() {
    try {
      await this.#checkAvailableUpdates();
    } catch (error) {
      logger.error(`Error during initial OTA update check:`, error);
    }
    this.internal.checkForUpdateTimer.stop();
    this.internal.checkForUpdateTimer = Time.getPeriodicTimer(
      "checkAvailableUpdates",
      this.state.updateCheckInterval,
      this.callback(this.#checkAvailableUpdates)
    ).start();
  }
  get storage() {
    return this.internal.otaService.storage;
  }
  /**
   * Returns a snapshot of the current update queue for introspection.
   */
  get queuedUpdates() {
    const now = Time.nowMs;
    return this.internal.updateQueue.map((entry) => {
      let status;
      if (entry.lastProgressUpdateTime === void 0) {
        status = "queued";
      } else if (entry.lastProgressUpdateTime + Minutes(15) < now) {
        status = "stalled";
      } else {
        status = "in-progress";
      }
      return {
        peerAddress: entry.peerAddress,
        vendorId: entry.vendorId,
        productId: entry.productId,
        targetSoftwareVersion: entry.targetSoftwareVersion,
        status,
        lastProgressStatus: entry.lastProgressStatus,
        lastProgressUpdateTime: entry.lastProgressUpdateTime
      };
    });
  }
  /** Validate that we know the peer the update is requested for and the details match to what we know */
  async #validatePeerDetails(peerAddress, details) {
    const { softwareVersion, vendorId, productId } = details;
    const peers = Node.forEndpoint(this.endpoint).peers;
    const node = peers.get(peerAddress);
    const basicInfo = node?.maybeStateOf(BasicInformationClient);
    if (basicInfo?.softwareVersion === softwareVersion && basicInfo?.vendorId === vendorId && basicInfo?.productId === productId) {
      return true;
    }
    return false;
  }
  /**
   * Used to determine if an update is existing in our storage for a peer with a certain software version.
   *
   * It uses the already checked details and does not check again on-demand. It considers consents already given,
   * but validates the peer data.
   */
  async updateExistsFor(peerAddress, { softwareVersion, vendorId, productId, requestorCanConsent }) {
    if (!await this.#validatePeerDetails(peerAddress, { softwareVersion, vendorId, productId })) {
      logger.info(
        `Peer details for node ${peerAddress.toString()} do not match values the update was requested for, ignoring`
      );
      return void 0;
    }
    const availableUpdates = await this.internal.otaService.find({ vendorId, productId });
    const candidates = availableUpdates.filter(
      ({
        softwareVersion: candidateSoftwareVersion,
        minApplicableSoftwareVersion = 0,
        maxApplicableSoftwareVersion = softwareVersion,
        mode
      }) => softwareVersion < candidateSoftwareVersion && softwareVersion >= minApplicableSoftwareVersion && softwareVersion <= maxApplicableSoftwareVersion && (mode === "prod" || this.state.allowTestOtaImages)
    ).sort((a, b) => b.softwareVersion - a.softwareVersion);
    if (candidates.length === 0) {
      return void 0;
    }
    const candidatesWithConsent = candidates.map((candidate2) => {
      const consent = this.internal.consents.filter((consent2) => consent2.vendorId === candidate2.vendorId && consent2.productId === candidate2.productId).filter((consent2) => consent2.targetSoftwareVersion >= candidate2.softwareVersion);
      return {
        ...candidate2,
        consentPeers: consent.map((c) => c.peerAddress)
      };
    });
    let candidate;
    let consentRequired = false;
    if (requestorCanConsent) {
      candidate = candidatesWithConsent[0];
      consentRequired = !candidate.consentPeers.some(
        ({ fabricIndex, nodeId }) => fabricIndex === peerAddress.fabricIndex && nodeId === peerAddress.nodeId
      );
    } else {
      candidate = candidatesWithConsent.find(
        ({ consentPeers }) => consentPeers.some(
          ({ fabricIndex, nodeId }) => fabricIndex === peerAddress.fabricIndex && nodeId === peerAddress.nodeId
        )
      );
    }
    if (candidate) {
      const { softwareVersion: softwareVersion2, softwareVersionString, filename } = candidate;
      return {
        newSoftwareVersion: softwareVersion2,
        newSoftwareVersionString: softwareVersionString,
        fileDesignator: new FileDesignator(`ota/${filename}`),
        // We do not provide any httpsUri here, as we expect the node to download from our DCL proxy via BDX
        consentRequired
      };
    } else {
      logger.info(
        `No update candidate found for node ${peerAddress.nodeId} in fabric ${peerAddress.fabricIndex} for any update`
      );
    }
  }
  /** Triggered by a Timer to call the update with different parameters */
  async #checkAvailableUpdates() {
    await this.queryUpdates();
    await this.#cleanupObsoleteUpdates();
  }
  /**
   * Clean up stored OTA files that no node in the system needs anymore.
   * A stored version is obsolete when ALL nodes with that vendor/product ID are already at or above that version.
   * Only cleans "prod" and "test" mode files; "local" files are user-managed.
   */
  async #cleanupObsoleteUpdates() {
    const rootNode = Node.forEndpoint(this.endpoint);
    const nodeVersions = /* @__PURE__ */ new Map();
    for (const peer of rootNode.peers) {
      const basicInfo = peer.maybeStateOf(BasicInformationClient);
      if (basicInfo === void 0) {
        continue;
      }
      const { vendorId, productId, softwareVersion } = basicInfo;
      const key = `${vendorId}-${productId}`;
      const existing = nodeVersions.get(key);
      if (existing === void 0 || softwareVersion < existing) {
        nodeVersions.set(key, softwareVersion);
      }
    }
    if (nodeVersions.size === 0) {
      return;
    }
    const storedUpdates = await this.internal.otaService.find({});
    for (const update of storedUpdates) {
      if (update.mode === "local") {
        continue;
      }
      const key = `${update.vendorId}-${update.productId}`;
      const minNodeVersion = nodeVersions.get(key);
      if (minNodeVersion === void 0) {
        continue;
      }
      if (update.softwareVersion <= minNodeVersion) {
        try {
          await this.internal.otaService.delete({ filename: update.filename });
          logger.info(
            `Cleaned up obsolete OTA file ${update.filename} (all nodes at version >= ${minNodeVersion})`
          );
        } catch (error) {
          logger.warn(`Failed to clean up OTA file ${update.filename}:`, error);
        }
      }
    }
  }
  /**
   * Checks all nodes, or optionally a defined one, for available updates from the DCL OTA Update Service.
   *
   * Returns a list of peers for which updates are available along with the collected update info.
   *
   * If `includeStoredUpdates` is set to true available and known local update will be returned without checking the
   * DCL again.
   */
  async queryUpdates(options = {}) {
    const { peerToCheck, includeStoredUpdates = false } = options;
    const rootNode = Node.forEndpoint(this.endpoint);
    const updateDetails = /* @__PURE__ */ new Map();
    for (const peer of rootNode.peers) {
      if (peerToCheck !== void 0 && peerToCheck !== peer) {
        continue;
      }
      const details = this.#preparePeerForUpdate(peer);
      if (details === void 0) {
        continue;
      }
      const { otaEndpoint, peerAddress, vendorId, productId, softwareVersion, softwareVersionString } = details;
      const key = `${vendorId}-${productId}-${softwareVersion}`;
      const versionData = updateDetails.get(key) ?? {
        vendorId,
        productId,
        softwareVersion,
        softwareVersionString,
        otaEndpoints: /* @__PURE__ */ new Set()
      };
      versionData.otaEndpoints.add({ endpoint: otaEndpoint, peerAddress });
      updateDetails.set(key, versionData);
    }
    if (updateDetails.size === 0) {
      logger.info(`No updatable nodes found`);
      return [];
    }
    const peersWithUpdates = new Array();
    for (const infos of updateDetails.values()) {
      try {
        const peers = await this.#checkProductForUpdates(infos, includeStoredUpdates);
        for (const peer of peers) {
          const info = this.internal.knownUpdates.get(peer.toString());
          if (info === void 0) {
            continue;
          }
          peersWithUpdates.push({ peerAddress: peer, info });
        }
      } catch (error) {
        logger.warn(
          `Error while checking for updates for product ${infos.vendorId}-${infos.productId}:`,
          error
        );
      }
    }
    return peersWithUpdates;
  }
  /**
   * Check storage and DCL for updates for the given product/version, downloads it, and notify nodes if an update was
   * found
   */
  async #checkProductForUpdates(infos, includeStoredUpdates) {
    const { vendorId, productId, softwareVersion, otaEndpoints } = infos;
    if (includeStoredUpdates && [...otaEndpoints.values()].every(
      ({ peerAddress }) => this.internal.knownUpdates.has(peerAddress.toString())
    )) {
      return [...otaEndpoints.values()].map(({ peerAddress }) => peerAddress);
    }
    const updateDetails = await this.internal.otaService.checkForUpdate({
      vendorId,
      productId,
      currentSoftwareVersion: softwareVersion,
      includeStoredUpdates,
      isProduction: this.state.allowTestOtaImages ? void 0 : true
    });
    if (!updateDetails) {
      return [];
    }
    const fd = await this.internal.otaService.downloadUpdate(updateDetails);
    logger.info(
      "OTA update available for",
      Diagnostic.dict({
        vendorId,
        productId,
        softwareVersion,
        file: `ota/${fd.text}`,
        peers: [...otaEndpoints.values()].map(({ endpoint }) => Node.forEndpoint(endpoint).id)
      })
    );
    for (const { endpoint, peerAddress } of otaEndpoints) {
      const { vid, pid, softwareVersion: softwareVersion2, softwareVersionString, releaseNotesUrl, specificationVersion, source } = updateDetails;
      const details = {
        vendorId: vid,
        productId: pid,
        softwareVersion: softwareVersion2,
        softwareVersionString,
        releaseNotesUrl,
        specificationVersion,
        source
      };
      this.internal.knownUpdates.set(peerAddress.toString(), details);
      const hasConsent = this.internal.consents.some(
        (consent) => consent.vendorId === vendorId && consent.productId === productId && consent.targetSoftwareVersion === updateDetails.softwareVersion && consent.peerAddress.fabricIndex === peerAddress.fabricIndex && consent.peerAddress.nodeId === peerAddress.nodeId
      );
      if (hasConsent) {
        this.#queueUpdate({
          endpoint,
          vendorId,
          productId,
          targetSoftwareVersion: updateDetails.softwareVersion,
          peerAddress
        });
      } else {
        this.requestConsentForUpdate(peerAddress, details);
      }
    }
    return [...otaEndpoints.values()].map(({ peerAddress }) => peerAddress);
  }
  /**
   * 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
   */
  #preparePeerForUpdate(peer) {
    const otaData = this.internal.announcements?.peerApplicableForUpdate(peer);
    if (otaData === void 0) {
      return;
    }
    const { peerAddress, otaEndpoint } = otaData;
    const { vendorId, productId, softwareVersion, softwareVersionString } = peer.stateOf(BasicInformationClient);
    const versionEvent = peer.eventsOf(BasicInformationClient).softwareVersion$Changed;
    if (!this.internal.versionUpdateObservers.observes(versionEvent)) {
      let triggerVersionChange2 = function(newVersion) {
        that.#onSoftwareVersionChanged(peerAddress, newVersion);
      };
      var triggerVersionChange = triggerVersionChange2;
      const that = this;
      this.internal.versionUpdateObservers.on(versionEvent, this.callback(triggerVersionChange2));
    }
    const startUpEvent = peer.eventsOf(BasicInformationClient).startUp;
    if (!this.internal.versionUpdateObservers.observes(startUpEvent)) {
      let triggerStartUp2 = function({ softwareVersion: softwareVersion2 }) {
        that.#onSoftwareVersionChanged(peerAddress, softwareVersion2, true);
      };
      var triggerStartUp = triggerStartUp2;
      const that = this;
      this.internal.versionUpdateObservers.on(startUpEvent, this.callback(triggerStartUp2));
    }
    return { otaEndpoint, peerAddress, vendorId, productId, softwareVersion, softwareVersionString };
  }
  /**
   * Handler for softwareVersion changes on a peer.
   *
   * Also called from the startUp event with `isStartUp = true`. When a device reboots after applying an update,
   * softwareVersion$Changed fires if the version actually changed (success). But if the device reboots with the
   * same version (failed apply), softwareVersion$Changed does NOT fire — so startUp feeds the version through
   * this same handler to detect the failure.
   */
  #onSoftwareVersionChanged(peerAddress, newVersion, isStartUp = false) {
    const entryIndex = this.internal.updateQueue.findIndex(
      (e) => e.peerAddress.fabricIndex === peerAddress.fabricIndex && e.peerAddress.nodeId === peerAddress.nodeId
    );
    if (entryIndex === -1) {
      return;
    }
    const entry = this.internal.updateQueue[entryIndex];
    if (entry.lastProgressUpdateTime !== void 0) {
      logger.info(
        `Clearing in-progress update for node ${peerAddress.toString()} due to software version change. Last State was ${OtaUpdateStatus[entry.lastProgressStatus]}`
      );
      entry.lastProgressUpdateTime = void 0;
      entry.lastProgressStatus = 0 /* Unknown */;
    }
    const expectedVersion = entry.targetSoftwareVersion;
    if (newVersion < expectedVersion) {
      if (isStartUp && (entry.lastProgressStatus === 5 /* Applying */ || entry.lastProgressStatus === 4 /* WaitForApply */)) {
        logger.warn(
          `Device ${peerAddress.toString()} rebooted after applying update but reports softwareVersion ${newVersion} (expected >= ${expectedVersion}), update failed to apply`
        );
        this.internal.updateQueue.splice(entryIndex, 1);
        this.events.updateFailed.emit(peerAddress);
        this.#triggerQueuedUpdate();
        return;
      }
      logger.info(
        `Software version for node ${peerAddress.toString()} changed to ${newVersion}, but still below target version ${expectedVersion}, keeping in update queue`
      );
      return;
    }
    logger.info(
      `Software version changed to ${newVersion} (expected ${expectedVersion}) for node ${peerAddress.toString()}, removing from update queue`
    );
    this.internal.knownUpdates.delete(peerAddress.toString());
    this.events.updateDone.emit(peerAddress);
    this.internal.updateQueue.splice(entryIndex, 1);
    this.internal.consents = this.internal.consents.filter(
      ({ peerAddress: consentAddress, targetSoftwareVersion }) => !PeerAddress.is(peerAddress, consentAddress) || targetSoftwareVersion > newVersion
    );
    this.#triggerQueuedUpdate();
  }
  /**
   * Notify the application that consent is needed for the given update on the given peer
   */
  requestConsentForUpdate(peerAddress, updateDetails) {
    this.events.updateAvailable.emit(peerAddress, updateDetails);
  }
  /**
   * Add an update to the update queue and execute it.
   */
  #queueUpdate(entry) {
    const existing = this.internal.updateQueue.find(
      (e) => e.peerAddress.fabricIndex === entry.peerAddress.fabricIndex && e.peerAddress.nodeId === entry.peerAddress.nodeId
    );
    if (existing !== void 0) {
      if (existing.lastProgressUpdateTime !== void 0) {
        logger.info(
          `Update for node ${entry.peerAddress.toString()} already in progress, skipping queue update`
        );
        return;
      }
      existing.vendorId = entry.vendorId;
      existing.productId = entry.productId;
      existing.targetSoftwareVersion = entry.targetSoftwareVersion;
      existing.endpoint = entry.endpoint;
      logger.info(`Updated existing queued update for node ${entry.peerAddress.toString()}`);
    } else {
      logger.info(`Queuing update consent for node ${entry.peerAddress.toString()}`);
      this.internal.updateQueue.push(entry);
    }
    this.#triggerQueuedUpdate();
  }
  /**
   * Triggers updates for queued entries in the update queue, ensuring proper prioritization and handling of stalled
   * updates. This method checks for ongoing updates, resets states for stalled updates, and processes the next entry
   * in the queue if no update is currently in progress.
   *
   * The queue is re-sorted to prioritize entries with an unknown status, and a periodic timer is activated to
   * monitor for stalled updates.
   */
  #triggerQueuedUpdate() {
    const now = Time.nowMs;
    const inProgressEntries = this.internal.updateQueue.filter(
      ({ lastProgressUpdateTime }) => lastProgressUpdateTime !== void 0
    );
    let inProgressCount = inProgressEntries.length;
    if (inProgressCount === 0 && this.internal.updateQueueTimer?.isRunning) {
      this.internal.updateQueueTimer.stop();
    } else if (inProgressCount > 0) {
      for (const entry of inProgressEntries) {
        if (entry.lastProgressUpdateTime + Minutes(15) < now) {
          logger.info(
            `Resetting stalled OTA update state for node ${entry.peerAddress.toString()} due to inactivity`
          );
          entry.lastProgressUpdateTime = void 0;
          entry.lastProgressStatus = 0 /* Unknown */;
          inProgressCount--;
        }
      }
    }
    if (inProgressCount > 0) {
      return;
    }
    this.internal.updateQueue = this.internal.updateQueue.sort((a, b) => {
      const aStatus = a.lastProgressStatus === 0 /* Unknown */ ? 1 : 0;
      const bStatus = b.lastProgressStatus === 0 /* Unknown */ ? 1 : 0;
      return aStatus - bStatus;
    });
    const nextEntry = this.internal.updateQueue[0];
    if (nextEntry) {
      this.#triggerUpdateOnNode(nextEntry).catch((error) => {
        logger.error(`Error while triggering OTA update on node ${nextEntry.peerAddress.toString()}:`, error);
      });
      if (!this.internal.updateQueueTimer?.isRunning) {
        this.internal.updateQueueTimer = Time.getPeriodicTimer(
          "checkQueuedUpdates",
          Minutes(5),
          this.callback(this.#triggerQueuedUpdate)
        ).start();
      }
    }
  }
  /**
   * Triggers an update on the given node. This method announces ourselves as OTA Provider to the node with an
   * available update and then triggers the update on the node.
   * The node usually calls queryImage as a result of this when it processes the announcement.
   */
  async #triggerUpdateOnNode(entry) {
    if (this.internal.announcements == void 0) {
      logger.info(`Not yet initialized with peers, can not trigger update on node`, entry.peerAddress);
      return;
    }
    const { endpoint, peerAddress } = entry;
    try {
      logger.info(`Announcing OTA provider to node ${peerAddress.toString()}`);
      await this.internal.announcements.announceOtaProvider(
        endpoint,
        peerAddress,
        OtaSoftwareUpdateRequestor.AnnouncementReason.UpdateAvailable
      );
      if (this.internal.updateQueue.indexOf(entry) >= 0) {
        entry.lastProgressUpdateTime = Time.nowMs;
      }
    } catch (error) {
      logger.error(`Failed to announce OTA provider to node ${peerAddress.toString()}:`, error);
      entry.lastProgressUpdateTime = void 0;
      entry.lastProgressStatus = 0 /* Unknown */;
      this.#triggerQueuedUpdate();
    }
  }
  /**
   * Forces an OTA update for a specific node identified by its peer address, vendor ID, product ID,
   * and target software version. This method will override any ongoing updates by explicitly adding an update
   * consent for the specified node and processing the update immediately if applicable.
   * This can be used when an exact timing of the update is needed. When the update can be executed in a delayed/queued
   * manner, please use `addUpdateConsent()`.
   */
  async forceUpdate(peerAddress, vendorId, productId, targetSoftwareVersion) {
    const existingEntry = this.internal.updateQueue.find(
      (e) => e.peerAddress.fabricIndex === peerAddress.fabricIndex && e.peerAddress.nodeId === peerAddress.nodeId && e.lastProgressUpdateTime !== void 0
    );
    if (existingEntry !== void 0) {
      const bdxProtocol = this.env.get(BdxProtocol);
      const activeSession = bdxProtocol.sessionFor(peerAddress, this.storage.scope);
      if (activeSession !== void 0) {
        logger.info(
          `Force update for node ${peerAddress.toString()} skipped: BDX transfer is actively in progress`
        );
        return;
      }
      logger.info(`Cleaning up stale update entry for node ${peerAddress.toString()} before forcing new update`);
      const staleIndex = this.internal.updateQueue.indexOf(existingEntry);
      if (staleIndex >= 0) {
        this.internal.updateQueue.splice(staleIndex, 1);
      }
      try {
        await bdxProtocol.disablePeerForScope(peerAddress, this.storage, true);
      } catch (error) {
        MatterError.accept(error);
        logger.debug(`Error cleaning up stale BDX registration:`, error);
      }
    }
    const added = await this.addUpdateConsent(peerAddress, vendorId, productId, targetSoftwareVersion);
    if (!added) {
      throw new OtaUpdateError(`Node at ${peerAddress.toString()} is not currently applicable for OTA updates`);
    }
    const entry = this.internal.updateQueue.find(
      (e) => e.peerAddress.fabricIndex === peerAddress.fabricIndex && e.peerAddress.nodeId === peerAddress.nodeId && e.vendorId === vendorId && e.productId === productId && e.targetSoftwareVersion === targetSoftwareVersion
    );
    if (!entry) {
      throw new InternalError(`Failed to find queued update after adding consent`);
    }
    if (this.internal.updateQueue.length === 1) {
      return;
    }
    await this.#triggerUpdateOnNode(entry);
  }
  /** Tries to cancel an ongoing OTA update for the given peer address. */
  async #cancelUpdate(peerAddress) {
    const bdxProtocol = this.env.get(BdxProtocol);
    await bdxProtocol.disablePeerForScope(peerAddress, this.storage, true);
    const entryIndex = this.internal.updateQueue.findIndex(
      (e) => e.peerAddress.fabricIndex === peerAddress.fabricIndex && e.peerAddress.nodeId === peerAddress.nodeId
    );
    if (entryIndex < 0) {
      logger.warn(`No Ota update queued for node ${peerAddress.toString()}`);
      return;
    }
    const entry = this.internal.updateQueue[entryIndex];
    if (entry.lastProgressStatus === 5 /* Applying */ || entry.lastProgressStatus === 6 /* Done */) {
      logger.info(`Cannot cancel update for node ${peerAddress.toString()}, already applying or done`);
      return;
    }
    this.internal.updateQueue.splice(entryIndex, 1);
    logger.info(`Cancelled OTA update for node ${peerAddress.toString()}`);
    this.events.updateFailed.emit(peerAddress);
    this.#triggerQueuedUpdate();
  }
  /**
   * Adds or updates a consent for a given peer address, vendor ID, product ID, and target software version.
   * Filters out existing consents for the given peer address and replaces them with the new one.
   * If the node associated with the peer address is applicable for an update, it schedules the update to happen with
   * the next queue slot, so potentially delayed.
   * This can be used when the update can be executed in a delayed/queued manner and it does not matter exactly when.
   */
  async addUpdateConsent(peerAddress, vendorId, productId, targetSoftwareVersion) {
    const consents = this.internal.consents.filter(
      (consent) => consent.peerAddress.fabricIndex !== peerAddress.fabricIndex || consent.peerAddress.nodeId !== peerAddress.nodeId
    );
    const node = await Node.forEndpoint(this.endpoint).peers.forAddress(peerAddress);
    const {
      otaEndpoint,
      vendorId: nodeVendorId,
      productId: nodeProductId
    } = this.#preparePeerForUpdate(node) ?? {};
    if (otaEndpoint !== void 0 && (nodeVendorId !== vendorId || nodeProductId !== productId)) {
      throw new ImplementationError(`Node at ${peerAddress.toString()} does not match given vendorId/productId`);
    }
    consents.push({
      vendorId,
      productId,
      targetSoftwareVersion,
      peerAddress
    });
    this.internal.consents = consents;
    if (otaEndpoint === void 0) {
      logger.info(
        `Node ${peerAddress.toString()} has no OTA requestor and is currently not applicable for OTA updates, update delayed`
      );
      return false;
    }
    this.#queueUpdate({ vendorId, productId, targetSoftwareVersion, peerAddress, endpoint: otaEndpoint });
    return true;
  }
  /**
   * Checks if consent exists for the given peer address and optionally for a specific target software version.
   */
  hasConsent(peerAddress, targetSoftwareVersion) {
    return this.internal.consents.some(
      (consent) => consent.peerAddress.fabricIndex === peerAddress.fabricIndex && consent.peerAddress.nodeId === peerAddress.nodeId && (targetSoftwareVersion === void 0 || consent.targetSoftwareVersion === targetSoftwareVersion)
    );
  }
  /**
   * Checks for consent and removes it if present, also cancels if in progress. Use this to remove a formerly given
   * consent.
   */
  removeConsent(peerAddress, targetSoftwareVersion) {
    const consentIndex = this.internal.consents.findIndex(
      (consent) => consent.peerAddress.fabricIndex === peerAddress.fabricIndex && consent.peerAddress.nodeId === peerAddress.nodeId && (targetSoftwareVersion === void 0 || consent.targetSoftwareVersion === targetSoftwareVersion)
    );
    if (consentIndex >= 0) {
      this.internal.consents.splice(consentIndex, 1);
    } else {
      logger.info(
        `No consent to remove found for node ${peerAddress.toString()}${targetSoftwareVersion !== void 0 ? ` for version ${targetSoftwareVersion}` : ""}`
      );
    }
    return this.#cancelUpdate(peerAddress);
  }
  /**
   * Handles the status change of an OTA update for a given peer device.
   *
   * This method processes OTA update status notifications received from a specified device.
   * Based on the status, it updates the internal state of the update queue, logs relevant
   * messages, and triggers the necessary events.
   */
  onOtaStatusChange(peerAddress, status, toVersion) {
    const entryIndex = this.internal.updateQueue.findIndex(
      (e) => e.peerAddress.fabricIndex === peerAddress.fabricIndex && e.peerAddress.nodeId === peerAddress.nodeId
    );
    if (entryIndex < 0) {
      if (status !== 7 /* Cancelled */) {
        logger.warn(
          `Received OTA status update from unknown node ${peerAddress.toString()}, status=${OtaUpdateStatus[status]}`
        );
      }
      return;
    }
    const entry = this.internal.updateQueue[entryIndex];
    if (status === 6 /* Done */) {
      logger.info(`OTA update completed for node`, peerAddress.toString());
      this.internal.updateQueue.splice(entryIndex, 1);
      this.internal.knownUpdates.delete(peerAddress.toString());
      this.events.updateDone.emit(peerAddress);
      this.#triggerQueuedUpdate();
    } else if (status === 7 /* Cancelled */) {
      logger.info(`OTA update cancelled for node`, peerAddress.toString());
      this.internal.updateQueue.splice(entryIndex, 1);
      this.events.updateFailed.emit(peerAddress);
      this.#triggerQueuedUpdate();
    } else {
      logger.info(
        `OTA update status for node ${peerAddress.toString()} changed to ${OtaUpdateStatus[status]}${toVersion !== void 0 ? ` for version ${toVersion}` : ""}`
      );
      entry.lastProgressUpdateTime = Time.nowMs;
      entry.lastProgressStatus = status;
    }
  }
  async [Symbol.asyncDispose]() {
    this.internal.checkForUpdateTimer?.stop();
    this.internal.updateQueueTimer?.stop();
    await this.internal.announcements?.close();
    await this.internal.services?.close();
    this.internal.versionUpdateObservers.close();
    await super[Symbol.asyncDispose]?.();
  }
}
((SoftwareUpdateManager2) => {
  class State {
    /** Set this to true to also allow updates from the Test DCL */
    allowTestOtaImages = false;
    /** Default Update check Interval */
    updateCheckInterval = Hours(24);
    /** Announce this controller as Update provider to all nodes */
    announceAsDefaultProvider = false;
    /** Interval to Announces this controller as Update provider. Must not be lower than 24h! */
    announcementInterval = Hours(24);
  }
  SoftwareUpdateManager2.State = State;
  class Internal {
    /** Use this to pre-initialize consent to allow nodes to update automatically. The content will not be persisted! */
    consents = new Array();
    services;
    otaService;
    checkForUpdateTimer;
    updateQueue = new Array();
    updateQueueTimer;
    announcements;
    versionUpdateObservers = new ObserverGroup();
    knownUpdates = /* @__PURE__ */ new Map();
  }
  SoftwareUpdateManager2.Internal = Internal;
  class Events extends EventEmitter {
    /** Emitted when an update is available for a Peer and there is no consent stored and contains update details */
    updateAvailable = Observable();
    /** Emitted when an update for a Peer is finished */
    updateDone = Observable();
    /** Emitted when an update for a Peer has failed or was cancelled */
    updateFailed = Observable();
    announceAsDefaultProvider$Changed = Observable();
    announcementInterval$Changed = Observable();
  }
  SoftwareUpdateManager2.Events = Events;
})(SoftwareUpdateManager || (SoftwareUpdateManager = {}));
export {
  OtaUpdateStatus,
  SoftwareUpdateManager
};
//# sourceMappingURL=SoftwareUpdateManager.js.map
