// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
  AbstractModule,
  AsHandler,
  type BootstrapConfig,
  type CallAction,
  ErrorCode,
  EventGroup,
  type HandlerProperties,
  type ICache,
  type IFileStorage,
  type IMessage,
  type IMessageHandler,
  type IMessageSender,
  MessageOrigin,
  OCPP_CallAction,
  OCPP_2_VER_LIST,
  OcppError,
  OCPPValidator,
  type SystemConfig,
  OCPP2_response_types,
  OCPP2_request_types,
  OCPP2_common_types,
  type CertificateUseEnumType,
  type CertificateSigningUseEnumType,
  type InstallCertificateStatusEnumType,
  Iso15118EVCertificateStatusEnum,
  GetCertificateStatusEnum,
  GenericStatusEnum,
  GetInstalledCertificateStatusEnum,
  DeleteCertificateStatusEnum,
  CertificateSigningUseEnum,
  AttributeEnum,
  OCPPVersion,
  OCPP2_1,
} from '@citrineos/base';
import type {
  ICertificateRepository,
  IDeleteCertificateAttemptRepository,
  IDeviceModelRepository,
  IInstallCertificateAttemptRepository,
  IInstalledCertificateRepository,
  IOCPPMessageRepository,
} from '@dal/interfaces/repositories.js';
import {
  InstalledCertificate,
  SequelizeOCPPMessageRepository,
} from '@dal/layers/sequelize/index.js';
import { sequelize } from '@dal/index.js';
import {
  parseCSRForVerification,
  sendOCSPRequest,
  validatePEMEncodedCSR,
  WebsocketNetworkConnection,
  CertificateAuthorityService,
} from '@util/index.js';
import { Crypto } from '@peculiar/webcrypto';
import jsrsasign from 'jsrsasign';
import * as pkijs from 'pkijs';
import { CertificationRequest } from 'pkijs';
import type { ILogObj } from 'tslog';
import { Logger } from 'tslog';
import { InstallCertificateHelperService } from './installCertificateHelperService.js';

const cryptoEngine = new pkijs.CryptoEngine({
  crypto: new Crypto(),
});
pkijs.setEngine('crypto', cryptoEngine as pkijs.ICryptoEngine);

/**
 * Component that handles provisioning related messages.
 */
export class CertificatesModule extends AbstractModule {
  /**
   * Fields
   */

  _requests: CallAction[] = [];

  _responses: CallAction[] = [];

  protected _deviceModelRepository: IDeviceModelRepository;
  protected _certificateRepository: ICertificateRepository;
  protected _installedCertificateRepository: IInstalledCertificateRepository;
  protected _installCertificateAttemptRepository: IInstallCertificateAttemptRepository;
  protected _deleteCertificateAttemptRepository: IDeleteCertificateAttemptRepository;
  protected _ocppMessageRepository: IOCPPMessageRepository;
  protected _certificateAuthorityService: CertificateAuthorityService;
  protected _fileStorage: IFileStorage;
  protected _installCertificateHelperService: InstallCertificateHelperService;

  /**
   * Constructor
   */

