// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0

using System.Globalization;
using System.Formats.Asn1;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;

namespace ZB.MOM.NatsNet.Server;

/// <summary>
/// OCSP helper functions mapped from server/ocsp.go package-level helpers.
/// </summary>
internal static class OcspHandler
{
    private const string CertPemLabel = "CERTIFICATE";
    private const string TlsFeaturesOid = "1.3.6.1.5.5.7.1.24";
    private const int StatusRequestExtension = 5;

    internal const string OcspResponseCacheDefaultDir = "_rc_";
    internal const string OcspResponseCacheTypeNone = "none";
    internal const string OcspResponseCacheTypeLocal = "local";
    internal static readonly TimeSpan OcspResponseCacheMinimumSaveInterval = TimeSpan.FromSeconds(1);
    internal static readonly TimeSpan OcspResponseCacheDefaultSaveInterval = TimeSpan.FromMinutes(5);
    internal const string OcspResponseCacheDefaultFilename = "cache.json";
    internal const string OcspResponseCacheDefaultTempFilePrefix = "ocsprc-";

    internal static OcspResponseCacheConfig NewOCSPResponseCacheConfig() =>
        new()
        {
            Type = OcspResponseCacheTypeLocal,
            LocalStore = OcspResponseCacheDefaultDir,
            PreserveRevoked = false,
            SaveInterval = OcspResponseCacheDefaultSaveInterval.TotalSeconds,
        };

    internal static (OcspResponseCacheConfig? config, Exception? error) ParseOCSPResponseCache(object? value)
    {
        if (value is not IDictionary<string, object?> map)
        {
            return (null, new InvalidOperationException(
                string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrIllegalCacheOptsConfig, value ?? "null")));
        }

        var config = NewOCSPResponseCacheConfig();

        foreach (var (key, raw) in map)
        {
            switch (key.ToLowerInvariant())
            {
                case "type":
                    if (raw is not string cacheType)
                    {
                        return (null, new InvalidOperationException(
                            string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldGeneric, key)));
                    }

                    var normalizedType = cacheType.ToLowerInvariant();
                    if (normalizedType != OcspResponseCacheTypeLocal && normalizedType != OcspResponseCacheTypeNone)
                    {
                        return (null, new InvalidOperationException(
                            string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrUnknownCacheType, cacheType)));
                    }

                    config.Type = normalizedType;
                    break;

