import type { OPCUACertificateManager } from "node-opcua-certificate-manager";
import type { OPCUASecureObject } from "node-opcua-common";

import { type Certificate, exploreCertificate, explorePrivateKey, publicKeyAndPrivateKeyMatches } from "node-opcua-crypto/web";
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
import type { VerifyCertificateOptions } from "node-opcua-pki";

const _doDebug = checkDebugFlag(__filename);
const _debugLog = make_debugLog(__filename);
const errorLog = make_errorLog(__filename);
const warningLog = make_warningLog(__filename);

export function verifyIsOPCUAValidCertificate(
    certificate: Certificate,
    certificateFile: string,
    type: "client" | "server",
    applicationUri: string
): void {
    const certificateInfo = exploreCertificate(certificate);
    const now = new Date();

    if (certificateInfo.tbsCertificate.validity.notBefore.getTime() > now.getTime()) {
        // check that certificate is active
        // certificate is not active yet
        warningLog(
            `[NODE-OPCUA-W02] The certificate is not active yet\n` +
                `notBefore       ${certificateInfo.tbsCertificate.validity.notBefore.toISOString()}\n` +
                `certificateFile ${certificateFile}`
        );
    }
    //  check that certificate has not expired
    if (certificateInfo.tbsCertificate.validity.notAfter.getTime() <= now.getTime()) {
        // certificate is obsolete
        warningLog(
            `[NODE-OPCUA-W03] The certificate has expired\n` +
                `Please regenerate a valid certificate\n` +
                `notAfter       = ${certificateInfo.tbsCertificate.validity.notAfter.toISOString()}\n` +
                `certificateFile= ${certificateFile}`
        );
    } else {
        const tenDays = 10 * 24 * 60 * 60 * 1000;
        if (certificateInfo.tbsCertificate.validity.notAfter.getTime() <= now.getTime() + tenDays) {
            // certificate is going to expired very soon
            warningLog(
                `[NODE-OPCUA-W05] The certificate is about to expire in less than 10 days.\n` +
                    `Please regenerate a valid certificate as soon as possible\n` +
                    `notAfter       = ${certificateInfo.tbsCertificate.validity.notAfter.toISOString()}\n` +
                    `certificateFile= ${certificateFile}\n`
            );
        }
    }
    // check that server certificate matches Application URI
    const uniformResourceIdentifier = certificateInfo?.tbsCertificate?.extensions?.subjectAltName?.uniformResourceIdentifier;
    if (!uniformResourceIdentifier) {
        warningLog(
            `[NODE-OPCUA-W14] The certificate subjectAltName uniformResourceIdentifier is missing.\n` +
                `Please regenerate a specific certificate with a uniformResourceIdentifier that matches your ${type} applicationUri\n` +
                `applicationUri  = ${applicationUri}\n` +
                `certificateFile = ${certificateFile}\n`
        );
        return;
    } else if (uniformResourceIdentifier[0] !== applicationUri) {
        warningLog(
            `[NODE-OPCUA-W06] The certificate subjectAltName does not match the ${type} applicationUri\n` +
                `Please regenerate a specific certificate that matches your ${type} applicationUri\n` +
                `certificate subjectAltName  = ${uniformResourceIdentifier[0]}\n` +
                `${type} applicationUri  = ${applicationUri}\n` +
                `certificateFile         = ${certificateFile}\n`
        );
    }
    const keyUsage = certificateInfo.tbsCertificate.extensions?.keyUsage;
    if (!keyUsage) {
        warningLog(`[NODE-OPCUA-W15] The certificate keyUsage is missing\ncertificateFile = ${certificateFile}`);
    } else {
        // spec says that certificate shall include digitalSignature, nonRepudiation, keyEncipherment and dataEncipherment.
        // Other key uses are allowed.
        if (!keyUsage.digitalSignature || !keyUsage.nonRepudiation || !keyUsage.keyEncipherment || !keyUsage.dataEncipherment) {
            warningLog(
                `[NODE-OPCUA-W16] The certificate keyUsage must include digitalSignature, nonRepudiation, keyEncipherment and dataEncipherment.\n` +
                    `see https://reference.opcfoundation.org/v104/Core/docs/Part6/6.2.2/\n` +
                    `certificateFile = ${certificateFile}`
            );
            warningLog(`keyUsage = ${JSON.stringify(keyUsage, null, " ")}`);
        }
    }
    const extKeyUsage = certificateInfo.tbsCertificate.extensions?.extKeyUsage;
    if (!extKeyUsage) {
        warningLog(`[NODE-OPCUA-W17] The certificate extKeyUsage is missing\ncertificateFile = ${certificateFile}`);
    } else {
        // spec says that certificate shall include digitalSignature, nonRepudiation, keyEncipherment and dataEncipherment.
        // Other key uses are allowed.
        if (!extKeyUsage.clientAuth && !extKeyUsage.serverAuth) {
            warningLog(
                `[NODE-OPCUA-W18] The certificate extKeyUsage must include clientAuth and/or serverAuth.\n` +
                    `see https://reference.opcfoundation.org/v104/Core/docs/Part6/6.2.2/\n` +
                    `certificateFile = ${certificateFile}`
            );
        }
    }

    const keyLengthInBits = certificateInfo.tbsCertificate.subjectPublicKeyInfo.keyLength * 8;
    if (keyLengthInBits < 1024) {
        errorLog(
            `[NODE-OPCUA-W19] The public key length shall be greater than or equal to 1024 bits. key length is ${keyLengthInBits}.\n` +
                `see https://reference.opcfoundation.org/v104/GDS/docs/7.6.3/\n` +
                `certificateFile = ${certificateFile}`
        );
    } else if (keyLengthInBits < 2048) {
        warningLog(
            `[NODE-OPCUA-W23] key lengths less than 2048 are considered insecure. key length is ${keyLengthInBits}\n` +
                `see https://reference.opcfoundation.org/v104/Core/docs/Part2/6.8/\n` +
                `certificateFile = ${certificateFile}`
        );
    }
}