  /**
   * This is the constructor function that initializes the {@link CertificatesModule}.
   *
   * @param {BootstrapConfig & SystemConfig} config - The `config` contains configuration settings for the module.
   *
   * @param {ICache} [cache] - The cache instance which is shared among the modules & Central System to pass information such as blacklisted actions or boot status.
   *
   * @param {IMessageSender} [sender] - The `sender` parameter is an optional parameter that represents an instance of the {@link IMessageSender} interface.
   * It is used to send messages from the central system to external systems or devices. If no `sender` is provided, a default {@link RabbitMqSender} instance is created and used.
   *
   * @param {IMessageHandler} [handler] - The `handler` parameter is an optional parameter that represents an instance of the {@link IMessageHandler} interface.
   * It is used to handle incoming messages and dispatch them to the appropriate methods or functions. If no `handler` is provided, a default {@link RabbitMqReceiver} instance is created and used.
   *
   * @param {IFileStorage} [fileStorage]  - file storage for persisting certs
   *
   * @param {WebsocketNetworkConnection} [networkConnection] - network connection
   *
   * @param {Logger<ILogObj>} [logger] - The `logger` parameter is an optional parameter that represents an instance of {@link Logger<ILogObj>}.
   * It is used to propagate system wide logger settings and will serve as the parent logger for any sub-component logging. If no `logger` is provided, a default {@link Logger<ILogObj>} instance is created and used.
   *
   * @param {IDeviceModelRepository} [deviceModelRepository] - An optional parameter of type {@link IDeviceModelRepository} which represents a repository for accessing and manipulating variable data.
   * If no `deviceModelRepository` is provided, a default {@link sequelize.deviceModelRepository} instance is created and used.
   *
   * @param {ICertificateRepository} [certificateRepository] - An optional parameter of type {@link ICertificateRepository} which
   * represents a repository for accessing and manipulating variable data.
   * If no `deviceModelRepository` is provided, a default {@link sequelize.certificateRepository} instance is created and used.
   *
   * @param {IInstalledCertificateRepository} [installedCertificateRepository] - An optional parameter of type {@link IInstalledCertificateRepository} which
   * represents a repository for accessing and manipulating installed certificate data.
   * If no `installedCertificateRepository` is provided, a default {@link sequelize.InstalledCertificateRepository} instance is created and used.
   *
   * @param {IInstallCertificateAttemptRepository} [installCertificateAttemptRepository] - An optional parameter of type {@link IInstallCertificateAttemptRepository} which
   * represents a repository for accessing and manipulating installed certificate attempt data.
   * If no `installCertificateAttemptRepository` is provided, a default {@link sequelize.InstallCertificateAttemptRepository} instance is created and used.
   *
   * @param {IDeleteCertificateAttemptRepository} [deleteCertificateAttemptRepository] - An optional parameter of type {@link IDeleteCertificateAttemptRepository} which
   * represents a repository for accessing and manipulating deleted certificate attempt data.
   * If no `deleteCertificateAttemptRepository` is provided, a default {@link sequelize.DeleteCertificateAttemptRepository} instance is created and used.
   *
   * @param {IOCPPMessageRepository} [ocppMessageRepository] - repository to check ocpp messages
   *
   * @param {CertificateAuthorityService} [certificateAuthorityService] - An optional parameter of type {@link CertificateAuthorityService} which handles certificate authority operations.
   *
   * @param {InstallCertificateHelperService} [installCertificateHelperService] - helper service for installing certificates
   */
  constructor(
    config: BootstrapConfig & SystemConfig,
    cache: ICache,
    sender: IMessageSender,
    handler: IMessageHandler,
    fileStorage: IFileStorage,
    networkConnection: WebsocketNetworkConnection,
    logger?: Logger<ILogObj>,
    ocppValidator?: OCPPValidator,
    deviceModelRepository?: IDeviceModelRepository,
    certificateRepository?: ICertificateRepository,
    installedCertificateRepository?: IInstalledCertificateRepository,
    installCertificateAttemptRepository?: IInstallCertificateAttemptRepository,
    deleteCertificateAttemptRepository?: IDeleteCertificateAttemptRepository,
    ocppMessageRepository?: IOCPPMessageRepository,
    certificateAuthorityService?: CertificateAuthorityService,
    installCertificateHelperService?: InstallCertificateHelperService,
  ) {
    super(config, cache, handler, sender, EventGroup.Certificates, logger, ocppValidator);

    this._requests = config.modules.certificates?.requests ?? [];
    this._responses = config.modules.certificates?.responses ?? [];
    this._fileStorage = fileStorage;

    this._deviceModelRepository =
      deviceModelRepository || new sequelize.SequelizeDeviceModelRepository(config, logger);
    this._certificateRepository =
      certificateRepository || new sequelize.SequelizeCertificateRepository(config, logger);
    this._installedCertificateRepository =
      installedCertificateRepository ||
      new sequelize.SequelizeInstalledCertificateRepository(config, logger);
    this._installCertificateAttemptRepository =
      installCertificateAttemptRepository ||
      new sequelize.SequelizeInstallCertificateAttemptRepository(config, logger);
    this._deleteCertificateAttemptRepository =
      deleteCertificateAttemptRepository ||
      new sequelize.SequelizeDeleteCertificateAttemptRepository(config, logger);
    this._ocppMessageRepository =
      ocppMessageRepository || new SequelizeOCPPMessageRepository(config, this._logger);
    this._certificateAuthorityService =
      certificateAuthorityService || new CertificateAuthorityService(config, cache, this._logger);

    this._installCertificateHelperService =
      installCertificateHelperService ||
      new InstallCertificateHelperService(
        this._certificateRepository,
        this._installedCertificateRepository,
        this._installCertificateAttemptRepository,
        this._deleteCertificateAttemptRepository,
        this._certificateAuthorityService,
        networkConnection,
        this._fileStorage,
        this._logger,
      );
  }

