/**
 * @license
 * Copyright 2022-2024 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { AccessControl as AccessControlTypes } from "#clusters/access-control";
import { deepCopy, InternalError, isDeepEqual, Logger } from "#general";
import { AccessLevel } from "#model";
import { AccessControlManager, FabricManager } 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 {
  initialize() {
    this.reactTo(this.events.acl$Changing, this.#validateAccessControlListChanges);
    this.reactTo(this.events.acl$Changed, this.#handleAccessControlListChange);
    if (this.state.extension !== void 0 && this.events.extension$Changing !== void 0 && this.events.extension$Changed !== void 0) {
      this.reactTo(this.events.extension$Changing, this.#validateAccessControlExtensionChanges);
      this.reactTo(this.events.extension$Changed, this.#handleAccessControlExtensionChange);
    }
    const lifecycle = this.endpoint.lifecycle;
    if (lifecycle.online !== void 0) {
      this.reactTo(lifecycle.online, this.#online);
    }
  }
  #online() {
    const fabrics = this.env.get(FabricManager).getFabrics();
    const acl = deepCopy(this.state.acl);
    const originalAclLength = acl.length;
    for (const fabric of fabrics) {
      if (!acl.some((entry) => entry.fabricIndex === fabric.fabricIndex)) {
        acl.push({
          fabricIndex: fabric.fabricIndex,
          privilege: AccessControlTypes.AccessControlEntryPrivilege.Administer,
          authMode: AccessControlTypes.AccessControlEntryAuthMode.Case,
          subjects: [fabric.rootNodeId],
          targets: null
          // entire node
        });
        logger.warn(
          "Added missing ACL entry for fabric",
          fabric.fabricIndex,
          "for Node ID",
          fabric.rootNodeId,
          ". This should only happen once after upgrading to matter.js 0.9.1"
        );
      }
    }
    if (acl.length > originalAclLength) {
      this.state.acl = acl;
    }
    logger.info("initializing ACL manager with ACL", acl);
    this.internal.aclManager = new AccessControlManager(
      acl,
      (aclList, aclEntry, subjectDesc, endpoint, clusterId) => this.extensionEntryAccessCheck(
        aclList,
        aclEntry,
        subjectDesc,
        endpoint,
        clusterId
      )
    );
    this.reactTo(this.events.acl$Changed, this.#updateAccessControlList);
  }
  #validateAccessControlListChanges(value) {
    const relevantFabricIndex = this.context.session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0) {
      return;
    }
    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) {
      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(subject) === GroupId.UNSPECIFIED_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
            );
          }
        }
      }
    }
  }
  #handleAccessControlListChange(value, oldValue) {
    if (this.internal.aclManager === void 0) {
      return;
    }
    const { session } = this.context;
    const relevantFabricIndex = session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0 || this.events.accessControlEntryChanged === void 0) {
      return;
    }
    const adminPasscodeId = session === void 0 || session?.isPase ? 0 : null;
    const adminNodeId = adminPasscodeId === null ? session?.associatedFabric.rootNodeId : null;
    if (adminNodeId === void 0) {
      return;
    }
    const fabricAcls = value.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    const oldFabricAcls = oldValue.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    let i = 0;
    for (; i < fabricAcls.length; i++) {
      if (!isDeepEqual(fabricAcls[i], oldFabricAcls[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) {
    const relevantFabricIndex = this.context.session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0) {
      return;
    }
    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.aclManager === void 0) {
      return;
    }
    const { session } = this.context;
    const relevantFabricIndex = session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0 || this.events.accessControlExtensionChanged === void 0) {
      return;
    }
    const adminPasscodeId = session === void 0 || session?.isPase ? 0 : null;
    const adminNodeId = adminPasscodeId === null ? session?.associatedFabric.rootNodeId : null;
    if (adminNodeId === void 0) {
      return;
    }
    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
    );
  }
  /**
   * Implements the access control check for the given context, location and endpoint and is called by the
   * InteractionServer. The method returns the list of granted Access privileges for the given context, location and
   * endpoint.
   */
  accessLevelsFor(context, location, endpoint) {
    if (location.cluster === void 0) {
      logger.warn("Access control check without cluster, returning View access level");
      return [AccessLevel.View];
    }
    if (context.session === void 0) {
      logger.warn("Access control check without session, returning View access level");
      return [AccessLevel.View];
    }
    if (endpoint === void 0) {
      logger.warn("Access control check without endpoint, returning View access level");
      return [AccessLevel.View];
    }
    return this.aclManager.getGrantedPrivileges(context.session, endpoint, location.cluster);
  }
  /**
   * 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;
    if (data.length < 2 || data[0] !== TlvType.List || data[data.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;
  }
  /**
   * The AccessControlManager instance that is used to manage the ACL for this behavior.
   */
  get aclManager() {
    if (this.internal.aclManager === void 0) {
      throw new InternalError("ACL manager not initialized yet");
    }
    return this.internal.aclManager;
  }
  #updateAccessControlList(acl) {
    if (!this.aclUpdateDelayed) {
      logger.info("ACL updated, updating ACL manager", acl);
      this.aclManager.updateAccessControlList(deepCopy(acl));
    } else {
      logger.info("ACL updated, but ACL manager update is delayed", acl);
      this.internal.delayedAclData = acl;
    }
  }
  /**
   * If set to true, the ACL will not be updated immediately when it changes, but only when the `aclUpdateDelayed`
   * property is set to false again.
   * This is a hack to prevent the ACL from updating while we are in the middle of a write transaction and will be
   * removed again once we somehow handle relevant sub transactions.
   */
  get aclUpdateDelayed() {
    return this.internal.aclUpdateDelayed;
  }
  /**
   * If set to true, the ACL will not be updated immediately when it changes, but only when the `aclUpdateDelayed`
   * property is set to false again.
   * This is a hack to prevent the ACL from updating while we are in the middle of a write transaction and will be
   * removed again once we somehow handle relevant sub transactions.
   */
  set aclUpdateDelayed(value) {
    logger.info("Setting ACL update delayed to", value);
    if (!value) {
      this.#updateDelayedAccessControlList();
    }
    this.internal.aclUpdateDelayed = value;
  }
  #updateDelayedAccessControlList() {
    if (this.internal.delayedAclData === void 0) {
      return;
    }
    const delayedData = deepCopy(this.internal.delayedAclData);
    this.internal.delayedAclData = void 0;
    logger.info("Updating ACL manager with ACL", delayedData);
    this.aclManager.updateAccessControlList(delayedData);
  }
}
((AccessControlServer2) => {
  class Internal {
    /** AccessControlManager instance that is used to manage the ACL checks for this device. */
    aclManager;
    /** If set to true ACL updates are delayed while in a write transaction. More details see getter/setter above. */
    aclUpdateDelayed = false;
    /** Latest delayed data of acl */
    delayedAclData;
  }
  AccessControlServer2.Internal = Internal;
})(AccessControlServer || (AccessControlServer = {}));
export {
  AccessControlServer
};
//# sourceMappingURL=AccessControlServer.js.map
