// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
  AbstractModuleApi,
  AsDataEndpoint,
  DEFAULT_TENANT_ID,
  HttpMethod,
  type IFileStorage,
  type IMessageConfirmation,
  type IMessageQuerystring,
  IMessageQuerystringSchema,
  Namespace,
  OCPP1_6_Namespace,
  OCPP2_0_1,
  OCPP_CallAction,
  OCPP2_Namespace,
  OCPPVersion,
  type WebsocketServerConfig,
} from '@citrineos/base';
import {
  Certificate,
  CountryNameEnumType,
  InstalledCertificate,
  SignatureAlgorithmEnumType,
} from '@dal/layers/sequelize/index.js';
import {
  GenerateCertificateChainRequest,
  GenerateCertificateChainSchema,
  InstallRootCertificateRequest,
  InstallRootCertificateSchema,
  RegenerateExistingCertificate,
  RegenerateInstalledCertificateSchema,
  TenantQuerySchema,
  type TenantQueryString,
  UploadExistingCertificate,
  UploadExistingCertificateSchema,
} from '@dal/interfaces/index.js';
import { generateCertificate } from '@util/index.js';
import type { FastifyInstance, FastifyRequest } from 'fastify';
import jsrsasign from 'jsrsasign';
import moment from 'moment';
import type { ILogObj } from 'tslog';
import { Logger } from 'tslog';
import type { ICertificatesModuleApi } from './interface.js';
import { CertificatesModule } from './module.js';
import { PemType } from './installCertificateHelperService.js';

/**
 * Server API for the Certificates module.
 */