  get certificateAuthorityService(): CertificateAuthorityService {
    return this._certificateAuthorityService;
  }

  get certificateRepository(): ICertificateRepository {
    return this._certificateRepository;
  }

  get installedCertificateRepository(): IInstalledCertificateRepository {
    return this._installedCertificateRepository;
  }

  get deleteCertificateAttemptRepository(): IDeleteCertificateAttemptRepository {
    return this._deleteCertificateAttemptRepository;
  }

  get installCertificateHelperService(): InstallCertificateHelperService {
    return this._installCertificateHelperService;
  }

  /**
   * Handle requests
   */

  @AsHandler(OCPP_2_VER_LIST, OCPP_CallAction.Get15118EVCertificate)
  protected async _handleGet15118EVCertificate(
    message: IMessage<OCPP2_request_types.Get15118EVCertificateRequest>,
    props?: HandlerProperties,
  ): Promise<void> {
    this._logger.debug('Get15118EVCertificate received:', message, props);
    const request: OCPP2_request_types.Get15118EVCertificateRequest = message.payload;

    try {
      const exiResponse = await this._certificateAuthorityService.getSignedContractData(
        request.iso15118SchemaVersion,
        request.exiRequest,
      );
      await this.sendCallResultWithMessage(message, {
        status: Iso15118EVCertificateStatusEnum.Accepted,
        exiResponse: exiResponse,
      } as OCPP2_response_types.Get15118EVCertificateResponse);
    } catch (error) {
      await this.sendCallResultWithMessage(message, {
        status: Iso15118EVCertificateStatusEnum.Failed,
        statusInfo: {
          reasonCode: ErrorCode.GenericError,
          additionalInfo: error instanceof Error ? error.message : undefined,
        },
        exiResponse: '',
      } as OCPP2_response_types.Get15118EVCertificateResponse);
    }
  }

  @AsHandler(OCPP_2_VER_LIST, OCPP_CallAction.GetCertificateStatus)
  protected async _handleGetCertificateStatus(
    message: IMessage<OCPP2_request_types.GetCertificateStatusRequest>,
    props?: HandlerProperties,
  ): Promise<void> {
    this._logger.debug('GetCertificateStatusRequest received:', message, props);
    const reqData = message.payload.ocspRequestData;
    try {
      const ocspRequest = new jsrsasign.KJUR.asn1.ocsp.Request({
        alg: reqData.hashAlgorithm,
        keyhash: reqData.issuerKeyHash,
        namehash: reqData.issuerNameHash,
        serial: reqData.serialNumber,
      });
      const ocspResponse = await sendOCSPRequest(ocspRequest, reqData.responderURL);
      await this.sendCallResultWithMessage(message, {
        status: GetCertificateStatusEnum.Accepted,
        ocspResponse: ocspResponse,
      } as OCPP2_response_types.GetCertificateStatusResponse);
    } catch (error) {
      this._logger.error(`GetCertificateStatus failed: ${error}`);
      await this.sendCallResultWithMessage(message, {
        status: GetCertificateStatusEnum.Failed,
        statusInfo: { reasonCode: ErrorCode.GenericError },
      } as OCPP2_response_types.GetCertificateStatusResponse);
    }
  }

