// Copyright (C) 2026, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System.Formats.Asn1;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace Duplicati.Library.Certificates;

/// <summary>
/// Represents a certificate authority with its certificate and private key.
/// </summary>
/// <param name="Certificate">The CA certificate.</param>
/// <param name="PrivateKey">The CA private key.</param>
public record CACertificatePair(X509Certificate2 Certificate, ECDsa PrivateKey);

/// <summary>
/// Represents a server certificate with its certificate and private key.
/// </summary>
/// <param name="Certificate">The server certificate.</param>
/// <param name="PrivateKey">The server private key.</param>
public record ServerCertificatePair(X509Certificate2 Certificate, ECDsa PrivateKey);

/// <summary>
/// Generates certificates for HTTPS server using ECDSA P-256 keys.
/// </summary>
public static class CertificateGenerator
{
    /// <summary>
    /// Time a CA is valid, for now 10 years.
    /// </summary>
    private static readonly TimeSpan CA_VALIDITY = TimeSpan.FromDays(365 * 10);
    /// <summary>
    /// Time a server certificate is valid, for now 90 days to satisfy Browser limits.
    /// </summary>
    private static readonly TimeSpan SERVER_VALIDITY = TimeSpan.FromDays(90);
    /// <summary>
    /// Common name for the CA certificate.
    /// </summary>
    private static readonly string CA_COMMON_NAME = $"{AutoUpdater.AutoUpdateSettings.AppName} Local CA";
    /// <summary>
    /// Common name for the server certificate.
    /// </summary>
    private static readonly string SERVER_COMMON_NAME = $"{AutoUpdater.AutoUpdateSettings.AppName} Server";

    /// <summary>
    /// Generates a self-signed CA certificate with CA constraints.
    /// </summary>
    /// <returns>A CA certificate pair containing the certificate and private key.</returns>
    public static CACertificatePair GenerateCACertificate()
    {
        using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);

        var request = new CertificateRequest(
            $"CN={CA_COMMON_NAME}, O=Duplicati, C=US",
            key,
            HashAlgorithmName.SHA256);

        // Add CA basic constraints with pathLenConstraint=0 (this CA cannot sign other CAs)
        request.CertificateExtensions.Add(
            new X509BasicConstraintsExtension(
                certificateAuthority: true,
                hasPathLengthConstraint: true,
                pathLengthConstraint: 0,
                critical: true));

        // Key usage for CA
        request.CertificateExtensions.Add(
            new X509KeyUsageExtension(
                X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign,
                critical: true));

        // Subject Key Identifier
        request.CertificateExtensions.Add(
            new X509SubjectKeyIdentifierExtension(
                request.PublicKey,
                critical: false));

        // Not before now, not after validity
        var notBefore = DateTimeOffset.UtcNow.AddMinutes(-5); // Allow for clock skew
        var notAfter = DateTimeOffset.UtcNow.Add(CA_VALIDITY);

        // Create self-signed certificate
        var certificate = request.CreateSelfSigned(notBefore, notAfter);

        // Export and re-import to make it exportable with private key
        // Use a temporary password to avoid unprotected key material in memory
        var tempPassword = CertificateStorageHelper.GeneratePfxPassword();
        var export = certificate.Export(X509ContentType.Pfx, tempPassword);
        var certWithKey = X509CertificateLoader.LoadPkcs12(
            export,
            tempPassword,
            X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
        CryptographicOperations.ZeroMemory(export);

        // Create a new ECDsa instance with the same key parameters for the return value
        // since we're disposing the original key
        var keyParams = key.ExportECPrivateKey();
        var newKey = ECDsa.Create();
        newKey.ImportECPrivateKey(keyParams, out _);
        CryptographicOperations.ZeroMemory(keyParams);

        return new CACertificatePair(certWithKey, newKey);
    }

