/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { AccessControlServer } from "#behaviors/access-control";
import { GroupKeyManagementServer } from "#behaviors/group-key-management";
import { Bytes, deepCopy, ImplementationError, Logger, ObservableValue, Seconds, Time } from "@matter/general";
import { AccessLevel, DataModelPath } from "@matter/model";
import {
  AccessControl,
  assertRemoteActor,
  FabricManager,
  SessionManager
} from "@matter/protocol";
import { EndpointNumber, FabricIndex, GroupId, NodeId, Status, StatusResponseError } from "@matter/types";
import { AccessControl as AccessControlTypes } from "@matter/types/clusters/access-control";
import { Groupcast } from "@matter/types/clusters/groupcast";
import { GroupcastBehavior } from "./GroupcastBehavior.js";
const logger = Logger.get("GroupcastServer");
const UNMAPPED_KEYSET_ID = 65535;
const GROUPCAST_IS_PROVISIONAL = true;
class GroupcastServer extends GroupcastBehavior {
  /** Timer for GroupcastTesting auto-disable. */
  #testingTimer;
  initialize() {
    if (GROUPCAST_IS_PROVISIONAL) {
      throw new ImplementationError(
        "The Groupcast cluster is provisional in Matter 1.6. Do not add it to a node."
      );
    }
    const lifecycle = this.endpoint.lifecycle;
    this.reactTo(lifecycle.online, this.#online);
  }
  async #online() {
    this.agent.get(AccessControlServer).registerAuxAclProvider(this.internal.auxAcl);
    const fabrics = this.env.get(FabricManager);
    for (const fabric of fabrics) {
      this.#syncFabricGroups(fabric);
    }
    await this.#migrate();
    this.#reconcileUnmappedKeys();
    this.#updateUsedMcastAddrCount();
    this.#emitAuxAcl();
    this.reactTo(fabrics.events.added, this.#handleFabricAdded);
    this.reactTo(fabrics.events.deleted, this.#handleFabricDeleted);
    const gkmEvents = this.endpoint.eventsOf(GroupKeyManagementServer);
    this.reactTo(gkmEvents.groupKeyMap$Changed, this.#reconcileUnmappedKeys);
    this.reactTo(this.events.fabricUnderTest$Changed, this.#handleFabricUnderTestChanged);
    if (this.state.fabricUnderTest !== FabricIndex.NO_FABRIC) {
      this.#attachGroupMessageListener();
    }
  }
  /**
   * Check whether the current session has Admin privileges for this cluster/endpoint.
   * Key and useAuxiliaryACL fields in Manage-access commands require Admin
   * privilege to prevent privilege escalation.
   */
  #requireAdmin() {
    const session = this.context;
    const location = {
      path: DataModelPath.none,
      endpoint: this.endpoint.number,
      cluster: GroupcastBehavior.cluster.id,
      owningFabric: session.fabric
    };
    if (session.authorityAt(AccessLevel.Administer, location) !== AccessControl.Authority.Granted) {
      throw new StatusResponseError(
        "Admin privilege required for key or ACL operations",
        Status.UnsupportedAccess
      );
    }
  }
  async joinGroup(request) {
    assertRemoteActor(this.context);
    const fabricIndex = this.context.session.associatedFabric.fabricIndex;
    const { groupId, endpoints, keySetId, key, useAuxiliaryAcl, replaceEndpoints, mcastAddrPolicy } = request;
    if (groupId < 1 || groupId > 65527) {
      throw new StatusResponseError("Invalid group ID", Status.ConstraintError);
    }
    if (key !== void 0 || useAuxiliaryAcl !== void 0) {
      this.#requireAdmin();
    }
    for (const ep of endpoints) {
      if (ep === 0 || ep > 65534) {
        throw new StatusResponseError(`Endpoint ${ep} is invalid`, Status.UnsupportedEndpoint);
      }
    }
    if (!this.features.sender && endpoints.length === 0) {
      throw new StatusResponseError("Empty endpoint list requires Sender feature", Status.ConstraintError);
    }
    const policy = mcastAddrPolicy ?? Groupcast.MulticastAddrPolicy.IanaAddr;
    if (policy === Groupcast.MulticastAddrPolicy.PerGroup && !this.features.perGroup) {
      throw new StatusResponseError(
        "PerGroup multicast policy requires PerGroupAddr feature",
        Status.ConstraintError
      );
    }
    await this.#applyKeySet(fabricIndex, keySetId, key);
    const membership = deepCopy(this.state.membership);
    const existingIdx = membership.findIndex((m) => m.groupId === groupId && m.fabricIndex === fabricIndex);
    if (existingIdx >= 0) {
      const existing = membership[existingIdx];
      const lnFields = this.features.listener ? {
        endpoints: replaceEndpoints ? endpoints : [.../* @__PURE__ */ new Set([...existing.endpoints ?? [], ...endpoints])],
        hasAuxiliaryAcl: useAuxiliaryAcl ?? existing.hasAuxiliaryAcl
      } : {};
      membership[existingIdx] = {
        ...existing,
        ...lnFields,
        keySetId,
        mcastAddrPolicy: policy
      };
    } else {
      const fabricMemberships = membership.filter((m) => m.fabricIndex === fabricIndex);
      const perFabricLimit = Math.floor(this.state.maxMembershipCount / 2);
      if (fabricMemberships.length >= perFabricLimit) {
        throw new StatusResponseError("Per-fabric membership limit reached", Status.ResourceExhausted);
      }
      if (membership.length >= this.state.maxMembershipCount) {
        throw new StatusResponseError("Total membership limit reached", Status.ResourceExhausted);
      }
      if (policy === Groupcast.MulticastAddrPolicy.PerGroup) {
        const currentUsed = this.#computeUsedMcastAddrCount(membership);
        if (currentUsed >= this.state.maxMcastAddrCount) {
          throw new StatusResponseError("MaxMcastAddrCount limit reached", Status.ResourceExhausted);
        }
      } else if (policy === Groupcast.MulticastAddrPolicy.IanaAddr) {
        const hasIanaAddr = membership.some((m) => m.mcastAddrPolicy === Groupcast.MulticastAddrPolicy.IanaAddr);
        if (!hasIanaAddr) {
          const currentUsed = this.#computeUsedMcastAddrCount(membership);
          if (currentUsed >= this.state.maxMcastAddrCount) {
            throw new StatusResponseError("MaxMcastAddrCount limit reached", Status.ResourceExhausted);
          }
        }
      }
      const lnFields = this.features.listener ? { endpoints, hasAuxiliaryAcl: useAuxiliaryAcl ?? false } : {};
      membership.push({
        groupId,
        ...lnFields,
        keySetId,
        mcastAddrPolicy: policy,
        fabricIndex
      });
    }
    this.state.membership = membership;
    this.#updateUsedMcastAddrCount();
    this.#syncFabricGroups(this.env.get(FabricManager).for(fabricIndex));
    this.#emitAuxAcl();
  }
  leaveGroup(request) {
    assertRemoteActor(this.context);
    const fabricIndex = this.context.session.associatedFabric.fabricIndex;
    const { groupId, endpoints: requestedEndpoints } = request;
    if (groupId === GroupId.NO_GROUP_ID) {
      const fabricMemberships = this.state.membership.filter((m) => m.fabricIndex === fabricIndex);
      if (fabricMemberships.length === 0) {
        throw new StatusResponseError("No groups to leave", Status.NotFound);
      }
      this.state.membership = this.state.membership.filter((m) => m.fabricIndex !== fabricIndex);
      this.#updateUsedMcastAddrCount();
      this.#syncFabricGroups(this.env.get(FabricManager).for(fabricIndex));
      this.#emitAuxAcl();
      return { groupId: GroupId.NO_GROUP_ID, endpoints: [] };
    }
    const membership = deepCopy(this.state.membership);
    const entryIdx = membership.findIndex((m) => m.groupId === groupId && m.fabricIndex === fabricIndex);
    if (entryIdx < 0) {
      throw new StatusResponseError(`Group ${groupId} not found`, Status.NotFound);
    }
    const entry = membership[entryIdx];
    let removedEndpoints;
    if (!requestedEndpoints || requestedEndpoints.length === 0) {
      removedEndpoints = entry.endpoints ?? [];
      membership.splice(entryIdx, 1);
    } else {
      const currentEndpoints = entry.endpoints ?? [];
      removedEndpoints = requestedEndpoints.filter((ep) => currentEndpoints.includes(ep));
      const remainingEndpoints = currentEndpoints.filter((ep) => !requestedEndpoints.includes(ep));
      if (remainingEndpoints.length === 0) {
        if (this.features.sender) {
          const { endpoints: _omit, ...entryWithoutEndpoints } = entry;
          membership[entryIdx] = this.features.listener ? { ...entry, endpoints: [] } : entryWithoutEndpoints;
        } else {
          membership.splice(entryIdx, 1);
        }
      } else {
        membership[entryIdx] = { ...entry, endpoints: remainingEndpoints };
      }
    }
    this.state.membership = membership;
    this.#updateUsedMcastAddrCount();
    this.#syncFabricGroups(this.env.get(FabricManager).for(fabricIndex));
    this.#emitAuxAcl();
    return { groupId, endpoints: removedEndpoints };
  }
  async updateGroupKey(request) {
    assertRemoteActor(this.context);
    const fabricIndex = this.context.session.associatedFabric.fabricIndex;
    const { groupId, keySetId, key } = request;
    if (key !== void 0) {
      this.#requireAdmin();
    }
    const membership = deepCopy(this.state.membership);
    const entryIdx = membership.findIndex((m) => m.groupId === groupId && m.fabricIndex === fabricIndex);
    if (entryIdx < 0) {
      throw new StatusResponseError(`Group ${groupId} not found`, Status.NotFound);
    }
    await this.#applyKeySet(fabricIndex, keySetId, key);
    membership[entryIdx] = { ...membership[entryIdx], keySetId };
    this.state.membership = membership;
    this.#syncFabricGroups(this.env.get(FabricManager).for(fabricIndex));
  }
  configureAuxiliaryAcl(request) {
    assertRemoteActor(this.context);
    const fabricIndex = this.context.session.associatedFabric.fabricIndex;
    const { groupId, useAuxiliaryAcl } = request;
    const membership = deepCopy(this.state.membership);
    const entryIdx = membership.findIndex((m) => m.groupId === groupId && m.fabricIndex === fabricIndex);
    if (entryIdx < 0) {
      throw new StatusResponseError(`Group ${groupId} not found`, Status.NotFound);
    }
    membership[entryIdx] = { ...membership[entryIdx], hasAuxiliaryAcl: useAuxiliaryAcl };
    this.state.membership = membership;
    this.#emitAuxAcl();
  }
  groupcastTesting(request) {
    assertRemoteActor(this.context);
    const fabricIndex = this.context.session.associatedFabric.fabricIndex;
    const { testOperation, durationSeconds } = request;
    if (this.#testingTimer) {
      this.#testingTimer.stop();
      this.#testingTimer = void 0;
    }
    if (testOperation === Groupcast.GroupcastTesting.DisableTesting) {
      this.state.fabricUnderTest = FabricIndex.NO_FABRIC;
      return;
    }
    this.state.fabricUnderTest = fabricIndex;
    if (durationSeconds !== void 0 && durationSeconds > 0) {
      this.#testingTimer = Time.getTimer(
        `groupcast-testing-${fabricIndex}`,
        Seconds(durationSeconds),
        this.callback(this.#testingTimerExpired)
      );
      this.#testingTimer.start();
    }
  }
  #testingTimerExpired() {
    this.#testingTimer = void 0;
    this.state.fabricUnderTest = FabricIndex.NO_FABRIC;
  }
  #handleFabricUnderTestChanged(value) {
    if (value !== FabricIndex.NO_FABRIC) {
      this.#attachGroupMessageListener();
    } else {
      this.#detachGroupMessageListener();
    }
  }
  #attachGroupMessageListener() {
    if (this.internal.groupMessageHandler !== void 0) {
      return;
    }
    const sessions = this.env.get(SessionManager);
    const handler = this.callback(this.#onGroupMessage);
    sessions.onGroupMessage.on(handler);
    this.internal.groupMessageHandler = handler;
  }
  #detachGroupMessageListener() {
    if (this.internal.groupMessageHandler === void 0) {
      return;
    }
    const sessions = this.env.get(SessionManager);
    sessions.onGroupMessage.off(this.internal.groupMessageHandler);
    this.internal.groupMessageHandler = void 0;
  }
  #onGroupMessage(info) {
    if (info.fabric === void 0 || info.fabric.fabricIndex !== this.state.fabricUnderTest) {
      return;
    }
    this.events.groupcastTesting?.emit(
      {
        groupId: info.groupId,
        endpointId: info.endpointId,
        clusterId: info.clusterId,
        elementId: info.elementId,
        accessAllowed: info.accessAllowed,
        groupcastTestResult: info.result,
        fabricIndex: info.fabric.fabricIndex
      },
      this.context
    );
  }
  /** Sync the FabricGroups instance for a fabric from the current Membership state. */
  #handleFabricAdded(fabric) {
    this.#syncFabricGroups(fabric);
  }
  #handleFabricDeleted(fabric) {
    this.#cleanupFabric(fabric.fabricIndex);
  }
  #syncFabricGroups(fabric) {
    const fabricIndex = fabric.fabricIndex;
    const fabricMemberships = this.state.membership.filter((m) => m.fabricIndex === fabricIndex);
    for (const m of fabricMemberships) {
      const policy = m.mcastAddrPolicy === Groupcast.MulticastAddrPolicy.PerGroup ? "perGroupId" : "ianaAddr";
      fabric.groups.setGroupMulticastPolicy(m.groupId, policy);
    }
    const groupKeyIdMap = /* @__PURE__ */ new Map();
    for (const m of fabricMemberships) {
      groupKeyIdMap.set(m.groupId, m.keySetId);
    }
    fabric.groups.groupKeyIdMap = groupKeyIdMap;
    const gkm = this.agent.get(GroupKeyManagementServer);
    const otherFabricsMap = gkm.state.groupKeyMap.filter((e) => e.fabricIndex !== fabricIndex);
    const mappings = fabricMemberships.filter((m) => m.keySetId !== UNMAPPED_KEYSET_ID).map((m) => ({ groupId: m.groupId, groupKeySetId: m.keySetId, fabricIndex }));
    gkm.state.groupKeyMap = [...otherFabricsMap, ...mappings];
    const otherFabricsTable = gkm.state.groupTable.filter((e) => e.fabricIndex !== fabricIndex);
    const tableEntries = fabricMemberships.filter((m) => m.endpoints !== void 0 && m.endpoints.length > 0).map((m) => ({
      groupId: m.groupId,
      endpoints: [...m.endpoints],
      groupName: "",
      fabricIndex
    }));
    gkm.state.groupTable = [...otherFabricsTable, ...tableEntries];
    const previousGroupIds = new Set(fabric.groups.endpoints.keys());
    for (const m of fabricMemberships) {
      const eps = m.endpoints ?? [];
      if (eps.length > 0) {
        fabric.groups.endpoints.set(m.groupId, [...eps]);
      } else {
        fabric.groups.endpoints.delete(m.groupId);
      }
      previousGroupIds.delete(m.groupId);
    }
    for (const gid of previousGroupIds) {
      fabric.groups.endpoints.delete(gid);
    }
  }
  /**
   * Mark membership entries as unmapped (KeySetId=UNMAPPED_KEYSET_ID) when their referenced key set no longer exists in GKM.
   * Runs at startup to reconcile state after potential GKM KeySetRemove operations.
   */
  #reconcileUnmappedKeys() {
    const gkmState = this.endpoint.stateOf(GroupKeyManagementServer);
    const membership = this.state.membership;
    let dirty = false;
    for (const m of membership) {
      if (m.keySetId !== UNMAPPED_KEYSET_ID && m.keySetId !== 0) {
        const exists = gkmState.groupKeySets.some(
          (ks) => ks.fabricIndex === m.fabricIndex && ks.groupKeySetId === m.keySetId
        );
        if (!exists) {
          m.keySetId = UNMAPPED_KEYSET_ID;
          dirty = true;
        }
      }
    }
    if (dirty) {
      this.state.membership = [...membership];
    }
  }
  /**
   * Validate and apply a key set for a group operation.
   * If `key` is provided, validates and creates a new key set in GKM.
   * Otherwise, validates that the key set already exists.
   */
  async #applyKeySet(fabricIndex, keySetId, key) {
    const gkm = this.agent.get(GroupKeyManagementServer);
    if (key !== void 0) {
      if (key.byteLength !== 16) {
        throw new StatusResponseError("Key must be exactly 16 bytes", Status.ConstraintError);
      }
      if (gkm.validateKeySetId(fabricIndex, keySetId)) {
        throw new StatusResponseError(`KeySet ${keySetId} already exists for fabric`, Status.AlreadyExists);
      }
      await gkm.createKeySetForGroupcast(this.env.get(FabricManager).for(fabricIndex), keySetId, Bytes.of(key));
    } else {
      if (!gkm.validateKeySetId(fabricIndex, keySetId)) {
        throw new StatusResponseError(`KeySet ${keySetId} not found for fabric`, Status.NotFound);
      }
    }
  }
  /** Recompute auxiliary ACL entries and emit them via the observable so AccessControlServer updates. */
  #emitAuxAcl() {
    if (!this.endpoint.behaviors.has(AccessControlServer)) return;
    const entries = this.state.membership.filter((m) => m.hasAuxiliaryAcl).map((m) => ({
      privilege: AccessControlTypes.AccessControlEntryPrivilege.Operate,
      authMode: AccessControlTypes.AccessControlEntryAuthMode.Group,
      subjects: [NodeId(BigInt(m.groupId))],
      targets: m.endpoints?.map((ep) => ({
        cluster: null,
        endpoint: ep,
        deviceType: null
      })) ?? null,
      auxiliaryType: AccessControlTypes.AccessControlAuxiliaryType.Groupcast,
      fabricIndex: m.fabricIndex
    }));
    this.internal.auxAcl.emit(entries, this.context);
  }
  /**
   * Compute UsedMcastAddrCount from a membership array:
   * - Each PerGroup group contributes 1 unique address
   * - All IanaAddr groups together share 1 address
   */
  #computeUsedMcastAddrCount(membership) {
    const perGroupCount = membership.filter(
      (m) => m.mcastAddrPolicy === Groupcast.MulticastAddrPolicy.PerGroup
    ).length;
    const hasIanaAddr = membership.some((m) => m.mcastAddrPolicy === Groupcast.MulticastAddrPolicy.IanaAddr);
    return perGroupCount + (hasIanaAddr ? 1 : 0);
  }
  /** Recompute and update the UsedMcastAddrCount attribute. */
  #updateUsedMcastAddrCount() {
    this.state.usedMcastAddrCount = this.#computeUsedMcastAddrCount(this.state.membership);
  }
  /** Remove all state for a departed fabric. */
  #cleanupFabric(fabricIndex) {
    this.state.membership = this.state.membership.filter((m) => m.fabricIndex !== fabricIndex);
    this.#updateUsedMcastAddrCount();
    this.#emitAuxAcl();
  }
  /**
   * Migrate legacy group data from the GKM groupTable into Groupcast Membership entries.
   * Only migrates fabrics that have existing GKM groups but no Membership entries yet.
   * Legacy groups are assigned PerGroup multicast policy for backwards compatibility.
   */
  async #migrate() {
    const fabrics = this.env.get(FabricManager);
    const gkmState = this.endpoint.stateOf(GroupKeyManagementServer);
    let migrated = false;
    for (const fabric of fabrics) {
      const fi = fabric.fabricIndex;
      const hasMembership = this.state.membership.some((m) => m.fabricIndex === fi);
      if (hasMembership) continue;
      const fabricGroups = gkmState.groupTable.filter((g) => g.fabricIndex === fi);
      if (fabricGroups.length === 0) continue;
      logger.info(`Migrating ${fabricGroups.length} legacy group(s) for fabric ${fi} to Groupcast`);
      const newEntries = fabricGroups.map((group) => {
        const keyMapping = gkmState.groupKeyMap.find((m) => m.fabricIndex === fi && m.groupId === group.groupId);
        const mcastAddrPolicy = this.features.perGroup ? Groupcast.MulticastAddrPolicy.PerGroup : Groupcast.MulticastAddrPolicy.IanaAddr;
        const lnFields = this.features.listener ? {
          endpoints: group.endpoints.filter((ep) => ep !== EndpointNumber(0)),
          hasAuxiliaryAcl: false
        } : {};
        return {
          groupId: group.groupId,
          ...lnFields,
          keySetId: keyMapping?.groupKeySetId ?? 0,
          mcastAddrPolicy,
          fabricIndex: fi
        };
      });
      this.state.membership = [...this.state.membership, ...newEntries];
      this.#syncFabricGroups(fabric);
      migrated = true;
    }
    if (migrated) {
      this.#updateUsedMcastAddrCount();
      logger.info("Groupcast migration complete");
    }
  }
  async [Symbol.asyncDispose]() {
    this.#testingTimer?.stop();
    this.#testingTimer = void 0;
    this.#detachGroupMessageListener();
    await super[Symbol.asyncDispose]?.();
  }
}
((GroupcastServer2) => {
  class Internal {
    /**
     * Observable that holds all auxiliary ACL entries supplied by this cluster.
     * AccessControlServer subscribes to this and rebuilds its ACL cache whenever it emits.
     */
    auxAcl = ObservableValue([]);
    /** Active subscription handler on SessionManager.groupMessage while fabricUnderTest is set. */
    groupMessageHandler;
  }
  GroupcastServer2.Internal = Internal;
  class State extends GroupcastBehavior.State {
    /**
     * Implementation-defined maximum membership count (min 10 per spec).
     * Set to 2 * GKM.maxGroupsPerFabric (44) so per-fabric quota floor(44/2)=22 aligns exactly
     * with GKM's per-fabric cap. Even number avoids spillover semantics.
     */
    maxMembershipCount = 44;
    /** Implementation-defined maximum multicast address count (min 1 per spec). */
    maxMcastAddrCount = 44;
  }
  GroupcastServer2.State = State;
})(GroupcastServer || (GroupcastServer = {}));
export {
  GroupcastServer
};
//# sourceMappingURL=GroupcastServer.js.map