                case "local_store":
                    if (raw is not string store)
                    {
                        return (null, new InvalidOperationException(
                            string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldGeneric, key)));
                    }

                    config.LocalStore = store;
                    break;

                case "preserve_revoked":
                    if (raw is not bool preserveRevoked)
                    {
                        return (null, new InvalidOperationException(
                            string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldGeneric, key)));
                    }

                    config.PreserveRevoked = preserveRevoked;
                    break;

                case "save_interval":
                    var (seconds, parseError) = ParseCacheSaveIntervalSeconds(raw);
                    if (parseError != null)
                    {
                        return (null, parseError);
                    }

                    var parsedDuration = TimeSpan.FromSeconds(seconds);
                    if (parsedDuration < OcspResponseCacheMinimumSaveInterval)
                    {
                        parsedDuration = OcspResponseCacheMinimumSaveInterval;
                    }

                    config.SaveInterval = parsedDuration.TotalSeconds;
                    break;

                default:
                    return (null, new InvalidOperationException(
                        string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldGeneric, key)));
            }
        }

        return (config, null);
    }

    internal static (List<X509Certificate2>? certificates, Exception? error) ParseCertPEM(string name)
    {
        try
        {
            var text = File.ReadAllText(name);
            var span = text.AsSpan();
            var certificates = new List<X509Certificate2>();

            while (PemEncoding.TryFind(span, out var fields))
            {
                var label = span[fields.Label].ToString();
                if (!string.Equals(label, CertPemLabel, StringComparison.Ordinal))
                {
                    return (null, new InvalidOperationException($"unexpected PEM certificate type: {label}"));
                }

                var derBytes = Convert.FromBase64String(span[fields.Base64Data].ToString());
                certificates.Add(new X509Certificate2(derBytes));
                span = span[fields.Location.End..];
            }

            if (certificates.Count == 0)
            {
                return (null, new InvalidOperationException("failed to parse certificate pem"));
            }

            return (certificates, null);
        }
        catch (Exception ex)
        {
            return (null, ex);
        }
    }

    internal static (OcspPeerConfig? config, Exception? error) ParseOCSPPeer(object? value)
    {
        if (value is not IDictionary<string, object?> map)
        {
            return (null, new InvalidOperationException(
                string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrIllegalPeerOptsConfig, value ?? "null")));
        }

        var config = OcspPeerConfig.Create();
        foreach (var (key, raw) in map)
        {
            switch (key.ToLowerInvariant())
            {
                case "verify":
                    if (raw is not bool verify)
                    {
                        return (null, new InvalidOperationException(
                            string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldGeneric, key)));
                    }
                    config.Verify = verify;
                    break;

                case "allowed_clockskew":
                    var (clockSkew, skewError) = ParsePeerDurationValue(raw);
                    if (skewError != null)
                    {
                        return (null, skewError);
                    }
                    if (clockSkew >= 0)
                    {
                        config.ClockSkew = clockSkew;
                    }
                    break;

                case "ca_timeout":
                    var (timeout, timeoutError) = ParsePeerDurationValue(raw);
                    if (timeoutError != null)
                    {
                        return (null, timeoutError);
                    }
                    if (timeout >= 0)
                    {
                        config.Timeout = timeout;
                    }
                    break;

                case "cache_ttl_when_next_update_unset":
                    var (ttl, ttlError) = ParsePeerDurationValue(raw);
                    if (ttlError != null)
                    {
                        return (null, ttlError);
                    }
                    if (ttl >= 0)
                    {
                        config.TTLUnsetNextUpdate = ttl;
                    }
                    break;

                case "warn_only":
                    if (raw is not bool warnOnly)
                    {
                        return (null, new InvalidOperationException(
                            string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldGeneric, key)));
                    }
                    config.WarnOnly = warnOnly;
                    break;

                case "unknown_is_good":
                    if (raw is not bool unknownIsGood)
                    {
                        return (null, new InvalidOperationException(
                            string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldGeneric, key)));
                    }
                    config.UnknownIsGood = unknownIsGood;
                    break;

                case "allow_when_ca_unreachable":
                    if (raw is not bool allowWhenCaUnreachable)
                    {
                        return (null, new InvalidOperationException(
                            string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldGeneric, key)));
                    }
                    config.AllowWhenCAUnreachable = allowWhenCaUnreachable;
                    break;

                default:
                    return (null, new InvalidOperationException(
                        string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldGeneric, key)));
            }
        }

        return (config, null);
    }

    internal static X509Certificate2? PeerFromVerifiedChains(X509Certificate2[][] chains)
    {
        if (chains.Length == 0 || chains[0].Length == 0)
        {
            return null;
        }

        return chains[0][0];
    }

    internal static bool HasOCSPStatusRequest(X509Certificate2 cert)
    {
        foreach (var extension in cert.Extensions)
        {
            if (!string.Equals(extension.Oid?.Value, TlsFeaturesOid, StringComparison.Ordinal))
            {
                continue;
            }

            try
            {
                var reader = new AsnReader(extension.RawData, AsnEncodingRules.DER);
                var seq = reader.ReadSequence();
                while (seq.HasData)
                {
                    if (seq.ReadInteger() == StatusRequestExtension)
                    {
                        return true;
                    }
                }

                if (reader.HasData)
                {
                    return false;
                }
            }
            catch
            {
                return false;
            }

            break;
        }

        return false;
    }

    internal static (X509Certificate2? issuer, Exception? error) GetOCSPIssuerLocally(
        IReadOnlyList<X509Certificate2> trustedCAs,
        IReadOnlyList<X509Certificate2> certBundle)
    {
        if (certBundle.Count == 0)
        {
            return (null, new InvalidOperationException("invalid ocsp ca configuration"));
        }

        var leaf = certBundle[0];
        if (certBundle.Count > 1)
        {
            var issuerCandidate = certBundle[1];
            if (!string.Equals(leaf.Issuer, issuerCandidate.Subject, StringComparison.Ordinal))
            {
                return (null, new InvalidOperationException("invalid issuer configuration: issuer subject mismatch"));
            }
            return (issuerCandidate, null);
        }

        using var chain = new X509Chain();
        chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
        chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;

        if (trustedCAs.Count > 0)
        {
            chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
            foreach (var ca in trustedCAs)
            {
                chain.ChainPolicy.CustomTrustStore.Add(ca);
            }
        }

        if (!chain.Build(leaf) || chain.ChainElements.Count < 2)
        {
            if (string.Equals(leaf.Subject, leaf.Issuer, StringComparison.Ordinal))
            {
                return (leaf, null);
            }

            return (null, null);
        }

        return (chain.ChainElements[1].Certificate, null);
    }

    internal static (X509Certificate2? issuer, Exception? error) GetOCSPIssuer(string caFile, IReadOnlyList<byte[]> chain)
    {
        var trustedCAs = new List<X509Certificate2>();
        if (!string.IsNullOrEmpty(caFile))
        {
            var (parsed, parseError) = ParseCertPEM(caFile);
            if (parseError != null)
            {
                return (null, new InvalidOperationException($"failed to parse ca_file: {parseError.Message}", parseError));
            }

            trustedCAs.AddRange(parsed!);
        }

        var certBundle = new List<X509Certificate2>(chain.Count);
        foreach (var certBytes in chain)
        {
            try
            {
                certBundle.Add(new X509Certificate2(certBytes));
            }
            catch (Exception ex)
            {
                return (null, new InvalidOperationException($"failed to parse cert: {ex.Message}", ex));
            }
        }

        var (issuer, issuerError) = GetOCSPIssuerLocally(trustedCAs, certBundle);
        if (issuerError != null || issuer == null)
        {
            return (null, new InvalidOperationException("no issuers found"));
        }

        if (!IsCertificateAuthority(issuer))
        {
            return (null, new InvalidOperationException(
                string.Create(CultureInfo.InvariantCulture, $"{issuer.Subject} invalid ca basic constraints: is not ca")));
        }

        return (issuer, null);
    }

    internal static string OcspStatusString(int status) => status switch
    {
        0 => "good",
        1 => "revoked",
        _ => "unknown",
    };

    internal static Exception? ValidOCSPResponse(OcspResponse response, DateTime? nowUtc = null)
    {
        var now = nowUtc ?? DateTime.UtcNow;

        if (response.NextUpdate != DateTime.MinValue && response.NextUpdate < now)
        {
            var t = response.NextUpdate.ToString("O", CultureInfo.InvariantCulture);
            return new InvalidOperationException($"invalid ocsp NextUpdate, is past time: {t}");
        }

        if (response.ThisUpdate > now)
        {
            var t = response.ThisUpdate.ToString("O", CultureInfo.InvariantCulture);
            return new InvalidOperationException($"invalid ocsp ThisUpdate, is future time: {t}");
        }

        return null;
    }

    internal static (OcspResponse? response, Exception? error) ParseOcspResponse(byte[] raw)
    {
        try
        {
            var parsed = JsonSerializer.Deserialize<SerializedOcspResponse>(raw);
            if (parsed == null)
            {
                return (null, new InvalidOperationException("failed to parse OCSP response"));
            }

            var status = parsed.Status switch
            {
                0 => OcspStatusAssertion.Good,
                1 => OcspStatusAssertion.Revoked,
                _ => OcspStatusAssertion.Unknown,
            };

            var response = new OcspResponse
            {
                Status = status,
                ThisUpdate = parsed.ThisUpdate,
                NextUpdate = parsed.NextUpdate ?? DateTime.MinValue,
            };

            return (response, null);
        }
        catch (Exception ex)
        {
            return (null, ex);
        }
    }

    private static bool IsCertificateAuthority(X509Certificate2 cert)
    {
        foreach (var extension in cert.Extensions)
        {
            if (extension is X509BasicConstraintsExtension basicConstraints)
            {
                return basicConstraints.CertificateAuthority;
            }
        }

        return false;
    }

    private static (double value, Exception? error) ParsePeerDurationValue(object? value)
    {
        return value switch
        {
            null => (0, new InvalidOperationException(
                string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldTypeConversion, "unexpected type"))),
            int i => (i, null),
            long l => (l, null),
            float f => (f, null),
            double d => (d, null),
            string s => ParseDurationSeconds(s),
            _ => (0, new InvalidOperationException(
                string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldTypeConversion, "unexpected type"))),
        };
    }

    private static (double value, Exception? error) ParseDurationSeconds(string duration)
    {
        if (TimeSpan.TryParse(duration, CultureInfo.InvariantCulture, out var parsed))
        {
            return (parsed.TotalSeconds, null);
        }

        return (0, new InvalidOperationException(
            string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldTypeConversion, "unexpected type")));
    }

    private static (double seconds, Exception? error) ParseCacheSaveIntervalSeconds(object? value)
    {
        return value switch
        {
            int i => (i, null),
            long l => (l, null),
            float f => (f, null),
            double d => (d, null),
            string s => ParseCacheDurationSeconds(s),
            _ => (0, new InvalidOperationException(
                string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldTypeConversion, "unexpected type"))),
        };
    }

    private static (double seconds, Exception? error) ParseCacheDurationSeconds(string value)
    {
        if (TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out var parsed))
        {
            return (parsed.TotalSeconds, null);
        }

        var match = Regex.Match(value.Trim(), "^([0-9]*\\.?[0-9]+)\\s*(ms|s|m|h)$", RegexOptions.IgnoreCase);
        if (!match.Success)
        {
            return (0, new InvalidOperationException(
                string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldTypeConversion, "unexpected type")));
        }

        if (!double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var amount))
        {
            return (0, new InvalidOperationException(
                string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldTypeConversion, "unexpected type")));
        }

        var unit = match.Groups[2].Value.ToLowerInvariant();
        var seconds = unit switch
        {
            "ms" => amount / 1000.0,
            "s" => amount,
            "m" => amount * 60.0,
            "h" => amount * 3600.0,
            _ => 0,
        };

        return (seconds, null);
    }

    private sealed class SerializedOcspResponse
    {
        public int Status { get; set; }
        public DateTime ThisUpdate { get; set; }
        public DateTime? NextUpdate { get; set; }
    }
}