    /// <summary>
    /// Generates a server certificate signed by the CA.
    /// </summary>
    /// <param name="caCert">The CA certificate.</param>
    /// <param name="caKey">The CA private key.</param>
    /// <param name="hostnames">The hostnames and IP addresses for the Subject Alternative Names (SANs).</param>
    /// <returns>A server certificate pair containing the certificate and private key.</returns>
    public static ServerCertificatePair GenerateServerCertificate(
        X509Certificate2 caCert,
        ECDsa caKey,
        IEnumerable<string> hostnames)
    {
        using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);

        var request = new CertificateRequest(
            $"CN={SERVER_COMMON_NAME}, O=Duplicati, C=US",
            key,
            HashAlgorithmName.SHA256);

        // Basic constraints - not a CA
        request.CertificateExtensions.Add(
            new X509BasicConstraintsExtension(
                certificateAuthority: false,
                hasPathLengthConstraint: false,
                pathLengthConstraint: 0,
                critical: true));

        // Key usage for server
        request.CertificateExtensions.Add(
            new X509KeyUsageExtension(
                X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
                critical: true));

        // Extended key usage for TLS server
        request.CertificateExtensions.Add(
            new X509EnhancedKeyUsageExtension(
                new OidCollection
                {
                    new Oid("1.3.6.1.5.5.7.3.1", "Server Authentication")
                },
                critical: false));

        // Subject Key Identifier
        request.CertificateExtensions.Add(
            new X509SubjectKeyIdentifierExtension(
                request.PublicKey,
                critical: false));

        // Authority Key Identifier
        // Only need to check for the Subject Key Identifier extension or public key availability
        var skiExtension = caCert.Extensions["2.5.29.14"];
        if (skiExtension != null || caCert.GetECDsaPublicKey() != null)
        {
            request.CertificateExtensions.Add(
                AuthorityKeyIdentifierExtension.Create(
                    caCert,
                    false));
        }

        // Subject Alternative Names (SANs)
        var sanBuilder = new SubjectAlternativeNameBuilder();
        foreach (var hostname in hostnames.Distinct(StringComparer.OrdinalIgnoreCase))
        {
            if (IPAddress.TryParse(hostname, out var ipAddress))
                sanBuilder.AddIpAddress(ipAddress);
            else
                sanBuilder.AddDnsName(hostname);
        }
        request.CertificateExtensions.Add(sanBuilder.Build());

        // Not before now (with clock skew buffer), not after validity
        var notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
        var notAfter = DateTimeOffset.UtcNow.Add(SERVER_VALIDITY);

        // Get the CA certificate with private key for signing
        X509Certificate2 caCertWithKey;
        IDisposable? certToDispose = null;
        if (caCert.HasPrivateKey)
        {
            caCertWithKey = caCert;
        }
        else
        {
            caCertWithKey = caCert.CopyWithPrivateKey(caKey);
            certToDispose = caCertWithKey;
        }

        try
        {
            // Create certificate signed by CA
            var serialNumber = RandomNumberGenerator.GetBytes(8);

            var certificate = request.Create(
                caCertWithKey,
                notBefore,
                notAfter,
                serialNumber);

            // Export and re-import with private key
            var certWithKey = certificate.CopyWithPrivateKey(key);

            // Export to PFX format to get a usable certificate with private key
            // Use a temporary password to avoid unprotected key material in memory
            var tempPassword = CertificateStorageHelper.GeneratePfxPassword();
            var export = certWithKey.Export(X509ContentType.Pfx, tempPassword);
            var finalCert = X509CertificateLoader.LoadPkcs12(
                export,
                tempPassword,
                X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
            CryptographicOperations.ZeroMemory(export);

            // Create a new ECDsa instance with the same key parameters
            var keyParams = key.ExportECPrivateKey();
            var newKey = ECDsa.Create();
            newKey.ImportECPrivateKey(keyParams, out _);
            CryptographicOperations.ZeroMemory(keyParams);

            return new ServerCertificatePair(finalCert, newKey);
        }
        finally
        {
            certToDispose?.Dispose();
        }
    }

