import fs from "fs";
import path from "path";
import forge from "node-forge";

const cwd = process.cwd();

function computeFingerprint(cert: forge.pki.Certificate): string {
    const asn1Cert = forge.pki.certificateToAsn1(cert);
    const der = forge.asn1.toDer(asn1Cert).getBytes();
    const md = forge.md.sha1.create();
    md.update(der);
    return md.digest().toHex();
}

// Save the certificate as a PEM file (named by its fingerprint)
function saveCertificate(count: number, certName: string, cert: forge.pki.Certificate, outputDir: string): string {
    const pem = forge.pki.certificateToPem(cert);
    const filePath = path.join(outputDir, `${count}-${certName}.pem`);
    if (!fs.existsSync(filePath)) {
        fs.writeFileSync(filePath, pem);
        console.log(`Saved certificate: ${filePath}`);
    }
    return filePath;
}

/**
 * Decodes the DER-encoded AIA extension value and extracts any URLs from the
 * accessLocation fields (tagged as [6] for uniformResourceIdentifier).
 *
 * @param extensionValue - The raw value from the AIA extension (DER encoded).
 * @returns An array of URL strings.
 */
const parseAIAExtension = (extensionValue: string): string[] => {
    const urls: string[] = [];
    // Create a binary buffer from the extension value
    const buffer = forge.util.createBuffer(extensionValue, "binary");
    const asn1Obj = forge.asn1.fromDer(buffer);

    // Validate that we have a SEQUENCE
    if (asn1Obj.tagClass !== forge.asn1.Class.UNIVERSAL || asn1Obj.type !== forge.asn1.Type.SEQUENCE) {
        throw new Error("Invalid AIA extension structure; expected a SEQUENCE.");
    }

    // Each element in the sequence should be an AccessDescription sequence
    for (const accessDesc of asn1Obj.value) {
        if (
            accessDesc.tagClass === forge.asn1.Class.UNIVERSAL &&
            accessDesc.type === forge.asn1.Type.SEQUENCE &&
            Array.isArray(accessDesc.value) &&
            accessDesc.value.length >= 2
        ) {
            // The second element is the accessLocation (GeneralName)
            const accessLocation = accessDesc.value[1];
            // Look for context-specific tag 6 (uniformResourceIdentifier)
            if (accessLocation.tagClass === forge.asn1.Class.CONTEXT_SPECIFIC && accessLocation.type === 6) {
                // Depending on parsing, accessLocation.value might be a string
                // or an array containing an IA5String.
                if (typeof accessLocation.value === "string") {
                    urls.push(accessLocation.value);
                } else if (
                    Array.isArray(accessLocation.value) &&
                    accessLocation.value.length > 0 &&
                    typeof accessLocation.value[0].value === "string"
                ) {
                    urls.push(accessLocation.value[0].value);
                }
            }
        }
    }
    return urls;
};

// Extract AIA URLs (specifically CA Issuers URLs) from the certificate extensions
const extractAIAUrls = (cert: forge.pki.Certificate): string[] => {
    // Look for the authorityInfoAccess extension
    const aiaExt = cert.extensions.find((ext) => ext.name === "authorityInfoAccess");
    if (!aiaExt || !aiaExt.value) {
        console.warn("No Authority Info Access extension found in the certificate.");
        return [];
    }
    try {
        const urls = parseAIAExtension(aiaExt.value);
        return urls;
    } catch (error) {
        console.error("Error parsing AIA extension:", error);
        return [];
    }
};

/**
 * Downloads a certificate from the given URL using fetch.
 * If the URL suggests DER format (via extension), the certificate is converted accordingly.
 */
async function fetchCertificate(url: string): Promise<forge.pki.Certificate | null> {
    try {
        console.log(`Downloading certificate from: ${url}`);
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        const arrayBuffer = await response.arrayBuffer();
        const buffer = Buffer.from(arrayBuffer);
        let cert: forge.pki.Certificate;
        // If the URL ends with common DER extensions, assume DER encoding
        if (url.endsWith(".crt") || url.endsWith(".cer") || url.endsWith(".der")) {
            const derBuffer = forge.util.createBuffer(buffer.toString("binary"));
            const asn1Obj = forge.asn1.fromDer(derBuffer);
            cert = forge.pki.certificateFromAsn1(asn1Obj);
        } else {
            // Otherwise, assume the certificate is in PEM format
            const pem = buffer.toString("utf8");
            cert = forge.pki.certificateFromPem(pem);
        }
        return cert;
    } catch (error) {
        console.error(`Failed to download certificate from ${url}:`, error);
        return null;
    }
}

async function processCertificateRecursively(
    certName: string,
    cert: forge.pki.Certificate,
    outputDir: string,
    visited: Set<string>,
    count: number,
): Promise<void> {
    const fingerprint = computeFingerprint(cert);
    if (visited.has(fingerprint)) {
        console.log(`Already processed certificate ${fingerprint}`);
        return;
    }
    visited.add(fingerprint);

    // Save the current certificate
    saveCertificate(count, certName, cert, outputDir);

    // Extract URLs from the AIA extension
    const aiaUrls = extractAIAUrls(cert);
    for (const url of aiaUrls) {
        console.log({
            url
        });
        // Process only HTTP/HTTPS URLs
        if (!url.startsWith("http")) {
            continue;
        }

        if (!url.includes('.crt')) {
            continue;
        }

        const currentCertName = url.split('/').pop();

        const downloadedCert = await fetchCertificate(url);
        if (downloadedCert) {
            // Recursively process the downloaded certificate
            await processCertificateRecursively(currentCertName, downloadedCert, outputDir, visited, count+1);
        }
    }
}

const run = async () => {
    const certName = 'inicial-cert.crt';
    const certPath = path.join(cwd, "inicial-cert.crt");
    const outputDir = path.join(cwd, "cert-chain");

    const pem = fs.readFileSync(certPath, "utf8");
    const cert = forge.pki.certificateFromPem(pem);
    const visited = new Set<string>();
    const count = 1;

    if (!fs.existsSync(outputDir)) {
        fs.mkdirSync(outputDir, { recursive: true });
    }

    await processCertificateRecursively(certName, cert, outputDir, visited, count);
}

(async () => {
    await run();
})();