  @AsHandler(OCPP_2_VER_LIST, OCPP_CallAction.SignCertificate)
  protected async _handleSignCertificate(
    message: IMessage<OCPP2_request_types.SignCertificateRequest>,
    props?: HandlerProperties,
  ): Promise<void> {
    this._logger.debug('Sign certificate request received:', message, props);
    const tenantId = message.context.tenantId;
    const ocppConnectionName: string = message.context.ocppConnectionName;
    const csrString: string = message.payload.csr.replace(/\n/g, '');
    const certificateType: CertificateSigningUseEnumType | undefined | null =
      message.payload.certificateType;
    let requestId: number | undefined | null;
    if (message.protocol === OCPPVersion.OCPP2_1) {
      const payload21 = message.payload as OCPP2_1.SignCertificateRequest;
      requestId = payload21.requestId;
    }

    // Validate PEM format
    const validationResult = validatePEMEncodedCSR(message.payload.csr);
    if (!validationResult.isValid) {
      this._logger.warn(`Invalid CSR format: ${validationResult.errorMessage}`);
      await this.sendCallErrorWithMessage(
        message,
        new OcppError(
          message.context.correlationId,
          ErrorCode.FormatViolation,
          'Invalid CSR format.',
        ),
      );
      return;
    }

    // TODO OCTT Currently fails the CSMS on test case TC_A_14_CSMS if an invalid csr is rejected
    //  Despite explicitly saying in the protocol "The CSMS may do some checks on the CSR"
    //  So it is necessary to accept before checking the csr. when this is fixed, this line can be removed
    //  And the other sendCallResultWithMessage for SignCertificateResponse can be uncommented
    await this.sendCallResultWithMessage(message, {
      status: GenericStatusEnum.Accepted,
    } as OCPP2_response_types.SignCertificateResponse);

    let certificateChainPem: string;
    try {
      await this._verifySignCertRequest(csrString, tenantId, ocppConnectionName, certificateType);

      certificateChainPem = await this._certificateAuthorityService.getCertificateChain(
        csrString,
        ocppConnectionName,
        certificateType,
      );
    } catch (error) {
      this._logger.error('Sign certificate failed:', error);

      // TODO uncomment after OCTT issue is fixed
      // this.sendCallResultWithMessage(message, {
      //   status: GenericStatusEnumType.Rejected,
      //   statusInfo: {
      //     reasonCode: ErrorCode.GenericError,
      //     additionalInfo: error instanceof Error ? error.message : undefined,
      //   },
      // } as SignCertificateResponse);

      return;
    }

    // TODO uncomment after OCTT issue is fixed
    // this.sendCallResultWithMessage(message, {
    //   status: GenericStatusEnumType.Accepted,
    // } as SignCertificateResponse);

    await this.installCertificateHelperService.prepareToInstallCertificate(
      tenantId,
      ocppConnectionName,
      certificateChainPem,
      certificateType as unknown as CertificateUseEnumType,
      requestId,
    );

    const certSignedRequest = {
      certificateChain: certificateChainPem,
      certificateType: certificateType,
    } as OCPP2_request_types.CertificateSignedRequest;

    if (message.protocol === OCPPVersion.OCPP2_1 && requestId != null) {
      (certSignedRequest as OCPP2_1.CertificateSignedRequest).requestId = requestId;
    }

    await this.sendCall(
      ocppConnectionName,
      tenantId,
      message.protocol,
      OCPP_CallAction.CertificateSigned,
      certSignedRequest,
    );
  }

  /**
   * Handle responses
   */

  @AsHandler(OCPP_2_VER_LIST, OCPP_CallAction.CertificateSigned)
  protected async _handleCertificateSigned(
    message: IMessage<OCPP2_response_types.CertificateSignedResponse>,
    props?: HandlerProperties,
  ): Promise<void> {
    this._logger.debug('CertificateSigned received:', message, props);
    const tenantId = message.context.tenantId;
    const ocppConnectionName = message.context.ocppConnectionName;

    let requestId: number | undefined;
    if (message.protocol === OCPPVersion.OCPP2_1) {
      const originalRequest = await this._ocppMessageRepository.readOnlyOneByQuery(tenantId, {
        where: {
          ocppConnectionName,
          correlationId: message.context.correlationId,
          origin: MessageOrigin.ChargingStationManagementSystem,
        },
      });
      if (originalRequest) {
        const certSignedPayload = originalRequest.message[3] as OCPP2_1.CertificateSignedRequest;
        requestId = certSignedPayload?.requestId ?? undefined;
      }
    }

    await this.installCertificateHelperService.finalizeInstalledCertificate(
      tenantId,
      ocppConnectionName,
      message.payload.status as unknown as InstallCertificateStatusEnumType,
      requestId,
    );
    // TODO: If rejected, retry and/or send to callbackUrl if originally part of a triggered refresh
    // TODO: If accepted, revoke old certificate
  }