    /// <summary>
    /// Creates a PFX bundle containing the server certificate with its private key.
    /// </summary>
    /// <param name="serverCert">The server certificate pair (contains the original exportable key).</param>
    /// <param name="caCert">The CA certificate pair (not included in output due to Windows limitations).</param>
    /// <param name="password">The password to protect the PFX file.</param>
    /// <returns>The PFX containing the server certificate with private key.</returns>
    public static byte[] CreatePfxBundle(
        ServerCertificatePair serverCert,
        CACertificatePair caCert,
        string password)
    {
        // On Windows, X509Certificate2Collection.Export fails with ECDSA keys
        // due to CAPI/CNG limitations.
        // To work around this, we manually re-create the certificates 
        // so the keys are not bound to CAPI/CNG and then export.

        // Load just the certificate without any key
        using var certOnly = X509CertificateLoader.LoadCertificate(serverCert.Certificate.RawData);
        using var caCertOnly = X509CertificateLoader.LoadCertificate(caCert.Certificate.RawData);

        // Create a new certificate with the private key explicitly associated
        // using the original exportable key from the pair
        using var serverCertWithKey = certOnly.CopyWithPrivateKey(serverCert.PrivateKey);
        var collection = new X509Certificate2Collection
        {
            caCertOnly,
            serverCertWithKey
        };

        return collection.Export(X509ContentType.Pfx, password) ?? throw new Exception("Failed to generate PFX bundle?");
    }

    /// <summary>
    /// Checks if a certificate is expired or will expire within the specified number of days.
    /// </summary>
    /// <param name="certificate">The certificate to check.</param>
    /// <param name="daysBeforeExpiration">Number of days before expiration to consider as expiring.</param>
    /// <returns>True if the certificate is expired or will expire within the specified days.</returns>
    public static bool IsExpiringSoon(X509Certificate2 certificate, int daysBeforeExpiration = 30)
    {
        return certificate.NotAfter < DateTimeOffset.UtcNow.AddDays(daysBeforeExpiration);
    }
}

/// <summary>
/// Helper class for creating Authority Key Identifier extensions.
/// </summary>
internal static class AuthorityKeyIdentifierExtension
{
    /// <summary>
    /// Creates an Authority Key Identifier extension from a CA certificate.
    /// </summary>
    /// <param name="caCert">The CA certificate.</param>
    /// <param name="critical">Whether the extension is critical.</param>
    /// <returns>An Authority Key Identifier extension.</returns>
    public static X509Extension Create(X509Certificate2 caCert, bool critical)
    {
        // Calculate the Subject Key Identifier from the CA certificate
        var subjectKeyId = GetSubjectKeyIdentifier(caCert);

        if (subjectKeyId == null)
        {
            // Return empty extension if no subject key ID available
            return new X509Extension(
                "2.5.29.35",
                new byte[] { 0x30, 0x00 }, // Empty SEQUENCE
                critical);
        }

        // Build the AuthorityKeyIdentifier ASN.1 structure
        var writer = new AsnWriter(AsnEncodingRules.DER);
        writer.PushSequence();

        // keyIdentifier [0] IMPLICIT OCTET STRING OPTIONAL
        // Write as context-specific tagged octet string
        writer.WriteOctetString(subjectKeyId, new Asn1Tag(TagClass.ContextSpecific, 0, true));

        writer.PopSequence();

        return new X509Extension(
            "2.5.29.35", // Authority Key Identifier OID
            writer.Encode(),
            critical);
    }

    /// <summary>
    /// Gets the Subject Key Identifier from a certificate.
    /// </summary>
    /// <param name="certificate">The certificate to get the SKI from.</param>
    /// <returns>The Subject Key Identifier as a byte array, or null if not available.</returns>
    private static byte[]? GetSubjectKeyIdentifier(X509Certificate2 certificate)
    {
        var skiExtension = certificate.Extensions["2.5.29.14"];
        if (skiExtension is X509SubjectKeyIdentifierExtension ski && ski.SubjectKeyIdentifier != null)
            return Convert.FromHexString(ski.SubjectKeyIdentifier);

        // Calculate SKI from public key if not present
        using var key = certificate.GetECDsaPublicKey();
        if (key != null)
            return SHA1.HashData(key.ExportSubjectPublicKeyInfo());

        return null;
    }
}
