using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Serilog;
using ZB.MOM.WW.LmxProxy.Host.Configuration;

namespace ZB.MOM.WW.LmxProxy.Host.Security
{
    /// <summary>
    ///     Manages TLS certificates for the LmxProxy service, including generation and validation
    /// </summary>
    public class TlsCertificateManager
    {
        private static readonly ILogger Logger = Log.ForContext<TlsCertificateManager>();
        private readonly TlsConfiguration _tlsConfiguration;

        public TlsCertificateManager(TlsConfiguration tlsConfiguration)
        {
            _tlsConfiguration = tlsConfiguration ?? throw new ArgumentNullException(nameof(tlsConfiguration));
        }

        /// <summary>
        ///     Checks TLS certificate status and creates new certificates if needed
        /// </summary>
        /// <returns>True if certificates are valid or were successfully created</returns>
        public bool EnsureCertificatesValid()
        {
            if (!_tlsConfiguration.Enabled)
            {
                Logger.Information("TLS is disabled, skipping certificate check");
                return true;
            }

            try
            {
                // Check if certificate files exist
                bool certificateExists = File.Exists(_tlsConfiguration.ServerCertificatePath);
                bool keyExists = File.Exists(_tlsConfiguration.ServerKeyPath);

                if (!certificateExists || !keyExists)
                {
                    Logger.Warning("TLS certificate or key not found, generating new certificate");
                    return GenerateNewCertificate();
                }

                // Check certificate expiration
                if (IsCertificateExpiringSoon(_tlsConfiguration.ServerCertificatePath))
                {
                    Logger.Warning("TLS certificate is expiring within the next year, generating new certificate");
                    return GenerateNewCertificate();
                }

                Logger.Information("TLS certificate is valid");
                return true;
            }
            catch (Exception ex)
            {
                Logger.Error(ex, "Error checking TLS certificates");
                return false;
            }
        }

        /// <summary>
        ///     Checks if a certificate is expiring within the next year
        /// </summary>
        private bool IsCertificateExpiringSoon(string certificatePath)
        {
            try
            {
                string certPem = File.ReadAllText(certificatePath);
                byte[] certBytes = GetBytesFromPem(certPem, "CERTIFICATE");

                using var cert = new X509Certificate2(certBytes);
                DateTime expirationDate = cert.NotAfter;
                double daysUntilExpiration = (expirationDate - DateTime.Now).TotalDays;

                Logger.Information("Certificate expires on {ExpirationDate} ({DaysUntilExpiration:F0} days from now)",
                    expirationDate, daysUntilExpiration);

                // Check if expiring within the next year (365 days)
                return daysUntilExpiration <= 365;
            }
            catch (Exception ex)
            {
                Logger.Error(ex, "Error checking certificate expiration");
                // If we can't check expiration, assume it needs renewal
                return true;
            }
        }

        /// <summary>
        ///     Generates a new self-signed certificate
        /// </summary>
        private bool GenerateNewCertificate()
        {
            try
            {
                Logger.Information("Generating new self-signed TLS certificate");

                // Ensure directory exists
                string? certDir = Path.GetDirectoryName(_tlsConfiguration.ServerCertificatePath);
                if (!string.IsNullOrEmpty(certDir) && !Directory.Exists(certDir))
                {
                    Directory.CreateDirectory(certDir);
                    Logger.Information("Created certificate directory: {Directory}", certDir);
                }

                // Generate a new self-signed certificate
                using var rsa = RSA.Create(2048);
                var request = new CertificateRequest(
                    "CN=LmxProxy, O=SCADA Bridge, C=US",
                    rsa,
                    HashAlgorithmName.SHA256,
                    RSASignaturePadding.Pkcs1);

                // Add certificate extensions
                request.CertificateExtensions.Add(
                    new X509BasicConstraintsExtension(false, false, 0, false));

                request.CertificateExtensions.Add(
                    new X509KeyUsageExtension(
                        X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
                        false));

                request.CertificateExtensions.Add(
                    new X509EnhancedKeyUsageExtension(
                        new OidCollection
                        {
                            new Oid("1.3.6.1.5.5.7.3.1") // Server Authentication
                        },
                        false));

                // Add Subject Alternative Names
                var sanBuilder = new SubjectAlternativeNameBuilder();
                sanBuilder.AddDnsName("localhost");
                sanBuilder.AddDnsName(Environment.MachineName);
                sanBuilder.AddIpAddress(IPAddress.Loopback);
                sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
                request.CertificateExtensions.Add(sanBuilder.Build());

                // Create the certificate with 2-year validity
                DateTimeOffset notBefore = DateTimeOffset.Now.AddDays(-1);
                DateTimeOffset notAfter = DateTimeOffset.Now.AddYears(2);

                using X509Certificate2? cert = request.CreateSelfSigned(notBefore, notAfter);

                // Export certificate to PEM format
                string certPem = ExportCertificateToPem(cert);
                File.WriteAllText(_tlsConfiguration.ServerCertificatePath, certPem);
                Logger.Information("Saved certificate to {Path}", _tlsConfiguration.ServerCertificatePath);

                // Export private key to PEM format
                string keyPem = ExportPrivateKeyToPem(rsa);
                File.WriteAllText(_tlsConfiguration.ServerKeyPath, keyPem);
                Logger.Information("Saved private key to {Path}", _tlsConfiguration.ServerKeyPath);

                // If client CA path is specified and doesn't exist, create it
                if (!string.IsNullOrWhiteSpace(_tlsConfiguration.ClientCaCertificatePath) &&
                    !File.Exists(_tlsConfiguration.ClientCaCertificatePath))
                {
                    // For self-signed certificates, the CA cert is the same as the server cert
                    File.WriteAllText(_tlsConfiguration.ClientCaCertificatePath, certPem);
                    Logger.Information("Saved CA certificate to {Path}", _tlsConfiguration.ClientCaCertificatePath);
                }

                Logger.Information("Successfully generated new TLS certificate valid until {NotAfter}", notAfter);
                return true;
            }
            catch (Exception ex)
            {
                Logger.Error(ex, "Failed to generate new TLS certificate");
                return false;
            }
        }

