/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { LocalActorContext } from "#behavior/context/server/LocalActorContext.js";
import {
  camelize,
  Diagnostic,
  ImplementationError,
  isObject,
  Logger,
  MaybePromise,
  Observable,
  Transaction
} from "@matter/general";
import { AcceptedCommandList, AttributeList, ElementTag, GeneratedCommandList, Matter } from "@matter/model";
import {
  FabricManager,
  hasRemoteActor,
  Mark,
  OccurrenceManager,
  toWildcardOrHexPath
} from "@matter/protocol";
import {
  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;
    }
    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 = {};
  constructor(type, backing) {
    this.type = type;
    this.#datasource = backing.datasource;
    this.#endpointId = backing.endpoint.number;
    for (const cmd of type.commands) {
      this.commands[cmd.id] = (args, session) => invokeCommand(backing, cmd, args, session);
    }
  }
  get version() {
    return this.#datasource.version;
  }
  get location() {
    return this.#datasource.location;
  }
  readState(session) {
    const supervisorSession = {
      ...LocalActorContext.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();
  const attrDef = {};
  for (const attr of Object.values(cluster.attributes)) {
    attrDef[attr.id] = attr;
  }
  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 eventDef = {};
  for (const ev of Object.values(cluster.events)) {
    eventDef[ev.id] = ev;
  }
  const eventList = Array();
  const events = {
    [Symbol.iterator]: eventList[Symbol.iterator].bind(eventList)
  };
  const cmdDef = {};
  for (const cmd of Object.values(cluster.commands)) {
    cmdDef[cmd.requestId] = cmd;
  }
  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;
    }
    const name = camelize(member.name);
    switch (tag) {
      case "attribute": {
        if (!member.effectiveConformance.isMandatory && !supportedElements.attributes.has(name)) {
          continue;
        }
        const tlv = attrDef[id]?.schema;
        if (tlv === void 0) {
          continue;
        }
        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 = eventDef[id]?.schema;
        if (tlv === void 0) {
          continue;
        }
        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 (!member.effectiveConformance.isMandatory && !supportedElements.commands.has(name) || !member.isRequest) {
          continue;
        }
        const def = cmdDef[id];
        if (def === void 0) {
          continue;
        }
        const { requestSchema: requestTlv, responseSchema: responseTlv, responseId } = def;
        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 invokeCommand(backing, command, request, session) {
  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.toString()}.${command.name}`),
    session.transaction.via,
    requestDiagnostic
  );
  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 (isObject(result2)) {
          logger.info(
            "Invoke",
            Mark.OUTBOUND,
            Diagnostic.strong(`${path.toString()}.${command.name}`),
            session.transaction.via,
            Diagnostic.dict(result2)
          );
        }
        return result2;
      }).finally(() => activity?.[Symbol.dispose]());
    } else {
      if (isObject(result)) {
        logger.info(
          "Invoke",
          Mark.OUTBOUND,
          Diagnostic.strong(`${path.toString()}.${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 isUrgentString = "isUrgent" in path && path.isUrgent ? "!" : "";
  const listIndexString = "listIndex" in path && path.listIndex === null ? "[ADD]" : "";
  const postString = `${listIndexString}${isUrgentString}`;
  const elementId = "attributeId" in path ? path.attributeId : "eventId" in path ? path.eventId : "commandId" in path ? path.commandId : void 0;
  if (endpointId === void 0) {
    return `*.${toWildcardOrHexPath("", clusterId)}.${toWildcardOrHexPath("", elementId)}${postString}`;
  }
  const endpoint = node[endpointId];
  if (endpoint === void 0) {
    return `${toWildcardOrHexPath("?", endpointId)}.${toWildcardOrHexPath("", clusterId)}.${toWildcardOrHexPath("", elementId)}${postString}`;
  }
  const endpointName = toWildcardOrHexPath(endpoint.name, endpointId);
  if (clusterId === void 0) {
    return `${endpointName}.*.${toWildcardOrHexPath("", elementId)}${postString}`;
  }
  const cluster = endpoint[clusterId];
  if (cluster === void 0) {
    return `${endpointName}.${toWildcardOrHexPath("?", clusterId)}.${toWildcardOrHexPath("", elementId)}${postString}`;
  }
  const clusterName = toWildcardOrHexPath(cluster.type.name, clusterId);
  if (elementId !== void 0) {
    if ("eventId" in path) {
      const event = cluster.type.events[elementId];
      return `${endpointName}.${clusterName}.${toWildcardOrHexPath(event?.name ?? "?", elementId)}${postString}`;
    } else if ("attributeId" in path) {
      const attribute = cluster.type.attributes[elementId];
      return `${endpointName}.${clusterName}.${toWildcardOrHexPath(attribute?.name ?? "?", elementId)}${postString}`;
    } else if ("commandId" in path) {
      const command = cluster.type.commands[elementId];
      return `${endpointName}.${clusterName}.${toWildcardOrHexPath(command?.name ?? "?", elementId)}${postString}`;
    } else {
      throw new ImplementationError("Invalid path");
    }
  } else {
    return `${endpointName}.${clusterName}.*${postString}`;
  }
}
export {
  ProtocolService
};
//# sourceMappingURL=ProtocolService.js.map
