using System.Net.Security;
using System.Formats.Asn1;
using System.Text.RegularExpressions;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace NATS.Server.Tls;

public static class TlsHelper
{
    private const string AuthorityInfoAccessOid = "1.3.6.1.5.5.7.1.1";
    private const string OcspAccessMethodOid = "1.3.6.1.5.5.7.48.1";
    private const string OcspSigningEkuOid = "1.3.6.1.5.5.7.3.9";

    public static X509Certificate2 LoadCertificate(string certPath, string? keyPath)
    {
        if (keyPath != null)
            return X509Certificate2.CreateFromPemFile(certPath, keyPath);
        return X509CertificateLoader.LoadCertificateFromFile(certPath);
    }

    public static X509Certificate2Collection LoadCaCertificates(string caPath)
    {
        var pem = File.ReadAllText(caPath);
        return ParseCertPem(pem);
    }

    /// <summary>
    /// Parses one or more PEM blocks and requires all blocks to be CERTIFICATE.
    /// Mirrors Go parseCertPEM behavior by rejecting unexpected block types.
    /// </summary>
    public static X509Certificate2Collection ParseCertPem(string pemData)
    {
        if (string.IsNullOrWhiteSpace(pemData))
            throw new InvalidDataException("PEM data is empty.");

        var beginMatches = Regex.Matches(pemData, "-----BEGIN ([^-]+)-----");
        if (beginMatches.Count == 0)
            throw new InvalidDataException("No PEM certificate block found.");

        foreach (Match match in beginMatches)
        {
            var label = match.Groups[1].Value;
            if (!string.Equals(label, "CERTIFICATE", StringComparison.Ordinal))
                throw new InvalidDataException($"unexpected PEM certificate type: {label}");
        }

        var certs = new X509Certificate2Collection();
        var certMatches = Regex.Matches(
            pemData,
            "-----BEGIN CERTIFICATE-----\\s*(?<body>[A-Za-z0-9+/=\\r\\n]+?)\\s*-----END CERTIFICATE-----",
            RegexOptions.Singleline);

        foreach (Match certMatch in certMatches)
        {
            var body = certMatch.Groups["body"].Value;
            var normalized = Regex.Replace(body, "\\s+", "", RegexOptions.Singleline);
            var der = Convert.FromBase64String(normalized);
            certs.Add(X509CertificateLoader.LoadCertificate(der));
        }

        if (certs.Count == 0)
            throw new InvalidDataException("No PEM certificate block found.");

        return certs;
    }

    public static SslServerAuthenticationOptions BuildServerAuthOptions(NatsOptions opts)
    {
        var cert = LoadCertificate(opts.TlsCert!, opts.TlsKey);
        var authOpts = new SslServerAuthenticationOptions
        {
            ServerCertificate = cert,
            EnabledSslProtocols = opts.TlsMinVersion,
            ClientCertificateRequired = opts.TlsVerify,
        };

        if (opts.TlsVerify && opts.TlsCaCert != null)
        {
            var revocationMode = opts.OcspPeerVerify
                ? X509RevocationMode.Online
                : X509RevocationMode.NoCheck;

            var caCerts = LoadCaCertificates(opts.TlsCaCert);
            authOpts.RemoteCertificateValidationCallback = (_, cert, chain, errors) =>
            {
                if (cert == null) return false;
                using var chain2 = new X509Chain();
                chain2.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
                foreach (var ca in caCerts)
                    chain2.ChainPolicy.CustomTrustStore.Add(ca);
                chain2.ChainPolicy.RevocationMode = revocationMode;
                var cert2 = cert as X509Certificate2 ?? X509CertificateLoader.LoadCertificate(cert.GetRawCertData());
                return chain2.Build(cert2);
            };
        }
        else if (opts.OcspPeerVerify)
        {
            // No custom CA — still enable online revocation checking against the system store
            authOpts.RemoteCertificateValidationCallback = (_, cert, chain, errors) =>
            {
                if (cert == null) return false;
                using var chain2 = new X509Chain();
                chain2.ChainPolicy.RevocationMode = X509RevocationMode.Online;
                var cert2 = cert as X509Certificate2 ?? X509CertificateLoader.LoadCertificate(cert.GetRawCertData());
                return chain2.Build(cert2);
            };
        }

        return authOpts;
    }

