// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0

import * as pkijs from 'pkijs';
import { CertificationRequest } from 'pkijs';
import * as asn1js from 'asn1js';
import { fromBER } from 'asn1js';
import { CountryNameEnumType, SignatureAlgorithmEnumType } from '@dal/layers/sequelize/index.js';
import { Certificate } from '@dal/layers/sequelize/index.js';
import jsrsasign from 'jsrsasign';
import { fromBase64, stringToArrayBuffer } from 'pvutils';
import moment from 'moment';
import type { ILogObj } from 'tslog';
import { Logger } from 'tslog';
import KJUR = jsrsasign.KJUR;
import OCSPRequest = jsrsasign.KJUR.asn1.ocsp.OCSPRequest;
import Request = jsrsasign.KJUR.asn1.ocsp.Request;
import X509 = jsrsasign.X509;
import KEYUTIL = jsrsasign.KEYUTIL;

export const dateTimeFormat = 'YYMMDDHHmmssZ';

export function getValidityTimeString(time: moment.Moment) {
  return time.utc().format('YYMMDDHHmmss').concat('Z');
}

export function createPemBlock(content: string) {
  return `-----BEGIN CERTIFICATE-----\n${content}\n-----END CERTIFICATE-----\n`;
}

/*
 * Parse the certificate chain and extract certificates
 * @param pem - certificate chain pem containing multiple certificate blocks
 * @return array of certificate pem blocks
 */
export function parseCertificateChainPem(pem: string): string[] {
  const certs: string[] = [];
  const beginMarker = '-----BEGIN CERTIFICATE-----';
  const endMarker = '-----END CERTIFICATE-----';

  let startIndex = pem.indexOf(beginMarker);
  while (startIndex !== -1) {
    const endIndex = pem.indexOf(endMarker, startIndex + beginMarker.length);
    if (endIndex === -1) {
      break;
    }
    certs.push(pem.substring(startIndex, endIndex + endMarker.length));
    startIndex = pem.indexOf(beginMarker, endIndex + endMarker.length);
  }

  return certs;
}

/**
 * Decode the pem and extract certificates
 * @param pem - base64 encoded certificate chain string without header and footer
 * @return array of pkijs.CertificateSetItem
 */
export function extractCertificateArrayFromEncodedString(pem: string): pkijs.CertificateSetItem[] {
  try {
    const cmsSignedBuffer = Buffer.from(pem, 'base64');
    const asn1 = asn1js.fromBER(cmsSignedBuffer);
    const cmsContent = new pkijs.ContentInfo({ schema: asn1.result });
    const cmsSigned = new pkijs.SignedData({ schema: cmsContent.content });
    if (cmsSigned.certificates && cmsSigned.certificates.length > 0) {
      return cmsSigned.certificates;
    } else {
      return [];
    }
  } catch (e) {
    throw new Error(`Failed to extract certificate ${pem} due to ${e}`);
  }
}

/**
 * extracts the base64-encoded content from a pem encoded csr
 * @param csrPem
 * @private
 * @return {string} The parsed CSR or the original CSR if it cannot be parsed
 */
export function extractEncodedContentFromCSR(csrPem: string): string {
  return csrPem
    .replace(/-----BEGIN CERTIFICATE REQUEST-----/, '')
    .replace(/-----END CERTIFICATE REQUEST-----/, '')
    .replace(/[\r\n]/g, '');
}

/**
 * Generate certificate and its private key
 *
 * @param certificateEntity - the certificate
 * @param logger - the logger
 * @param issuerKeyPem - the issuer private key
 * @param issuerCertPem - the issuer certificate
 *
 * @return generated certificate pem and its private key pem
 */