        /// <summary>
        ///     Exports a certificate to PEM format
        /// </summary>
        private static string ExportCertificateToPem(X509Certificate2 cert)
        {
            var builder = new StringBuilder();
            builder.AppendLine("-----BEGIN CERTIFICATE-----");
            builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert),
                Base64FormattingOptions.InsertLineBreaks));
            builder.AppendLine("-----END CERTIFICATE-----");
            return builder.ToString();
        }

        /// <summary>
        ///     Exports an RSA private key to PEM format
        /// </summary>
        private static string ExportPrivateKeyToPem(RSA rsa)
        {
            var builder = new StringBuilder();
            builder.AppendLine("-----BEGIN RSA PRIVATE KEY-----");

            // For .NET Framework 4.8, we need to use the older export method
            RSAParameters parameters = rsa.ExportParameters(true);
            byte[] keyBytes = EncodeRSAPrivateKey(parameters);
            builder.AppendLine(Convert.ToBase64String(keyBytes, Base64FormattingOptions.InsertLineBreaks));

            builder.AppendLine("-----END RSA PRIVATE KEY-----");
            return builder.ToString();
        }

        /// <summary>
        ///     Encodes RSA parameters to PKCS#1 format for .NET Framework 4.8
        /// </summary>
        private static byte[] EncodeRSAPrivateKey(RSAParameters parameters)
        {
            using (var stream = new MemoryStream())
            using (var writer = new BinaryWriter(stream))
            {
                // Write version
                writer.Write((byte)0x02); // INTEGER
                writer.Write((byte)0x01); // Length
                writer.Write((byte)0x00); // Version

                // Write modulus
                WriteIntegerBytes(writer, parameters.Modulus);

                // Write public exponent
                WriteIntegerBytes(writer, parameters.Exponent);

                // Write private exponent
                WriteIntegerBytes(writer, parameters.D);

                // Write prime1
                WriteIntegerBytes(writer, parameters.P);

                // Write prime2
                WriteIntegerBytes(writer, parameters.Q);

                // Write exponent1
                WriteIntegerBytes(writer, parameters.DP);

                // Write exponent2
                WriteIntegerBytes(writer, parameters.DQ);

                // Write coefficient
                WriteIntegerBytes(writer, parameters.InverseQ);

                byte[] innerBytes = stream.ToArray();

                // Create SEQUENCE wrapper
                using (var finalStream = new MemoryStream())
                using (var finalWriter = new BinaryWriter(finalStream))
                {
                    finalWriter.Write((byte)0x30); // SEQUENCE
                    WriteLength(finalWriter, innerBytes.Length);
                    finalWriter.Write(innerBytes);
                    return finalStream.ToArray();
                }
            }
        }

        private static void WriteIntegerBytes(BinaryWriter writer, byte[] bytes)
        {
            if (bytes == null)
            {
                bytes = new byte[] { 0 };
            }

            writer.Write((byte)0x02); // INTEGER

            if (bytes[0] >= 0x80)
            {
                // Add padding byte for positive number
                WriteLength(writer, bytes.Length + 1);
                writer.Write((byte)0x00);
                writer.Write(bytes);
            }
            else
            {
                WriteLength(writer, bytes.Length);
                writer.Write(bytes);
            }
        }

        private static void WriteLength(BinaryWriter writer, int length)
        {
            if (length < 0x80)
            {
                writer.Write((byte)length);
            }
            else if (length <= 0xFF)
            {
                writer.Write((byte)0x81);
                writer.Write((byte)length);
            }
            else
            {
                writer.Write((byte)0x82);
                writer.Write((byte)(length >> 8));
                writer.Write((byte)(length & 0xFF));
            }
        }

        /// <summary>
        ///     Extracts bytes from PEM format
        /// </summary>
        private static byte[] GetBytesFromPem(string pem, string section)
        {
            string header = $"-----BEGIN {section}-----";
            string footer = $"-----END {section}-----";

            int start = pem.IndexOf(header, StringComparison.Ordinal);
            if (start < 0)
            {
                throw new InvalidOperationException($"PEM {section} header not found");
            }

            start += header.Length;
            int end = pem.IndexOf(footer, start, StringComparison.Ordinal);

            if (end < 0)
            {
                throw new InvalidOperationException($"PEM {section} footer not found");
            }

            // Use Substring instead of range syntax for .NET Framework 4.8 compatibility
            string base64 = pem.Substring(start, end - start).Replace("\r", "").Replace("\n", "");
            return Convert.FromBase64String(base64);
        }
    }
}
