"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
var __typeError = (msg) => {
  throw TypeError(msg);
};
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __using = (stack, value, async) => {
  if (value != null) {
    if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
    var dispose, inner;
    if (async) dispose = value[__knownSymbol("asyncDispose")];
    if (dispose === void 0) {
      dispose = value[__knownSymbol("dispose")];
      if (async) inner = dispose;
    }
    if (typeof dispose !== "function") __typeError("Object not disposable");
    if (inner) dispose = function() {
      try {
        inner.call(this);
      } catch (e) {
        return Promise.reject(e);
      }
    };
    stack.push([async, dispose, value]);
  } else if (async) {
    stack.push([async]);
  }
  return value;
};
var __callDispose = (stack, error, hasError) => {
  var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
    return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
  };
  var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
  var next = (it) => {
    while (it = stack.pop()) {
      try {
        var result = it[1] && it[1].call(it[2]);
        if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
      } catch (e) {
        fail(e);
      }
    }
    if (hasError) throw error;
  };
  return next();
};
var ClientNodeInteraction_exports = {};
__export(ClientNodeInteraction_exports, {
  ClientNodeInteraction: () => ClientNodeInteraction
});
module.exports = __toCommonJS(ClientNodeInteraction_exports);
var import_IcdPeerAsleepError = require("#behavior/system/icd/IcdPeerAsleepError.js");
var import_NetworkClient = require("#behavior/system/network/NetworkClient.js");
var import_icd_management = require("#behaviors/icd-management");
var import_EndpointInitializer = require("#endpoint/properties/EndpointInitializer.js");
var import_general = require("@matter/general");
var import_protocol = require("@matter/protocol");
var import_ClientNodePhysicalProperties = require("./ClientNodePhysicalProperties.js");
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_general.Logger.get("ClientNodeInteraction");
class ClientNodeInteraction {
  #node;
  #observers = new import_general.ObserverGroup();
  #interactable;
  #interactableClosed;
  #icd;
  #icdPeerNodeId;
  constructor(node) {
    this.#node = node;
    this.#observers.on(this.#node.events.commissioning.peerAddress$Changed, () => {
      this.#icd = void 0;
      this.#icdPeerNodeId = void 0;
      this.#closeInteraction(new import_protocol.OperationalAddressChangedError());
    });
    this.#observers.on(this.#node.owner?.lifecycle.goingOffline, () => this.#closeInteraction(new import_protocol.ShutdownError()));
  }
  async close() {
    this.#observers.close();
    this.#closeInteraction(new import_protocol.ShutdownError());
    await this.#interactableClosed;
  }
  /**
   * Read chosen attributes remotely from the node. Known data versions are automatically injected into the request to
   * optimize the read when the fabric filter matches the active subscription. Set `includeKnownVersions` in the
   * request to skip version injection and always receive a full response from the server.
   */
  async *read(request, context) {
    const { wakefulness, useIcdLit } = this.#awaitModeIcdRouting(request);
    if (useIcdLit) {
      request = { ...request, network: "icdLit" };
    }
    const hold = this.#holdUntilAwake(wakefulness, request.icdAwaitTimeout);
    if (hold !== void 0) {
      await hold;
    }
    if (!request.includeKnownVersions && (request.isFabricFiltered ?? true) === this.#structure.subscribedFabricFiltered) {
      request = this.#structure.injectVersionFilters(request);
    }
    const response = this.#interaction.read(request, context);
    yield* this.#structure.mutate(request, response);
  }
  /**
   * Subscribe to remote events and attributes as defined by {@link request}.
   *
   * By default, matter.js subscribes to all attributes and events of the peer and updates {@link ClientNode} state
   * automatically.  So you normally do not need to subscribe manually.
   *
   * When providing the "sustain" flag, a SustainedSubscription is returned immediately. You need to use the events to
   * know when/if a subscription could be established.  This class handles reconnections automatically.
   * When not providing the "sustain" flag, a PeerSubscription is returned after a subscription have been successfully
   * established; or an error is returned if this was not possible.
   */
  async subscribe(request, context) {
    const intermediateRequest = {
      ...this.#structure.injectVersionFilters(request),
      ...import_protocol.PhysicalDeviceProperties.subscriptionIntervalBoundsFor({
        description: this.#node.toString(),
        properties: (0, import_ClientNodePhysicalProperties.ClientNodePhysicalProperties)(this.#node),
        request
      }),
      network: request.network,
      sustain: !!request.sustain,
      updated: async (data) => {
        const result = this.#structure.mutate(request, data);
        if (request.updated) {
          await request.updated(result);
        } else {
          for await (const _chunk of result) ;
        }
      },
      refreshRequest: (request2) => {
        const updated = {
          ...request2,
          dataVersionFilters: void 0,
          eventFilters: [{ eventMin: this.#node.stateOf(import_NetworkClient.NetworkClient).maxEventNumber + 1n }]
        };
        return this.#structure.injectVersionFilters(updated);
      },
      closed: request.closed?.bind(request),
      // Resolved live so a peer registered after subscribe, or flipped SIT⇄LIT at runtime, is honored without
      // re-subscribing.
      icdWakefulness: () => this.#icdWakefulness(),
      // A subscription established before its peer was fed holds no wakefulness to observe the first
      // registration-induced flip on; the feed signal lets it recreate on that flip.
      icdPeerFed: () => this.#peerIcd()?.icd.peerFed
    };
    return this.#interaction.subscribe(intermediateRequest, context);
  }
  /**
   * Write chosen attributes remotely to the node.
   * The returned attribute write status information is returned.
   */
  async write(request, context) {
    const { wakefulness, useIcdLit } = this.#awaitModeIcdRouting(request);
    if (useIcdLit) {
      request = { ...request, network: "icdLit" };
    }
    const hold = this.#holdUntilAwake(wakefulness, request.icdAwaitTimeout);
    if (hold !== void 0) {
      await hold;
    }
    return this.#interaction.write(request, context);
  }
  /**
   * Invoke a command remotely on the node.
   * The returned command response is returned as response chunks (attr-status).
   *
   * When the number of commands exceeds the peer's MaxPathsPerInvoke limit (or 1 for older nodes),
   * commands are split across multiple parallel exchanges automatically by ClientInteraction.
   *
   * Single commands may be automatically batched with other commands invoked in the same timer tick.
   */
  async *invoke(request, context) {
    if (request.network === void 0) {
      request = { ...request, network: "unlimited" };
    }
    const hold = this.#holdUntilAwake(this.#icdWakefulness(), request.icdAwaitTimeout);
    if (hold !== void 0) {
      await hold;
    }
    yield* this.#interaction.invoke(request, context);
  }
  /**
   * Initiate a BDX Message Exchange with the node.
   *
   * The provided function is called with the established context to perform BDX operations.
   *
   * Request options may be omitted to use defaults.
   */
  async initBdx(request = {}, context) {
    return this.#interaction.initBdx(request, context);
  }
  async probe(options) {
    if (this.#node.construction.status !== import_general.Lifecycle.Status.Active || !this.#node.owner?.lifecycle.isOnline || this.#node.state.commissioning.peerAddress === void 0) {
      return false;
    }
    return this.#interaction.probe(options);
  }
  get subscriptions() {
    return this.#node.env.get(import_protocol.ClientSubscriptions);
  }
  get #interaction() {
    if (this.#node.construction.status !== import_general.Lifecycle.Status.Active) {
      throw new import_general.ImplementationError(
        `Cannot interact with ${this.#node} because it is ${this.#node.construction.status}`
      );
    }
    if (!this.#node.owner?.lifecycle.isOnline) {
      throw new import_general.ImplementationError(`Cannot interact with ${this.#node} because the local node is not online`);
    }
    if (this.#interactable) {
      return this.#interactable;
    }
    const address = this.#node.state.commissioning.peerAddress;
    if (address === void 0) {
      throw new import_general.ImplementationError(`Cannot interact with ${this.#node} because it is uncommissioned`);
    }
    const peer = this.#node.env.get(import_protocol.PeerSet).for(address);
    this.#interactable = new import_protocol.ClientInteraction({
      environment: this.#node.env,
      exchangeProvider: peer.exchangeProvider
    });
    return this.#interactable;
  }
  /**
   * Close currently open interaction.
   */
  #closeInteraction(reason) {
    if (!this.#interactable) {
      return;
    }
    const closed = this.#interactable.close(reason).catch((e) => {
      logger.warn(`Unhandled error closing client interaction`, e);
    });
    this.#interactable = void 0;
    if (this.#interactableClosed) {
      this.#interactableClosed = import_general.MatterAggregateError.allSettled([this.#interactableClosed, closed]);
    } else {
      this.#interactableClosed = closed;
    }
  }
  get #structure() {
    return this.#node.env.get(import_EndpointInitializer.EndpointInitializer).structure;
  }
  /**
   * Resolve the peer wakefulness and whether an await-mode (LIT) ICD interaction should route through the
   * unthrottled `icdLit` network profile, so a Check-In-triggered interaction is not queued behind bulk traffic and
   * lands inside the peer's brief active window.  Non-LIT peers and a caller-pinned network keep their profile.
   *
   * The caller applies the routing to a copy of its request rather than mutating the caller's object, so a reused
   * request never caches the decision across a DSLS SIT⇄LIT flip.  The wakefulness is returned so the caller hands
   * it to {@link #holdUntilAwake} without a second lookup.
   */
  #awaitModeIcdRouting(request) {
    const wakefulness = this.#icdWakefulness();
    return { wakefulness, useIcdLit: request.network === void 0 && wakefulness?.requiresAwait === true };
  }
  /**
   * Hold a one-shot operation until an idle LIT peer wakes, bounded by a timeout.
   *
   * Returns `undefined` for the common passthrough cases (non-LIT peer, unregistered peer, or an already-awake LIT
   * peer) so callers add no latency and no microtask boundary on the hot path — important for same-tick invoke
   * batching.  Returns a promise only when the operation must actually park; it resolves when a Check-In re-arms the
   * awake window and rejects with {@link IcdPeerAsleepError} if the timeout elapses first.
   *
   * requiresAwait is read live so a DSLS SIT⇄LIT flip is honored per interaction.
   */
  #holdUntilAwake(wakefulness, timeout) {
    if (wakefulness === void 0 || !wakefulness.requiresAwait || wakefulness.awake.value === true) {
      return void 0;
    }
    const address = this.#node.state.commissioning.peerAddress;
    if (address === void 0) {
      return void 0;
    }
    const idle = this.#node.maybeStateOf(import_icd_management.IcdManagementClient)?.idleModeDuration;
    const effectiveTimeout = timeout ?? (0, import_general.Millis)(
      (idle === void 0 ? import_protocol.IcdPeerWakefulness.DEFAULT_IDLE : (0, import_general.Seconds)(idle)) + import_protocol.IcdPeerWakefulness.CHECK_IN_MARGIN
    );
    return this.#awaitWake(wakefulness, address, effectiveTimeout);
  }
  async #awaitWake(wakefulness, address, timeout) {
    var _stack = [];
    try {
      const nextCheckIn = wakefulness.availableUntil;
      logger.info(
        "Peer is a LIT ICD in idle mode; holding interaction until it wakes",
        import_general.Diagnostic.dict({
          peer: (0, import_protocol.PeerAddress)(address),
          timeout: import_general.Duration.format(timeout),
          nextCheckInWithin: nextCheckIn === void 0 ? void 0 : import_general.Duration.format((0, import_general.Millis)(nextCheckIn - import_general.Time.nowMs))
        })
      );
      const abort = __using(_stack, import_general.Abort.subtask(void 0, timeout));
      await abort.race(wakefulness.awake);
      if (abort.aborted) {
        throw new import_IcdPeerAsleepError.IcdPeerAsleepError(address, timeout);
      }
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      __callDispose(_stack, _error, _hasError);
    }
  }
  /**
   * Wakefulness of a LIT peer, or undefined when the peer is uncommissioned or has no registered ICD entry yet.  An
   * undefined result routes the subscription to the non-LIT path rather than throwing.
   */
  #icdWakefulness() {
    const icd = this.#peerIcd();
    return icd?.icd.wakefulnessFor(icd.nodeId);
  }
  /**
   * The peer's {@link FabricIcd} and node ID, cached because the fabric backing a commissioned peer is stable for its
   * lifetime.  Cleared on `peerAddress$Changed`; a not-yet-loaded fabric is not cached, so it is retried next access.
   */
  #peerIcd() {
    if (this.#icd !== void 0 && this.#icdPeerNodeId !== void 0) {
      return { icd: this.#icd, nodeId: this.#icdPeerNodeId };
    }
    const address = this.#node.state.commissioning.peerAddress;
    if (address === void 0) {
      return void 0;
    }
    const icd = this.#node.env.get(import_protocol.FabricManager).maybeFor(address.fabricIndex)?.icd;
    if (icd === void 0) {
      return void 0;
    }
    this.#icd = icd;
    this.#icdPeerNodeId = address.nodeId;
    return { icd, nodeId: address.nodeId };
  }
  /**
   * Temporary accessor of cached data, if any are stored. This will be implemented by the ClientNodeInteraction and
   * point to the node state of the relevant endpoint and is needed to support the old API behavior for
   * AttributeClient.
   * TODO Remove when we remove the legacy controller API
   * @deprecated
   */
  localStateFor(endpointId) {
    if (!this.#node.endpoints.has(endpointId)) {
      return;
    }
    const endpoint = this.#node.endpoints.for(endpointId);
    return endpoint.state;
  }
}
//# sourceMappingURL=ClientNodeInteraction.js.map