export function generateCertificate(
  certificateEntity: Certificate,
  logger: Logger<ILogObj>,
  issuerKeyPem?: string,
  issuerCertPem?: string,
): [string, string] {
  // Generate a key pair
  let keyPair;
  logger.debug(`Private key signAlgorithm: ${certificateEntity.signatureAlgorithm}`);
  if (certificateEntity.signatureAlgorithm === SignatureAlgorithmEnumType.RSA) {
    keyPair = jsrsasign.KEYUTIL.generateKeypair(
      'RSA',
      certificateEntity.keyLength ? certificateEntity.keyLength : 2048,
    );
  } else {
    keyPair = jsrsasign.KEYUTIL.generateKeypair('EC', 'secp256r1');
  }
  const privateKeyPem = jsrsasign.KEYUTIL.getPEM(keyPair.prvKeyObj, 'PKCS8PRV');
  const publicKeyPem = jsrsasign.KEYUTIL.getPEM(keyPair.pubKeyObj);
  logger.debug(`Created publicKeyPem: ${publicKeyPem}`);

  let issuerCertObj: X509 | undefined;
  if (issuerCertPem) {
    issuerCertObj = new X509();
    issuerCertObj.readCertPEM(issuerCertPem);
  }

  // Prepare certificate attributes
  let subjectNotAfter = certificateEntity.validBefore
    ? moment(certificateEntity.validBefore)
    : moment().add(1, 'year');
  const subjectString = `/CN=${certificateEntity.commonName}/O=${certificateEntity.organizationName}/C=${certificateEntity.countryName}`;
  let issuerParam = { str: subjectString };
  if (issuerCertObj) {
    const issuerNotAfter = moment(issuerCertObj.getNotAfter(), dateTimeFormat);
    if (subjectNotAfter.isAfter(issuerNotAfter)) {
      subjectNotAfter = issuerNotAfter;
    }
    issuerParam = { str: issuerCertObj.getSubjectString() };
  }

  // Prepare certificate extensions
  const keyUsages = ['digitalSignature', 'keyCertSign', 'crlSign'];
  if (!certificateEntity.isCA) {
    keyUsages.push('keyEncipherment');
  }
  let basicConstraints: any = {
    extname: 'basicConstraints',
    critical: true,
    cA: certificateEntity.isCA,
  };
  if (certificateEntity.pathLen) {
    basicConstraints = {
      extname: 'basicConstraints',
      cA: certificateEntity.isCA,
      pathLen: certificateEntity.pathLen,
    };
  }
  const extensions = [
    basicConstraints,
    { extname: 'keyUsage', critical: true, names: keyUsages },
    { extname: 'subjectKeyIdentifier', kid: publicKeyPem },
  ];
  if (issuerCertObj) {
    extensions.push({
      extname: 'authorityKeyIdentifier',
      kid: issuerCertPem,
      isscert: issuerCertPem,
    });
  }

  // Prepare certificate sign parameters
  const signAlgorithm =
    certificateEntity.signatureAlgorithm === SignatureAlgorithmEnumType.RSA
      ? SignatureAlgorithmEnumType.RSA
      : SignatureAlgorithmEnumType.ECDSA;
  logger.debug(`Certificate SignAlgorithm: ${signAlgorithm}`);
  const caKey = issuerKeyPem ? issuerKeyPem : privateKeyPem;

  // Generate certificate
  const certificate: KJUR.asn1.x509.Certificate = new KJUR.asn1.x509.Certificate({
    version: 3,
    serial: { int: moment().valueOf() },
    notbefore: getValidityTimeString(moment()),
    notafter: getValidityTimeString(subjectNotAfter),
    issuer: issuerParam,
    subject: { str: subjectString },
    sbjpubkey: keyPair.pubKeyObj,
    ext: extensions,
    sigalg: signAlgorithm,
    cakey: caKey,
  });

  return [certificate.getPEM(), privateKeyPem];
}

/**
 * Create a signed certificate for the provided CSR using the issuer certificate, and its private key.
 *
 * @param csrPem - The CSR that need to be signed.
 * @param issuerCertPem - The issuer certificate.
 * @param issuerPrivateKeyPem - The issuer private key.
 * @return {KJUR.asn1.x509.Certificate} The signed certificate.
 */
export function createSignedCertificateFromCSR(
  csrPem: string,
  issuerCertPem: string,
  issuerPrivateKeyPem: string,
): KJUR.asn1.x509.Certificate {
  const csrObj: KJUR.asn1.csr.ParamResponse = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csrPem);
  const issuerCertObj = new X509();
  issuerCertObj.readCertPEM(issuerCertPem);

  let subjectNotAfter = moment().add(1, 'year');
  const issuerNotAfter = moment(issuerCertObj.getNotAfter(), dateTimeFormat);
  if (subjectNotAfter.isAfter(issuerNotAfter)) {
    subjectNotAfter = issuerNotAfter;
  }

  let extensions: any[];
  if (csrObj.extreq) {
    extensions = csrObj.extreq;
  } else {
    extensions = [
      { extname: 'basicConstraints', cA: false },
      {
        extname: 'keyUsage',
        critical: true,
        names: ['digitalSignature', 'keyEncipherment'],
      },
    ];
  }
  extensions.push({ extname: 'subjectKeyIdentifier', kid: csrObj.sbjpubkey });
  extensions.push({
    extname: 'authorityKeyIdentifier',
    kid: issuerCertPem,
    isscert: issuerCertPem,
  });

  return new KJUR.asn1.x509.Certificate({
    version: 3,
    serial: { int: moment().valueOf() },
    issuer: { str: issuerCertObj.getSubjectString() },
    subject: { str: csrObj.subject.str },
    notbefore: getValidityTimeString(moment()),
    notafter: getValidityTimeString(subjectNotAfter),
    sbjpubkey: csrObj.sbjpubkey,
    ext: extensions,
    sigalg: csrObj.sigalg,
    cakey: issuerPrivateKeyPem,
  });
}

