using System.Security.Cryptography.X509Certificates;
using MemberCenter.Domain.Entities;
using MemberCenter.Infrastructure.Persistence;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;

namespace MemberCenter.Infrastructure.Configuration;

public static class CertificateLoader
{
    public const string OpenIddictSigningKey = "certificates:openiddict:signing";
    public const string OpenIddictEncryptionKey = "certificates:openiddict:encryption";
    public const string DataProtectionKey = "certificates:data-protection";

    public static void LogExpirationWarning(
        ILogger logger,
        string name,
        X509Certificate2? certificate,
        int warningDays)
    {
        if (certificate is null)
        {
            return;
        }

        var remaining = certificate.NotAfter.ToUniversalTime() - DateTime.UtcNow;
        if (remaining <= TimeSpan.FromDays(Math.Clamp(warningDays, 1, 180)))
        {
            logger.LogWarning(
                "Certificate {CertificateName} ({Thumbprint}) expires at {ExpiresAtUtc} ({RemainingDays:F1} days remaining).",
                name,
                certificate.Thumbprint,
                certificate.NotAfter.ToUniversalTime(),
                remaining.TotalDays);
        }
    }

    public static IReadOnlyList<X509Certificate2> LoadPreviousFromConfiguration(
        IConfiguration configuration,
        string sectionPath)
    {
        return configuration.GetSection($"{sectionPath}:Previous")
            .GetChildren()
            .Select(section => LoadFromConfiguration(configuration, section.Path, required: true)!)
            .ToArray();
    }

    public static X509Certificate2? LoadFromConfiguration(
        IConfiguration configuration,
        string sectionPath,
        bool required)
    {
        var path = configuration[$"{sectionPath}:Path"]?.Trim();
        var password = configuration[$"{sectionPath}:Password"];

        if (string.IsNullOrWhiteSpace(path))
        {
            if (required)
            {
                throw new InvalidOperationException($"{sectionPath}:Path is required outside Development.");
            }

            return null;
        }

        if (!File.Exists(path))
        {
            throw new InvalidOperationException($"Certificate file configured by {sectionPath}:Path does not exist: {path}");
        }

        X509Certificate2 certificate;
        try
        {
            certificate = new X509Certificate2(path, password, X509KeyStorageFlags.EphemeralKeySet);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException($"Certificate configured by {sectionPath} could not be loaded.", ex);
        }

        ValidateCertificate(certificate, $"configured by {sectionPath}");

        return certificate;
    }

    public static async Task<X509Certificate2?> LoadFromConfigurationOrDatabaseAsync(
        IConfiguration configuration,
        string connectionString,
        string sectionPath,
        string systemFlagKey,
        bool required,
        CancellationToken cancellationToken = default)
    {
        var configured = LoadFromConfiguration(configuration, sectionPath, required: false);
        if (configured is not null)
        {
            return configured;
        }

        var stored = await LoadFromDatabaseAsync(connectionString, systemFlagKey, cancellationToken);
        if (stored is not null)
        {
            return stored;
        }

        if (required)
        {
            throw new InvalidOperationException(
                $"{sectionPath}:Path or DB certificate '{systemFlagKey}' is required outside Development. Run installer init or migrate to provision certificates.");
        }

        return null;
    }

    public static async Task<X509Certificate2?> LoadFromDatabaseAsync(
        string connectionString,
        string systemFlagKey,
        CancellationToken cancellationToken = default)
    {
        await using var db = CreateDbContext(connectionString);
        var flag = await db.SystemFlags
            .AsNoTracking()
            .SingleOrDefaultAsync(item => item.Key == systemFlagKey, cancellationToken);
        if (flag is null || string.IsNullOrWhiteSpace(flag.Value))
        {
            return null;
        }

        X509Certificate2 certificate;
        try
        {
            certificate = new X509Certificate2(
                Convert.FromBase64String(flag.Value),
                password: (string?)null,
                X509KeyStorageFlags.EphemeralKeySet);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException($"Certificate stored in DB flag '{systemFlagKey}' could not be loaded.", ex);
        }

        ValidateCertificate(certificate, $"stored in DB flag '{systemFlagKey}'");
        return certificate;
    }

    public static async Task EnsureDatabaseCertificatesAsync(
        MemberCenterDbContext db,
        CancellationToken cancellationToken = default)
    {
        await EnsureDatabaseCertificateAsync(
            db,
            OpenIddictSigningKey,
            "membercenter-openiddict-signing",
            cancellationToken);
        await EnsureDatabaseCertificateAsync(
            db,
            OpenIddictEncryptionKey,
            "membercenter-openiddict-encryption",
            cancellationToken);
        await EnsureDatabaseCertificateAsync(
            db,
            DataProtectionKey,
            "membercenter-data-protection",
            cancellationToken);
    }

    private static async Task EnsureDatabaseCertificateAsync(
        MemberCenterDbContext db,
        string key,
        string subjectName,
        CancellationToken cancellationToken)
    {
        var flag = await db.SystemFlags.SingleOrDefaultAsync(item => item.Key == key, cancellationToken);
        if (flag is not null && !string.IsNullOrWhiteSpace(flag.Value))
        {
            return;
        }

        using var rsa = System.Security.Cryptography.RSA.Create(4096);
        var request = new System.Security.Cryptography.X509Certificates.CertificateRequest(
            $"CN={subjectName}",
            rsa,
            System.Security.Cryptography.HashAlgorithmName.SHA256,
            System.Security.Cryptography.RSASignaturePadding.Pkcs1);

        request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
        request.CertificateExtensions.Add(new X509KeyUsageExtension(
            X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
            critical: false));
        request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));

        using var certificate = request.CreateSelfSigned(
            DateTimeOffset.UtcNow.AddMinutes(-5),
            DateTimeOffset.UtcNow.AddYears(5));

        var value = Convert.ToBase64String(certificate.Export(X509ContentType.Pfx));
        if (flag is null)
        {
            db.SystemFlags.Add(new SystemFlag
            {
                Id = Guid.NewGuid(),
                Key = key,
                Value = value,
                UpdatedAt = DateTimeOffset.UtcNow
            });
            return;
        }

        flag.Value = value;
        flag.UpdatedAt = DateTimeOffset.UtcNow;
    }

    private static MemberCenterDbContext CreateDbContext(string connectionString)
    {
        var options = new DbContextOptionsBuilder<MemberCenterDbContext>()
            .UseNpgsql(connectionString)
            .UseOpenIddict()
            .Options;

        return new MemberCenterDbContext(options);
    }

    private static void ValidateCertificate(X509Certificate2 certificate, string source)
    {
        var now = DateTime.UtcNow;
        if (!certificate.HasPrivateKey)
        {
            certificate.Dispose();
            throw new InvalidOperationException($"Certificate {source} must contain a private key.");
        }

        if (now < certificate.NotBefore.ToUniversalTime() || now >= certificate.NotAfter.ToUniversalTime())
        {
            certificate.Dispose();
            throw new InvalidOperationException($"Certificate {source} is not currently valid.");
        }
    }
}
