var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
var __typeError = (msg) => {
  throw TypeError(msg);
};
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();
};
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { AccessControl, hasRemoteActor } from "#action/server/AccessControl.js";
import { DataResponse, FallbackLimits } from "#action/server/DataResponse.js";
import { Diagnostic, InternalError, Logger } from "@matter/general";
import { DataModelPath, ElementTag, FabricIndex as FabricIndexField } from "@matter/model";
import {
  FabricIndex,
  Status,
  StatusCode,
  StatusResponseError,
  ValidationError
} from "@matter/types";
import { Subject } from "./Subject.js";
const logger = Logger.get("CommandInvokeResponse");
class CommandInvokeResponse extends DataResponse {
  #fabricIndex;
  // The initial "chunk" may be a list of errors.  As producers execute it is a set of records associated with the
  // most recently touched endpoint.  When the endpoint changes the previous chunk emits
  #chunk;
  // Each input CommandDataIB that does not have an error installs a producer.  Producers run after validation and
  // generate actual command data
  #invokers;
  // The following state updates as data producers execute.  This serves both to convey state between functions and as
  // a cache between producers that touch the same endpoint and/or cluster
  #currentEndpoint;
  #registeredPaths = /* @__PURE__ */ new Set();
  #registeredCommandRefs = /* @__PURE__ */ new Set();
  // Count how many command status (on error) and command invokes (success) we have emitted
  #statusCount = 0;
  #successCount = 0;
  #errorCount = 0;
  constructor(node, session) {
    super(node, session);
    this.#fabricIndex = session.fabric ?? FabricIndex.NO_FABRIC;
  }
  async *process({ invokeRequests, suppressResponse }) {
    var _stack = [];
    try {
      const _invoking = __using(_stack, this.join("invoking"));
      const multipleInvokes = invokeRequests.length > 1;
      for (const command of invokeRequests) {
        const { commandPath: path, commandFields, commandRef } = command;
        if (path.endpointId === void 0 || path.clusterId === void 0 || path.commandId === void 0) {
          if (multipleInvokes) {
            throw new StatusResponseError(
              "Wildcard path must not be used with multiple invokes",
              StatusCode.InvalidAction
            );
          }
          this.#processWildcard(path, commandRef, commandFields);
        } else {
          if (Subject.isGroup(this.session.subject)) {
            throw new StatusResponseError("Group commands connot be concrete paths", StatusCode.InvalidAction);
          }
          if (multipleInvokes && commandRef === void 0) {
            throw new StatusResponseError(
              "The CommandRef field must be specified for all commands in a batch invoke",
              StatusCode.InvalidAction
            );
          }
          this.#processConcrete(path, commandRef, commandFields);
        }
      }
      if (this.#invokers) {
        for (const invoker of this.#invokers) {
          for await (const chunk of invoker.apply(this)) {
            if (!suppressResponse) {
              yield chunk;
            }
          }
        }
      }
      if (!suppressResponse && this.#chunk !== void 0) {
        yield this.#chunk;
      }
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      __callDispose(_stack, _error, _hasError);
    }
  }
  get counts() {
    return {
      status: this.#statusCount,
      success: this.#successCount,
      existent: this.#successCount + this.#errorCount
    };
  }
  /**
   * Process a wildcard path and invoke commands on all endpoints that match the path.
   */
  #processWildcard(path, commandRef, commandFields) {
    const { clusterId, endpointId, commandId } = path;
    const isGroupPath = Subject.isGroup(this.session.subject);
    if (isGroupPath && endpointId !== void 0) {
      throw new StatusResponseError(
        "Illegal command invoke with group ID and endpoint ID",
        StatusCode.InvalidAction
      );
    }
    if (clusterId === void 0 || commandId === void 0) {
      throw new StatusResponseError(
        "Wildcard path write must specify a clusterId and commandId",
        StatusCode.InvalidAction
      );
    }
    let groupEndpoints;
    if (isGroupPath) {
      if (this.session.subject.endpoints.length) {
        groupEndpoints = this.session.subject.endpoints;
      } else {
        logger.debug(`No endpoints mapped to group ${this.session.subject.id}, skipping wildcard invoke`);
        return;
      }
    }
    this.#addInvoker(async function* invokeWildcardEndpoints() {
      for (const endpoint of this.node) {
        if (groupEndpoints !== void 0 && !groupEndpoints.includes(endpoint.id)) {
          continue;
        }
        yield* this.#processEndpointForWildcard(endpoint, path, commandRef, commandFields);
      }
    });
  }
  /**
   * Invoke a command specified by a concrete path
   */
  #processConcrete(path, commandRef, commandFields) {
    const { endpointId, clusterId, commandId } = path;
    const pathKey = `${endpointId}-${clusterId}-${commandId}`;
    if (this.#registeredPaths.has(pathKey)) {
      throw new StatusResponseError(
        `Duplicate concrete command path ${this.node.inspectPath(path)} on batch invoke`,
        StatusCode.InvalidAction
      );
    }
    this.#registeredPaths.add(pathKey);
    if (commandRef !== void 0) {
      if (this.#registeredCommandRefs.has(commandRef)) {
        throw new StatusResponseError(
          `Duplicate commandRef ${commandRef} on batch invoke`,
          StatusCode.InvalidAction
        );
      }
      this.#registeredCommandRefs.add(commandRef);
    }
    const endpoint = this.node[endpointId];
    const cluster = endpoint?.[clusterId];
    const command = cluster?.type.commands[commandId];
    let limits;
    if (command === void 0) {
      const modelAttr = this.node.matter.member(path.clusterId, [ElementTag.Cluster])?.member(path.commandId, [ElementTag.Command]);
      if (modelAttr) {
        limits = AccessControl(modelAttr).limits;
      } else {
        limits = FallbackLimits;
      }
    } else {
      limits = command.limits;
    }
    const location = {
      ...cluster?.location ?? {
        path: DataModelPath.none,
        endpoint: endpointId,
        cluster: clusterId
      },
      owningFabric: this.session.fabric
    };
    if (hasRemoteActor(this.session)) {
      const permission = this.session.authorityAt(limits.writeLevel, location);
      switch (permission) {
        case AccessControl.Authority.Granted:
          break;
        case AccessControl.Authority.Unauthorized:
          return this.#addStatus(path, commandRef, Status.UnsupportedAccess);
        case AccessControl.Authority.Restricted:
          return this.#addStatus(path, commandRef, Status.AccessRestricted);
        default:
          throw new InternalError(`Unsupported authorization state ${permission}`);
      }
    }
    if (endpoint === void 0) {
      return this.#addStatus(path, commandRef, Status.UnsupportedEndpoint);
    }
    if (cluster === void 0) {
      return this.#addStatus(path, commandRef, Status.UnsupportedCluster);
    }
    if (command === void 0 || !cluster.type.commands[command.id]) {
      return this.#addStatus(path, commandRef, Status.UnsupportedCommand);
    }
    if (hasRemoteActor(this.session)) {
      if (limits.largeMessage && !this.session.largeMessage) {
        this.#errorCount++;
        return this.#addStatus(path, commandRef, Status.InvalidTransportType);
      }
      if (limits.fabricScoped && !this.session.fabric) {
        this.#errorCount++;
        return this.#addStatus(path, commandRef, Status.UnsupportedAccess);
      }
      if (limits.timed && !this.session.timed) {
        this.#errorCount++;
        return this.#addStatus(path, commandRef, Status.NeedsTimedInteraction);
      }
    }
    this.#addInvoker(async function* invokeConcretePath() {
      if (this.#currentEndpoint !== endpoint) {
        if (this.#chunk) {
          yield this.#chunk;
          this.#chunk = void 0;
        }
        this.#currentEndpoint = endpoint;
      }
      await this.#invokeCommand(
        command,
        path,
        commandRef,
        commandFields,
        cluster.commands[command.id],
        cluster.skipCommandValidation
      );
    });
  }
  /**
   * Starts new chunk or adds to current chunk all values from {@link endpoint} selected by {@link path}.
   *
   * Emits previous chunk if it exists and was not for this endpoint.  This means that our chunk size is one endpoint
   * worth of data, except for the initial error chunk if there are path errors.
   **
   * TODO - skip endpoints for which subject is unauthorized as optimization
   */
  async *#processEndpointForWildcard(endpoint, path, commandRef, commandFields) {
    const { clusterId } = path;
    if (this.#currentEndpoint !== endpoint) {
      if (this.#chunk) {
        yield this.#chunk;
        this.#chunk = void 0;
      }
      this.#currentEndpoint = endpoint;
    }
    const cluster = endpoint[clusterId];
    if (cluster !== void 0) {
      const { commandId } = path;
      const command = cluster.type.commands[commandId];
      if (command !== void 0) {
        if (hasRemoteActor(this.session)) {
          const { limits } = command;
          if (this.session.authorityAt(command.limits.writeLevel, cluster.location) !== AccessControl.Authority.Granted) {
            return;
          }
          if (limits.largeMessage && !this.session.largeMessage) {
            return;
          }
          if (limits.fabricScoped && !this.session.fabric) {
            return;
          }
          if (limits.timed && !this.session.timed) {
            return;
          }
        }
        await this.#invokeCommand(
          command,
          {
            ...path,
            endpointId: endpoint.id
          },
          commandRef,
          commandFields,
          cluster.commands[command.id],
          cluster.skipCommandValidation
        );
      }
    }
  }
  /**
   * Add a function that invokes commands and produces data.  These functions are run after validation of input paths.
   */
  #addInvoker(producer) {
    if (this.#invokers) {
      this.#invokers.push(producer);
    } else {
      this.#invokers = [producer];
    }
  }
  #addResponse(chunk) {
    if (this.#chunk) {
      this.#chunk.push(chunk);
    } else {
      this.#chunk = [chunk];
    }
  }
  /**
   * Add a status value.
   */
  #addStatus(path, commandRef, status, clusterStatus) {
    if (status !== StatusCode.Success) {
      logger.info(
        () => `Invoke error ${this.node.inspectPath(path)}: Status=${StatusCode[status]}(${status}), ClusterStatus=${clusterStatus}`
      );
    }
    const response = {
      kind: "cmd-status",
      path,
      status,
      clusterStatus,
      commandRef
    };
    if (status !== StatusCode.Success) {
      this.#statusCount++;
    }
    this.#addResponse(response);
  }
  async #invokeCommand(command, path, commandRef, commandFields, invoker, skipValidation) {
    try {
      const { requestTlv, responseTlv } = command;
      const request = this.#decodeWithSchema(requestTlv, commandFields);
      if (!skipValidation) {
        requestTlv.validate(request);
      }
      const response = await invoker(request, this.session);
      await this.session.transaction?.commit();
      this.#successCount++;
      const encodedResponse = responseTlv.encodeTlv(response);
      if (encodedResponse.length === 0) {
        this.#addStatus(path, commandRef, StatusCode.Success);
      } else {
        this.#addResponse({
          kind: "cmd-response",
          path: {
            ...path,
            commandId: command.responseId
          },
          data: encodedResponse,
          commandRef
        });
      }
    } catch (error) {
      await this.session.transaction?.rollback();
      const sre = StatusResponseError.of(error);
      if (sre) {
        this.#errorCount++;
        let errorCode = sre.code;
        const errorLogText = `Error ${Diagnostic.hex(errorCode)}${sre.clusterCode !== void 0 ? `/${Diagnostic.hex(sre.clusterCode)}` : ""} while invoking command: ${sre.message}`;
        if (sre instanceof ValidationError) {
          logger.info(
            `Validation-${errorLogText}${sre.fieldName !== void 0 ? ` in field ${sre.fieldName}` : ""}`
          );
          if (errorCode === StatusCode.InvalidAction) {
            errorCode = StatusCode.InvalidCommand;
          }
        } else {
          logger.info(errorLogText);
        }
        this.#addStatus(path, commandRef, errorCode, sre.clusterCode);
        return;
      }
      throw error;
    }
  }
  #decodeWithSchema(tlv, value) {
    if (value === void 0) {
      return void 0;
    }
    return tlv.injectField(
      tlv.decodeTlv(value),
      FabricIndexField.id,
      this.#fabricIndex,
      () => true
      // We always inject the current fabricIndex for invokes
    );
  }
}
export {
  CommandInvokeResponse
};
//# sourceMappingURL=CommandInvokeResponse.js.map