export class CertificatesDataApi
  extends AbstractModuleApi<CertificatesModule>
  implements ICertificatesModuleApi
{
  private readonly _websocketServersConfig: WebsocketServerConfig[];
  private readonly _fileStorage: IFileStorage;

  /**
   * Constructs a new instance of the class.
   *
   * @param {CertificatesModule} certificatesModule - The Certificates module.
   * @param {FastifyInstance} server - The Fastify server instance.
   * @param {IFileStorage} fileStorage - The fileStorage
   * @param {WebsocketServerConfig[]} websocketServersConfig - Configuration for websocket servers
   * @param {Logger<ILogObj>} [logger] - The logger instance.
   */
  constructor(
    certificatesModule: CertificatesModule,
    server: FastifyInstance,
    fileStorage: IFileStorage,
    websocketServersConfig: WebsocketServerConfig[],
    logger?: Logger<ILogObj>,
  ) {
    super(certificatesModule, server, OCPPVersion.OCPP2_0_1, logger);
    this._fileStorage = fileStorage;
    this._websocketServersConfig = websocketServersConfig;
  }

  /**
   * Data Endpoint Methods
   */

  /**
   * This endpoint is used to create certificate chain, root CA, sub CA and leaf certificate
   *
   * @param request - GenerateRootCertificatesRequest
   * @return Promise<Certificate[]> - An array of generated certificates
   */
  @AsDataEndpoint(
    OCPP2_Namespace.CertificateChain,
    HttpMethod.Post,
    TenantQuerySchema,
    GenerateCertificateChainSchema,
  )
  async generateCertificateChain(
    request: FastifyRequest<{
      Body: GenerateCertificateChainRequest;
      Querystring: TenantQueryString;
    }>,
  ): Promise<Certificate[]> {
    this._logger.info(
      `Receiving generate certificate chain request ${JSON.stringify(request.body)}`,
    );

    const tenantId = request.query.tenantId;
    const certRequest = request.body as GenerateCertificateChainRequest;

    let certificateFromReq = new Certificate();
    certificateFromReq.serialNumber = moment().valueOf();
    certificateFromReq.keyLength = certRequest.keyLength ? certRequest.keyLength : 2048;
    certificateFromReq.organizationName = certRequest.organizationName;
    certificateFromReq.commonName = certRequest.commonName + ` ${PemType.Root}`;
    if (certRequest.validBefore) {
      certificateFromReq.validBefore = certRequest.validBefore;
    } else {
      const defaultValidityDate: Date = new Date();
      defaultValidityDate.setFullYear(defaultValidityDate.getFullYear() + 1);
      certificateFromReq.validBefore = defaultValidityDate.toISOString();
    }
    certificateFromReq.countryName = certRequest.countryName
      ? certRequest.countryName
      : CountryNameEnumType.US;
    certificateFromReq.signatureAlgorithm = certRequest.signatureAlgorithm
      ? certRequest.signatureAlgorithm
      : SignatureAlgorithmEnumType.ECDSA;
    certificateFromReq.isCA = true;
    certificateFromReq.pathLen = certRequest.pathLen ? certRequest.pathLen : 1;

    let leafCertPem: string;
    let subCACertPem: string; // combined with leafCertPem to form the chain
    let leafKeyPem: string;
    let subCAKeyPem: string;
    let rootCACertPem: string | undefined;
    let responseBody: Certificate[];

    if (certRequest.selfSigned) {
      // Generate self-signed root CA certificate
      const [rootCertificatePem, rootPrivateKeyPem] = generateCertificate(
        certificateFromReq,
        this._logger,
      );
      certificateFromReq =
        await this._module.installCertificateHelperService.storeCertificateAndKey(
          tenantId,
          certificateFromReq,
          rootCertificatePem,
          rootPrivateKeyPem,
          PemType.Root,
          certRequest.filePath,
        );

      // Generate sub CA certificate
      let subCertificate: Certificate = new Certificate();
      subCertificate.serialNumber = certificateFromReq.serialNumber + 1;
      subCertificate.keyLength = certificateFromReq.keyLength;
      subCertificate.organizationName = certificateFromReq.organizationName;
      subCertificate.commonName = certRequest.commonName + ` ${PemType.SubCA}`;
      subCertificate.validBefore = certificateFromReq.validBefore;
      subCertificate.signedBy = certificateFromReq.id;
      subCertificate.countryName = certificateFromReq.countryName;
      subCertificate.signatureAlgorithm = certificateFromReq.signatureAlgorithm;
      subCertificate.isCA = true;
      subCertificate.pathLen = 0;
      const [subCertificatePem, subPrivateKeyPem] = generateCertificate(
        subCertificate,
        this._logger,
        rootPrivateKeyPem,
        rootCertificatePem,
      );
      subCertificate = await this._module.installCertificateHelperService.storeCertificateAndKey(
        tenantId,
        subCertificate,
        subCertificatePem,
        subPrivateKeyPem,
        PemType.SubCA,
        certRequest.filePath,
      );

      // Generate leaf certificate
      let leafCertificate: Certificate = new Certificate();
      leafCertificate.serialNumber = subCertificate.serialNumber + 1;
      leafCertificate.keyLength = subCertificate.keyLength;
      leafCertificate.organizationName = subCertificate.organizationName;
      leafCertificate.commonName = certRequest.commonName;
      leafCertificate.validBefore = subCertificate.validBefore;
      leafCertificate.signedBy = subCertificate.id;
      leafCertificate.countryName = subCertificate.countryName;
      leafCertificate.signatureAlgorithm = subCertificate.signatureAlgorithm;
      leafCertificate.isCA = false;
      const [leafCertificatePem, leafPrivateKeyPem] = generateCertificate(
        leafCertificate,
        this._logger,
        subPrivateKeyPem,
        subCertificatePem,
      );
      leafCertificate = await this._module.installCertificateHelperService.storeCertificateAndKey(
        tenantId,
        leafCertificate,
        leafCertificatePem,
        leafPrivateKeyPem,
        PemType.Leaf,
        certRequest.filePath,
      );

      leafCertPem = leafCertificatePem;
      subCACertPem = subCertificatePem;
      leafKeyPem = leafPrivateKeyPem;
      subCAKeyPem = subPrivateKeyPem;
      rootCACertPem = rootCertificatePem;
      responseBody = [leafCertificate, subCertificate, certificateFromReq];
    } else {
      // Generate sub CA certificate and private key signed by external CA server
      // commonName should be a valid domain name
      certificateFromReq.commonName = certRequest.commonName;
      certificateFromReq.pathLen = 0;
      const [certificatePem, privateKeyPem] =
        await this._module.installCertificateHelperService.generateSubCACertificateSignedByCAServer(
          certificateFromReq,
        );
      certificateFromReq =
        await this._module.installCertificateHelperService.storeCertificateAndKey(
          tenantId,
          certificateFromReq,
          certificatePem,
          privateKeyPem,
          PemType.SubCA,
          certRequest.filePath,
        );

      // Generate leaf certificate
      let leafCertificate: Certificate = new Certificate();
      leafCertificate.serialNumber = certificateFromReq.serialNumber + 1;
      leafCertificate.keyLength = certificateFromReq.keyLength;
      leafCertificate.organizationName = certificateFromReq.organizationName;
      leafCertificate.commonName = certRequest.commonName;
      leafCertificate.validBefore = certificateFromReq.validBefore;
      leafCertificate.signedBy = certificateFromReq.id;
      leafCertificate.countryName = certificateFromReq.countryName;
      leafCertificate.signatureAlgorithm = certificateFromReq.signatureAlgorithm;
      leafCertificate.isCA = false;
      leafCertificate.pathLen = undefined;
      const [leafCertificatePem, leafPrivateKeyPem] = generateCertificate(
        leafCertificate,
        this._logger,
        privateKeyPem,
        certificatePem,
      );
      leafCertificate = await this._module.installCertificateHelperService.storeCertificateAndKey(
        tenantId,
        leafCertificate,
        leafCertificatePem,
        leafPrivateKeyPem,
        PemType.Leaf,
        certRequest.filePath,
      );

      leafCertPem = leafCertificatePem;
      subCACertPem = certificatePem;
      leafKeyPem = leafPrivateKeyPem;
      subCAKeyPem = privateKeyPem;
      responseBody = [leafCertificate, certificateFromReq];
    }

    // Concatenate certificate chain
    // store chain and keys for websocket servers
    await this._module.installCertificateHelperService.saveCertificatesToServerConfigs(
      this._websocketServersConfig,
      leafCertPem + subCACertPem,
      leafKeyPem,
      subCAKeyPem,
      rootCACertPem,
    );

    return responseBody;
  }

  @AsDataEndpoint(
    OCPP2_Namespace.RootCertificate,
    HttpMethod.Put,
    undefined,
    InstallRootCertificateSchema,
  )
  async installRootCertificate(
    request: FastifyRequest<{
      Body: InstallRootCertificateRequest;
    }>,
  ): Promise<IMessageConfirmation> {
    const installReq = request.body as InstallRootCertificateRequest;
    this._logger.info(
      `Installing ${installReq.certificateType} on charger ${installReq.ocppConnectionName}`,
    );

    let rootCAPem: string;
    if (installReq.fileId) {
      rootCAPem = (await this._fileStorage.getFile(installReq.fileId))!.toString();
    } else {
      rootCAPem = await this._module.certificateAuthorityService.getRootCACertificateFromExternalCA(
        installReq.certificateType,
      );
    }

    await this._module
      .sendCall(
        installReq.ocppConnectionName,
        installReq.tenantId,
        OCPPVersion.OCPP2_0_1,
        OCPP_CallAction.InstallCertificate,
        {
          certificateType: installReq.certificateType,
          certificate: rootCAPem,
        } as OCPP2_0_1.InstallCertificateRequest,
        installReq.callbackUrl,
      )
      .then((confirmation) => {
        if (!confirmation.success) {
          throw new Error(`Send InstallCertificateRequest failed: ${confirmation.payload}`);
        }
        this._logger.debug('InstallCertificate confirmation sent:', confirmation);
      });

    return {
      success: true,
    };
  }

  /**
   * Endpoint to upload an existing certificate that is already installed on a given station to the CSMS
   * @param request - UploadExistingCertificateSchema
   * @return Promise<InstalledCertificate> - the installed certificate record
   */
  @AsDataEndpoint(
    OCPP2_Namespace.UploadExistingCertificate,
    HttpMethod.Post,
    IMessageQuerystringSchema,
    UploadExistingCertificateSchema,
  )
  async uploadExistingCertificate(
    request: FastifyRequest<{
      Body: UploadExistingCertificate;
      Querystring: IMessageQuerystring;
    }>,
  ): Promise<InstalledCertificate[]> {
    const uploadExistingCertificate = request.body as UploadExistingCertificate;
    const messageQuerystring = request.query as IMessageQuerystring;
    const tenantId = messageQuerystring.tenantId || DEFAULT_TENANT_ID;
    const identifier = messageQuerystring.identifier;
    const isIdentifierList = Array.isArray(identifier);
    if (isIdentifierList) {
      const promises: Promise<InstalledCertificate>[] = [];
      for (const identifierElement of identifier) {
        promises.push(
          this._module.installCertificateHelperService.handleUploadExistingCertificate(
            tenantId,
            identifierElement,
            uploadExistingCertificate,
            request.body.filePath,
          ),
        );
      }
      return await Promise.all(promises);
    } else {
      return [
        await this._module.installCertificateHelperService.handleUploadExistingCertificate(
          tenantId,
          identifier,
          uploadExistingCertificate,
          request.body.filePath,
        ),
      ];
    }
  }

  /**
   * Endpoint to regenerate an existing certificate that is already installed on a given station.
   * Updates the InstalledCertificate record with the new certificate.
   *
   * @param request RegenerateInstalledCertificateSchema
   * @return Promise<InstalledCertificate> - the updated installed certificate record
   */
  @AsDataEndpoint(
    OCPP2_Namespace.RegenerateExistingCertificate,
    HttpMethod.Post,
    IMessageQuerystringSchema,
    RegenerateInstalledCertificateSchema,
  )
  async regenerateExistingCertificate(
    request: FastifyRequest<{
      Body: RegenerateExistingCertificate;
      Querystring: IMessageQuerystring;
    }>,
  ): Promise<InstalledCertificate> {
    const installedCertificateId = request.body.installedCertificateId;
    const validBeforeParam = request.body.validBefore;
    const ocppConnectionName = request.query.identifier;
    const tenantId = request.query.tenantId || DEFAULT_TENANT_ID;
    this._logger.info(
      `Regenerating existing certificate ${installedCertificateId} for charger ${ocppConnectionName}`,
    );
    const existingInstalledCertificate =
      await this._module.installedCertificateRepository.readOnlyOneByQuery(tenantId, {
        where: {
          id: installedCertificateId,
          ocppConnectionName: ocppConnectionName,
        },
      });
    if (!existingInstalledCertificate) {
      throw new Error('Installed certificate not found');
    }
    const existingCertificateRecord = await existingInstalledCertificate.$get('certificate');
    if (!existingCertificateRecord) {
      throw new Error('Certificate not found');
    }
    const fileId = existingCertificateRecord.certificateFileId;
    if (!fileId) {
      throw new Error('Certificate file not found');
    }
    const privateKeyFileId = existingCertificateRecord.privateKeyFileId;
    if (!privateKeyFileId) {
      throw new Error('Certificate privateKeyFileId not found');
    }
    const existingCertificateBuffer = await this._fileStorage.getFile(fileId);
    const existingPrivateKeyBuffer = await this._fileStorage.getFile(privateKeyFileId);
    if (!existingCertificateBuffer || !existingPrivateKeyBuffer) {
      throw new Error('Certificate files not found');
    }
    const existingCertificateString = existingCertificateBuffer.toString();
    const existingPrivateKey = existingPrivateKeyBuffer.toString();
    const existingCertificate = new jsrsasign.X509();
    existingCertificate.readCertPEM(existingCertificateString);
    const existingSubjectString = existingCertificate.getSubjectString();
    let newCertificateRecord = new Certificate();
    newCertificateRecord.serialNumber = moment().valueOf();
    newCertificateRecord.issuerName = existingSubjectString;
    newCertificateRecord.organizationName = existingCertificateRecord.organizationName;
    newCertificateRecord.commonName = existingCertificateRecord.commonName;
    newCertificateRecord.keyLength = existingCertificateRecord.keyLength;
    newCertificateRecord.validBefore = validBeforeParam;
    newCertificateRecord.signatureAlgorithm = existingCertificateRecord.signatureAlgorithm;
    newCertificateRecord.countryName = existingCertificateRecord.countryName;
    newCertificateRecord.isCA = existingCertificateRecord.isCA;
    newCertificateRecord.pathLen = existingCertificateRecord.pathLen;
    newCertificateRecord.signedBy = existingCertificateRecord.id;
    newCertificateRecord.certificateFileHash = existingCertificateRecord.certificateFileHash;
    const [newCertificatePem, newPrivateKeyPem] = generateCertificate(
      newCertificateRecord,
      this._logger,
      existingPrivateKey,
      existingCertificateString,
    );
    newCertificateRecord.certificateFileHash =
      this._module.installCertificateHelperService.getCertificateHash(newCertificatePem);
    newCertificateRecord.certificateFileId = await this._fileStorage.saveFile(
      `Regenerated_Cert_${newCertificateRecord.serialNumber}.pem`,
      Buffer.from(newCertificatePem),
    );
    newCertificateRecord.privateKeyFileId = await this._fileStorage.saveFile(
      `Regenerated_Key_${newCertificateRecord.serialNumber}.pem`,
      Buffer.from(newPrivateKeyPem),
    );
    newCertificateRecord = await newCertificateRecord.save();
    existingInstalledCertificate.certificateId = newCertificateRecord.id;
    await existingInstalledCertificate.save();
    return existingInstalledCertificate;
  }

  /**
   * Overrides superclass method to generate the URL path based on the input {@link Namespace}
   * and the module's endpoint prefix configuration.
   *
   * @param {Namespace} input - The input {@link Namespace}.
   * @return {string} - The generated URL path.
   */
  protected _toDataPath(input: OCPP2_Namespace | OCPP1_6_Namespace | Namespace): string {
    const endpointPrefix = this._module.config.modules.certificates?.endpointPrefix;
    return super._toDataPath(input, endpointPrefix);
  }
}
