/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  OtaUpdateStatus,
  SoftwareUpdateManager
} from "#behavior/system/software-update/SoftwareUpdateManager.js";
import { AccessControlServer } from "#behaviors/access-control";
import { DescriptorServer } from "#behaviors/descriptor";
import { Node } from "#node/Node.js";
import {
  Bytes,
  Crypto,
  Duration,
  Logger,
  MatterError,
  Minutes,
  Seconds,
  Time,
  Timestamp
} from "@matter/general";
import {
  assertRemoteActor,
  BdxProtocol,
  FabricAuthority,
  Flow,
  NodeSession,
  PeerAddress
} from "@matter/protocol";
import { AccessControl } from "@matter/types/clusters/access-control";
import { OtaSoftwareUpdateProvider } from "@matter/types/clusters/ota-software-update-provider";
import { OtaSoftwareUpdateProviderBehavior } from "./OtaSoftwareUpdateProviderBehavior.js";
const logger = Logger.get("OtaSoftwareUpdateProviderServer");
const OTA_UPDATE_TOKEN_LENGTH_BYTES = 32;
var OtaSoftwareUpdateConsentState = /* @__PURE__ */ ((OtaSoftwareUpdateConsentState2) => {
  OtaSoftwareUpdateConsentState2[OtaSoftwareUpdateConsentState2["Granted"] = 0] = "Granted";
  OtaSoftwareUpdateConsentState2[OtaSoftwareUpdateConsentState2["Denied"] = 1] = "Denied";
  OtaSoftwareUpdateConsentState2[OtaSoftwareUpdateConsentState2["Obtaining"] = 2] = "Obtaining";
  OtaSoftwareUpdateConsentState2[OtaSoftwareUpdateConsentState2["Unknown"] = 3] = "Unknown";
  return OtaSoftwareUpdateConsentState2;
})(OtaSoftwareUpdateConsentState || {});
class OtaSoftwareUpdateProviderServer extends OtaSoftwareUpdateProviderBehavior {
  async initialize() {
    (await this.agent.load(DescriptorServer)).addDeviceTypes("OtaProvider");
    this.agent.require(SoftwareUpdateManager);
    await this.agent.load(SoftwareUpdateManager);
    const node = Node.forEndpoint(this.endpoint);
    this.reactTo(node.lifecycle.online, this.#nodeOnline);
    if (node.lifecycle.isOnline) {
      await this.#nodeOnline();
    }
  }
  get updateStorage() {
    if (this.internal.updateStorage === void 0) {
      this.internal.updateStorage = this.agent.get(SoftwareUpdateManager).storage;
    }
    return this.internal.updateStorage;
  }
  async #nodeOnline() {
    const fabricAuthority = this.env.get(FabricAuthority);
    const ownFabric = fabricAuthority.fabrics[0];
    if (!ownFabric) {
      logger.error(`No owning fabric, delay initialization`);
      fabricAuthority.fabricAdded.once(() => this.#nodeOnline());
      return;
    }
    const node = Node.forEndpoint(this.endpoint);
    await node.act((agent) => agent.load(AccessControlServer));
    if (node.behaviors.has(AccessControlServer)) {
      if (!node.stateOf(AccessControlServer).acl.some(
        ({ fabricIndex, privilege, authMode, subjects, targets }) => fabricIndex === ownFabric.fabricIndex && privilege === AccessControl.AccessControlEntryPrivilege.Operate && authMode === AccessControl.AccessControlEntryAuthMode.Case && subjects?.length === 0 && targets?.length === 1 && targets[0].endpoint === this.endpoint.number && targets[0].cluster === OtaSoftwareUpdateProvider.Cluster.id
      )) {
        const acl = [
          ...node.stateOf(AccessControlServer).acl,
          {
            fabricIndex: ownFabric.fabricIndex,
            privilege: AccessControl.AccessControlEntryPrivilege.Operate,
            authMode: AccessControl.AccessControlEntryAuthMode.Case,
            subjects: [],
            targets: [{ endpoint: this.endpoint.number, cluster: OtaSoftwareUpdateProvider.Cluster.id }]
          }
        ];
        await node.setStateOf(AccessControlServer, { acl });
        logger.info(`Added ACL entry to allow access to OTA Software Update Provider for anyone in the fabric`);
      }
    }
  }
  /**
   * Default implementation of the QueryImage command according to Matter specification.
   */
  async queryImage(request) {
    const { protocolsSupported } = request;
    assertRemoteActor(this.context);
    const session = this.context.session;
    NodeSession.assert(session);
    const peerAddress = session.peerAddress;
    const updateDetails = await this.checkUpdateAvailable(request, peerAddress);
    if (updateDetails === void 0) {
      return {
        status: OtaSoftwareUpdateProvider.Status.NotAvailable
      };
    }
    const {
      fileDesignator,
      httpsUri,
      newSoftwareVersion,
      newSoftwareVersionString,
      consentRequired,
      metadataForRequestor
    } = updateDetails;
    const updateInProgress = this.#inProgressDetailsForPeer(peerAddress);
    if (updateInProgress !== void 0) {
      logger.info(
        `OTA Update for Requestor`,
        peerAddress,
        `already in progress (${OtaUpdateStatus[updateInProgress.lastState]})`
      );
      return {
        status: OtaSoftwareUpdateProvider.Status.Busy,
        delayedActionTime: Seconds.of(Minutes(5))
        // the usual bdx session timeout is 5 minutes, so let's use this
      };
    }
    const crypto = this.env.get(Crypto);
    const updateToken = crypto.randomBytes(OTA_UPDATE_TOKEN_LENGTH_BYTES);
    if (consentRequired && !request.requestorCanConsent) {
      this.#updateInProgressDetails(peerAddress, updateToken, OtaUpdateStatus.WaitForConsent, newSoftwareVersion);
      const { consentState, delayTime = Seconds(120) } = await this.requestUserConsentForUpdate(
        request,
        updateDetails,
        peerAddress
      );
      logger.info(
        `OTA Update for Requestor`,
        peerAddress,
        `(${Bytes.toHex(updateToken)}) consent is ${OtaSoftwareUpdateConsentState[consentState]}`
      );
      switch (consentState) {
        case 0 /* Granted */:
          this.#updateInProgressDetails(
            peerAddress,
            updateToken,
            OtaUpdateStatus.Querying,
            newSoftwareVersion,
            true
          );
          break;
        case 2 /* Obtaining */:
          return {
            status: OtaSoftwareUpdateProvider.Status.Busy,
            delayedActionTime: Seconds.of(delayTime)
          };
        case 1 /* Denied */:
        case 3 /* Unknown */:
        default:
          this.#removeInProgressDetails(peerAddress);
          return {
            status: OtaSoftwareUpdateProvider.Status.NotAvailable
          };
      }
    } else {
      this.#updateInProgressDetails(peerAddress, updateToken, OtaUpdateStatus.Querying, newSoftwareVersion);
    }
    let imageUri;
    if (protocolsSupported.includes(OtaSoftwareUpdateProvider.DownloadProtocol.BdxSynchronous)) {
      const bdxProtocol = this.env.get(BdxProtocol);
      if (!bdxProtocol.enablePeerForScope(peerAddress, this.updateStorage, {
        preferredDriverModes: [Flow.DriverMode.ReceiverDrive],
        // That's also the default but especially states for OTA, but let's set it explicitly
        messageTimeout: Minutes(5)
        // maxBlockSize 1024 (non-TCP), 8192 (TCP) - We support whatever the peer wants, so do not set that
      })) {
        logger.info("Could not enable Bdx");
        return {
          status: OtaSoftwareUpdateProvider.Status.Busy,
          delayedActionTime: Seconds.of(Minutes(5))
        };
      }
      const sessionListener = (bdxSession, scope) => {
        if (scope !== this.updateStorage.scope || !PeerAddress.is(bdxSession.peerAddress, peerAddress)) {
          return;
        }
        bdxProtocol.sessionStarted.off(sessionListener);
        if (this.#inProgressDetailsForPeer(peerAddress, updateToken) === void 0) {
          return;
        }
        this.#updateInProgressDetails(
          peerAddress,
          updateToken,
          OtaUpdateStatus.Downloading,
          newSoftwareVersion
        );
        bdxSession.progressFinished.on(
          () => this.#updateInProgressDetails(
            peerAddress,
            updateToken,
            OtaUpdateStatus.WaitForApply,
            newSoftwareVersion
          )
        );
        bdxSession.progressCancelled.on(
          () => this.#updateInProgressDetails(
            peerAddress,
            updateToken,
            OtaUpdateStatus.Cancelled,
            newSoftwareVersion
          )
        );
        bdxSession.closed.on(() => {
          const details2 = this.#inProgressDetailsForPeer(peerAddress, updateToken);
          if (details2 !== void 0 && details2.lastState !== OtaUpdateStatus.WaitForApply && details2.lastState !== OtaUpdateStatus.Applying && details2.lastState !== OtaUpdateStatus.Done && details2.lastState !== OtaUpdateStatus.Cancelled) {
            this.#updateInProgressDetails(
              peerAddress,
              updateToken,
              OtaUpdateStatus.Cancelled,
              newSoftwareVersion
            );
          }
        });
      };
      bdxProtocol.sessionStarted.on(sessionListener);
      const details = this.internal.inProgressDetails.get(
        `${peerAddress.nodeId}-${peerAddress.fabricIndex}-${Bytes.toHex(updateToken)}`
      );
      if (details !== void 0) {
        details.cleanup = () => bdxProtocol.sessionStarted.off(sessionListener);
      }
      imageUri = fileDesignator.asBdxUri(this.context.session.associatedFabric.rootNodeId);
    } else if (httpsUri !== void 0 && protocolsSupported.includes(OtaSoftwareUpdateProvider.DownloadProtocol.Https)) {
      imageUri = httpsUri;
    } else {
      return {
        status: OtaSoftwareUpdateProvider.Status.DownloadProtocolNotSupported
      };
    }
    return {
      status: OtaSoftwareUpdateProvider.Status.UpdateAvailable,
      imageUri,
      softwareVersion: newSoftwareVersion,
      softwareVersionString: newSoftwareVersionString,
      updateToken,
      userConsentNeeded: consentRequired === true ? true : void 0,
      metadataForRequestor
    };
  }
  /**
   * Default implementation of the ApplyUpdate command according to Matter specification.
   * We always allow updated to be executed immediately by the device.
   */
  async applyUpdateRequest({
    updateToken,
    newVersion
  }) {
    assertRemoteActor(this.context);
    const session = this.context.session;
    NodeSession.assert(session);
    const progressInfo = this.#inProgressDetailsForPeer(session.peerAddress, updateToken);
    const bdxProtocol = this.env.get(BdxProtocol);
    try {
      await bdxProtocol.disablePeerForScope(
        session.peerAddress,
        this.updateStorage,
        progressInfo?.lastState !== OtaUpdateStatus.WaitForApply
        // Force close when not known as completed
      );
    } catch (error) {
      MatterError.accept(error);
      logger.info("Error while closing BDX session for OTA update apply request, continuing anyway:", error);
    }
    if (!this.#hasUpdateConsent(session.peerAddress, newVersion)) {
      this.#updateInProgressDetails(session.peerAddress, updateToken, OtaUpdateStatus.Cancelled, newVersion);
      return {
        action: OtaSoftwareUpdateProvider.ApplyUpdateAction.Discontinue,
        delayedActionTime: Minutes(2)
      };
    }
    this.#updateInProgressDetails(session.peerAddress, updateToken, OtaUpdateStatus.Applying, newVersion);
    return {
      action: OtaSoftwareUpdateProvider.ApplyUpdateAction.Proceed,
      delayedActionTime: 0
      // Allow immediate update
    };
  }
  /**
   * Default implementation of the NotifyUpdateApplied command according to Matter specification.
   */
  notifyUpdateApplied({
    updateToken,
    softwareVersion
  }) {
    assertRemoteActor(this.context);
    const session = this.context.session;
    NodeSession.assert(session);
    this.#updateInProgressDetails(session.peerAddress, updateToken, OtaUpdateStatus.Done, softwareVersion);
  }
  /**
   * Extension method if the node wants to handle automatic user consent gathering itself. By default, it declines
   * the update request and informs the node of the available update.
   */
  requestUserConsentForUpdate(_request, _updateDetails, _peerAddress) {
    return { consentState: 3 /* Unknown */ };
  }
  /**
   * Override to customize how to check for available updates.
   * The default logic also validates the node details like vendorId and productId and the current version.
   * When the requestorCanConsent is true, we send the latest update we have also without a consent.
   * All additional data like hardware, location, and MetadataForProvider can be checked here for specific logic
   */
  checkUpdateAvailable(request, peerAddress) {
    return this.endpoint.act((agent) => agent.get(SoftwareUpdateManager).updateExistsFor(peerAddress, request));
  }
  #hasUpdateConsent(peerAddress, targetSoftwareVersion) {
    return !!this.#inProgressDetailsForPeer(peerAddress)?.directConsentObtained || this.endpoint.act((agent) => agent.get(SoftwareUpdateManager).hasConsent(peerAddress, targetSoftwareVersion));
  }
  /**
   * Retrieves the in-progress details for a specific peer based on the peer address and an optional update token.
   * Auto-removes entries older than 15 minutes to prevent stale entries from blocking future updates.
   */
  #inProgressDetailsForPeer(peerAddress, updateToken) {
    const { fabricIndex, nodeId: requestorNodeId } = peerAddress;
    const now = Time.nowMs;
    if (updateToken !== void 0) {
      const key = `${requestorNodeId}-${fabricIndex}-${Bytes.toHex(updateToken)}`;
      const details = this.internal.inProgressDetails.get(key);
      if (details !== void 0 && this.#removeIfStale(key, details, peerAddress, now)) {
        return void 0;
      }
      return details;
    }
    for (const [key, details] of this.internal.inProgressDetails.entries()) {
      if (details.requestorNodeId === requestorNodeId && details.fabricIndex === fabricIndex) {
        if (this.#removeIfStale(key, details, peerAddress, now)) {
          return void 0;
        }
        return details;
      }
    }
  }
  #removeIfStale(key, details, peerAddress, now) {
    if (details.timestamp + Minutes(15) >= now) {
      return false;
    }
    logger.info(
      `Removing stale in-progress OTA entry for Requestor`,
      peerAddress,
      `(age: ${Duration.format(Timestamp.delta(details.timestamp, now))})`
    );
    details.cleanup?.();
    this.internal.inProgressDetails.delete(key);
    return true;
  }
  /**
   * Removes all in-progress details entries for a specific peer address.
   */
  #removeInProgressDetails(peerAddress) {
    const { fabricIndex, nodeId: requestorNodeId } = peerAddress;
    for (const [key, details] of this.internal.inProgressDetails.entries()) {
      if (details.requestorNodeId === requestorNodeId && details.fabricIndex === fabricIndex) {
        details.cleanup?.();
        this.internal.inProgressDetails.delete(key);
      }
    }
  }
  /**
   * Updates the details of an in-progress OTA update process for a specific requestor,
   * tracking the update's state and optionally the target version to apply.
   */
  #updateInProgressDetails(peerAddress, updateToken, lastState, versionToApply, directConsentObtained = false) {
    const { fabricIndex, nodeId: requestorNodeId } = peerAddress;
    const key = `${requestorNodeId}-${fabricIndex}-${Bytes.toHex(updateToken)}`;
    const origDetails = this.internal.inProgressDetails.get(key);
    const previousState = origDetails?.lastState;
    const details = origDetails ?? {
      requestorNodeId,
      fabricIndex,
      lastState,
      timestamp: Time.nowMs,
      directConsentObtained
    };
    details.lastState = lastState;
    details.timestamp = Time.nowMs;
    if (versionToApply !== void 0) {
      if (details.versionToApply !== void 0 && details.versionToApply !== versionToApply) {
        logger.warn(
          `OTA Update for Requestor`,
          peerAddress.toString(),
          `(${Bytes.toHex(updateToken)}) versionToApply changed from ${details.versionToApply} to ${versionToApply}`
        );
      }
      details.versionToApply = versionToApply;
    }
    details.directConsentObtained = directConsentObtained;
    logger.info(
      `OTA Update ${details.versionToApply !== void 0 ? `to version ${details.versionToApply} ` : ""}for Requestor`,
      peerAddress.toString(),
      `(${Bytes.toHex(updateToken)}) is now ${OtaUpdateStatus[lastState]}${previousState === void 0 ? "" : ` (formerly ${OtaUpdateStatus[previousState]})`}`
    );
    this.internal.inProgressDetails.set(key, details);
    this.endpoint.act(
      (agent) => agent.get(SoftwareUpdateManager).onOtaStatusChange(peerAddress, lastState, details.versionToApply)
    );
    if (lastState === OtaUpdateStatus.Done || lastState === OtaUpdateStatus.Cancelled) {
      details.cleanup?.();
      this.internal.inProgressDetails.delete(key);
    }
  }
}
((OtaSoftwareUpdateProviderServer2) => {
  class Internal {
    /** Keyed by the requestorNodeId+fabricIndex+updateToken */
    inProgressDetails = /* @__PURE__ */ new Map();
    updateStorage;
  }
  OtaSoftwareUpdateProviderServer2.Internal = Internal;
})(OtaSoftwareUpdateProviderServer || (OtaSoftwareUpdateProviderServer = {}));
export {
  OtaSoftwareUpdateConsentState,
  OtaSoftwareUpdateProviderServer
};
//# sourceMappingURL=OtaSoftwareUpdateProviderServer.js.map
