/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { isClientBehavior } from "#behavior/cluster/cluster-behavior-utils.js";
import { LocalActorContext } from "#behavior/context/server/LocalActorContext.js";
import { maybeConfigOfConstructor } from "#behavior/supervision/SupervisionConfig.js";
import {
  asError,
  camelize,
  Diagnostic,
  ImplementationError,
  isObject,
  Logger,
  MaybePromise,
  Observable,
  Transaction
} from "@matter/general";
import {
  AcceptedCommandList,
  AttributeList,
  ClusterModel,
  DataModelPath,
  ElementTag,
  GeneratedCommandList,
  Matter
} from "@matter/model";
import { FabricManager, hasRemoteActor, Mark, OccurrenceManager } from "@matter/protocol";
import {
  CommandId,
  StatusResponse,
  TlvOfModel,
  TlvVoid,
  WildcardPathFlags as WildcardPathFlagsType
} from "@matter/types";
const logger = Logger.get("ProtocolService");
class ProtocolService {
  #state;
  constructor(node) {
    this.#state = new NodeState(node);
  }
  /**
   * Invoked by a backing when initialized.
   */
  addCluster(backing) {
    const { schema } = backing.type;
    if (schema?.tag !== ElementTag.Cluster || schema.id === void 0) {
      return;
    }
    if (isClientBehavior(backing.type)) {
      return;
    }
    this.#state.stateFor(backing.endpoint).addCluster(backing);
  }
  /**
   * Invoked by a backing when closed.
   */
  deleteCluster(backing) {
    if (this.#state.hasEndpoint(backing.endpoint)) {
      this.#state.stateFor(backing.endpoint).deleteCluster(backing);
    }
  }
  /**
   * Invoked by a backing when there is a state change.
   *
   * This optimized path allows us to broadcast state changes without registering observers for every change.
   */
  handleChange(backing, props) {
    const clusterId = backing.type.schema.id;
    if (clusterId === void 0) {
      return;
    }
    const namesToIds = backing.type.supervisor.propertyNamesAndIds;
    const attrs = props.map((name) => namesToIds.get(name)).filter((id) => id !== void 0);
    this.protocol.attrsChanged.emit(backing.endpoint.number, clusterId, attrs, backing.datasource.version);
  }
  /**
   * The {@link NodeProtocol}.
   */
  get protocol() {
    return this.#state.protocol;
  }
}
const WildcardPathFlags = {
  skipRootNode: 1 << WildcardPathFlagsType.wildcardSkipRootNode.offset,
  skipGlobalAttributes: 1 << WildcardPathFlagsType.wildcardSkipGlobalAttributes.offset,
  skipAttributeList: 1 << WildcardPathFlagsType.wildcardSkipAttributeList.offset,
  skipCommandLists: 1 << WildcardPathFlagsType.wildcardSkipCommandLists.offset,
  skipCustomElements: 1 << WildcardPathFlagsType.wildcardSkipCustomElements.offset,
  skipFixedAttributes: 1 << WildcardPathFlagsType.wildcardSkipFixedAttributes.offset,
  skipChangesOmittedAttributes: 1 << WildcardPathFlagsType.wildcardSkipChangesOmittedAttributes.offset,
  skipDiagnosticsClusters: 1 << WildcardPathFlagsType.wildcardSkipDiagnosticsClusters.offset
};
class NodeState {
  protocol;
  #endpoints = /* @__PURE__ */ new Set();
  #endpointStates = {};
  constructor(node) {
    let fabrics;
    let eventHandler;
    this.protocol = {
      matter: Matter,
      nodeIdFor(index) {
        if (!fabrics) {
          fabrics = node.env.get(FabricManager);
        }
        return fabrics.maybeFor(index)?.nodeId;
      },
      get eventHandler() {
        if (eventHandler === void 0) {
          eventHandler = node.env.get(OccurrenceManager);
        }
        return eventHandler;
      },
      [Symbol.iterator]: this.#endpoints[Symbol.iterator].bind(this.#endpoints),
      attrsChanged: new Observable(),
      toString() {
        return `node-proto#${node.id}`;
      },
      inspect() {
        return this.toString();
      },
      inspectPath(path) {
        return resolvePathForNode(this, path);
      }
    };
  }
  stateFor(endpoint) {
    const { number } = endpoint;
    let state = this.#endpointStates[number];
    if (state !== void 0) {
      return state;
    }
    state = new EndpointState(this, endpoint);
    this.protocol[number] = state.protocol;
    this.#endpoints.add(state.protocol);
    this.#endpointStates[number] = state;
    return state;
  }
  hasEndpoint(endpoint) {
    return endpoint.number in this.#endpointStates;
  }
  deleteEndpoint(endpoint) {
    delete this.protocol[endpoint.id];
    this.#endpoints.delete(endpoint);
    delete this.#endpointStates[endpoint.id];
  }
}
class EndpointState {
  protocol;
  #node;
  #activeClusters = /* @__PURE__ */ new Set();
  #clusters = /* @__PURE__ */ new Set();
  constructor(node, endpoint) {
    this.#node = node;
    const number = endpoint.number;
    this.protocol = {
      id: number,
      wildcardPathFlags: number === 0 ? WildcardPathFlags.skipRootNode : 0,
      path: endpoint.path,
      name: endpoint.type.name,
      deviceTypes: [],
      toString() {
        return `endpoint-proto#${this.id}`;
      },
      inspect() {
        return this.toString();
      },
      [Symbol.iterator]: this.#clusters[Symbol.iterator].bind(this.#clusters)
    };
  }
  addCluster(backing) {
    const type = clusterTypeProtocolOf(backing);
    if (!type) {
      return;
    }
    const cluster = new ClusterState(type, backing);
    if (backing.type.id === "descriptor") {
      this.#updateDeviceTypes(backing.endpoint.state.descriptor.deviceTypeList);
      backing.events.deviceTypeList$Changed.on(
        this.#updateDeviceTypes.bind(this)
      );
    }
    this.protocol[cluster.type.id] = cluster;
    this.#activeClusters.add(cluster.type.id);
    this.#clusters.add(cluster);
    const attrs = [...cluster.type.attributes].filter((attr) => attr.limits.readable && !attr.changesOmitted).map((attr) => attr.id);
    if (attrs.length) {
      this.#node.protocol.attrsChanged.emit(this.protocol.id, cluster.type.id, attrs, cluster.version);
    }
  }
  deleteCluster(backing) {
    const { schema } = backing.type;
    if (schema === void 0) {
      return;
    }
    const { tag, id } = schema;
    if (tag !== ElementTag.Cluster || id === void 0) {
      return;
    }
    const protocol = this.protocol[id];
    if (protocol) {
      this.#clusters.delete(protocol);
      delete this.protocol[id];
    }
    this.#activeClusters.delete(id);
    if (!this.#activeClusters.size) {
      this.#node.deleteEndpoint(this.protocol);
    }
  }
  #updateDeviceTypes(deviceTypeList) {
    this.protocol.deviceTypes = deviceTypeList.map((dt) => dt.deviceType);
  }
}
class ClusterState {
  type;
  #datasource;
  #endpointId;
  commands = {};
  skipCommandValidation;
  constructor(type, backing) {
    this.type = type;
    this.#datasource = backing.datasource;
    this.#endpointId = backing.endpoint.number;
    for (const cmd of type.commands) {
      const commandModels = this.#commandModels(backing, cmd);
      this.commands[cmd.id] = (args, session) => invokeCommand(backing, cmd, args, session, commandModels);
    }
    for (const cmd of type.commands) {
      if (maybeConfigOfConstructor(backing.type.prototype, camelize(cmd.name))) {
        this.skipCommandValidation = true;
        break;
      }
    }
  }
  #commandModels(backing, cmd) {
    const { schema } = backing.type;
    if (!(schema instanceof ClusterModel)) {
      return;
    }
    const requestModel = schema.commands.find((c) => c.id === cmd.id && c.isRequest);
    if (!requestModel) {
      logger.warn(`No schema for command ${cmd.name} (${cmd.id})`);
      return;
    }
    return {
      supervisor: backing.type.supervisor,
      requestModel,
      responseModel: requestModel.responseModel
    };
  }
  get version() {
    return this.#datasource.version;
  }
  get location() {
    return this.#datasource.location;
  }
  readState(session) {
    const { interactionComplete: _ic, ...readOnly } = LocalActorContext.ReadOnly;
    const supervisorSession = {
      ...readOnly,
      ...session
    };
    return this.#datasource.reference(supervisorSession);
  }
  async openForWrite(session) {
    if (session.transaction === void 0) {
      throw new ImplementationError("Cluster protocol must be opened with a supervisor session");
    }
    await session.transaction.addResources(this.#datasource);
    await session.transaction.begin();
    return this.#datasource.reference(session);
  }
  toString() {
    return `cluster-proto#${this.#endpointId}:${this.type.id}`;
  }
  inspect() {
    return this.toString();
  }
}
const behaviorCache = /* @__PURE__ */ new WeakMap();
function clusterTypeProtocolOf(backing) {
  const behavior = backing.type;
  const { cluster, schema } = behavior;
  if (cluster === void 0 || schema?.id === void 0) {
    return;
  }
  const supportedElements = backing.endpoint.behaviors.elementsOf(behavior);
  const nonMandatorySupportedAttributes = /* @__PURE__ */ new Set();
  const nonMandatorySupportedEvents = /* @__PURE__ */ new Set();
  const nonMandatorySupportedCommands = /* @__PURE__ */ new Set();
  let wildcardPathFlags = schema.effectiveQuality.diagnostics ? WildcardPathFlags.skipDiagnosticsClusters : 0;
  if (schema.id & 4294901760) {
    wildcardPathFlags |= WildcardPathFlags.skipCustomElements;
  }
  const attrList = Array();
  const attributes = {
    [Symbol.iterator]: attrList[Symbol.iterator].bind(attrList)
  };
  const eventList = Array();
  const events = {
    [Symbol.iterator]: eventList[Symbol.iterator].bind(eventList)
  };
  const commandList = Array();
  const commands = {
    [Symbol.iterator]: commandList[Symbol.iterator].bind(commandList)
  };
  for (const member of behavior.supervisor.membersOf(schema, {
    tags: [ElementTag.Attribute, ElementTag.Event, ElementTag.Command]
  })) {
    const { id, tag, effectiveQuality: quality } = member;
    if (id === void 0) {
      continue;
    }
    if (member.isDeprecated || member.isDisallowed) {
      continue;
    }
    const name = member.propertyName;
    switch (tag) {
      case "attribute": {
        if (!member.effectiveConformance.isMandatory && !supportedElements.attributes.has(name)) {
          continue;
        }
        const tlv = TlvOfModel(member);
        let wildcardPathFlags2;
        switch (id) {
          case GeneratedCommandList.id:
          case AcceptedCommandList.id:
            wildcardPathFlags2 = WildcardPathFlags.skipGlobalAttributes | WildcardPathFlags.skipCommandLists;
            break;
          case AttributeList.id:
            wildcardPathFlags2 = WildcardPathFlags.skipGlobalAttributes | WildcardPathFlags.skipAttributeList;
            break;
          default:
            wildcardPathFlags2 = 0;
            break;
        }
        if (id & 4294901760) {
          wildcardPathFlags2 |= WildcardPathFlags.skipGlobalAttributes;
        }
        if (quality.fixed) {
          wildcardPathFlags2 |= WildcardPathFlags.skipFixedAttributes;
        }
        if (quality.changesOmitted) {
          wildcardPathFlags2 |= WildcardPathFlags.skipChangesOmittedAttributes;
        }
        const {
          access: { limits }
        } = behavior.supervisor.get(member);
        const {
          changesOmitted,
          effectiveQuality: { quieter }
        } = member;
        const attr = {
          id,
          tlv,
          wildcardPathFlags: wildcardPathFlags2,
          limits,
          name
        };
        if (changesOmitted) {
          attr.changesOmitted = true;
        }
        if (quieter) {
          attr.quieter = true;
        }
        attrList.push(attr);
        attributes[id] = attr;
        if (!member.effectiveConformance.isMandatory) {
          nonMandatorySupportedAttributes.add(id);
        }
        break;
      }
      case "event": {
        if (!member.effectiveConformance.isMandatory && !supportedElements.events.has(name)) {
          continue;
        }
        const tlv = TlvOfModel(member);
        const {
          access: { limits }
        } = behavior.supervisor.get(member);
        const event = { id, tlv, limits, name };
        eventList.push(event);
        events[id] = event;
        if (!member.effectiveConformance.isMandatory) {
          nonMandatorySupportedEvents.add(id);
        }
        break;
      }
      case "command": {
        if (id === CommandId.NONE || !member.effectiveConformance.isMandatory && !supportedElements.commands.has(name) || !member.isRequest) {
          continue;
        }
        const requestTlv = TlvOfModel(member);
        const responseModel = member.responseModel;
        const responseTlv = responseModel ? TlvOfModel(responseModel) : TlvVoid;
        const responseId = responseModel?.id ?? id;
        const {
          access: { limits }
        } = behavior.supervisor.get(member);
        const command = { id, responseId, requestTlv, responseTlv, limits, name };
        commandList.push(command);
        commands[id] = command;
        if (!member.effectiveConformance.isMandatory) {
          nonMandatorySupportedCommands.add(id);
        }
        break;
      }
    }
  }
  const elementsCacheKey = `a:${[...nonMandatorySupportedAttributes.values()].sort().join(",")},e:${[...nonMandatorySupportedEvents.values()].sort().join(",")},c:${[...nonMandatorySupportedCommands.values()].sort().join(",")}`;
  const existingCache = behaviorCache.get(behavior)?.get(elementsCacheKey);
  if (existingCache) {
    return existingCache;
  }
  const descriptor = {
    id: schema.id,
    name: schema.name,
    attributes,
    events,
    commands,
    wildcardPathFlags
  };
  const elementCache = behaviorCache.get(behavior) ?? /* @__PURE__ */ new Map();
  elementCache.set(elementsCacheKey, descriptor);
  behaviorCache.set(behavior, elementCache);
  return descriptor;
}
function validateCommandPayload(data, model, supervisor, session, outerResolve, config) {
  if (data === void 0) {
    return;
  }
  const validate = supervisor.get(model).validate;
  if (!validate) {
    return;
  }
  validate(data, session, {
    path: new DataModelPath(model.path),
    outerResolve,
    config
  });
}
function invokeCommand(backing, command, request, session, commandModels) {
  if (session.transaction === void 0) {
    throw new ImplementationError("Cluster protocol must be opened with a supervisor session");
  }
  let requestDiagnostic;
  if (isObject(request)) {
    requestDiagnostic = Diagnostic.dict(request);
  } else if (request !== void 0) {
    requestDiagnostic = request;
  } else {
    requestDiagnostic = Diagnostic.weak("(no payload)");
  }
  const { path, endpoint } = backing;
  const context = session;
  logger.info(
    "Invoke",
    Mark.INBOUND,
    Diagnostic.strong(path.at(camelize(command.name))),
    session.transaction.via,
    requestDiagnostic
  );
  const commandInputConfig = maybeConfigOfConstructor(backing.type.prototype, camelize(command.name));
  const onValidationError = commandInputConfig?.supervision?.onValidationError;
  if (commandModels) {
    try {
      validateCommandPayload(
        request,
        commandModels.requestModel,
        commandModels.supervisor,
        session,
        void 0,
        commandInputConfig
      );
    } catch (e) {
      if (onValidationError) {
        onValidationError(asError(e));
      } else {
        logger.warn("Request validation error for", command.name, Diagnostic.errorMessage(asError(e)));
      }
    }
  }
  const outerResolve = commandModels && request ? (name2) => name2 === camelize(commandModels.requestModel.name) ? request : void 0 : void 0;
  const agent = endpoint.agentFor(context);
  const behavior = agent.get(backing.type);
  let isAsync = false;
  let activity;
  let result;
  const { name } = command;
  try {
    activity = hasRemoteActor(context) ? context.activity?.frame(`invoke ${name}`) : void 0;
    const invoke = behavior[camelize(name)].bind(
      behavior
    );
    if (behavior.constructor.lockOnInvoke) {
      const tx = session.transaction;
      if (Transaction.Resource.isLocked(behavior)) {
        result = (async function invokeAsync() {
          await tx.addResources(behavior);
          await tx.begin();
          return invoke(request);
        })();
      } else {
        tx.addResourcesSync(behavior);
        tx.beginSync();
        result = invoke(request);
      }
    } else {
      result = invoke(request);
    }
    if (MaybePromise.is(result)) {
      isAsync = true;
      result = Promise.resolve(result).then((result2) => {
        if (commandModels?.responseModel) {
          try {
            validateCommandPayload(
              result2,
              commandModels.responseModel,
              commandModels.supervisor,
              session,
              outerResolve
            );
          } catch (e) {
            const error = new StatusResponse.FailureError(
              `Response validation failed for ${command.name}: ${asError(e).message}`
            );
            error.cause = e;
            throw error;
          }
        }
        if (isObject(result2)) {
          logger.info(
            "Invoke",
            Mark.OUTBOUND,
            Diagnostic.strong(path.at(camelize(command.name))),
            session.transaction.via,
            Diagnostic.dict(result2)
          );
        }
        return result2;
      }).finally(() => activity?.[Symbol.dispose]());
    } else {
      if (commandModels?.responseModel) {
        try {
          validateCommandPayload(
            result,
            commandModels.responseModel,
            commandModels.supervisor,
            session,
            outerResolve
          );
        } catch (e) {
          const error = new StatusResponse.FailureError(
            `Response validation failed for ${command.name}: ${asError(e).message}`
          );
          error.cause = e;
          throw error;
        }
      }
      if (isObject(result)) {
        logger.info(
          "Invoke",
          Mark.OUTBOUND,
          Diagnostic.strong(path.at(camelize(command.name))),
          session.transaction.via,
          Diagnostic.dict(result)
        );
      }
    }
  } finally {
    if (!isAsync) {
      activity?.[Symbol.dispose]();
    }
  }
  return result;
}
function resolvePathForNode(node, path) {
  const { endpointId, clusterId } = path;
  const endpoint = endpointId !== void 0 ? node[endpointId] : void 0;
  const cluster = endpoint && clusterId !== void 0 ? endpoint[clusterId] : void 0;
  let dmPath = new DataModelPath(endpointId ?? "*", "endpoint");
  dmPath = dmPath.at(nameOf(cluster?.type.name, clusterId), "cluster");
  if ("attributeId" in path) {
    const attribute = cluster && path.attributeId !== void 0 ? cluster.type.attributes[path.attributeId] : void 0;
    dmPath = dmPath.at("state").at(nameOf(attribute?.name, path.attributeId));
    if (path.listIndex === null) {
      dmPath = dmPath.at("[ADD]", "marker");
    }
    return dmPath;
  }
  if ("eventId" in path) {
    const event = cluster && path.eventId !== void 0 ? cluster.type.events[path.eventId] : void 0;
    dmPath = dmPath.at("events").at(nameOf(event?.name, path.eventId));
    if ("isUrgent" in path && path.isUrgent) {
      dmPath = dmPath.at("!", "marker");
    }
    return dmPath;
  }
  if ("commandId" in path) {
    const command = cluster && path.commandId !== void 0 ? cluster.type.commands[path.commandId] : void 0;
    return dmPath.at(nameOf(command?.name, path.commandId));
  }
  return dmPath.at("*", "element");
  function nameOf(name, id) {
    if (name !== void 0) {
      return camelize(name);
    }
    if (id === void 0) {
      return "*";
    }
    return `0x${id.toString(16)}`;
  }
}
export {
  ProtocolService
};
//# sourceMappingURL=ProtocolService.js.map