  @AsHandler(OCPP_2_VER_LIST, OCPP_CallAction.DeleteCertificate)
  protected async _handleDeleteCertificate(
    message: IMessage<OCPP2_response_types.DeleteCertificateResponse>,
    props?: HandlerProperties,
  ): Promise<void> {
    this._logger.debug('DeleteCertificate received:', message, props);
    const tenantId = message.context.tenantId;
    const ocppConnectionName = message.context.ocppConnectionName;
    const existingPendingDeleteCertificateAttempt =
      await this.deleteCertificateAttemptRepository.readOnlyOneByQuery(tenantId, {
        where: {
          ocppConnectionName: ocppConnectionName,
          status: null,
        },
      });
    // should always be true
    if (existingPendingDeleteCertificateAttempt) {
      existingPendingDeleteCertificateAttempt.status = message.payload.status;
      await existingPendingDeleteCertificateAttempt.save();
      if (existingPendingDeleteCertificateAttempt.status === DeleteCertificateStatusEnum.Accepted) {
        const existingInstalledCertificates =
          await this.installedCertificateRepository.readAllByQuery(tenantId, {
            where: {
              ocppConnectionName: ocppConnectionName,
              hashAlgorithm: existingPendingDeleteCertificateAttempt.hashAlgorithm,
              issuerNameHash: existingPendingDeleteCertificateAttempt.issuerNameHash,
              issuerKeyHash: existingPendingDeleteCertificateAttempt.issuerKeyHash,
              serialNumber: existingPendingDeleteCertificateAttempt.serialNumber,
            },
          });
        // should always be true
        if (existingInstalledCertificates) {
          for (const existingInstalledCertificate of existingInstalledCertificates) {
            await existingInstalledCertificate.destroy();
          }
        }
      }
    }
  }

  @AsHandler(OCPP_2_VER_LIST, OCPP_CallAction.GetInstalledCertificateIds)
  protected async _handleGetInstalledCertificateIds(
    message: IMessage<OCPP2_response_types.GetInstalledCertificateIdsResponse>,
    props?: HandlerProperties,
  ): Promise<void> {
    this._logger.debug('GetInstalledCertificateIds received:', message, props);
    const tenantId = message.context.tenantId;
    const ocppConnectionName = message.context.ocppConnectionName;
    const correlationId = message.context.correlationId;
    const certificateHashDataList: OCPP2_common_types.CertificateHashDataChainType[] =
      message.payload.certificateHashDataChain!;
    if (message.payload.status === GetInstalledCertificateStatusEnum.NotFound) {
      const request = await this._ocppMessageRepository.readOnlyOneByQuery(tenantId, {
        where: {
          ocppConnectionName: ocppConnectionName,
          correlationId,
          origin: MessageOrigin.ChargingStationManagementSystem,
        },
      });
      if (request) {
        // should always be true
        const getInstalledCertificateIdsRequest = request
          .message[3] as OCPP2_request_types.GetInstalledCertificateIdsRequest;
        let certificateType;
        if (
          getInstalledCertificateIdsRequest &&
          getInstalledCertificateIdsRequest.certificateType
        ) {
          certificateType = getInstalledCertificateIdsRequest.certificateType;
        }
        if (certificateType) {
          this._logger.debug(
            `GetInstalledCertificateIdsRequest sent to ${ocppConnectionName} had certificateType: ${certificateType}. Cleaning up installed certificates of this type in DB if any.`,
          );
          await this.installedCertificateRepository.deleteAllByQuery(tenantId, {
            where: {
              ocppConnectionName: ocppConnectionName,
              certificateType,
            },
          });
        } else {
          this._logger.debug(
            `GetInstalledCertificateIdsRequest sent to ${ocppConnectionName} had no certificateType. Cleaning up all installed certificates in DB if any.`,
          );
          await this.installedCertificateRepository.deleteAllByQuery(tenantId, {
            where: {
              ocppConnectionName: ocppConnectionName,
            },
          });
        }
      }
      return;
    }
    if (certificateHashDataList && certificateHashDataList.length > 0) {
      for (const certificateHashDataWrap of certificateHashDataList) {
        const certificateHashData = certificateHashDataWrap.certificateHashData;
        const certificateType =
          certificateHashDataWrap.certificateType as unknown as CertificateUseEnumType;
        let existingInstalledCertificate =
          await this._installedCertificateRepository.readOnlyOneByQuery(tenantId, {
            where: {
              ocppConnectionName: ocppConnectionName,
              certificateType: certificateType,
            },
          });
        if (existingInstalledCertificate) {
          existingInstalledCertificate.hashAlgorithm = certificateHashData.hashAlgorithm;
          existingInstalledCertificate.issuerNameHash = certificateHashData.issuerNameHash;
          existingInstalledCertificate.issuerKeyHash = certificateHashData.issuerKeyHash;
          existingInstalledCertificate.serialNumber = certificateHashData.serialNumber;
          await existingInstalledCertificate.save();
          this._logger.debug('Updated installed certificate record', existingInstalledCertificate);
        } else {
          existingInstalledCertificate = new InstalledCertificate();
          existingInstalledCertificate.hashAlgorithm = certificateHashData.hashAlgorithm;
          existingInstalledCertificate.issuerNameHash = certificateHashData.issuerNameHash;
          existingInstalledCertificate.issuerKeyHash = certificateHashData.issuerKeyHash;
          existingInstalledCertificate.serialNumber = certificateHashData.serialNumber;
          existingInstalledCertificate.ocppConnectionName = ocppConnectionName;
          existingInstalledCertificate.certificateType = certificateType;
          await existingInstalledCertificate.save();
          this._logger.debug(
            'Created new installed certificate record',
            existingInstalledCertificate,
          );
        }
      }
    }
  }