    /// <summary>
    /// Builds an <see cref="SslStreamCertificateContext"/> for OCSP stapling.
    /// Returns null when TLS is not configured or OCSP mode is Never.
    /// When <paramref name="offline"/> is false the runtime will contact the
    /// certificate's OCSP responder to obtain a fresh stapled response.
    /// </summary>
    public static SslStreamCertificateContext? BuildCertificateContext(NatsOptions opts, bool offline = false)
    {
        if (!opts.HasTls) return null;
        if (opts.OcspConfig is null || opts.OcspConfig.Mode == OcspMode.Never) return null;

        var cert = LoadCertificate(opts.TlsCert!, opts.TlsKey);
        var chain = new X509Certificate2Collection();
        if (!string.IsNullOrEmpty(opts.TlsCaCert))
            chain.ImportFromPemFile(opts.TlsCaCert);

        return SslStreamCertificateContext.Create(cert, chain, offline: offline);
    }

    public static string GetCertificateHash(X509Certificate2 cert)
    {
        var spki = cert.PublicKey.ExportSubjectPublicKeyInfo();
        var hash = SHA256.HashData(spki);
        return Convert.ToHexStringLower(hash);
    }

    public static string GenerateFingerprint(X509Certificate2 cert)
    {
        var hash = SHA256.HashData(cert.RawData);
        return Convert.ToBase64String(hash);
    }

