/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { deepCopy, ImplementationError, Logger } from "@matter/general";
import { FieldElement } from "@matter/model";
import {
  assertRemoteActor,
  FabricManager,
  hasRemoteActor,
  IPK_DEFAULT_EPOCH_START_TIME
} from "@matter/protocol";
import { GroupId, Status, StatusResponseError } from "@matter/types";
import { GroupKeyManagement } from "@matter/types/clusters/group-key-management";
import { GroupKeyManagementBehavior } from "./GroupKeyManagementBehavior.js";
const logger = Logger.get("GroupKeyManagementServer");
const MAX_64BIT_TIME = BigInt("0xffffffffffffffff");
const groupKeySetStruct = GroupKeyManagementBehavior.schema.datatypes.require("GroupKeySetStruct");
const groupKeySetStructFS = groupKeySetStruct.extend(
  {
    name: "GroupKeySetStructFS"
  },
  FieldElement({ name: "FabricIndex", id: 254, type: "FabricIndex", conformance: "M" })
);
const schema = GroupKeyManagementBehavior.schema.extend(
  {},
  groupKeySetStructFS,
  FieldElement(
    {
      name: "groupKeySets",
      type: "list",
      quality: "N",
      access: "RW VM F",
      conformance: "M"
    },
    FieldElement({ name: "entry", type: "GroupKeySetStructFS" })
  )
);
class GroupKeyManagementServer extends GroupKeyManagementBehavior {
  static schema = schema;
  initialize() {
    if (this.features.cacheAndSync) {
      throw new ImplementationError("The CacheAndSync feature is provisional. Do not use it.");
    }
    this.reactTo(this.events.groupKeyMap$Changing, this.#validateGroupKeyMap);
    this.reactTo(this.events.groupTable$Changing, this.#validateGroupTable);
    this.reactTo(this.events.groupKeyMap$Changed, this.#updateGroupKeyMap);
    const lifecycle = this.endpoint.lifecycle;
    this.reactTo(lifecycle.online, this.#online);
  }
  async #online() {
    if (this.state.maxGroupKeysPerFabric === 1 && this.state.maxGroupsPerFabric === 0) {
      let groupsFound = false;
      this.endpoint.visit((endpoint) => {
        if (!groupsFound && "groups" in endpoint.behaviors.supported) {
          groupsFound = true;
        }
      });
      if (groupsFound) {
        throw new ImplementationError(
          "One of the device types you use has a Groups cluster. Please adjust the Group Key Management cluster maximum defaults. You need to support groups management."
        );
      }
    }
    const fabrics = this.env.get(FabricManager);
    this.reactTo(fabrics.events.replaced, this.#handleFabricUpdate);
    if (this.state.groupKeySets.length) {
      const groupKeysForFabric = /* @__PURE__ */ new Map();
      const groupKeySets = deepCopy(this.state.groupKeySets);
      for (let i = 0; i < groupKeySets.length; i++) {
        const groupKeySet = groupKeySets[i];
        const fabricIndex = groupKeySet.fabricIndex;
        const keys = groupKeysForFabric.get(fabricIndex) ?? [];
        if (keys.length === 0 && !fabrics.has(fabricIndex)) {
          logger.warn(
            `Stored GroupKeySets for FabricIndex ${fabricIndex}, but no such fabric exists anymore. Cleaning up stale entries.`
          );
          groupKeySets.splice(i, 1);
          i--;
          continue;
        }
        keys.push(groupKeySet);
        groupKeysForFabric.set(fabricIndex, keys);
      }
      if (groupKeySets.length !== this.state.groupKeySets.length) {
        this.state.groupKeySets = groupKeySets;
      }
      for (const [fabricIndex, keys] of groupKeysForFabric.entries()) {
        const fabric = fabrics.for(fabricIndex);
        for (const groupKeySet of keys) {
          await fabric.groups.setFromGroupKeySet(groupKeySet);
        }
      }
    }
    if (this.state.groupKeyMap.length) {
      this.#updateGroupKeyMap(this.state.groupKeyMap);
    }
  }
  /** Handle the recreation (update) of a fabric, so we need to reinitialize the group key sets */
  async #handleFabricUpdate(fabric) {
    if (this.state.groupKeySets.length === 0) {
      return;
    }
    const fabricIndex = fabric.fabricIndex;
    const groupKeysForFabric = this.state.groupKeySets.filter(
      ({ fabricIndex: entryIndex }) => entryIndex === fabricIndex
    );
    for (const groupKeySet of groupKeysForFabric) {
      await fabric.groups.setFromGroupKeySet(groupKeySet);
    }
    if (this.state.groupKeyMap.length) {
      fabric.groups.groupKeyIdMap = new Map(
        this.state.groupKeyMap.filter(({ fabricIndex: entryIndex }) => entryIndex === fabricIndex).map(({ groupId, groupKeySetId }) => [groupId, groupKeySetId])
      );
    }
    if (this.state.groupTable.length) {
      const groupTable = this.state.groupTable.filter(
        ({ fabricIndex: entryIndex }) => entryIndex === fabricIndex
      );
      fabric.groups.endpoints.clear();
      for (const entry of groupTable) {
        fabric.groups.endpoints.set(entry.groupId, entry.endpoints);
      }
    }
  }
  #validateGroupKeyMap(groupKeyMap) {
    const knownGroupKeys = /* @__PURE__ */ new Set();
    for (const keySetId of this.state.groupKeySets) {
      const { groupKeySetId, fabricIndex } = keySetId;
      const id = `${fabricIndex}-${groupKeySetId}`;
      knownGroupKeys.add(id);
    }
    const knownGroupIds = /* @__PURE__ */ new Set();
    const groupIdsPerFabric = new Array();
    for (const entry of groupKeyMap) {
      const { groupId, fabricIndex } = entry;
      if (!GroupId.isApplicationGroupId(groupId)) {
        throw new StatusResponseError(
          "Only operational GroupIds are allowed in GroupKeyMap",
          Status.InvalidAction
        );
      }
      groupIdsPerFabric[fabricIndex] = (groupIdsPerFabric[fabricIndex] ?? 0) + 1;
      const id = `${fabricIndex}-${groupId}`;
      if (knownGroupIds.has(id)) {
        throw new StatusResponseError(
          `Duplicate GroupId ${groupId} for FabricIndex ${fabricIndex}`,
          Status.ConstraintError
        );
      }
      knownGroupIds.add(id);
    }
    if (groupIdsPerFabric.some((count) => count > this.state.maxGroupsPerFabric)) {
      throw new StatusResponseError(
        `Too many groups per fabric, maximum is ${this.state.maxGroupsPerFabric}`,
        Status.ResourceExhausted
      );
    }
  }
  #validateGroupTable(groupTable) {
    const knownGroupIds = /* @__PURE__ */ new Set();
    for (const entry of groupTable) {
      const { groupId, fabricIndex, endpoints } = entry;
      if (groupId === 0) {
        throw new StatusResponseError(
          "GroupId 0 can not be used as operational group id",
          Status.ConstraintError
        );
      }
      const id = `${fabricIndex}-${groupId}`;
      if (knownGroupIds.has(id)) {
        throw new StatusResponseError(
          `Duplicate GroupId ${groupId} for FabricIndex ${fabricIndex}`,
          Status.ConstraintError
        );
      }
      knownGroupIds.add(id);
      const endpointIds = new Set(endpoints);
      if (endpointIds.size !== endpoints.length) {
        throw new StatusResponseError(
          `Duplicate endpoint IDs in GroupId ${groupId} for FabricIndex ${fabricIndex}`,
          Status.ConstraintError
        );
      }
    }
  }
  #updateGroupKeyMap(groupKeyMap, _oldMap, context) {
    if (context !== void 0 && hasRemoteActor(context)) {
      const fabric = context.session?.associatedFabric;
      const fabricIndex = fabric?.fabricIndex;
      if (fabric !== void 0 && fabricIndex !== void 0) {
        fabric.groups.groupKeyIdMap = new Map(
          groupKeyMap.filter(({ fabricIndex: entryIndex }) => entryIndex === fabricIndex).map(({ groupId, groupKeySetId }) => [groupId, groupKeySetId])
        );
        return;
      }
    }
    const fabrics = this.env.get(FabricManager);
    const fabricMaps = /* @__PURE__ */ new Map();
    for (const entry of groupKeyMap) {
      const { fabricIndex, groupId, groupKeySetId } = entry;
      const fabricMap = fabricMaps.get(fabricIndex) ?? /* @__PURE__ */ new Map();
      fabricMap.set(groupId, groupKeySetId);
      fabricMaps.set(fabricIndex, fabricMap);
    }
    for (const fabric of fabrics) {
      const fabricIndex = fabric.fabricIndex;
      const map = fabricMaps.get(fabricIndex);
      if (map === void 0) {
        continue;
      }
      fabric.groups.groupKeyIdMap = map;
    }
  }
  async keySetWrite({ groupKeySet }) {
    assertRemoteActor(this.context);
    const {
      groupKeySetId,
      epochKey0,
      epochKey1,
      epochKey2,
      epochStartTime0,
      epochStartTime1,
      epochStartTime2,
      groupKeySecurityPolicy,
      groupKeyMulticastPolicy = GroupKeyManagement.GroupKeyMulticastPolicy.PerGroupId
    } = groupKeySet;
    if (groupKeySet.epochStartTime0 === MAX_64BIT_TIME) {
      groupKeySet.epochStartTime0 = null;
      groupKeySet.epochKey0 = null;
    }
    if (groupKeySet.epochStartTime1 === MAX_64BIT_TIME) {
      groupKeySet.epochStartTime1 = null;
      groupKeySet.epochKey1 = null;
    }
    if (groupKeySet.epochStartTime2 === MAX_64BIT_TIME) {
      groupKeySet.epochStartTime2 = null;
      groupKeySet.epochKey2 = null;
    }
    if (epochKey0 === null || epochStartTime0 === null) {
      throw new StatusResponseError("EpochKey0 and EpochStartTime0 must be set", Status.InvalidCommand);
    }
    if (epochStartTime0 <= IPK_DEFAULT_EPOCH_START_TIME) {
      throw new StatusResponseError("EpochStartTime0 must not be 0", Status.InvalidCommand);
    }
    if (epochKey1 !== null && (epochStartTime1 === null || epochStartTime1 <= epochStartTime0)) {
      throw new StatusResponseError(
        "EpochStartTime1 must be set and greater than EpochStartTime0",
        Status.InvalidCommand
      );
    }
    if (epochKey1 === null && epochStartTime1 !== null) {
      throw new StatusResponseError("EpochKey1 must be set if EpochStartTime1 is set", Status.InvalidCommand);
    }
    if (epochKey2 !== null && epochKey1 === null) {
      throw new StatusResponseError("EpochKey1 must be set if EpochKey2 is set", Status.InvalidCommand);
    }
    if (epochKey2 !== null && (epochStartTime2 === null || epochStartTime1 === null || epochStartTime2 <= epochStartTime1)) {
      throw new StatusResponseError(
        "EpochStartTime2 must be set and greater than EpochStartTime1",
        Status.InvalidCommand
      );
    }
    if (epochKey2 === null && epochStartTime2 !== null) {
      throw new StatusResponseError("EpochKey2 must be set if EpochStartTime2 is set", Status.InvalidCommand);
    }
    if (groupKeySecurityPolicy !== GroupKeyManagement.GroupKeySecurityPolicy.TrustFirst) {
      throw new StatusResponseError("GroupKeySecurityPolicy must be TrustFirst", Status.InvalidCommand);
    }
    if (groupKeyMulticastPolicy !== GroupKeyManagement.GroupKeyMulticastPolicy.PerGroupId) {
      throw new StatusResponseError("GroupKeyMulticastPolicy must be PerGroupId", Status.InvalidCommand);
    }
    const fabric = this.context.session.associatedFabric;
    const fabricIndex = fabric.fabricIndex;
    const existingIndex = this.state.groupKeySets.findIndex(
      ({ groupKeySetId: entryId, fabricIndex: entryIndex }) => entryIndex === fabricIndex && entryId === groupKeySetId
    );
    if (existingIndex !== -1) {
      this.state.groupKeySets[existingIndex] = { ...groupKeySet, fabricIndex };
    } else {
      const keySetsOfFabric = this.state.groupKeySets.filter(({ fabricIndex: entryIndex }) => entryIndex === fabricIndex).length + 1;
      if (keySetsOfFabric >= this.state.maxGroupKeysPerFabric) {
        throw new StatusResponseError(
          `Too many group key sets for fabric ${fabricIndex}, maximum is ${this.state.maxGroupKeysPerFabric}`,
          Status.ResourceExhausted
        );
      }
      this.state.groupKeySets.push({ ...groupKeySet, fabricIndex });
    }
    await fabric.groups.setFromGroupKeySet(groupKeySet);
  }
  keySetRead({
    groupKeySetId
  }) {
    assertRemoteActor(this.context);
    const fabric = this.context.session.associatedFabric;
    const groupKeySet = fabric.groups.keySets.asGroupKeySet(groupKeySetId);
    if (groupKeySet === void 0) {
      throw new StatusResponseError(`GroupKeySet ${groupKeySetId} not found`, Status.NotFound);
    }
    return {
      groupKeySet: {
        ...groupKeySet,
        epochKey0: null,
        epochKey1: null,
        epochKey2: null
      }
    };
  }
  keySetRemove({ groupKeySetId }) {
    if (groupKeySetId === 0) {
      throw new StatusResponseError(`GroupKeySet ${groupKeySetId} cannot be removed`, Status.InvalidCommand);
    }
    assertRemoteActor(this.context);
    const fabric = this.context.session.associatedFabric;
    const fabricIndex = fabric.fabricIndex;
    const existingIndex = this.state.groupKeySets.findIndex(
      ({ groupKeySetId: entryId, fabricIndex: entryIndex }) => entryIndex === fabricIndex && entryId === groupKeySetId
    );
    if (existingIndex === -1) {
      throw new StatusResponseError(`GroupKeySet ${groupKeySetId} not found`, Status.NotFound);
    }
    this.state.groupKeySets.splice(existingIndex, 1);
    this.state.groupKeyMap = this.state.groupKeyMap.filter(
      ({ groupKeySetId: entryId }) => groupKeySetId !== entryId
    );
    fabric.groups.removeGroupKeySet(groupKeySetId);
  }
  keySetReadAllIndices() {
    assertRemoteActor(this.context);
    const fabric = this.context.session.associatedFabric;
    const fabricIndex = fabric.fabricIndex;
    const groupKeySetIDs = this.state.groupKeySets.filter(({ fabricIndex: entryIndex }) => entryIndex === fabricIndex).map(({ groupKeySetId }) => groupKeySetId);
    groupKeySetIDs.unshift(0);
    return {
      groupKeySetIDs
    };
  }
  addEndpointForGroup(fabric, groupId, endpointId, groupName) {
    const fabricIndex = fabric.fabricIndex;
    const existingGroupIndex = this.state.groupTable.findIndex(
      (entry) => entry.groupId === groupId && entry.fabricIndex === fabricIndex
    );
    if (existingGroupIndex !== -1) {
      if (!this.state.groupTable[existingGroupIndex].endpoints.includes(endpointId)) {
        this.state.groupTable[existingGroupIndex].endpoints.push(endpointId);
        fabric.groups.endpoints.set(groupId, this.state.groupTable[existingGroupIndex].endpoints);
      }
      this.state.groupTable[existingGroupIndex].groupName = groupName;
    } else {
      if (this.state.groupTable.filter(({ fabricIndex: entryFabricIndex }) => entryFabricIndex === fabricIndex).length >= this.state.maxGroupsPerFabric) {
        throw new StatusResponseError(
          `Too many groups for fabric ${fabricIndex}, maximum is ${this.state.maxGroupsPerFabric}`,
          Status.ResourceExhausted
        );
      }
      this.state.groupTable.push({
        groupId,
        endpoints: [endpointId],
        groupName,
        fabricIndex
      });
      fabric.groups.endpoints.set(groupId, [endpointId]);
    }
    logger.info(
      `Added endpoint ${endpointId} to group ${groupId} on fabric ${fabricIndex} with name "${groupName}"`
    );
  }
  /**
   * Remove endpoint from the provided group, or all groups if no groupId is provided.
   */
  removeEndpoint(fabric, endpointId, groupId) {
    let existing = false;
    const groupTable = this.state.groupTable;
    const fabricIndex = fabric.fabricIndex;
    for (let i = groupTable.length - 1; i >= 0; i--) {
      const entry = groupTable[i];
      if (entry.fabricIndex !== fabricIndex || groupId !== void 0 && entry.groupId !== groupId) {
        continue;
      }
      const endpointExists = entry.endpoints.includes(endpointId);
      if (endpointExists) {
        if (entry.endpoints.length === 1 && entry.endpoints[0] === endpointId) {
          const groupId2 = entry.groupId;
          groupTable.splice(i, 1);
          fabric.groups.endpoints.delete(groupId2);
        } else {
          entry.endpoints = entry.endpoints.filter((id) => id !== endpointId);
          fabric.groups.endpoints.set(entry.groupId, entry.endpoints);
        }
        existing = true;
      }
    }
    return existing;
  }
}
((GroupKeyManagementServer2) => {
  class State extends GroupKeyManagementBehavior.State {
    /**
     * Extended state to hold and persist the group key sets for this server. This structure contains all
     * GroupKeySet entries for all fabrics beside the fabric specific entries of the groupKeyset 0
     */
    groupKeySets = [];
    // Overwrite defaults to allow more than 3 group keys and 4 groups per fabric, because we can
    maxGroupKeysPerFabric = 20;
    // The Minimum would be 3;
    maxGroupsPerFabric = 21;
    // The Minimum would be 4;
  }
  GroupKeyManagementServer2.State = State;
})(GroupKeyManagementServer || (GroupKeyManagementServer = {}));
export {
  GroupKeyManagementServer
};
//# sourceMappingURL=GroupKeyManagementServer.js.map
