/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { BasicInformationBehavior } from "#behaviors/basic-information";
import { DescriptorServer } from "#behaviors/descriptor";
import { OtaSoftwareUpdateProviderClient } from "#behaviors/ota-software-update-provider";
import { Node } from "#node/Node.js";
import { ServerNodeStore } from "#storage/server/ServerNodeStore.js";
import {
  AsyncObservable,
  Crypto,
  Diagnostic,
  Hours,
  ImplementationError,
  InternalError,
  Logger,
  MatterError,
  Millis,
  Minutes,
  Seconds,
  Time
} from "@matter/general";
import { FieldElement } from "@matter/model";
import {
  BdxClient,
  BdxError,
  FileDesignator,
  Flow,
  OtaImageError,
  OtaImageReader,
  PeerAddress,
  PersistedFileDesignator
} from "@matter/protocol";
import { NodeId, StatusResponse } from "@matter/types";
import { OtaSoftwareUpdateProvider } from "@matter/types/clusters/ota-software-update-provider";
import { OtaSoftwareUpdateRequestor } from "@matter/types/clusters/ota-software-update-requestor";
import { OtaSoftwareUpdateRequestorBehavior } from "./OtaSoftwareUpdateRequestorBehavior.js";
const logger = Logger.get("OtaSoftwareUpdateRequestorServer");
const OTA_BDX_MAX_BLOCK_SIZE_TCP = 8192;
const OTA_BDX_MAX_BLOCK_SIZE_NON_TCP = 1024;
const MAX_BUSY_RETRIES = 3;
var ScheduleReason = /* @__PURE__ */ ((ScheduleReason2) => {
  ScheduleReason2[ScheduleReason2["NewQuery"] = 0] = "NewQuery";
  ScheduleReason2[ScheduleReason2["NoUpdateAvailable"] = 1] = "NoUpdateAvailable";
  ScheduleReason2[ScheduleReason2["Busy"] = 2] = "Busy";
  ScheduleReason2[ScheduleReason2["Announced"] = 3] = "Announced";
  ScheduleReason2[ScheduleReason2["Commissioned"] = 4] = "Commissioned";
  return ScheduleReason2;
})(ScheduleReason || {});
class OtaDownloadError extends MatterError {
  constructor(code, message) {
    super(`OTA Download Error ${code}: ${message}`);
    this.code = code;
  }
  code;
  bytesTransferred = 0;
  totalBytesLength;
}
const schema = OtaSoftwareUpdateRequestorBehavior.schema.extend(
  {},
  FieldElement(
    { name: "activeOtaProviders", type: "list", quality: "N", conformance: "M" },
    FieldElement(
      { name: "entry", type: "struct" },
      FieldElement({ name: "location", type: "ProviderLocation", conformance: "M" }),
      FieldElement({ name: "previousQueryTimestamp", type: "uint64", quality: "X", conformance: "M" }),
      FieldElement({ name: "metadataForNode", type: "octstr", conformance: "O" })
    )
  ),
  FieldElement(
    { name: "updateInProgressDetails", type: "struct", quality: "NX", conformance: "M" },
    FieldElement({ name: "location", type: "ProviderLocation", conformance: "O" }),
    FieldElement({ name: "newSoftwareVersion", type: "uint32" })
  )
);
class OtaSoftwareUpdateRequestorServer extends OtaSoftwareUpdateRequestorBehavior {
  // Enhance the Schema to store the flag that we expect an upgrade to happen
  static schema = schema;
  async initialize() {
    if (this.state.updateState === OtaSoftwareUpdateRequestor.UpdateState.Unknown) {
      this.state.updateState = OtaSoftwareUpdateRequestor.UpdateState.Idle;
      this.state.updateStateProgress = null;
    }
    (await this.agent.load(DescriptorServer)).addDeviceTypes("OtaRequestor");
    this.reactTo(this.events.updatePossible$Changed, this.#enableOrDisableUpdates);
    this.reactTo(this.events.defaultOtaProviders$Changing, this.#assertDefaultOtaProviders);
    this.reactTo(this.events.defaultOtaProviders$Changed, this.#handleUpdatedDefaultOtaProviders);
    const node = Node.forEndpoint(this.endpoint);
    this.reactTo(node.lifecycle.online, this.#online);
    this.reactTo(node.lifecycle.goingOffline, this.#goingOffline);
    this.reactTo(node.lifecycle.commissioned, this.#scheduleInitialQuery);
  }
  async getDownloadLocation() {
    if (this.internal.downloadLocation === void 0) {
      if (this.state.downloadLocation === void 0) {
        const nodeStore = this.env.get(ServerNodeStore);
        const blobDriver = await nodeStore.bdxStore();
        const { productId, vendorId } = this.#basicInformationState();
        this.internal.downloadLocation = new PersistedFileDesignator(
          `ota-${vendorId}-${productId}`,
          blobDriver
        );
      } else {
        this.internal.downloadLocation = this.state.downloadLocation;
      }
    }
    return this.internal.downloadLocation;
  }
  /** When we come back online after an update, we notify that the update was applied */
  async #online() {
    if (this.state.activeOtaProviders.length === 0 && this.state.defaultOtaProviders.length > 0) {
      for (const provider of this.state.defaultOtaProviders) {
        await this.#addActiveOtaProvider(provider);
      }
      logger.debug(`No active OTA providers, adding ${this.state.activeOtaProviders.length} default providers`);
    }
    if (this.state.activeOtaProviders.length > 0) {
      this.#scheduleUpdateQuery(
        this.state.updateInProgressDetails === null ? Seconds(Math.floor(Math.random() * 599) + 120) : void 0
      );
    }
    if (this.state.updateInProgressDetails !== null) {
      await this.#handlePreviousUpdateOnStart(this.state.updateInProgressDetails);
      this.state.updateInProgressDetails = null;
    }
  }
  #goingOffline() {
    this.internal.updateQueryTimer?.stop();
  }
  #scheduleInitialQuery() {
    this.#scheduleUpdateQuery(Seconds(30), 4 /* Commissioned */);
  }
  async #handlePreviousUpdateOnStart({ newSoftwareVersion, location, updateToken }) {
    const { productId, softwareVersion } = this.#basicInformationState();
    if (newSoftwareVersion !== softwareVersion) {
      logger.warn(
        `The device restarted while an update was in progress, but new softwareVersion is not as expected (${newSoftwareVersion} != ${softwareVersion}).`
      );
      this.events.stateTransition.emit(
        {
          previousState: OtaSoftwareUpdateRequestor.UpdateState.Applying,
          newState: OtaSoftwareUpdateRequestor.UpdateState.Idle,
          reason: OtaSoftwareUpdateRequestor.ChangeReason.Failure,
          targetSoftwareVersion: null
        },
        this.context
      );
      this.#scheduleUpdateQuery(
        Seconds(Math.floor(Math.random() * 599) + 120),
        2 /* Busy */,
        // Handle failed update like a busy provider to retry
        location
      );
      return;
    }
    this.#emitVersionAppliedEvent(productId, softwareVersion);
    this.events.stateTransition.emit(
      {
        previousState: OtaSoftwareUpdateRequestor.UpdateState.Applying,
        newState: OtaSoftwareUpdateRequestor.UpdateState.Idle,
        reason: OtaSoftwareUpdateRequestor.ChangeReason.Success,
        targetSoftwareVersion: null
      },
      this.context
    );
    if (location === void 0 || updateToken === void 0) {
      return;
    }
    try {
      const ep = await this.#connectOtaProviderFor(location);
      await ep.commandsOf(OtaSoftwareUpdateProviderClient).notifyUpdateApplied({ softwareVersion: newSoftwareVersion, updateToken });
      logger.info(
        `Notified OTA Provider`,
        Diagnostic.dict(location),
        `that update to version ${newSoftwareVersion} was applied`
      );
    } catch (error) {
      MatterError.accept(error);
      logger.info(`Could not call notifyUpdateApplied on`, Diagnostic.dict(location), error);
    }
  }
  /** Enables or disables the update queries based on the new value of updatePossible */
  #enableOrDisableUpdates(newValue) {
    if (newValue) {
      if (!this.internal.updateQueryTimer?.isRunning || this.state.updateState === OtaSoftwareUpdateRequestor.UpdateState.Idle) {
        const seconds = Seconds(Math.floor(Math.random() * 599) + 60);
        logger.info(`Enabling OTA update queries because updatePossible was set to true, next try in`, seconds);
        this.#scheduleUpdateQuery(seconds);
      }
    } else {
      logger.info("Disabling OTA update queries because updatePossible was set to false.");
      this.internal.updateQueryTimer?.stop();
      this.internal.updateQueryTimer = void 0;
    }
  }
  /** Validate changes to defaultOtaProviders, ensure only one entry per FabricIndex */
  #assertDefaultOtaProviders(newValues) {
    const seen = /* @__PURE__ */ new Set();
    for (const { fabricIndex } of newValues) {
      if (seen.has(fabricIndex)) {
        throw new StatusResponse.ConstraintErrorError(
          `Only one defaultOtaProviders entry per FabricIndex ${fabricIndex} is allowed.`
        );
      }
      seen.add(fabricIndex);
    }
  }
  /** Handle changes to defaultOtaProviders list, ensure we add a changed entry to the list of active providers */
  #handleUpdatedDefaultOtaProviders(newValues) {
    const updatedProvider = newValues.find(({ fabricIndex }) => fabricIndex === this.context.fabric);
    if (updatedProvider !== void 0) {
      logger.info(
        `Fabric ${this.context.fabric} default OTA provider changed to`,
        Diagnostic.dict(updatedProvider)
      );
      this.#addActiveOtaProvider(updatedProvider).catch(
        (error) => logger.info("Adding default OTA provider failed", error)
      );
    }
    if (this.internal.updateQueryTimer === void 0 && this.state.activeOtaProviders.length > 0) {
      this.#scheduleUpdateQuery(Seconds(Math.floor(Math.random() * 599) + 60), 3 /* Announced */);
    }
  }
  /**
   * Default implementation for the announceOtaProvider command.
   * A Node announced itself as OTA Update Provider, so schedule an update check with it.
   * Depending on the announcementReason, we schedule the update query earlier with this provider or wait for the
   * next regular check.
   */
  async announceOtaProvider({
    providerNodeId,
    vendorId,
    announcementReason,
    metadataForNode,
    endpoint
  }) {
    const fabricIndex = this.context.fabric;
    if (!fabricIndex) {
      throw new StatusResponse.UnsupportedAccessError(
        "announceOtaProvider not allowed without an accessing fabric."
      );
    }
    const provider = { fabricIndex, providerNodeId, endpoint };
    await this.#addActiveOtaProvider(provider, metadataForNode);
    logger.info(
      `${announcementReason === OtaSoftwareUpdateRequestor.AnnouncementReason.UrgentUpdateAvailable ? "Urgent " : ""}OTA Provider announcement received:`,
      Diagnostic.dict({
        announcementReason: OtaSoftwareUpdateRequestor.AnnouncementReason[announcementReason],
        ...provider,
        vendorId
      })
    );
    if (announcementReason !== OtaSoftwareUpdateRequestor.AnnouncementReason.SimpleAnnouncement) {
      const delay = Seconds(Math.floor(Math.random() * 599) + 1);
      logger.info(`Scheduling urgent update query in`, delay);
      this.#scheduleUpdateQuery(delay, 3 /* Announced */, provider);
    } else {
      this.#scheduleUpdateQuery();
    }
    const peerAddress = PeerAddress({ nodeId: providerNodeId, fabricIndex });
    await Node.forEndpoint(this.endpoint).peers.forAddress(peerAddress);
  }
  /** Adds or updates an active OTA provider entry for a fabric index */
  async #addActiveOtaProvider(provider, metadataForNode) {
    const { fabricIndex, providerNodeId } = provider;
    const activeProviderIndex = this.state.activeOtaProviders.findIndex(
      (p) => p.location.fabricIndex === fabricIndex && p.location.providerNodeId === providerNodeId
    );
    const activeProviderEntry = {
      location: provider,
      previousQueryTimestamp: null,
      // We set to null to try this provider first next time
      metadataForNode
    };
    if (activeProviderIndex === -1) {
      this.state.activeOtaProviders.push(activeProviderEntry);
    } else {
      this.state.activeOtaProviders[activeProviderIndex] = activeProviderEntry;
    }
    const peerAddress = PeerAddress({ nodeId: providerNodeId, fabricIndex });
    await Node.forEndpoint(this.endpoint).peers.forAddress(peerAddress);
  }
  /** Removes an OTA provider from the active and default list */
  #removeOtaProvider(provider) {
    const { fabricIndex, providerNodeId } = provider;
    const activeProviderIndex = this.state.activeOtaProviders.findIndex(
      (p) => p.location.fabricIndex === fabricIndex && p.location.providerNodeId === providerNodeId
    );
    if (activeProviderIndex !== -1) {
      this.state.activeOtaProviders.splice(activeProviderIndex, 1);
    }
    const defaultProviderIndex = this.state.defaultOtaProviders.findIndex(
      (p) => p.fabricIndex === fabricIndex && p.providerNodeId === providerNodeId
    );
    if (defaultProviderIndex !== -1) {
      this.state.defaultOtaProviders.splice(defaultProviderIndex, 1);
    }
  }
  /**  Marks an OTA provider as active but with no update available (timestamp 0) */
  #markActiveOtaProviderNoUpdate(provider) {
    const { fabricIndex, providerNodeId } = provider;
    const activeProviderIndex = this.state.activeOtaProviders.findIndex(
      (p) => p.location.fabricIndex === fabricIndex && p.location.providerNodeId === providerNodeId
    );
    if (activeProviderIndex !== -1) {
      this.state.activeOtaProviders[activeProviderIndex].previousQueryTimestamp = Time.nowMs;
    } else {
      this.state.activeOtaProviders.push({
        location: provider,
        previousQueryTimestamp: Time.nowMs
      });
    }
  }
  /**
   * Schedule an update query after the given delay (or latest as defined as update interval if no delay is provided)
   * and optionally for a specific provider (otherwise the most recently seen active provider or the default provider
   * is used).
   */
  #scheduleUpdateQuery(delay, reason = 0 /* NewQuery */, provider) {
    if (provider !== void 0) {
      this.internal.selectedProviderLocation = provider;
      if (reason === 2 /* Busy */) {
        this.internal.providerRetryCount++;
      }
    } else {
      this.internal.providerRetryCount = 0;
    }
    if (this.internal.updateQueryTimer) {
      if (delay === void 0 && this.internal.updateQueryTimer.isRunning) {
        return;
      }
      this.internal.updateQueryTimer.stop();
      this.internal.updateQueryTimer = void 0;
    }
    if (!this.state.updatePossible || !this.state.activeOtaProviders.length) {
      return;
    }
    if (delay === void 0 && this.state.updateState !== OtaSoftwareUpdateRequestor.UpdateState.Idle) {
      logger.info(
        `Cannot schedule update query, current state is ${OtaSoftwareUpdateRequestor.UpdateState[this.state.updateState]}`
      );
      return;
    }
    if (delay === void 0) {
      delay = this.state.updateQueryInterval;
    }
    logger.info(`Scheduling OTA update query in ${delay} (reason "${ScheduleReason[reason]}")`);
    this.internal.updateQueryTimer = Time.getTimer(
      "OTA Request",
      delay,
      this.callback(this.#performUpdateQuery)
    ).start();
  }
  /** Choose the next OTA provider to use for an update */
  #chooseUpdateProvider() {
    if (this.internal.selectedProviderLocation !== void 0) {
      const selected = this.internal.selectedProviderLocation;
      logger.debug(`Choosing pre-selected provider`, Diagnostic.dict(selected));
      this.internal.selectedProviderLocation = void 0;
      return selected;
    }
    if (this.state.activeOtaProviders.length > 0) {
      const sorted = this.state.activeOtaProviders.sort(
        // Sort by oldest timestamp to try all over time
        (a, b) => (a.previousQueryTimestamp ?? 0) - (b.previousQueryTimestamp ?? 0)
      );
      const location = sorted[0].location;
      logger.debug(
        `Choosing active provider`,
        PeerAddress({ nodeId: location.providerNodeId, fabricIndex: location.fabricIndex }),
        `ep${location.endpoint}`
      );
      return location;
    }
    return;
  }
  /** Helper to connect to an OTA provider, initialize an OTA Provider Cluster Client, and return this endpoint */
  async #connectOtaProviderFor(location) {
    const { providerNodeId, fabricIndex, endpoint } = location;
    const peerAddress = PeerAddress({ nodeId: providerNodeId, fabricIndex });
    logger.debug(`Establish connection for OTA to ${peerAddress}`);
    const node = await Node.forEndpoint(this.endpoint).peers.forAddress(peerAddress);
    const ep = node.endpoints.require(endpoint);
    ep.behaviors.require(OtaSoftwareUpdateProviderClient);
    return ep;
  }
  /** Perform an actual update query */
  async #performUpdateQuery() {
    const downloadLocation = await this.getDownloadLocation();
    if (await downloadLocation.exists()) {
      let otaHeader;
      try {
        otaHeader = await this.validateUpdateFile();
      } catch (error) {
        logger.notice(`OTA update file is invalid:`, error);
        await downloadLocation.delete();
        otaHeader = void 0;
      }
      if (otaHeader !== void 0) {
        const { softwareVersion } = otaHeader;
        logger.info(`OTA update file is already downloaded and valid, applying version ${softwareVersion}.`);
        await this.#triggerApplyUpdate(softwareVersion, downloadLocation);
        return;
      }
    }
    this.state.updateStateProgress = null;
    const provider = this.#chooseUpdateProvider();
    if (provider === void 0) {
      logger.info("No OTA Provider configured, cannot query for updates.");
      this.internal.updateQueryTimer?.stop();
      this.internal.updateQueryTimer = void 0;
      return;
    }
    this.#updateState(
      OtaSoftwareUpdateRequestor.UpdateState.Querying,
      OtaSoftwareUpdateRequestor.ChangeReason.Success
    );
    try {
      await this.#queryOtaProvider(await this.#connectOtaProviderFor(provider), provider);
    } catch (error) {
      logger.warn(`OTA provider communication failed to`, Diagnostic.dict(provider), error);
      this.#markActiveOtaProviderNoUpdate(provider);
    }
    this.#resetStateToIdle();
  }
  /** Query the given OTA provider for an update and handle all non-UpdateAvailable results and error cases */
  async #updateAvailableFromProvider(query, ep, providerLocation) {
    const queryResponse = await ep.commandsOf(OtaSoftwareUpdateProviderClient).queryImage(query);
    const {
      status,
      delayedActionTime = 0,
      imageUri,
      softwareVersion,
      softwareVersionString,
      updateToken,
      userConsentNeeded
    } = queryResponse;
    switch (status) {
      case OtaSoftwareUpdateProvider.Status.Busy:
      case OtaSoftwareUpdateProvider.Status.NotAvailable:
        if (status === OtaSoftwareUpdateProvider.Status.Busy && this.internal.providerRetryCount <= MAX_BUSY_RETRIES) {
          this.#updateState(
            OtaSoftwareUpdateRequestor.UpdateState.DelayedOnQuery,
            OtaSoftwareUpdateRequestor.ChangeReason.DelayByProvider
          );
          this.#scheduleUpdateQuery(
            Seconds(Math.max(delayedActionTime, 120)),
            2 /* Busy */,
            providerLocation
          );
          return;
        }
        this.#markActiveOtaProviderNoUpdate(providerLocation);
        this.#resetStateToIdle();
        return;
      case OtaSoftwareUpdateProvider.Status.DownloadProtocolNotSupported:
        logger.info(
          `Removing OTA Provider because no supported download protocol was accepted`,
          Diagnostic.dict(providerLocation)
        );
        this.#removeOtaProvider(providerLocation);
        this.#resetStateToIdle();
        return;
    }
    if (imageUri === void 0 || softwareVersion === void 0 || softwareVersionString === void 0 || updateToken === void 0) {
      logger.info(`Invalid OTA Provider response: mandatory fields missing. Ignoring update.`);
      this.#markActiveOtaProviderNoUpdate(providerLocation);
      this.#resetStateToIdle(OtaSoftwareUpdateRequestor.ChangeReason.Failure);
      return;
    }
    if (imageUri.startsWith("bdx://")) {
      const { providerNodeId } = providerLocation;
      const { sourceNodeId } = FileDesignator.fromBdxUri(imageUri);
      if (sourceNodeId !== providerNodeId) {
        logger.info(
          `Invalid OTA Provider response: BDX URI source node ID ${NodeId.strOf(
            sourceNodeId
          )} does not match provider node ID ${NodeId.strOf(providerNodeId)}. Ignoring update.`
        );
        this.#markActiveOtaProviderNoUpdate(providerLocation);
        this.#resetStateToIdle(OtaSoftwareUpdateRequestor.ChangeReason.Failure);
        return;
      }
    }
    const { softwareVersion: currentSoftwareVersion } = query;
    if (softwareVersion <= currentSoftwareVersion) {
      logger.info(
        `Ignoring OTA Provider response: softwareVersion ${softwareVersion} not newer than current ${currentSoftwareVersion}`
      );
      this.#markActiveOtaProviderNoUpdate(providerLocation);
      this.#resetStateToIdle();
      return;
    }
    if (userConsentNeeded) {
      if (!this.state.canConsent) {
        logger.info(`OTA update requires user consent, but we cannot consent, ignoring update.`);
        this.#markActiveOtaProviderNoUpdate(providerLocation);
        this.#resetStateToIdle();
        return;
      }
      if (this.#basicInformationState().localConfigDisabled) {
        logger.info("OTA update requires user consent, but local config is disabled. Update cancelled.");
        this.#resetStateToIdle(OtaSoftwareUpdateRequestor.ChangeReason.Failure);
        return;
      }
    }
    return {
      imageUri,
      softwareVersion,
      softwareVersionString,
      updateToken,
      userConsentNeeded,
      delayedActionTime
    };
  }
  /**
   * Sends the ApplyUpdate command to the provider and handles the result, including any delays or retries
   */
  async #validateApplyUpdate(applyRequest, ep, fileDesignator) {
    this.state.updateStateProgress = null;
    this.#updateState(
      OtaSoftwareUpdateRequestor.UpdateState.Applying,
      OtaSoftwareUpdateRequestor.ChangeReason.Success,
      applyRequest.newVersion
    );
    let action;
    let applyDelayedActionTime;
    try {
      ({ action, delayedActionTime: applyDelayedActionTime } = await ep.commandsOf(OtaSoftwareUpdateProviderClient).applyUpdateRequest(applyRequest));
    } catch (error) {
      logger.info(`OTA Provider applyUpdateRequest failed:`, error);
      this.#resetStateToIdle(OtaSoftwareUpdateRequestor.ChangeReason.Failure);
      return false;
    }
    if (action === OtaSoftwareUpdateProvider.ApplyUpdateAction.Discontinue) {
      try {
        await fileDesignator.delete();
      } catch (error) {
        logger.warn(`OTA update file delete failed:`, error);
      }
      this.#resetStateToIdle();
      return false;
    } else if (action === OtaSoftwareUpdateProvider.ApplyUpdateAction.AwaitNextAction) {
      this.#updateState(
        OtaSoftwareUpdateRequestor.UpdateState.DelayedOnApply,
        OtaSoftwareUpdateRequestor.ChangeReason.DelayByProvider
      );
      this.internal.updateDelayPromise = Time.sleep(
        "OTAUpdateApply-AwaitNextAction",
        Millis(Math.min(Math.max(Seconds(applyDelayedActionTime), Minutes(2)), Hours(24)))
      );
      await this.internal.updateDelayPromise;
      this.internal.updateDelayPromise = void 0;
      return await this.#validateApplyUpdate(applyRequest, ep, fileDesignator);
    } else if (action !== OtaSoftwareUpdateProvider.ApplyUpdateAction.Proceed) {
      logger.notice(`Invalid OTA Provider applyUpdateRequest response: unknown Action ${action}`);
      this.#resetStateToIdle(OtaSoftwareUpdateRequestor.ChangeReason.Failure);
      return false;
    }
    if (applyDelayedActionTime > 0) {
      this.#updateState(
        OtaSoftwareUpdateRequestor.UpdateState.DelayedOnApply,
        OtaSoftwareUpdateRequestor.ChangeReason.DelayByProvider
      );
      this.internal.updateDelayPromise = Time.sleep(
        "OTAUpdateApply-DelayedByProvider",
        Millis(Math.min(Seconds(applyDelayedActionTime), Hours(24)))
      );
      await this.internal.updateDelayPromise;
      this.internal.updateDelayPromise = void 0;
    }
    return true;
  }
  /** Query the given OTA provider for an update and handle the result */
  async #queryOtaProvider(ep, providerLocation) {
    const { vendorId, productId, softwareVersion, hardwareVersion, location, localConfigDisabled } = this.#basicInformationState();
    const updateDetails = await this.#updateAvailableFromProvider(
      {
        vendorId,
        productId,
        softwareVersion,
        protocolsSupported: this.state.transferProtocolsSupported,
        hardwareVersion,
        location,
        requestorCanConsent: this.state.canConsent && !localConfigDisabled ? true : void 0
      },
      ep,
      providerLocation
    );
    if (updateDetails === void 0) {
      return;
    }
    const {
      imageUri,
      softwareVersion: newSoftwareVersion,
      softwareVersionString: newSoftwareVersionString,
      updateToken,
      userConsentNeeded,
      delayedActionTime
    } = updateDetails;
    const now = Time.nowMs;
    if (userConsentNeeded) {
      this.#updateState(
        OtaSoftwareUpdateRequestor.UpdateState.DelayedOnUserConsent,
        OtaSoftwareUpdateRequestor.ChangeReason.Success
      );
      try {
        if (!await this.requestUserConsent(newSoftwareVersion, newSoftwareVersionString)) {
          this.#markActiveOtaProviderNoUpdate(providerLocation);
          this.#resetStateToIdle();
          return;
        }
      } catch (error) {
        logger.warn(`Failed to request user consent:`, error);
        this.#markActiveOtaProviderNoUpdate(providerLocation);
        this.#resetStateToIdle(OtaSoftwareUpdateRequestor.ChangeReason.Failure);
      }
    }
    if (delayedActionTime > 0) {
      const delayS = now - Time.nowMs + Seconds(delayedActionTime);
      if (delayS > 0) {
        this.#updateState(
          OtaSoftwareUpdateRequestor.UpdateState.DelayedOnQuery,
          OtaSoftwareUpdateRequestor.ChangeReason.DelayByProvider
        );
        logger.info(`Waiting for ${delayS}s before applying update`);
        this.internal.updateDelayPromise = Time.sleep(
          "OTAUpdateQuery-DelayedByProvider",
          Millis(Math.min(delayS, Hours(24)))
        );
        await this.internal.updateDelayPromise;
        this.internal.updateDelayPromise = void 0;
      }
    }
    let fileDesignator;
    try {
      fileDesignator = await this.#handleDownload(ep, imageUri, newSoftwareVersion);
    } catch (error) {
      MatterError.accept(error);
      logger.notice(`OTA download failed:`, error);
      if (error instanceof OtaDownloadError) {
        this.#emitDownloadErrorEvent(newSoftwareVersion, error.bytesTransferred, error.code);
      }
      this.#resetStateToIdle(OtaSoftwareUpdateRequestor.ChangeReason.Failure);
      try {
        await (await this.getDownloadLocation()).delete();
      } catch (error2) {
        MatterError.accept(error2);
        logger.warn(`OTA download failed and deleting partial file also failed:`, error2);
      }
      return;
    }
    if (!await this.#validateApplyUpdate(
      {
        updateToken,
        newVersion: newSoftwareVersion
      },
      ep,
      fileDesignator
    )) {
      return;
    }
    await this.#triggerApplyUpdate(newSoftwareVersion, fileDesignator, providerLocation, updateToken);
  }
  async #handleBdxDownload(endpoint, fileDesignator) {
    const node = Node.forEndpoint(endpoint);
    let downloadTryCount = 0;
    while (true) {
      this.state.updateStateProgress = 0;
      try {
        const { context, slot } = await node.interaction.initBdx();
        const { messenger } = context;
        const bdx = BdxClient.asReceiver(messenger, {
          transferFileDesignator: fileDesignator,
          // We need to use the original designator here for transfer
          fileDesignator: await this.getDownloadLocation(),
          // But we store in a different location
          preferredDriverModes: [Flow.DriverMode.ReceiverDrive],
          maxBlockSize: messenger.channel.type === "tcp" ? OTA_BDX_MAX_BLOCK_SIZE_TCP : OTA_BDX_MAX_BLOCK_SIZE_NON_TCP
        });
        bdx.progressInfo.on(this.callback(this.#updateProgress, { lock: true }));
        bdx.progressFinished.on(
          (bytesTransferred) => logger.info(`OTA download finished after ${bytesTransferred} bytes`)
        );
        await bdx.processTransfer();
        slot?.close();
        await context[Symbol.asyncDispose]();
        break;
      } catch (error) {
        logger.info(`OTA BDX download attempt ${downloadTryCount + 1} failed:`, error);
        if (++downloadTryCount >= 3) {
          const code = error instanceof BdxError ? error.code : void 0;
          const bdxError = new OtaDownloadError(
            code ?? OtaSoftwareUpdateRequestor.UpdateState.Unknown,
            `OTA BDX download failed after ${downloadTryCount} attempts: ${error instanceof Error ? error.message : "Unknown error"}`
          );
          bdxError.cause = error;
          throw bdxError;
        }
      }
    }
  }
  async #handleHttpsDownload(uri) {
    const res = await fetch(uri);
    if (!res.ok || res.body === null) {
      throw new OtaDownloadError(res.status, `HTTP Download error: ${res.statusText}`);
    }
    const fetchStream = res.body;
    const contentLength = res.headers.get("content-length");
    let totalBytes = contentLength ? parseInt(contentLength) : void 0;
    if (totalBytes === void 0 || !Number.isFinite(totalBytes) || totalBytes <= 0) {
      totalBytes = void 0;
    }
    this.state.updateStateProgress = 0;
    let bytesReceived = 0;
    try {
      const progressStream = new ReadableStream({
        start: (controller) => {
          const reader = fetchStream.getReader();
          const read = () => {
            reader.read().then(({ done, value }) => {
              if (done) {
                controller.close();
                return;
              }
              bytesReceived += value?.length ?? 0;
              this.#updateProgress(bytesReceived, totalBytes);
              controller.enqueue(value);
              read();
            }).catch((error) => {
              controller.error(error);
            });
          };
          read();
        }
      });
      await (await this.getDownloadLocation()).writeFromStream(progressStream);
      logger.info(`OTA download finished after ${bytesReceived} bytes`);
    } catch (error) {
      const httpError = new OtaDownloadError(
        res.status,
        error instanceof Error ? error.message : "Unknown error"
      );
      httpError.cause = error;
      httpError.bytesTransferred = bytesReceived;
      httpError.totalBytesLength = totalBytes;
      throw httpError;
    }
  }
  #updateProgress(bytesDownloaded, totalBytes) {
    if (totalBytes !== void 0) {
      this.state.updateStateProgress = Math.floor(bytesDownloaded / totalBytes * 99);
    } else {
      this.state.updateStateProgress = Math.floor(99 * (1 - Math.exp(-bytesDownloaded / 1e6)));
    }
  }
  async #handleDownload(endpoint, uri, newSoftwareVersion) {
    if (uri.startsWith("bdx://")) {
      if (!this.state.transferProtocolsSupported.includes(
        OtaSoftwareUpdateProvider.DownloadProtocol.BdxSynchronous
      )) {
        throw new InternalError("BDX Synchronous not supported");
      }
      this.#updateState(
        OtaSoftwareUpdateRequestor.UpdateState.Downloading,
        OtaSoftwareUpdateRequestor.ChangeReason.Success,
        newSoftwareVersion
      );
      const { fileDesignator: transferFd } = FileDesignator.fromBdxUri(uri);
      await this.#handleBdxDownload(endpoint, transferFd);
    } else if (uri.startsWith("https://")) {
      if (!this.state.transferProtocolsSupported.includes(OtaSoftwareUpdateProvider.DownloadProtocol.Https)) {
        throw new ImplementationError("HTTPS not supported");
      }
      this.#updateState(
        OtaSoftwareUpdateRequestor.UpdateState.Downloading,
        OtaSoftwareUpdateRequestor.ChangeReason.Success,
        newSoftwareVersion
      );
      await this.#handleHttpsDownload(uri);
    } else {
      throw new InternalError(`Unsupported download URI: ${uri}`);
    }
    await this.validateUpdateFile(newSoftwareVersion);
    this.state.updateStateProgress = 100;
    return await this.getDownloadLocation();
  }
  /**
   * Validate the update file
   * The default implementation does basic validations based on the matter OTA file format and expected software
   * version.
   * The method SHALL be overridden to add additional checks like validating the signature of the file.
   * throws an error if the file is invalid
   */
  async validateUpdateFile(newSoftwareVersion) {
    let totalSize;
    try {
      const blob = await (await this.getDownloadLocation()).openBlob();
      totalSize = blob.size;
      const crypto = this.env.get(Crypto);
      const header = await OtaImageReader.file(blob.stream().getReader(), crypto);
      const { softwareVersion: otaFileSoftwareVersion } = header;
      if (newSoftwareVersion === void 0) {
        const { softwareVersion: currentSoftwareVersion } = this.#basicInformationState();
        if (otaFileSoftwareVersion <= currentSoftwareVersion) {
          throw new OtaImageError(
            `OTA file software version ${otaFileSoftwareVersion} not newer than current ${currentSoftwareVersion}`
          );
        }
      } else if (otaFileSoftwareVersion !== newSoftwareVersion) {
        throw new OtaImageError(
          `OTA file software version ${otaFileSoftwareVersion} does not match expected ${newSoftwareVersion}`
        );
      }
      return header;
    } catch (error) {
      const otaDownloadError = new OtaDownloadError(
        OtaSoftwareUpdateRequestor.UpdateState.Unknown,
        `Downloaded OTA file is invalid: ${error instanceof Error ? error.message : "Unknown error"}`
      );
      otaDownloadError.cause = error;
      if (totalSize !== void 0) {
        otaDownloadError.bytesTransferred = totalSize;
      }
      throw otaDownloadError;
    }
  }
  /**
   * Update the state and emit the stateTransition event if the state changed.
   * If the new state is Idle the next update query is scheduled.
   */
  #updateState(newState, reason, targetSoftwareVersion) {
    const previousState = this.state.updateState;
    if (newState !== OtaSoftwareUpdateRequestor.UpdateState.Downloading && newState !== OtaSoftwareUpdateRequestor.UpdateState.Applying && newState !== OtaSoftwareUpdateRequestor.UpdateState.RollingBack) {
      targetSoftwareVersion = void 0;
    } else if (targetSoftwareVersion === void 0) {
      throw new InternalError("targetSoftwareVersion must be provided for state " + newState);
    }
    if (newState !== previousState) {
      this.events.stateTransition.emit(
        {
          previousState,
          newState,
          reason,
          targetSoftwareVersion: targetSoftwareVersion ?? null
        },
        this.context
      );
      this.state.updateState = newState;
    }
    if (newState === OtaSoftwareUpdateRequestor.UpdateState.Idle || newState === OtaSoftwareUpdateRequestor.UpdateState.Unknown) {
      this.#scheduleUpdateQuery();
      this.state.updateStateProgress = null;
    } else if (this.state.updateStateProgress !== null) {
      this.state.updateStateProgress = 0;
    }
  }
  #resetStateToIdle(reason = OtaSoftwareUpdateRequestor.ChangeReason.Success) {
    this.#updateState(OtaSoftwareUpdateRequestor.UpdateState.Idle, reason);
  }
  #emitVersionAppliedEvent(productId, softwareVersion) {
    this.events.versionApplied.emit({ softwareVersion, productId }, this.context);
  }
  #basicInformationState() {
    const rootEndpoint = Node.forEndpoint(this.endpoint);
    if (!rootEndpoint.behaviors.has(BasicInformationBehavior)) {
      throw new InternalError("BasicInformationBehavior missing");
    }
    return rootEndpoint.stateOf(BasicInformationBehavior);
  }
  #emitDownloadErrorEvent(softwareVersion, bytesDownloaded, platformCode) {
    this.events.downloadError.emit(
      {
        softwareVersion,
        bytesDownloaded,
        progressPercent: this.state.updateStateProgress,
        platformCode: platformCode ?? null
      },
      this.context
    );
  }
  #triggerApplyUpdate(newSoftwareVersion, fileDesignator, location, updateToken) {
    this.state.updateInProgressDetails = {
      newSoftwareVersion,
      location,
      updateToken
    };
    this.#updateState(
      OtaSoftwareUpdateRequestor.UpdateState.Applying,
      OtaSoftwareUpdateRequestor.ChangeReason.Success,
      newSoftwareVersion
    );
    return this.applyUpdate(newSoftwareVersion, fileDesignator);
  }
  applyUpdate(_newSoftwareVersion, _fileDesignator) {
    throw new ImplementationError("To apply the update the applyUpdate() method must be implemented.");
  }
  requestUserConsent(_newSoftwareVersion, _newSoftwareVersionString) {
    throw new ImplementationError(
      "Seems 'canConsent' is set, but requestUserConsent() not implemented. Declining update"
    );
  }
  async [Symbol.asyncDispose]() {
    this.internal.updateQueryTimer?.stop();
    this.internal.updateDelayPromise?.cancel(new MatterError("Update Requestion cluster shuts down"));
    await super[Symbol.asyncDispose]?.();
  }
}
((OtaSoftwareUpdateRequestorServer2) => {
  class State extends OtaSoftwareUpdateRequestorBehavior.State {
    /**
     * The list of OTA providers that were recently active (by announcement or by being used).
     * The error counter is increased when a provider could not be reached or returned an unexpected error.
     * After 3 errors the provider is removed from this list and also from the defaultProviders list.
     * This value is persisted.
     */
    activeOtaProviders = [];
    /**
     * Details of an upgrade in progress that is checked on restart if the upgrade was successful.
     * This value is persisted.
     */
    updateInProgressDetails = null;
    /** How often to query for updates. Default is 24 hours ("daily") as proposed by spec. */
    updateQueryInterval = Hours(24);
    /**
     * The transfer protocols supported by this requestor. Default is BDX Synchronous.
     * If it was verified that the node really have access to the public internet, HTTPS can be added as protocol
     * as well.
     */
    transferProtocolsSupported = [OtaSoftwareUpdateProvider.DownloadProtocol.BdxSynchronous];
    /**
     * If true the requestor is able to get user consent for an update. This requires the implementation of the
     * requestUserConsent() extension interface method.
     * If false the OTA provider will only provide updates that do not require user consent or where user consent
     * was already given.
     * Default is false.
     */
    canConsent = false;
    /**
     * Optional custom persisted location as PersistedFileDescriptor to store the downloaded update files.
     * If not provided, the default storage context for "bdx" of the node is used with a filename like
     * "ota-{vendorId}-{productId}.update".
     * This can be used to store the files in a different persistent storage if needed.
     */
    downloadLocation;
  }
  OtaSoftwareUpdateRequestorServer2.State = State;
  class Events extends OtaSoftwareUpdateRequestorBehavior.Events {
    /** Emitted when a new software update file was downloaded and should now be applied. */
    updateReadyToApply = AsyncObservable();
  }
  OtaSoftwareUpdateRequestorServer2.Events = Events;
  class Internal {
    /** Timer for the next update query */
    updateQueryTimer;
    /**
     * The preferred provider to use for the next update query.
     * Mainly used for the Busy case to reuse the same provider for the next try.
     */
    selectedProviderLocation;
    /** How often we already retried to connect to the current provider */
    providerRetryCount = 0;
    /**
     * Stores the time of a current update delay to allow cancelling it
     */
    updateDelayPromise;
    /**
     * Persisted location as PersistedFileDescriptor to store the downloaded update files.
     * It is initialized from the state or with an internal default on startup.
     */
    downloadLocation;
  }
  OtaSoftwareUpdateRequestorServer2.Internal = Internal;
})(OtaSoftwareUpdateRequestorServer || (OtaSoftwareUpdateRequestorServer = {}));
export {
  OtaSoftwareUpdateRequestorServer
};
//# sourceMappingURL=OtaSoftwareUpdateRequestorServer.js.map