    public static IReadOnlyList<Uri> GetWebEndpoints(IEnumerable<string> uris)
    {
        var urls = new List<Uri>();
        foreach (var uri in uris)
        {
            if (!Uri.TryCreate(uri, UriKind.Absolute, out var endpoint))
                continue;
            if (!string.Equals(endpoint.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
                !string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
                continue;
            urls.Add(endpoint);
        }

        return urls;
    }

    public static string GetSubjectDNForm(X509Certificate2? cert)
    {
        return cert?.SubjectName.Name ?? string.Empty;
    }

    public static string GetIssuerDNForm(X509Certificate2? cert)
    {
        return cert?.IssuerName.Name ?? string.Empty;
    }

    public static bool MatchesPinnedCert(X509Certificate2 cert, HashSet<string> pinned)
    {
        var hash = GetCertificateHash(cert);
        return pinned.Contains(hash);
    }

    /// <summary>
    /// Checks if a chain link is eligible for OCSP validation by ensuring the leaf
    /// certificate includes at least one valid HTTP(S) OCSP AIA endpoint.
    /// </summary>
    public static bool CertOCSPEligible(ChainLink? link)
    {
        if (link?.Leaf is null)
            return false;

        if (link.Leaf.RawData is null || link.Leaf.RawData.Length == 0)
            return false;

        var aiaUris = GetOcspResponderUris(link.Leaf);
        if (aiaUris.Count == 0)
            return false;

        var urls = GetWebEndpoints(aiaUris);
        if (urls.Count == 0)
            return false;

        link.OCSPWebEndpoints = urls;
        return true;
    }

    /// <summary>
    /// Returns the positional issuer certificate for a leaf in a verified chain.
    /// </summary>
    public static X509Certificate2? GetLeafIssuerCert(IReadOnlyList<X509Certificate2>? chain, int leafPos)
    {
        if (chain is null || chain.Count == 0 || leafPos < 0 || leafPos >= chain.Count - 1)
            return null;
        return chain[leafPos + 1];
    }

    /// <summary>
    /// Equivalent to Go certstore.GetLeafIssuer: verifies the leaf against the
    /// supplied trust root and returns the first issuer in the verified chain.
    /// </summary>
    public static X509Certificate2? GetLeafIssuer(X509Certificate2 leaf, X509Certificate2 trustedRoot)
    {
        using var chain = new X509Chain();
        chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
        chain.ChainPolicy.CustomTrustStore.Add(trustedRoot);
        chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
        chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;

        if (!chain.Build(leaf) || chain.ChainElements.Count < 2)
            return null;

        return X509CertificateLoader.LoadCertificate(chain.ChainElements[1].Certificate.RawData);
    }

    /// <summary>
    /// Checks OCSP response currency semantics with clock skew and fallback TTL.
    /// </summary>
    public static bool OcspResponseCurrent(OcspResponseInfo response, OCSPPeerConfig opts)
    {
        var skew = TimeSpan.FromSeconds(opts.ClockSkew);
        if (skew < TimeSpan.Zero)
            skew = OCSPPeerConfig.DefaultAllowedClockSkew;

        var now = DateTime.UtcNow;

        if (response.NextUpdate.HasValue && response.NextUpdate.Value < now - skew)
            return false;

        if (!response.NextUpdate.HasValue)
        {
            var ttl = TimeSpan.FromSeconds(opts.TTLUnsetNextUpdate);
            if (ttl < TimeSpan.Zero)
                ttl = OCSPPeerConfig.DefaultTTLUnsetNextUpdate;

            if (response.ThisUpdate + ttl < now - skew)
                return false;
        }

        if (response.ThisUpdate > now + skew)
            return false;

        return true;
    }

    /// <summary>
    /// Validates OCSP delegated signer semantics. Direct issuer signatures are valid;
    /// delegated certificates must include id-kp-OCSPSigning EKU.
    /// </summary>
    public static bool ValidDelegationCheck(X509Certificate2? issuer, X509Certificate2? responderCertificate)
    {
        if (issuer is null)
            return false;

        if (responderCertificate is null)
            return true;

        if (responderCertificate.Thumbprint == issuer.Thumbprint)
            return true;

        foreach (var extension in responderCertificate.Extensions)
        {
            if (extension is not X509EnhancedKeyUsageExtension eku)
                continue;
            foreach (var oid in eku.EnhancedKeyUsages)
            {
                if (oid.Value == OcspSigningEkuOid)
                    return true;
            }
        }

        return false;
    }

    [SlopwatchSuppress("SW003", "AsnContentException on a malformed AIA extension is intentionally swallowed; invalid extension shape means no usable OCSP URI")]
    private static IReadOnlyList<string> GetOcspResponderUris(X509Certificate2 cert)
    {
        var uris = new List<string>();

        foreach (var extension in cert.Extensions)
        {
            if (!string.Equals(extension.Oid?.Value, AuthorityInfoAccessOid, StringComparison.Ordinal))
                continue;

            try
            {
                var reader = new AsnReader(extension.RawData, AsnEncodingRules.DER);
                var seq = reader.ReadSequence();
                while (seq.HasData)
                {
                    var accessDescription = seq.ReadSequence();
                    var accessMethod = accessDescription.ReadObjectIdentifier();
                    if (!string.Equals(accessMethod, OcspAccessMethodOid, StringComparison.Ordinal))
                    {
                        accessDescription.ThrowIfNotEmpty();
                        continue;
                    }

                    var uri = accessDescription.ReadCharacterString(
                        UniversalTagNumber.IA5String,
                        new Asn1Tag(TagClass.ContextSpecific, 6));

                    accessDescription.ThrowIfNotEmpty();
                    if (!string.IsNullOrWhiteSpace(uri))
                        uris.Add(uri);
                }

                seq.ThrowIfNotEmpty();
                reader.ThrowIfNotEmpty();
            }
            catch (AsnContentException ex)
            {
                // Invalid AIA extension shape should behave as "no usable OCSP URI" — swallow is intentional.
                _ = ex.Message;
            }
        }

        return uris;
    }
}