export async function performCertificateSanityCheck(
    secureObject: OPCUASecureObject,
    serverOrClient: "server" | "client",
    certificateManager: OPCUACertificateManager,
    applicationUri: string
): Promise<void> {
    // verify that certificate is matching private key, and inform the developer if not
    const certificate = secureObject.getCertificate();
    const privateKey = secureObject.getPrivateKey();
    //
    if (!publicKeyAndPrivateKeyMatches(certificate, privateKey)) {
        errorLog("[NODE-OPCUA-E01] Configuration error : the certificate and the private key do not match !");
        errorLog("                  please check the configuration of the OPCUA Server");
        errorLog("                    privateKey= ", secureObject.privateKeyFile);
        errorLog(" certificateManager.privateKey= ", certificateManager.privateKey);
        errorLog("               certificateFile= ", secureObject.certificateFile);
        throw new Error(
            "[NODE-OPCUA-E01] Configuration error : the certificate and the private key do not match ! please fix your configuration"
        );
    }
    // verify that the certificate provided has the right key length ( at least 2048)
    const privateKeyInfo = explorePrivateKey(privateKey);
    const keyLengthInBits = privateKeyInfo.modulus.length * 8;
    if (keyLengthInBits <= 1024) {
        warningLog(
            `[NODE-OPCUA-W04] The public/private key pair uses a key length which is equal or lower than 1024 bits. ( key length was ${keyLengthInBits} )\n` +
                `OPCUA version 1.04 requires that security key length are greater or equal to 2048 bits.\n` +
                `The ${serverOrClient} is operating at risk.                                             `
        );
    }

    const options: VerifyCertificateOptions = {
        acceptOutdatedCertificate: false,
        acceptOutDatedIssuerCertificate: false,
        acceptPendingCertificate: false
    };

    const status = await certificateManager.verifyCertificate(certificate, options);

    // BadCertificateUntrusted is expected for the application's own
    // certificate — it does not need to be in its own trust list.
    // Only warn about genuinely problematic statuses (expired,
    // revoked, invalid signature, etc.).
    if (status !== "Good" && status !== "BadCertificateUntrusted") {
        warningLog("[NODE-OPCUA-W35] Warning: the certificate status is = ", status, " file = ", secureObject.certificateFile);
    }

    verifyIsOPCUAValidCertificate(certificate, secureObject.certificateFile, serverOrClient, applicationUri);
}
