/**
 * @license
 * Copyright 2022-2025 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { AccessControl as AccessControlTypes } from "#clusters/access-control";
import { Bytes, deepCopy, InternalError, Logger } from "#general";
import {
  FabricManager,
  hasLocalActor,
  hasRemoteActor,
  NodeSession
} from "#protocol";
import {
  CaseAuthenticatedTag,
  ClusterId,
  DeviceTypeId,
  EndpointNumber,
  GroupId,
  NodeId,
  StatusCode,
  StatusResponseError,
  TlvTaggedList,
  TlvType
} from "#types";
import { AccessControlBehavior } from "./AccessControlBehavior.js";
const logger = Logger.get("AccessControlServer");
class AccessControlServer extends AccessControlBehavior.with("Extension") {
  initialize() {
    this.reactTo(this.events.acl$Changing, this.#validateAccessControlListChanges);
    this.reactTo(this.events.acl$Changed, this.#handleAccessControlListChange);
    if (this.state.extension !== void 0) {
      this.maybeReactTo(this.events.extension$Changing, this.#validateAccessControlExtensionChanges);
      this.maybeReactTo(this.events.extension$Changed, this.#handleAccessControlExtensionChange);
    }
    const lifecycle = this.endpoint.lifecycle;
    this.reactTo(lifecycle.online, this.#online);
  }
  #online() {
    const aclsForFabric = this.#mapFabricAcls();
    const fabrics = this.env.get(FabricManager);
    for (const fabric of fabrics) {
      const fabricAcls = aclsForFabric.get(fabric.fabricIndex) ?? [];
      if (!fabricAcls.length) {
        const fallbackAcl = {
          privilege: AccessControlTypes.AccessControlEntryPrivilege.Administer,
          authMode: AccessControlTypes.AccessControlEntryAuthMode.Case,
          subjects: [fabric.rootNodeId],
          targets: null,
          // entire node
          fabricIndex: fabric.fabricIndex
        };
        this.state.acl.push(fallbackAcl);
        fabricAcls.push(fallbackAcl);
      }
      fabric.accessControl.aclList = fabricAcls;
      fabric.accessControl.extensionEntryAccessCheck = this.extensionEntryAccessCheck.bind(this);
    }
    this.reactTo(fabrics.events.updated, this.#updateFabricAcls);
    this.reactTo(fabrics.events.added, this.#updateFabricAcls);
    this.reactTo(this.events.interactionBegin, this.#handleInteractionBegin);
    this.reactTo(this.events.interactionEnd, this.#handleInteractionEnd);
    this.reactTo(this.events.acl$Changed, this.#updateAccessControlList);
    this.internal.initialized = true;
  }
  addDefaultCaseAcl(fabric, subjects) {
    const entry = {
      fabricIndex: fabric.fabricIndex,
      privilege: AccessControlTypes.AccessControlEntryPrivilege.Administer,
      authMode: AccessControlTypes.AccessControlEntryAuthMode.Case,
      subjects,
      targets: null
      // entire node
    };
    this.state.acl.push(entry);
    this.#updateFabricAcls(fabric);
    this.events.accessControlEntryChanged?.emit(
      {
        changeType: AccessControlTypes.ChangeType.Added,
        adminNodeId: null,
        // When we add it, it is always from a PASE session
        adminPasscodeId: 0,
        // When we add it, it is always from a PASE session
        latestValue: entry,
        fabricIndex: fabric.fabricIndex
      },
      this.context
    );
  }
  #validateAccessControlListChanges(value, _oldValue) {
    const { context } = this;
    if (!hasRemoteActor(context)) {
      return;
    }
    const relevantFabricIndex = context.session.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0) {
      return;
    }
    if (context !== void 0 && context.exchange !== void 0) {
      const delayedChangeExchange = this.internal.aclUpdateDelayed.get(relevantFabricIndex);
      if (delayedChangeExchange !== void 0 && delayedChangeExchange !== context.exchange) {
        logger.warn(
          "Decline parallel ACL changes from multiple exchanges",
          context.exchange.id,
          "vs.",
          delayedChangeExchange.id
        );
        throw new StatusResponseError("Parallel ACL change from multiple exchanges", StatusCode.Busy);
      }
    }
    const fabricAcls = value.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    if (fabricAcls.length > this.state.accessControlEntriesPerFabric) {
      throw new StatusResponseError("AccessControlEntriesPerFabric exceeded", StatusCode.ResourceExhausted);
    }
    for (const entry of fabricAcls) {
      if (entry.subjects !== null && entry.subjects.length === 0) {
        entry.subjects = null;
      }
      if (entry.targets !== null && entry.targets.length === 0) {
        entry.targets = null;
      }
      const { privilege, subjects, targets, authMode } = entry;
      if (subjects !== null && subjects.length > this.state.subjectsPerAccessControlEntry) {
        throw new StatusResponseError("SubjectsPerAccessControlEntry exceeded", StatusCode.ResourceExhausted);
      }
      if (targets !== null && targets.length > this.state.targetsPerAccessControlEntry) {
        throw new StatusResponseError("TargetsPerAccessControlEntry exceeded", StatusCode.ResourceExhausted);
      }
      if (authMode === AccessControlTypes.AccessControlEntryAuthMode.Pase) {
        throw new StatusResponseError("AuthMode for ACL must not be PASE", StatusCode.ConstraintError);
      } else if (authMode === AccessControlTypes.AccessControlEntryAuthMode.Case) {
        if (subjects !== null) {
          for (const subject of subjects) {
            if (NodeId.isCaseAuthenticatedTag(subject)) {
              const cat = NodeId.extractAsCaseAuthenticatedTag(subject);
              if (CaseAuthenticatedTag.getVersion(cat) === 0) {
                throw new StatusResponseError(
                  "CaseAuthenticatedTag version 0 is not allowed",
                  StatusCode.ConstraintError
                );
              }
            } else if (!NodeId.isOperationalNodeId(subject)) {
              throw new StatusResponseError(
                "Subject must be a valid OperationalNodeId or CaseAuthenticatedTag",
                StatusCode.ConstraintError
              );
            }
          }
        }
      } else if (authMode === AccessControlTypes.AccessControlEntryAuthMode.Group) {
        if (privilege === AccessControlTypes.AccessControlEntryPrivilege.Administer) {
          throw new StatusResponseError(
            "Group ACLs must not have Administer privilege",
            StatusCode.ConstraintError
          );
        }
        if (subjects !== null) {
          for (const subject of subjects) {
            if (GroupId(Number(subject)) === GroupId.NO_GROUP_ID) {
              throw new StatusResponseError(
                "Subject must be a valid GroupId for Group ACLs",
                StatusCode.ConstraintError
              );
            }
          }
        }
      }
      if (targets !== null) {
        for (const target of targets) {
          const { cluster, endpoint, deviceType } = target;
          if (deviceType !== null && endpoint !== null) {
            throw new StatusResponseError(
              "DeviceType and Endpoint are mutually exclusive",
              StatusCode.ConstraintError
            );
          }
          if (cluster === null && endpoint === null && deviceType === null) {
            throw new StatusResponseError("At least one field must be present", StatusCode.ConstraintError);
          }
          if (cluster !== null && !ClusterId.isValid(cluster)) {
            throw new StatusResponseError("Cluster must be a valid ClusterId", StatusCode.ConstraintError);
          }
          if (endpoint !== null && !EndpointNumber.isValid(endpoint)) {
            throw new StatusResponseError(
              "Endpoint must be a valid OperationalNodeId",
              StatusCode.ConstraintError
            );
          }
          if (deviceType !== null && !DeviceTypeId.isValid(deviceType)) {
            throw new StatusResponseError(
              "DeviceType must be a valid DeviceType",
              StatusCode.ConstraintError
            );
          }
        }
      }
    }
  }
  /**
   * Determine and return the adminPassCodeId and adminNodeId from the node that initiated the ACL change.
   * it is either the root node of the fabric or 0 if the session is undefined or a PASE session.
   */
  #adminDataFromSession(session) {
    if (session === void 0 || NodeSession.is(session) && session.isPase) {
      return { adminPasscodeId: 0, adminNodeId: null };
    }
    const adminNodeId = session?.associatedFabric.rootNodeId;
    if (adminNodeId === void 0) {
      throw new InternalError("Admin Node ID is undefined, should never happen");
    }
    return { adminPasscodeId: null, adminNodeId };
  }
  #handleAccessControlListChange(value, oldValue) {
    if (!this.internal.initialized) {
      return;
    }
    const session = hasRemoteActor(this.context) ? this.context.session : void 0;
    const relevantFabricIndex = session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0 || this.events.accessControlEntryChanged === void 0) {
      return;
    }
    const { adminPasscodeId, adminNodeId } = this.#adminDataFromSession(session);
    const fabricAcls = value.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    const oldFabricAcls = oldValue.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    let i = 0;
    for (; i < fabricAcls.length; i++) {
      const changeType = oldFabricAcls[i] === void 0 ? AccessControlTypes.ChangeType.Added : fabricAcls[i] === void 0 ? AccessControlTypes.ChangeType.Removed : AccessControlTypes.ChangeType.Changed;
      this.events.accessControlEntryChanged.emit(
        {
          changeType,
          adminNodeId,
          adminPasscodeId,
          latestValue: (changeType === AccessControlTypes.ChangeType.Removed ? oldFabricAcls[i] : fabricAcls[i]) ?? null,
          fabricIndex: relevantFabricIndex
        },
        this.context
      );
    }
    if (oldFabricAcls.length > i) {
      for (let j = oldFabricAcls.length - 1; j >= i; j--) {
        this.events.accessControlEntryChanged.emit(
          {
            changeType: AccessControlTypes.ChangeType.Removed,
            adminNodeId,
            adminPasscodeId,
            latestValue: oldValue[j],
            fabricIndex: relevantFabricIndex
          },
          this.context
        );
      }
    }
  }
  #validateAccessControlExtensionChanges(value) {
    if (!hasRemoteActor(this.context)) {
      return;
    }
    const relevantFabricIndex = this.context.session.associatedFabric.fabricIndex;
    const fabricExtensions = value.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    if (fabricExtensions.length === 0) {
      return;
    }
    if (fabricExtensions.length > 1) {
      throw new StatusResponseError("Extension list must contain a single entry", StatusCode.ConstraintError);
    }
    this.extensionEntryValidator(fabricExtensions[0]);
  }
  #handleAccessControlExtensionChange(value, oldValue) {
    if (!this.internal.initialized) {
      return;
    }
    const session = hasRemoteActor(this.context) ? this.context.session : void 0;
    const relevantFabricIndex = session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0 || this.events.accessControlExtensionChanged === void 0) {
      return;
    }
    const { adminPasscodeId, adminNodeId } = this.#adminDataFromSession(session);
    const fabricExtensions = value.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    const oldFabricExtensions = oldValue.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    const changeType = fabricExtensions.length > oldFabricExtensions.length ? AccessControlTypes.ChangeType.Added : fabricExtensions.length < oldFabricExtensions.length ? AccessControlTypes.ChangeType.Removed : AccessControlTypes.ChangeType.Changed;
    this.events.accessControlExtensionChanged.emit(
      {
        changeType,
        adminNodeId,
        adminPasscodeId,
        latestValue: (changeType === AccessControlTypes.ChangeType.Removed ? oldFabricExtensions[0] : fabricExtensions[0]) ?? null,
        fabricIndex: relevantFabricIndex
      },
      this.context
    );
  }
  /**
   * This method allows to implement the validation of manufacturer specific ACL extensions when an extension entry is
   * added or changed. The default implementation checks whether the extension is a valid TLV and possible to decode.
   *
   * In case of an Error throws StatusResponseError.
   *
   * Override this method in your own behavior to implement custom validation.
   */
  extensionEntryValidator(extension) {
    const { data } = extension;
    const extensionBytes = Bytes.of(data);
    if (extensionBytes.length < 2 || extensionBytes[0] !== TlvType.List || extensionBytes[extensionBytes.length - 1] !== TlvType.EndOfContainer) {
      throw new StatusResponseError("Extension must be a valid TLV", StatusCode.ConstraintError);
    }
    try {
      TlvTaggedList({}, true).decode(data);
    } catch (error) {
      logger.debug(`Extension TLV decoding failed:`, error);
      throw new StatusResponseError("Extension must be a valid TLV", StatusCode.ConstraintError);
    }
  }
  /**
   * This method allows to implement the validation of manufacturer specific ACL extensions when access control is
   * checked to decide if access is allowed or not.
   * The default implementation always returns true. Override this method in your own behavior to implement custom
   * validation.
   */
  extensionEntryAccessCheck(_aclList, _aclEntry, _subjectDesc, _endpoint, _clusterId) {
    return true;
  }
  /** A fabric was added or updated, so we need to initialize the ACL for this fabric */
  #updateFabricAcls(fabric) {
    const fabricIndex = fabric.fabricIndex;
    fabric.accessControl.aclList = deepCopy(this.state.acl).filter((entry) => entry.fabricIndex === fabricIndex);
  }
  /**
   * When beginning an interaction for an online session, we register the potential ACL change for the associated
   * fabric index. If ACL data are really changed later, the exchange gets added then.
   */
  #handleInteractionBegin(session) {
    if (hasRemoteActor(session)) {
      this.#prepareAclUpdateFor(session.fabric);
    }
  }
  /**
   * When an interaction is finished, we check if there was a delayed ACL update for the associated fabric and apply
   * it to the manager. For this we check if we have an exchange stored because otherwise the interaction was in fact
   * not changing the ACL.
   */
  #handleInteractionEnd(session) {
    if (hasRemoteActor(session)) {
      if (this.internal.aclUpdateDelayed.get(session.fabric) !== void 0) {
        this.#applyDelayedAclUpdateFor(session.fabric);
      }
    }
  }
  /** The ACL list was changed, so we need to determine if and when to apply the update to the ACL manager */
  #updateAccessControlList(acl, _oldAcl, context) {
    if (hasLocalActor(context)) {
      this.#updateAllFabricsAcls();
    } else {
      const fabric = context.session?.associatedFabric;
      if (fabric === void 0 || fabric.fabricIndex === void 0 || context.exchange === void 0) {
        throw new InternalError("We require a fabric bound online session to write ACL changes");
      }
      this.#handleFabricAclUpdate(fabric, acl, context.exchange);
    }
  }
  /**
   * Handles the ACL update for a specific fabric. If an exchange is present, we delay the update until the
   * interaction is finished.
   */
  #handleFabricAclUpdate(fabric, acl, exchange) {
    const fabricIndex = fabric.fabricIndex;
    if (this.internal.aclUpdateDelayed.has(fabricIndex)) {
      logger.debug(
        "ACL attribute updated, but interaction still in progress, delaying update of ACL manager for FabricIndex",
        fabricIndex
      );
      this.#delayAclUpdateFor(fabricIndex, exchange, acl);
    } else {
      logger.debug("ACL attribute updated, applying update to ACL manager", fabricIndex);
      fabric.accessControl.aclList = deepCopy(acl).filter((entry) => entry.fabricIndex === fabricIndex);
    }
  }
  #mapFabricAcls() {
    const acl = deepCopy(this.state.acl);
    const aclsForFabric = /* @__PURE__ */ new Map();
    for (const entry of acl) {
      const { fabricIndex } = entry;
      const acls = aclsForFabric.get(fabricIndex) ?? [];
      acls.push(entry);
      aclsForFabric.set(fabricIndex, acls);
    }
    return aclsForFabric;
  }
  /** Update all fabrics with the current ACL list */
  #updateAllFabricsAcls() {
    const aclsForFabric = this.#mapFabricAcls();
    const fabrics = this.env.get(FabricManager);
    for (const fabric of fabrics) {
      fabric.accessControl.aclList = aclsForFabric.get(fabric.fabricIndex) ?? [];
    }
  }
  /**
   * Register a potential change of ACL for a specific fabric index. if changes happened is checked when interaction
   * ends.
   */
  #prepareAclUpdateFor(fabricIndex) {
    if (!this.internal.aclUpdateDelayed.has(fabricIndex)) {
      logger.info("Register ACL update to be delayed for fabricIndex", fabricIndex);
      this.internal.aclUpdateDelayed.set(fabricIndex, void 0);
    }
  }
  /**
   * Register a concrete change of ACL for a specific fabric index. The exchange allows to also limit ACL changes to
   * that exchange until interaction is finished.
   */
  #delayAclUpdateFor(fabricIndex, exchange, acl) {
    if (!this.internal.aclUpdateDelayed.has(fabricIndex)) {
      logger.info("Register ACL update to be delayed for fabricIndex", fabricIndex);
    }
    this.internal.aclUpdateDelayed.set(fabricIndex, exchange);
    this.internal.delayedAclData.set(
      fabricIndex,
      deepCopy(acl).filter((entry) => entry.fabricIndex === fabricIndex)
    );
  }
  /** Applies the delayed ACL update for a specific fabric index, if existing */
  #applyDelayedAclUpdateFor(fabricIndex) {
    const updateDelayed = !!this.internal.aclUpdateDelayed.get(fabricIndex);
    const delayedData = this.internal.delayedAclData.get(fabricIndex);
    this.internal.delayedAclData.delete(fabricIndex);
    this.internal.aclUpdateDelayed.delete(fabricIndex);
    if (updateDelayed && delayedData !== void 0) {
      this.env.get(FabricManager).for(fabricIndex).accessControl.aclList = delayedData;
    }
  }
}
((AccessControlServer2) => {
  class Internal {
    /** Is the cluster logic initialized? Used to block events before full initialization. */
    initialized = false;
    /**
     * When an online and potentially chunked ACL writing happens, we will delay the update and store the exchange
     * used for the writing. With this we also verify that concurrent writes are blocked and will not mix the data.
     */
    aclUpdateDelayed = /* @__PURE__ */ new Map();
    /** Latest delayed data of acl attribute */
    delayedAclData = /* @__PURE__ */ new Map();
  }
  AccessControlServer2.Internal = Internal;
})(AccessControlServer || (AccessControlServer = {}));
export {
  AccessControlServer
};
//# sourceMappingURL=AccessControlServer.js.map
