/**
 * @license
 * Copyright 2022-2024 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Val } from "#behavior/state/Val.js";
import { CommissioningBehavior } from "#behavior/system/commissioning/CommissioningBehavior.js";
import { ProductDescriptionServer } from "#behavior/system/product-description/ProductDescriptionServer.js";
import { AccessControlServer } from "#behaviors/access-control";
import { AccessControl } from "#clusters/access-control";
import { OperationalCredentials } from "#clusters/operational-credentials";
import { CryptoVerifyError, Logger, MatterFlowError, UnexpectedDataError } from "#general";
import { AccessLevel } from "#model";
import {
  assertSecureSession,
  CertificateError,
  DeviceCertification,
  DeviceCommissioner,
  FabricAction,
  FabricManager,
  FabricTableFullError,
  MatterFabricConflictError,
  MatterFabricInvalidAdminSubjectError,
  PublicKeyError,
  TlvAttestation,
  TlvCertSigningRequest
} from "#protocol";
import {
  Command,
  FabricIndex,
  StatusCode,
  StatusResponseError,
  TlvBoolean,
  TlvByteString,
  TlvField,
  TlvObject,
  TlvOptionalField,
  ValidationError
} from "#types";
import { OperationalCredentialsBehavior } from "./OperationalCredentialsBehavior.js";
const logger = Logger.get("OperationalCredentials");
OperationalCredentials.Cluster.commands = {
  ...OperationalCredentials.Cluster.commands,
  attestationRequest: Command(
    0,
    TlvObject({ attestationNonce: TlvField(0, TlvByteString) }),
    1,
    OperationalCredentials.TlvAttestationResponse,
    { invokeAcl: AccessLevel.Administer }
  ),
  csrRequest: Command(
    4,
    TlvObject({
      csrNonce: TlvField(0, TlvByteString),
      isForUpdateNoc: TlvOptionalField(1, TlvBoolean)
    }),
    5,
    OperationalCredentials.TlvCsrResponse,
    { invokeAcl: AccessLevel.Administer }
  )
};
class OperationalCredentialsServer extends OperationalCredentialsBehavior {
  initialize() {
    if (this.state.supportedFabrics === void 0) {
      this.state.supportedFabrics = 254;
    }
    this.state.commissionedFabrics = this.state.fabrics.length;
    this.reactTo(this.endpoint.lifecycle.online, this.#nodeOnline);
  }
  async attestationRequest({ attestationNonce }) {
    if (attestationNonce.length !== 32) {
      throw new StatusResponseError("Invalid attestation nonce length", StatusCode.InvalidCommand);
    }
    const certification = await this.getCertification();
    const elements = TlvAttestation.encode({
      declaration: certification.declaration,
      attestationNonce,
      timestamp: 0
    });
    return {
      attestationElements: elements,
      attestationSignature: certification.sign(this.session, elements)
    };
  }
  async csrRequest({ csrNonce, isForUpdateNoc }) {
    if (csrNonce.length !== 32) {
      throw new StatusResponseError("Invalid csr nonce length", StatusCode.InvalidCommand);
    }
    if (isForUpdateNoc && this.session.isPase) {
      throw new StatusResponseError(
        "csrRequest for UpdateNoc received on a PASE session.",
        StatusCode.InvalidCommand
      );
    }
    const commissioner = this.env.get(DeviceCommissioner);
    const failsafeContext = commissioner.failsafeContext;
    if (failsafeContext.fabricIndex !== void 0) {
      throw new StatusResponseError(
        `csrRequest received after ${failsafeContext.forUpdateNoc ? "UpdateNOC" : "AddNOC"} already invoked.`,
        StatusCode.ConstraintError
      );
    }
    const certification = await this.getCertification();
    const certSigningRequest = failsafeContext.createCertificateSigningRequest(
      isForUpdateNoc ?? false,
      this.session.id
    );
    const nocsrElements = TlvCertSigningRequest.encode({ certSigningRequest, csrNonce });
    return { nocsrElements, attestationSignature: certification.sign(this.session, nocsrElements) };
  }
  async certificateChainRequest({ certificateType }) {
    const certification = await this.getCertification();
    switch (certificateType) {
      case OperationalCredentials.CertificateChainType.DacCertificate:
        return { certificate: certification.certificate };
      case OperationalCredentials.CertificateChainType.PaiCertificate:
        return { certificate: certification.intermediateCertificate };
      default:
        throw new StatusResponseError(
          `Unsupported certificate type: ${certificateType}`,
          StatusCode.InvalidCommand
        );
    }
  }
  #mapNocErrors(error) {
    if (error instanceof MatterFabricConflictError) {
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.FabricConflict,
        debugText: error.message
      };
    } else if (error instanceof FabricTableFullError) {
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.TableFull,
        debugText: error.message
      };
    } else if (error instanceof CryptoVerifyError || error instanceof CertificateError || error instanceof ValidationError || error instanceof UnexpectedDataError) {
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.InvalidNoc,
        debugText: error.message
      };
    } else if (error instanceof PublicKeyError) {
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.InvalidPublicKey,
        debugText: error.message
      };
    } else if (error instanceof MatterFabricInvalidAdminSubjectError) {
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.InvalidAdminSubject,
        debugText: error.message
      };
    }
    throw error;
  }
  async addNoc({
    nocValue,
    icacValue,
    ipkValue,
    caseAdminSubject,
    adminVendorId
  }) {
    const failsafeContext = this.#failsafeContext;
    if (failsafeContext.fabricIndex !== void 0) {
      throw new StatusResponseError(
        `addNoc received after ${failsafeContext.forUpdateNoc ? "UpdateNOC" : "AddNOC"} already invoked.`,
        StatusCode.ConstraintError
      );
    }
    if (!failsafeContext.hasRootCert) {
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.InvalidNoc,
        debugText: "Root certificate not found."
      };
    }
    if (failsafeContext.csrSessionId !== this.session.id) {
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.MissingCsr,
        debugText: "CSR not found in failsafe context."
      };
    }
    if (failsafeContext.forUpdateNoc) {
      throw new StatusResponseError(
        `addNoc received after csr request was invoked for UpdateNOC.`,
        StatusCode.ConstraintError
      );
    }
    const state = this.state;
    if (state.commissionedFabrics >= state.supportedFabrics) {
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.TableFull,
        debugText: `No more fabrics can be added because limit ${state.supportedFabrics} reached.`
      };
    }
    let fabric;
    try {
      fabric = await failsafeContext.buildFabric({
        nocValue,
        icacValue,
        adminVendorId,
        ipkValue,
        caseAdminSubject
      });
    } catch (error) {
      logger.info("Building fabric for addNoc failed", error);
      return this.#mapNocErrors(error);
    }
    const aclCluster = this.agent.get(AccessControlServer);
    aclCluster.state.acl.push({
      fabricIndex: fabric.fabricIndex,
      privilege: AccessControl.AccessControlEntryPrivilege.Administer,
      authMode: AccessControl.AccessControlEntryAuthMode.Case,
      subjects: [caseAdminSubject],
      targets: null
      // entire node
    });
    await failsafeContext.addFabric(fabric);
    try {
      if (this.session.isPase) {
        logger.debug(`Add Fabric ${fabric.fabricIndex} to PASE session ${this.session.name}.`);
        this.session.addAssociatedFabric(fabric);
      }
      const existingFabricIndex = this.state.fabrics.findIndex((f) => f.fabricIndex === fabric.fabricIndex);
      const existingNocIndex = this.state.nocs.findIndex((n) => n.fabricIndex === fabric.fabricIndex);
      if (existingFabricIndex !== -1 || existingNocIndex !== -1) {
        throw new MatterFlowError(
          `FabricIndex ${fabric.fabricIndex} already exists in state. This should not happen.`
        );
      }
    } catch (e) {
      await fabric.remove(this.session.id);
      throw e;
    }
    logger.info(`addNoc success, adminVendorId ${adminVendorId}, caseAdminSubject ${caseAdminSubject}`);
    return {
      statusCode: OperationalCredentials.NodeOperationalCertStatus.Ok,
      fabricIndex: fabric.fabricIndex
    };
  }
  async updateNoc({ nocValue, icacValue }) {
    assertSecureSession(this.session);
    const timedOp = this.#failsafeContext;
    if (timedOp.fabricIndex !== void 0) {
      throw new StatusResponseError(
        `updateNoc received after ${timedOp.forUpdateNoc ? "UpdateNOC" : "AddNOC"} already invoked.`,
        StatusCode.ConstraintError
      );
    }
    if (timedOp.forUpdateNoc) {
      throw new StatusResponseError(
        `addNoc received after csr request was invoked for UpdateNOC.`,
        StatusCode.ConstraintError
      );
    }
    if (timedOp.hasRootCert) {
      throw new StatusResponseError(
        "Trusted root certificate added in this session which is now allowed for UpdateNOC.",
        StatusCode.ConstraintError
      );
    }
    if (!timedOp.forUpdateNoc) {
      throw new StatusResponseError("csrRequest not invoked for UpdateNOC.", StatusCode.ConstraintError);
    }
    if (this.session.associatedFabric.fabricIndex !== timedOp.associatedFabric?.fabricIndex) {
      throw new StatusResponseError(
        "Fabric of this session and the failsafe context do not match.",
        StatusCode.ConstraintError
      );
    }
    try {
      const updateFabric = await timedOp.buildUpdatedFabric(nocValue, icacValue);
      await timedOp.updateFabric(updateFabric);
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.Ok,
        fabricIndex: updateFabric.fabricIndex
      };
    } catch (error) {
      logger.info("Building fabric for updateNoc failed", error);
      return this.#mapNocErrors(error);
    }
  }
  async updateFabricLabel({ label }) {
    const fabric = this.session.associatedFabric;
    const currentFabricIndex = fabric.fabricIndex;
    const fabrics = this.env.get(FabricManager);
    const conflictingLabelFabric = fabrics.getFabrics().find((f) => f.label === label && f.fabricIndex !== currentFabricIndex);
    if (conflictingLabelFabric !== void 0) {
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.LabelConflict,
        debugText: `Label ${label} already used by fabric ${conflictingLabelFabric.fabricIndex}`
      };
    }
    await fabric.setLabel(label);
    return { statusCode: OperationalCredentials.NodeOperationalCertStatus.Ok, fabricIndex: fabric.fabricIndex };
  }
  async removeFabric({ fabricIndex }) {
    const fabric = this.env.get(FabricManager).findByIndex(fabricIndex);
    if (fabric === void 0) {
      return {
        statusCode: OperationalCredentials.NodeOperationalCertStatus.InvalidFabricIndex,
        debugText: `Fabric ${fabricIndex} not found`
      };
    }
    await fabric.remove(this.session.id);
    return {
      statusCode: OperationalCredentials.NodeOperationalCertStatus.Ok,
      fabricIndex
    };
  }
  addTrustedRootCertificate({ rootCaCertificate }) {
    const failsafeContext = this.#failsafeContext;
    if (failsafeContext.hasRootCert) {
      throw new StatusResponseError(
        "Trusted root certificate already added in this FailSafe context.",
        StatusCode.ConstraintError
      );
    }
    if (failsafeContext.fabricIndex !== void 0) {
      throw new StatusResponseError(
        `Cannot add trusted root certificates after ${failsafeContext.forUpdateNoc ? "UpdateNOC" : "AddNOC"}.`,
        StatusCode.ConstraintError
      );
    }
    try {
      failsafeContext.setRootCert(rootCaCertificate);
    } catch (error) {
      logger.info("setting root certificate failed", error);
      if (error instanceof CryptoVerifyError || error instanceof CertificateError || error instanceof ValidationError || error instanceof UnexpectedDataError) {
        throw new StatusResponseError(error.message, StatusCode.InvalidCommand);
      }
      throw error;
    }
    const fabrics = this.env.get(FabricManager).getFabrics();
    const trustedRootCertificates = fabrics.map((fabric) => fabric.rootCert);
    trustedRootCertificates.push(rootCaCertificate);
    this.state.trustedRootCertificates = trustedRootCertificates;
  }
  async #updateFabrics() {
    const fabrics = this.env.get(FabricManager).getFabrics();
    this.state.fabrics = fabrics.map((fabric) => ({
      fabricId: fabric.fabricId,
      label: fabric.label,
      nodeId: fabric.nodeId,
      rootPublicKey: fabric.rootPublicKey,
      vendorId: fabric.rootVendorId,
      fabricIndex: fabric.fabricIndex
    }));
    this.state.nocs = fabrics.map((fabric) => ({
      noc: fabric.operationalCert,
      icac: fabric.intermediateCACert ?? null,
      fabricIndex: fabric.fabricIndex
    }));
    this.state.trustedRootCertificates = fabrics.map((fabric) => fabric.rootCert);
    this.state.commissionedFabrics = fabrics.length;
    await this.context.transaction.commit();
  }
  async getCertification() {
    const certification = this.internal.certification ?? (this.internal.certification = new DeviceCertification(
      this.state.certification,
      this.agent.get(ProductDescriptionServer).state
    ));
    await certification.construction;
    return certification;
  }
  async #handleAddedFabric({ fabricIndex }) {
    await this.#updateFabrics();
    this.agent.get(CommissioningBehavior).handleFabricChange(fabricIndex, FabricAction.Added);
  }
  async #handleUpdatedFabric({ fabricIndex }) {
    await this.#updateFabrics();
    this.agent.get(CommissioningBehavior).handleFabricChange(fabricIndex, FabricAction.Updated);
  }
  async #handleRemovedFabric({ fabricIndex }) {
    await this.#updateFabrics();
    this.agent.get(CommissioningBehavior).handleFabricChange(fabricIndex, FabricAction.Removed);
  }
  async #handleFailsafeClosed() {
    await this.#updateFabrics();
  }
  async #nodeOnline() {
    const fabricManager = this.env.get(FabricManager);
    this.reactTo(fabricManager.events.added, this.#handleAddedFabric, { lock: true });
    this.reactTo(fabricManager.events.updated, this.#handleUpdatedFabric, { lock: true });
    this.reactTo(fabricManager.events.deleted, this.#handleRemovedFabric, { lock: true });
    this.reactTo(fabricManager.events.failsafeClosed, this.#handleFailsafeClosed, { lock: true });
    await this.#updateFabrics();
  }
  get #failsafeContext() {
    return this.env.get(DeviceCommissioner).failsafeContext;
  }
}
((OperationalCredentialsServer2) => {
  class Internal {
    certification;
    commissionedFabric;
  }
  OperationalCredentialsServer2.Internal = Internal;
  class State extends OperationalCredentialsBehavior.State {
    /**
     * Device certification information.
     *
     * Device certification provides a cryptographic certificate that asserts the official status of a device.
     * Production consumer-facing devices are certified by the CSA.
     *
     * Development devices and those intended for personal use may use a development certificate.  This is the
     * default if you do not provide an official certification in {@link ServerOptions.certification}.
     */
    certification = void 0;
    [Val.properties](_endpoint, session) {
      return {
        get currentFabricIndex() {
          return session.fabric ?? FabricIndex.NO_FABRIC;
        }
      };
    }
  }
  OperationalCredentialsServer2.State = State;
})(OperationalCredentialsServer || (OperationalCredentialsServer = {}));
export {
  OperationalCredentialsServer
};
//# sourceMappingURL=OperationalCredentialsServer.js.map
