using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;

/// <summary>
/// Utility methods for OCSP peer certificate validation.
/// Mirrors certidp/certidp.go.
/// </summary>
public static class OcspUtilities
{
    // OCSP AIA extension OID.
    private const string OidAuthorityInfoAccess = "1.3.6.1.5.5.7.1.1";
    // OCSPSigning extended key usage OID.
    private const string OidOcspSigning = "1.3.6.1.5.5.7.3.9";

    /// <summary>Returns the SHA-256 fingerprint of the certificate's raw DER bytes, base64-encoded.</summary>
    public static string GenerateFingerprint(X509Certificate2 cert)
    {
        var hash = SHA256.HashData(cert.RawData);
        return Convert.ToBase64String(hash);
    }

    /// <summary>
    /// Filters a list of URI strings to those that are valid HTTP or HTTPS URLs.
    /// </summary>
    public static IReadOnlyList<Uri> GetWebEndpoints(IEnumerable<string> uris)
    {
        var result = new List<Uri>();
        foreach (var uri in uris)
        {
            if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsed))
                continue;
            if (parsed.Scheme != "http" && parsed.Scheme != "https")
                continue;
            result.Add(parsed);
        }
        return result;
    }

    /// <summary>
    /// Returns the certificate subject in RDN sequence form, for logging.
    /// Not suitable for reliable cache matching.
    /// </summary>
    public static string GetSubjectDNForm(X509Certificate2? cert) =>
        cert is null ? string.Empty : cert.Subject;

    /// <summary>
    /// Returns the certificate issuer in RDN sequence form, for logging.
    /// Not suitable for reliable cache matching.
    /// </summary>
    public static string GetIssuerDNForm(X509Certificate2? cert) =>
        cert is null ? string.Empty : cert.Issuer;

    /// <summary>
    /// Returns true if the leaf certificate in the chain has OCSP responder endpoints
    /// in its Authority Information Access extension.
    /// Also populates <see cref="ChainLink.OcspWebEndpoints"/> on the link.
    /// </summary>
    public static bool CertOCSPEligible(ChainLink? link)
    {
        if (link?.Leaf is null || link.Leaf.RawData is not { Length: > 0 })
            return false;

        var ocspUris = GetOcspUris(link.Leaf);
        var endpoints = GetWebEndpoints(ocspUris);
        if (endpoints.Count == 0)
            return false;

        link.OcspWebEndpoints = endpoints;
        return true;
    }

    /// <summary>
    /// Returns the issuer certificate at position <paramref name="leafPos"/> + 1 in the chain.
    /// Returns null if the chain is too short or the leaf is self-signed.
    /// </summary>
    public static X509Certificate2? GetLeafIssuerCert(IReadOnlyList<X509Certificate2> chain, int leafPos)
    {
        if (chain.Count == 0 || leafPos < 0)
            return null;
        if (leafPos >= chain.Count - 1)
            return null;
        return chain[leafPos + 1];
    }

    /// <summary>
    /// Returns true if the OCSP response is still current within the configured clock skew.
    /// </summary>
    public static bool OCSPResponseCurrent(OcspResponse response, OcspPeerConfig opts, OcspLog? log = null)
    {
        var skew = TimeSpan.FromSeconds(opts.ClockSkew < 0 ? OcspPeerConfig.DefaultAllowedClockSkew.TotalSeconds : opts.ClockSkew);
        var now = DateTime.UtcNow;

        // Check NextUpdate (when set by CA).
        if (response.NextUpdate != DateTime.MinValue && response.NextUpdate < now - skew)
        {
            log?.Debug(OcspMessages.DbgResponseExpired,
                response.NextUpdate.ToString("o"), now.ToString("o"), skew);
            return false;
        }

        // If NextUpdate not set, apply TTL from ThisUpdate.
        if (response.NextUpdate == DateTime.MinValue)
        {
            var ttl = TimeSpan.FromSeconds(opts.TTLUnsetNextUpdate < 0
                ? OcspPeerConfig.DefaultTTLUnsetNextUpdate.TotalSeconds
                : opts.TTLUnsetNextUpdate);
            var expiry = response.ThisUpdate + ttl;
            if (expiry < now - skew)
            {
                log?.Debug(OcspMessages.DbgResponseTTLExpired,
                    expiry.ToString("o"), now.ToString("o"), skew);
                return false;
            }
        }

        // Check ThisUpdate is not future-dated.
        if (response.ThisUpdate > now + skew)
        {
            log?.Debug(OcspMessages.DbgResponseFutureDated,
                response.ThisUpdate.ToString("o"), now.ToString("o"), skew);
            return false;
        }

        return true;
    }

    /// <summary>
    /// Validates that the OCSP response was signed by a valid CA issuer or authorised delegate
    /// per RFC 6960 §4.2.2.2.
    /// </summary>
    public static bool ValidDelegationCheck(X509Certificate2? issuer, OcspResponse? response)
    {
        if (issuer is null || response is null)
            return false;

        // Not a delegated response — the CA signed directly.
        if (response.Certificate is null)
            return true;

        // Delegate is the same as the issuer — effectively a direct signing.
        if (response.Certificate.Thumbprint == issuer.Thumbprint)
            return true;

        // Check the delegate has id-kp-OCSPSigning in its extended key usage.
        foreach (var ext in response.Certificate.Extensions)
        {
            if (ext is not X509EnhancedKeyUsageExtension eku)
                continue;
            foreach (var oid in eku.EnhancedKeyUsages)
            {
                if (oid.Value == OidOcspSigning)
                    return true;
            }
        }

        return false;
    }

    // --- Helpers ---

    private static IEnumerable<string> GetOcspUris(X509Certificate2 cert)
    {
        foreach (var ext in cert.Extensions)
        {
            if (ext.Oid?.Value != OidAuthorityInfoAccess)
                continue;
            foreach (var uri in ParseAiaUris(ext.RawData, isOcsp: true))
                yield return uri;
        }
    }

    private static List<string> ParseAiaUris(byte[] aiaExtDer, bool isOcsp)
    {
        // OID for id-ad-ocsp: 1.3.6.1.5.5.7.48.1 → 2B 06 01 05 05 07 30 01
        byte[] ocspOid = [0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01];
        // OID for id-ad-caIssuers: 1.3.6.1.5.5.7.48.2 → 2B 06 01 05 05 07 30 02
        byte[] caIssuersOid = [0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02];

        var target = isOcsp ? ocspOid : caIssuersOid;
        var result = new List<string>();
        int i = 0;

        while (i < aiaExtDer.Length - target.Length - 4)
        {
            // Look for OID tag (0x06) followed by length matching our OID.
            if (aiaExtDer[i] == 0x06 && i + 1 < aiaExtDer.Length && aiaExtDer[i + 1] == target.Length)
            {
                var match = true;
                for (int k = 0; k < target.Length; k++)
                {
                    if (aiaExtDer[i + 2 + k] != target[k]) { match = false; break; }
                }
                if (match)
                {
                    // Next element should be context [6] IA5String (GeneralName uniformResourceIdentifier).
                    int pos = i + 2 + target.Length;
                    if (pos < aiaExtDer.Length && aiaExtDer[pos] == 0x86)
                    {
                        pos++;
                        if (pos < aiaExtDer.Length)
                        {
                            int len = aiaExtDer[pos++];
                            if (pos + len <= aiaExtDer.Length)
                            {
                                result.Add(System.Text.Encoding.ASCII.GetString(aiaExtDer, pos, len));
                                i = pos + len;
                                continue;
                            }
                        }
                    }
                }
            }
            i++;
        }
        return result;
    }
}
