/**
 * @license
 * Copyright 2022-2025 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { NodeActivity } from "#behavior/context/NodeActivity.js";
import { RemoteActorContext } from "#behavior/context/server/RemoteActorContext.js";
import {
  Duration,
  Hours,
  Logger,
  MatterError,
  Millis,
  Minutes,
  NetworkError,
  NoResponseTimeoutError,
  ObserverGroup,
  Seconds,
  Time,
  Timestamp
} from "#general";
import { Specification } from "#model";
import {
  AttributeReadResponse,
  AttributeSubscriptionResponse,
  EventReadResponse,
  InteractionServerMessenger,
  NoChannelError,
  Read,
  Subscription
} from "#protocol";
import {
  ClusterId,
  EndpointNumber,
  EventNumber,
  INTERACTION_PROTOCOL_ID,
  StatusCode,
  StatusResponseError
} from "#types";
const logger = Logger.get("ServerSubscription");
const MAX_INTERVAL_PUBLISHER_LIMIT = Hours.one;
const INTERNAL_INTERVAL_PUBLISHER_LIMIT = Minutes(3);
const MIN_INTERVAL = Seconds(2);
const DEFAULT_RANDOMIZATION_WINDOW = Seconds(10);
var ServerSubscriptionConfig;
((ServerSubscriptionConfig2) => {
  function of(options) {
    return {
      maxInterval: options?.maxInterval ?? INTERNAL_INTERVAL_PUBLISHER_LIMIT,
      minInterval: Duration.max(options?.minInterval ?? MIN_INTERVAL, MIN_INTERVAL),
      randomizationWindow: options?.randomizationWindow ?? DEFAULT_RANDOMIZATION_WINDOW
    };
  }
  ServerSubscriptionConfig2.of = of;
})(ServerSubscriptionConfig || (ServerSubscriptionConfig = {}));
class ServerSubscription extends Subscription {
  #context;
  #lastUpdateTime = Timestamp.zero;
  #updateTimer;
  #sendDelayTimer = Time.getTimer(
    `Subscription ${this.id} delay`,
    Millis(50),
    () => this.#triggerSendUpdate()
  );
  #outstandingAttributeUpdates;
  #outstandingEventsMinNumber;
  #changeHandlers = new ObserverGroup();
  #sendUpdatesActivated = false;
  #seededClusterDetails = /* @__PURE__ */ new Map();
  #latestSeededEventNumber = EventNumber(0);
  #sendInterval;
  #minIntervalFloor;
  #maxIntervalCeiling;
  #peerAddress;
  #sendNextUpdateImmediately = false;
  #sendUpdateErrorCounter = 0;
  #currentUpdatePromise;
  constructor(options) {
    const {
      id,
      context,
      criteria,
      minIntervalFloor,
      maxIntervalCeiling,
      subscriptionOptions,
      useAsMaxInterval,
      useAsSendInterval
    } = options;
    super(context.session, id, criteria);
    this.#context = context;
    this.#peerAddress = this.session.peerAddress;
    this.#minIntervalFloor = minIntervalFloor;
    this.#maxIntervalCeiling = maxIntervalCeiling;
    let maxInterval;
    let sendInterval;
    if (useAsMaxInterval !== void 0 && useAsSendInterval !== void 0) {
      maxInterval = useAsMaxInterval;
      sendInterval = useAsSendInterval;
    } else {
      ({ maxInterval, sendInterval } = this.#determineSendingIntervals(
        subscriptionOptions.minInterval,
        subscriptionOptions.maxInterval,
        subscriptionOptions.randomizationWindow
      ));
    }
    this.maxInterval = maxInterval;
    this.#sendInterval = sendInterval;
    this.#updateTimer = Time.getTimer(
      `Subscription ${this.id} update`,
      this.#sendInterval,
      () => this.#prepareDataUpdate()
    );
  }
  #determineSendingIntervals(subscriptionMinInterval, subscriptionMaxInterval, subscriptionRandomizationWindow) {
    const maxInterval = Duration.min(
      Millis.floor(
        Millis(
          Duration.max(
            subscriptionMinInterval,
            Duration.max(
              this.#minIntervalFloor,
              Duration.min(subscriptionMaxInterval, this.#maxIntervalCeiling)
            )
          ) + subscriptionRandomizationWindow * Math.random()
        )
      ),
      MAX_INTERVAL_PUBLISHER_LIMIT
    );
    let sendInterval = Millis.floor(Millis(maxInterval / 2));
    if (sendInterval < Minutes.one) {
      sendInterval = Duration.max(this.#minIntervalFloor, Millis.floor(Millis(maxInterval * 0.8)));
    }
    if (sendInterval < subscriptionMinInterval) {
      logger.warn(
        `Determined subscription send interval of ${Duration.format(sendInterval)} is too low. Using maxInterval (${Duration.format(maxInterval)}) instead.`
      );
      sendInterval = subscriptionMinInterval;
    }
    return { maxInterval, sendInterval };
  }
  #addOutstandingAttributes(endpointId, clusterId, changedAttrs) {
    if (!changedAttrs.length) {
      return;
    }
    this.#outstandingAttributeUpdates = this.#outstandingAttributeUpdates ?? {};
    this.#outstandingAttributeUpdates[endpointId] = this.#outstandingAttributeUpdates[endpointId] ?? {};
    this.#outstandingAttributeUpdates[endpointId][clusterId] = this.#outstandingAttributeUpdates[endpointId][clusterId] ?? /* @__PURE__ */ new Set();
    for (const attributeId of changedAttrs) {
      this.#outstandingAttributeUpdates[endpointId][clusterId].add(attributeId);
    }
  }
  #handleClusterStateChanges(endpointId, clusterId, changedAttrs, version) {
    if (this.isClosed || !changedAttrs.length) {
      return;
    }
    if (this.#seededClusterDetails !== void 0) {
      const seededVersion = this.#seededClusterDetails.get(`${endpointId}-${clusterId}`);
      if (seededVersion === void 0 || seededVersion === version) {
        return;
      }
    }
    this.#addOutstandingAttributes(endpointId, clusterId, changedAttrs);
    this.#prepareDataUpdate();
  }
  #handleAddedEvents(occurrence) {
    if (this.isClosed) {
      return;
    }
    if (this.#outstandingEventsMinNumber === void 0 || this.#latestSeededEventNumber !== void 0) {
      this.#outstandingEventsMinNumber = occurrence.number;
    }
    if (this.#sendEventUrgently(occurrence)) {
      this.#prepareDataUpdate();
    }
  }
  #sendEventUrgently({ endpointId, clusterId, eventId }) {
    return (this.criteria.eventRequests ?? []).some(
      ({ endpointId: reqEndpointId, clusterId: reqClusterId, eventId: reqEventId, isUrgent }) => isUrgent && (reqEndpointId === void 0 || reqEndpointId === endpointId) && (reqClusterId === void 0 || reqClusterId === clusterId) && (reqEventId === void 0 || reqEventId === eventId)
    );
  }
  get sendInterval() {
    return this.#sendInterval;
  }
  get minIntervalFloor() {
    return this.#minIntervalFloor;
  }
  get maxIntervalCeiling() {
    return this.#maxIntervalCeiling;
  }
  activate() {
    super.activate();
    if (this.criteria.eventFilters !== void 0) this.criteria.eventFilters.length = 0;
    if (this.criteria.dataVersionFilters !== void 0) this.criteria.dataVersionFilters.length = 0;
    this.#sendUpdatesActivated = true;
    if (this.#outstandingEventsMinNumber !== void 0 && this.#latestSeededEventNumber !== void 0) {
      if (this.#latestSeededEventNumber < this.#outstandingEventsMinNumber) {
        this.#outstandingEventsMinNumber = EventNumber(BigInt(this.#latestSeededEventNumber) + BigInt(1));
      } else {
        this.#outstandingEventsMinNumber = void 0;
      }
    }
    this.#latestSeededEventNumber = void 0;
    this.#seededClusterDetails = void 0;
    if (this.#outstandingAttributeUpdates !== void 0 || this.#outstandingEventsMinNumber !== void 0) {
      this.#triggerSendUpdate();
    }
    this.#updateTimer = Time.getTimer(
      "Subscription update",
      this.#sendInterval,
      () => this.#prepareDataUpdate()
    ).start();
  }
  /**
   * Check if data should be sent straight away or delayed because the minimum interval is not reached. Delay real
   * sending by 50ms in any case to make sure to catch all updates.
   */
  #prepareDataUpdate() {
    if (this.#sendDelayTimer.isRunning || this.isClosed) {
      return;
    }
    if (!this.#sendUpdatesActivated) {
      return;
    }
    this.#updateTimer.stop();
    const now = Time.nowMs;
    const timeSinceLastUpdate = Millis(now - this.#lastUpdateTime);
    if (timeSinceLastUpdate < this.#minIntervalFloor) {
      this.#updateTimer = Time.getTimer(
        "Subscription update",
        Millis(this.#minIntervalFloor - timeSinceLastUpdate),
        () => this.#prepareDataUpdate()
      ).start();
      return;
    }
    this.#sendDelayTimer.start();
    this.#updateTimer = Time.getTimer(
      `Subscription update ${this.id}`,
      this.#sendInterval,
      () => this.#prepareDataUpdate()
    ).start();
  }
  #triggerSendUpdate(onlyWithData = false) {
    if (this.#currentUpdatePromise !== void 0) {
      logger.debug("Sending update already in progress, delaying update ...");
      this.#sendNextUpdateImmediately = true;
      return;
    }
    this.#currentUpdatePromise = this.#sendUpdate(onlyWithData).catch((error) => logger.warn("Sending subscription update failed:", error)).finally(() => this.#currentUpdatePromise = void 0);
  }
  /**
   * Determine all attributes that have changed since the last update and send them out to the subscriber.
   */
  async #sendUpdate(onlyWithData = false) {
    const attributeFilter = this.#outstandingAttributeUpdates;
    this.#outstandingAttributeUpdates = void 0;
    const eventsMinNumber = this.#outstandingEventsMinNumber;
    this.#outstandingEventsMinNumber = void 0;
    if (onlyWithData && attributeFilter === void 0 && eventsMinNumber === void 0) {
      return;
    }
    this.#lastUpdateTime = Time.nowMs;
    try {
      if (await this.#sendUpdateMessage(attributeFilter, eventsMinNumber, onlyWithData)) {
        this.#sendUpdateErrorCounter = 0;
      }
    } catch (error) {
      if (this.isClosed) {
        return;
      }
      this.#sendUpdateErrorCounter++;
      logger.info(
        `Error sending subscription update message (error count=${this.#sendUpdateErrorCounter}):`,
        error instanceof MatterError && error.message || error
      );
      if (this.#sendUpdateErrorCounter <= 2) {
        if (attributeFilter !== void 0) {
          for (const [endpointId, clusters] of Object.entries(attributeFilter)) {
            for (const [clusterId, attributes] of Object.entries(clusters)) {
              this.#addOutstandingAttributes(
                EndpointNumber(parseInt(endpointId)),
                ClusterId(parseInt(clusterId)),
                Array.from(attributes)
              );
            }
          }
        }
        if (eventsMinNumber !== void 0) {
          this.#outstandingEventsMinNumber = eventsMinNumber;
        }
      } else {
        logger.info(
          `Sending update failed 3 times in a row, canceling subscription ${this.id} and let controller subscribe again.`
        );
        this.#sendNextUpdateImmediately = false;
        if (error instanceof NoResponseTimeoutError || error instanceof NetworkError || error instanceof NoChannelError) {
          this.isCanceledByPeer = true;
          await this.destroy();
          return;
        } else {
          throw error;
        }
      }
    }
    if (this.#sendNextUpdateImmediately) {
      logger.debug("Sending delayed update immediately after last one was sent.");
      this.#sendNextUpdateImmediately = false;
      await this.#sendUpdate(true);
    }
  }
  /**
   * Returns an iterator that yields the data reports and events data for the given read request.
   */
  async *#processAttributesAndEventsReport(context, suppressStatusReports = false) {
    const request = {
      ...this.criteria,
      interactionModelRevision: Specification.INTERACTION_MODEL_REVISION
      // irrelevant here, set to our version
    };
    const delayedStatusReports = new Array();
    let hasValuesInResponse = false;
    let validAttributes = 0;
    let validEvents = 0;
    const session = RemoteActorContext(context).beginReadOnly();
    try {
      if (Read.containsAttribute(request)) {
        const attributeReader = new AttributeReadResponse(this.#context.node.protocol, session);
        for (const chunk of attributeReader.process(request)) {
          for (const report of chunk) {
            if (report.kind === "attr-status") {
              if (suppressStatusReports) {
                continue;
              }
              if (!hasValuesInResponse) {
                delayedStatusReports.push(report);
                continue;
              }
            } else if (!hasValuesInResponse && report.kind === "attr-value") {
              for (const delayedReport of delayedStatusReports) {
                yield InteractionServerMessenger.convertServerInteractionReport(delayedReport);
              }
              delayedStatusReports.length = 0;
              hasValuesInResponse = true;
            }
            if (this.#seededClusterDetails !== void 0 && report.kind === "attr-value") {
              const {
                path: { endpointId, clusterId },
                version
              } = report;
              this.#seededClusterDetails.set(`${endpointId}-${clusterId}`, version);
            }
            yield InteractionServerMessenger.convertServerInteractionReport(report);
          }
        }
        validAttributes = attributeReader.counts.existent;
      }
      if (Read.containsEvent(request)) {
        const eventReader = new EventReadResponse(this.#context.node.protocol, session);
        for await (const chunk of eventReader.process(request)) {
          for (const report of chunk) {
            if (report.kind === "event-status") {
              if (suppressStatusReports) {
                continue;
              }
              if (!hasValuesInResponse) {
                delayedStatusReports.push(report);
                continue;
              }
            } else if (!hasValuesInResponse && report.kind === "event-value") {
              for (const delayedReport of delayedStatusReports) {
                yield InteractionServerMessenger.convertServerInteractionReport(delayedReport);
              }
              delayedStatusReports.length = 0;
              hasValuesInResponse = true;
            }
            if (this.#latestSeededEventNumber !== void 0 && report.kind === "event-value") {
              this.#latestSeededEventNumber = report.number;
            }
            yield InteractionServerMessenger.convertServerInteractionReport(report);
          }
        }
        validEvents = eventReader.counts.existent;
      }
      if (validAttributes === 0 && validEvents === 0) {
        throw new StatusResponseError(
          "Subscription failed because no attributes or events are matching the query",
          StatusCode.InvalidAction
        );
      } else if (!hasValuesInResponse && delayedStatusReports.length) {
        for (const delayedReport of delayedStatusReports) {
          yield InteractionServerMessenger.convertServerInteractionReport(delayedReport);
        }
      }
    } finally {
      session[Symbol.dispose]();
    }
    this.#lastUpdateTime = Time.nowMs;
  }
  async sendInitialReport(messenger, readContext, suppressStatusReports) {
    this.#updateTimer.stop();
    if (this.criteria.attributeRequests?.length) {
      this.#changeHandlers.on(
        this.#context.node.protocol.attrsChanged,
        this.#handleClusterStateChanges.bind(this)
      );
    }
    if (this.criteria.eventRequests?.length) {
      this.#changeHandlers.on(this.#context.node.protocol.eventHandler.added, this.#handleAddedEvents.bind(this));
    }
    await messenger.sendDataReport({
      baseDataReport: {
        suppressResponse: false,
        // we always need proper response for initial report
        subscriptionId: this.id,
        interactionModelRevision: Specification.INTERACTION_MODEL_REVISION
      },
      forFabricFilteredRead: this.criteria.isFabricFiltered,
      payload: this.#processAttributesAndEventsReport(readContext, suppressStatusReports)
    });
  }
  async #flush() {
    this.#sendDelayTimer.stop();
    if (this.#outstandingAttributeUpdates !== void 0 || this.#outstandingEventsMinNumber !== void 0) {
      logger.debug(`Flushing subscription ${this.id}${this.isClosed ? " (for closing)" : ""}`);
      this.#triggerSendUpdate(true);
      if (this.#currentUpdatePromise) {
        await this.#currentUpdatePromise;
      }
    }
  }
  async destroy() {
    this.#sendUpdatesActivated = false;
    this.#changeHandlers.close();
    this.#updateTimer.stop();
    this.#sendDelayTimer.stop();
    await super.destroy();
  }
  /**
   * Closes the subscription and flushes all outstanding data updates if requested.
   */
  async close(flush = false, cancelledByPeer = false) {
    if (this.isClosed) {
      return;
    }
    if (cancelledByPeer) {
      this.isCanceledByPeer = true;
    }
    await this.destroy();
    if (flush) {
      await this.#flush();
    }
    if (this.#currentUpdatePromise) {
      await this.#currentUpdatePromise;
    }
  }
  /**
   * Iterates over all attributes and events that have changed since the last update and sends them to
   * the controller.
   * A thrown exception will cancel the sending process immediately.
   */
  async *#iterateDataUpdate(exchange, attributeFilter, eventsMinNumber) {
    const request = {
      ...this.criteria,
      interactionModelRevision: Specification.INTERACTION_MODEL_REVISION
      // irrelevant here, set to our version
    };
    const session = RemoteActorContext({
      activity: exchange[NodeActivity.activityKey],
      fabricFiltered: request.isFabricFiltered,
      message: {},
      exchange,
      node: this.#context.node
    }).beginReadOnly();
    try {
      if (attributeFilter !== void 0 && Read.containsAttribute(request)) {
        const attributeReader = new AttributeSubscriptionResponse(
          this.#context.node.protocol,
          session,
          attributeFilter
        );
        for (const chunk of attributeReader.process(request)) {
          for (const report of chunk) {
            yield InteractionServerMessenger.convertServerInteractionReport(report);
          }
        }
      }
      if (eventsMinNumber !== void 0 && Read.containsEvent(request)) {
        request.eventFilters = [{ eventMin: eventsMinNumber }];
        const eventReader = new EventReadResponse(this.#context.node.protocol, session);
        for await (const chunk of eventReader.process(request)) {
          for (const report of chunk) {
            if (report.kind === "event-status") {
              continue;
            }
            yield InteractionServerMessenger.convertServerInteractionReport(report);
          }
        }
      }
    } finally {
      session[Symbol.dispose]();
    }
  }
  async #sendUpdateMessage(attributeFilter, eventsMinNumber, onlyWithData) {
    const exchange = this.#context.initiateExchange(this.#peerAddress, INTERACTION_PROTOCOL_ID);
    if (exchange === void 0) return false;
    const messenger = new InteractionServerMessenger(exchange);
    try {
      if (attributeFilter === void 0 && eventsMinNumber === void 0) {
        await messenger.sendDataReport({
          baseDataReport: {
            suppressResponse: true,
            // suppressResponse true for empty DataReports
            subscriptionId: this.id,
            interactionModelRevision: Specification.INTERACTION_MODEL_REVISION
          },
          forFabricFilteredRead: this.criteria.isFabricFiltered,
          waitForAck: !this.isClosed
          // Do not wait for ack when closed
        });
      } else {
        await messenger.sendDataReport({
          baseDataReport: {
            suppressResponse: false,
            // Non-empty data reports always need to send response
            subscriptionId: this.id,
            interactionModelRevision: Specification.INTERACTION_MODEL_REVISION
          },
          forFabricFilteredRead: this.criteria.isFabricFiltered,
          payload: this.#iterateDataUpdate(exchange, attributeFilter, eventsMinNumber),
          waitForAck: !this.isClosed,
          // Do not wait for ack when closed
          suppressEmptyReport: onlyWithData
        });
      }
    } catch (error) {
      if (StatusResponseError.is(error, StatusCode.InvalidSubscription, StatusCode.Failure)) {
        logger.info(`Subscription ${this.id} cancelled by peer.`);
        this.isCanceledByPeer = true;
        await this.close(false);
      } else {
        StatusResponseError.accept(error);
        logger.info(`Subscription ${this.id} update failed:`, error);
        await this.close(false);
      }
    } finally {
      await messenger.close();
    }
    return true;
  }
}
export {
  DEFAULT_RANDOMIZATION_WINDOW,
  INTERNAL_INTERVAL_PUBLISHER_LIMIT,
  MAX_INTERVAL_PUBLISHER_LIMIT,
  MIN_INTERVAL,
  ServerSubscription,
  ServerSubscriptionConfig
};
//# sourceMappingURL=ServerSubscription.js.map