  @AsHandler(OCPP_2_VER_LIST, OCPP_CallAction.InstallCertificate)
  protected async _handleInstallCertificate(
    message: IMessage<OCPP2_response_types.InstallCertificateResponse>,
    props?: HandlerProperties,
  ): Promise<void> {
    this._logger.debug('InstallCertificate received:', message, props);
    await this.installCertificateHelperService.finalizeInstalledCertificate(
      message.context.tenantId,
      message.context.ocppConnectionName,
      message.payload.status,
    );
  }

  private async _verifySignCertRequest(
    csrString: string,
    tenantId: number,
    ocppConnectionName: string,
    certificateType?: CertificateSigningUseEnumType | null,
  ): Promise<void> {
    // Verify certificate type
    if (
      !certificateType ||
      (certificateType !== CertificateSigningUseEnum.V2GCertificate &&
        certificateType !== CertificateSigningUseEnum.V2G20Certificate &&
        certificateType !== CertificateSigningUseEnum.ChargingStationCertificate)
    ) {
      throw new Error(`Unsupported certificate type: ${certificateType}`);
    }

    // Verify CSR
    const csr: CertificationRequest = parseCSRForVerification(csrString);
    this._logger.info(`Verifying CSR: ${JSON.stringify(csr)}`);

    if (!(await csr.verify())) {
      throw new Error('Verify the signature on this csr using its public key failed');
    }

    if (certificateType === CertificateSigningUseEnum.ChargingStationCertificate) {
      // Verify organization name match the one stored in the device model
      const organizationName = await this._deviceModelRepository.readAllByQuerystring(tenantId, {
        tenantId: tenantId,
        ocppConnectionName: ocppConnectionName,
        component_name: 'SecurityCtrlr',
        variable_name: 'OrganizationName',
        type: AttributeEnum.Actual,
      });
      if (!organizationName || organizationName.length < 1) {
        throw new Error('Expected organizationName not found in DB');
      }
      // Find organizationName (its key is '2.5.4.10') attribute in CSR
      const organizationNameAttr = csr.subject.typesAndValues.find(
        (attr) => attr.type === '2.5.4.10',
      );
      if (!organizationNameAttr) {
        throw new Error('organizationName attribute not found in CSR');
      }
      if (organizationName[0].value !== organizationNameAttr.value.valueBlock.value) {
        throw new Error(
          `Expect organizationName ${organizationName[0].value} but get ${organizationNameAttr.value} from the csr`,
        );
      }
    }

    this._logger.info(`Verified SignCertRequest for station ${ocppConnectionName} successfully.`);
  }
}