export async function sendOCSPRequest(
  ocspRequest: OCSPRequest | Request,
  responderURL: string,
): Promise<string> {
  const response = await fetch(responderURL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/ocsp-request',
      Accept: 'application/ocsp-response',
    },
    body: ocspRequest.getEncodedHex(),
  });

  if (!response.ok) {
    throw new Error(
      `Failed to fetch OCSP response from ${responderURL}: ${response.status} with error: ${await response.text()}`,
    );
  }

  return await response.text();
}

export function parseCSRForVerification(csrPem: string): CertificationRequest {
  const certificateBuffer = stringToArrayBuffer(fromBase64(extractEncodedContentFromCSR(csrPem)));
  const asn1 = fromBER(certificateBuffer);
  if (asn1.offset === -1) {
    throw new Error('Failed to parse CSR: invalid ASN.1 BER encoding');
  }
  return new CertificationRequest({ schema: asn1.result });
}

export function generateCSR(certificate: Certificate): [string, string] {
  let keyPair;
  if (certificate.signatureAlgorithm === SignatureAlgorithmEnumType.RSA) {
    keyPair = KEYUTIL.generateKeypair('RSA', certificate.keyLength ? certificate.keyLength : 2048);
  } else {
    keyPair = KEYUTIL.generateKeypair('EC', 'secp256r1');
  }
  const privateKeyPem = jsrsasign.KEYUTIL.getPEM(keyPair.prvKeyObj, 'PKCS8PRV');
  const publicKeyPem = jsrsasign.KEYUTIL.getPEM(keyPair.pubKeyObj);

  let basicConstraintParam: any;
  if (certificate.pathLen) {
    basicConstraintParam = {
      cA: certificate.isCA,
      pathLen: certificate.pathLen,
    };
  } else {
    basicConstraintParam = { cA: certificate.isCA };
  }
  const csr = new KJUR.asn1.csr.CertificationRequest({
    subject: {
      str: `/CN=${certificate.commonName}/O=${certificate.organizationName}/C=${certificate.countryName}`,
    },
    sbjpubkey: publicKeyPem,
    extreq: [
      { extname: 'basicConstraints', array: [basicConstraintParam] },
      {
        extname: 'keyUsage',
        array: [
          {
            names: ['digitalSignature', 'keyEncipherment', 'keyCertSign', 'crlSign'],
          },
        ],
      },
    ],
    sigalg: certificate.signatureAlgorithm
      ? certificate.signatureAlgorithm
      : SignatureAlgorithmEnumType.ECDSA,
    sbjprvkey: privateKeyPem,
  });

  return [csr.getPEM(), privateKeyPem];
}

export const parseX509Date = (date: string): Date | null => {
  if (/^\d{14}Z$/.test(date)) {
    // GeneralizedTime: YYYYMMDDHHMMSSZ
    return moment.utc(date, 'YYYYMMDDHHmmss[Z]', true).toDate();
  } else if (/^\d{12}Z$/.test(date)) {
    // UTCTime: YYMMDDHHMMSSZ (YY interpreted as 1950-2049)
    return moment.utc(date, 'YYMMDDHHmmss[Z]', true).toDate();
  } else {
    console.error(`Invalid X.509 date format: ${date}`);
    return null;
  }
};

export const extractCertificateDetails = (
  pemString: string,
): {
  serialNumber: number | null;
  issuerName: string | null;
  organizationName: string | null;
  commonName: string | null;
  countryName: CountryNameEnumType | null;
  validBefore: Date | null;
  signatureAlgorithm: SignatureAlgorithmEnumType | null;
} => {
  try {
    const cert = new jsrsasign.X509();
    cert.readCertPEM(pemString);

    // Extract details
    const serialNumber = parseInt(cert.getSerialNumberHex());
    const issuerName = cert.getIssuerString();
    const organizationName = cert.getSubjectString().match(/\/O=([^/]+)/)?.[1] || null;
    const commonName = cert.getSubjectString().match(/\/CN=([^/]+)/)?.[1] || null;
    const countryName = (cert.getSubjectString().match(/\/C=([^/]+)/)?.[1] ||
      null) as CountryNameEnumType | null;
    const notAfter = cert.getNotAfter();
    const validBefore = parseX509Date(notAfter);
    const signatureAlgorithm = cert.getSignatureAlgorithmField() as SignatureAlgorithmEnumType;

    return {
      serialNumber,
      issuerName,
      organizationName,
      commonName,
      countryName,
      validBefore,
      signatureAlgorithm,
    };
  } catch (error) {
    console.error('Error extracting certificate details:', error);
    throw new Error('Invalid PEM format or unsupported certificate');
  }
};
