// Copyright 2021-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/ocsp.go, server/ocsp_peer.go, server/ocsp_responsecache.go
// in the NATS server Go source.

using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.IO.Compression;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;

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

/// <summary>
/// Controls how OCSP stapling behaves for a TLS certificate.
/// Mirrors Go <c>OCSPMode uint8</c> in server/ocsp.go.
/// </summary>
public enum OcspMode : byte
{
    /// <summary>
    /// Staple only if the "status_request" OID is present in the certificate.
    /// Mirrors Go <c>OCSPModeAuto</c>.
    /// </summary>
    Auto      = 0,

    /// <summary>
    /// Must staple — honors the Must-Staple flag and shuts down on revocation.
    /// Mirrors Go <c>OCSPModeMust</c>.
    /// </summary>
    MustStaple = 1,

    /// <summary>
    /// Always obtain OCSP status, regardless of certificate flags.
    /// Mirrors Go <c>OCSPModeAlways</c>.
    /// </summary>
    Always    = 2,

    /// <summary>
    /// Never check OCSP, even if the certificate has the Must-Staple flag.
    /// Mirrors Go <c>OCSPModeNever</c>.
    /// </summary>
    Never     = 3,
}

/// <summary>
/// Holds a cached OCSP staple response and its expiry information.
/// </summary>
internal sealed class OcspStaple
{
    /// <summary>The raw DER-encoded OCSP response bytes.</summary>
    public byte[]?  Response   { get; set; }

    /// <summary>When the OCSP response next needs to be refreshed.</summary>
    public DateTime NextUpdate { get; set; }
}

/// <summary>
/// Orchestrates OCSP stapling for a single TLS certificate.
/// Monitors certificate validity and refreshes the staple on a background timer.
/// Mirrors Go <c>OCSPMonitor</c> struct in server/ocsp.go.
/// Replaces the stub in NatsServerTypes.cs.
/// </summary>
internal sealed class OcspMonitor
{
    private const string DefaultOcspStoreDir = "ocsp";
    private static readonly TimeSpan DefaultOcspCheckInterval = TimeSpan.FromHours(24);
    private static readonly TimeSpan MinOcspCheckInterval = TimeSpan.FromMinutes(2);

    private readonly Lock _mu = new();
    private Timer? _timer;
    private readonly OcspStaple _staple = new();
    private byte[]? _raw;
    private OcspResponse? _response;

    /// <summary>Path to the TLS certificate file being monitored.</summary>
    public string?    CertFile       { get; set; }

    /// <summary>Connection kind this monitor applies to (client/router/gateway/leaf).</summary>
    public string Kind { get; set; } = string.Empty;

    /// <summary>Path to the CA certificate file used to verify OCSP responses.</summary>
    public string?    CaFile         { get; set; }

    /// <summary>Path to a persisted OCSP staple file (optional).</summary>
    public string?    OcspStapleFile { get; set; }

    /// <summary>The OCSP stapling mode for this monitor.</summary>
    public OcspMode   Mode           { get; set; }

    /// <summary>How often to check for a fresh OCSP response.</summary>
    public TimeSpan   CheckInterval  { get; set; } = TimeSpan.FromHours(24);

    /// <summary>The owning server instance.</summary>
    public NatsServer? Server        { get; set; }

    /// <summary>The monitored certificate leaf.</summary>
    public X509Certificate2? Leaf { get; set; }

    /// <summary>The monitored certificate issuer.</summary>
    public X509Certificate2? Issuer { get; set; }

    /// <summary>HTTP client for remote OCSP fetch attempts.</summary>
    public HttpClient? HttpClient { get; set; }

    /// <summary>When true, monitor exits on revoked/unknown status.</summary>
    public bool ShutdownOnRevoke { get; set; }

    /// <summary>The synchronisation lock for this monitor's mutable state.</summary>
    public Lock Mu => _mu;

    /// <summary>
    /// Calculates the next polling delay based on <see cref="OcspResponse.NextUpdate"/>.
    /// Mirrors Go <c>OCSPMonitor.getNextRun</c>.
    /// </summary>
    internal TimeSpan GetNextRun()
    {
        DateTime nextUpdate;
        lock (_mu)
        {
            nextUpdate = _response?.NextUpdate ?? DateTime.MinValue;
        }

        if (nextUpdate == DateTime.MinValue)
        {
            return DefaultOcspCheckInterval;
        }

        var duration = (nextUpdate - DateTime.UtcNow) / 2;
        if (duration < TimeSpan.Zero)
        {
            return MinOcspCheckInterval;
        }

        return duration;
    }

    /// <summary>
    /// Returns currently cached OCSP raw bytes and parsed response.
    /// Mirrors Go <c>OCSPMonitor.getCacheStatus</c>.
    /// </summary>
    internal (byte[]? raw, OcspResponse? response) GetCacheStatus()
    {
        lock (_mu)
        {
            return (_raw is null ? null : [.. _raw], _response);
        }
    }

    /// <summary>
    /// Resolves OCSP status from cache, local store, then remote fetch fallback.
    /// Mirrors Go <c>OCSPMonitor.getStatus</c>.
    /// </summary>
    internal (byte[]? raw, OcspResponse? response, Exception? error) GetStatus()
    {
        var (cachedRaw, cachedResponse) = GetCacheStatus();
        if (cachedRaw is { Length: > 0 } && cachedResponse != null)
        {
            var validityError = OcspHandler.ValidOCSPResponse(cachedResponse);
            if (validityError == null)
            {
                return (cachedRaw, cachedResponse, null);
            }
        }

        var (localRaw, localResponse, localError) = GetLocalStatus();
        if (localError == null)
        {
            return (localRaw, localResponse, null);
        }

        return GetRemoteStatus();
    }

    /// <summary>
    /// Loads and validates an OCSP response from local store_dir cache.
    /// Mirrors Go <c>OCSPMonitor.getLocalStatus</c>.
    /// </summary>
    internal (byte[]? raw, OcspResponse? response, Exception? error) GetLocalStatus()
    {
        var storeDir = Server?.Options.StoreDir ?? string.Empty;
        if (string.IsNullOrEmpty(storeDir))
        {
            return (null, null, new InvalidOperationException("store_dir not set"));
        }

        if (Leaf == null)
        {
            return (null, null, new InvalidOperationException("leaf certificate not set"));
        }

        var key = Convert.ToHexString(SHA256.HashData(Leaf.RawData)).ToLowerInvariant();
        var path = Path.Combine(storeDir, DefaultOcspStoreDir, key);

        byte[] raw;
        try
        {
            lock (_mu)
            {
                raw = File.ReadAllBytes(path);
            }
        }
        catch (Exception ex)
        {
            return (null, null, ex);
        }

        var (response, parseError) = OcspHandler.ParseOcspResponse(raw);
        if (parseError != null)
        {
            return (null, null, new InvalidOperationException($"failed to get local status: {parseError.Message}", parseError));
        }

        var validityError = OcspHandler.ValidOCSPResponse(response!);
        if (validityError != null)
        {
            return (null, null, validityError);
        }

        lock (_mu)
        {
            _raw = [.. raw];
            _response = response;
            _staple.Response = [.. raw];
            _staple.NextUpdate = response!.NextUpdate;
        }

        return (raw, response, null);
    }

    /// <summary>
    /// Attempts to fetch OCSP status remotely from configured responders.
    /// Mirrors Go <c>OCSPMonitor.getRemoteStatus</c>.
    /// </summary>
    internal (byte[]? raw, OcspResponse? response, Exception? error) GetRemoteStatus()
    {
        var responders = Server?.Options.OcspConfig?.OverrideUrls ?? [];
        if (responders.Count == 0)
        {
            return (null, null, new InvalidOperationException("no available ocsp servers"));
        }

        return (null, null, new InvalidOperationException("remote OCSP fetching is not implemented"));
    }

    /// <summary>
    /// Monitor loop that periodically refreshes OCSP status.
    /// Mirrors Go <c>OCSPMonitor.run</c>.
    /// </summary>
    internal async Task Run(CancellationToken cancellationToken = default)
    {
        var (_, response, error) = GetStatus();
        if (error != null || response == null)
        {
            return;
        }

        var nextRun = response.Status == OcspStatusAssertion.Good
            ? GetNextRun()
            : MinOcspCheckInterval;

        if (response.Status != OcspStatusAssertion.Good && ShutdownOnRevoke)
        {
            return;
        }

        while (!cancellationToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(nextRun, cancellationToken);
            }
            catch (OperationCanceledException)
            {
                return;
            }

            var (_, updated, updateError) = GetRemoteStatus();
            if (updateError != null || updated == null)
            {
                nextRun = GetNextRun();
                continue;
            }

            if (updated.Status != OcspStatusAssertion.Good)
            {
                return;
            }

            nextRun = GetNextRun();
        }
    }

    /// <summary>
    /// Writes OCSP bytes to a temporary file and atomically renames to target.
    /// Mirrors Go <c>OCSPMonitor.writeOCSPStatus</c>.
    /// </summary>
    internal Exception? WriteOCSPStatus(string storeDir, string file, byte[] data)
    {
        try
        {
            var ocspDir = Path.Combine(storeDir, DefaultOcspStoreDir);
            Directory.CreateDirectory(ocspDir);

            var tempPath = Path.Combine(ocspDir, $"tmp-cert-status-{Path.GetRandomFileName()}");
            File.WriteAllBytes(tempPath, data);

            lock (_mu)
            {
                File.Move(tempPath, Path.Combine(ocspDir, file), overwrite: true);
            }
            return null;
        }
        catch (Exception ex)
        {
            return ex;
        }
    }

    /// <summary>Starts the background OCSP refresh timer.</summary>
    public void Start()
    {
        lock (_mu)
        {
            if (_timer != null)
                return;

            _timer = new Timer(_ =>
            {
                lock (_mu)
                {
                    if (!string.IsNullOrEmpty(OcspStapleFile) && File.Exists(OcspStapleFile))
                    {
                        _staple.Response = File.ReadAllBytes(OcspStapleFile);
                        _raw = [.. _staple.Response];
                        var (response, _) = OcspHandler.ParseOcspResponse(_raw);
                        _response = response;
                    }
                    _staple.NextUpdate = DateTime.UtcNow + CheckInterval;
                }
            }, null, TimeSpan.Zero, CheckInterval);
        }
    }

    /// <summary>Stops the background OCSP refresh timer.</summary>
    public void Stop()
    {
        lock (_mu)
        {
            _timer?.Dispose();
            _timer = null;
        }
    }

    /// <summary>Returns the current cached OCSP staple bytes, or <c>null</c> if none.</summary>
    public byte[]? GetStaple()
    {
        lock (_mu)
        {
            return _staple.Response == null ? null : [.. _staple.Response];
        }
    }
}

/// <summary>
/// Interface for caching raw OCSP response bytes keyed by certificate fingerprint.
/// Mirrors Go <c>OCSPResponseCache</c> interface in server/ocsp_responsecache.go.
/// Replaces the stub in NatsServerTypes.cs.
/// </summary>
public interface IOcspResponseCache
{
    /// <summary>Returns the cached OCSP response for <paramref name="key"/>, or <c>null</c>.</summary>
    byte[]? Get(string key);

    /// <summary>Stores an OCSP response under <paramref name="key"/>.</summary>
    void    Put(string key, byte[] response);

    /// <summary>Removes the cached entry for <paramref name="key"/>.</summary>
    void    Remove(string key);
}

/// <summary>
/// Runtime counters for OCSP response cache behavior.
/// Mirrors Go <c>OCSPResponseCacheStats</c> shape.
/// </summary>
public sealed class OcspResponseCacheStats
{
    public long Responses { get; set; }
    public long Hits { get; set; }
    public long Misses { get; set; }
    public long Revokes { get; set; }
    public long Goods { get; set; }
    public long Unknowns { get; set; }
}

internal sealed class OcspResponseCacheItem
{
    public string Subject { get; set; } = string.Empty;
    public DateTime CachedAt { get; set; }
    public OcspStatusAssertion RespStatus { get; set; }
    public DateTime RespExpires { get; set; }
    public byte[] Resp { get; set; } = [];
}

/// <summary>
/// A no-op OCSP cache that never stores anything.
/// Mirrors Go <c>NoOpCache</c> in server/ocsp_responsecache.go.
/// </summary>
internal sealed class NoOpCache : IOcspResponseCache
{
    private readonly Lock _mu = new();
    private readonly OcspResponseCacheConfig _config;
    private OcspResponseCacheStats? _stats;
    private bool _online;

    public NoOpCache()
        : this(new OcspResponseCacheConfig { Type = "none" })
    {
    }

    public NoOpCache(OcspResponseCacheConfig config)
    {
        _config = config;
    }

    public byte[]? Get(string key)
    {
        _ = key;
        return null;
    }

    public void Put(string key, byte[] response)
    {
        _ = key;
        _ = response;
    }

    public void Remove(string key) => Delete(key);

    public void Delete(string key)
    {
        _ = key;
    }

    public void Start(NatsServer? server = null)
    {
        lock (_mu)
        {
            _stats = new OcspResponseCacheStats();
            _online = true;
        }
    }

    public void Stop(NatsServer? server = null)
    {
        lock (_mu)
        {
            _online = false;
        }
    }

    public bool Online()
    {
        lock (_mu)
        {
            return _online;
        }
    }

    public string Type() => "none";

    public OcspResponseCacheConfig Config()
    {
        lock (_mu)
        {
            return _config;
        }
    }

    public OcspResponseCacheStats? Stats()
    {
        lock (_mu)
        {
            if (_stats is null)
                return null;

            return new OcspResponseCacheStats
            {
                Responses = _stats.Responses,
                Hits = _stats.Hits,
                Misses = _stats.Misses,
                Revokes = _stats.Revokes,
                Goods = _stats.Goods,
                Unknowns = _stats.Unknowns,
            };
        }
    }
}

/// <summary>
/// An OCSP cache backed by a local directory on disk.
/// Mirrors Go <c>LocalCache</c> in server/ocsp_responsecache.go.
/// Full implementation is deferred to session 23.
/// </summary>
internal sealed class LocalDirCache : IOcspResponseCache
{
    private readonly ReaderWriterLockSlim _mu = new(LockRecursionPolicy.NoRecursion);
    private readonly OcspResponseCacheConfig _config;
    private readonly Dictionary<string, OcspResponseCacheItem> _cache = new(StringComparer.Ordinal);
    private OcspResponseCacheStats? _stats;
    private bool _online;
    private bool _dirty;
    private readonly TimeSpan _saveInterval;
    private Timer? _timer;

    public LocalDirCache(string dir)
        : this(new OcspResponseCacheConfig
        {
            Type = OcspHandler.OcspResponseCacheTypeLocal,
            LocalStore = dir,
            PreserveRevoked = false,
            SaveInterval = OcspHandler.OcspResponseCacheDefaultSaveInterval.TotalSeconds,
        })
    {
    }

    public LocalDirCache(OcspResponseCacheConfig config)
    {
        _config = config ?? OcspHandler.NewOCSPResponseCacheConfig();
        if (string.IsNullOrEmpty(_config.Type))
        {
            _config.Type = OcspHandler.OcspResponseCacheTypeLocal;
        }

        if (string.IsNullOrEmpty(_config.LocalStore))
        {
            _config.LocalStore = OcspHandler.OcspResponseCacheDefaultDir;
        }

        var saveSeconds = _config.SaveInterval <= 0
            ? OcspHandler.OcspResponseCacheDefaultSaveInterval.TotalSeconds
            : _config.SaveInterval;
        var configuredInterval = TimeSpan.FromSeconds(saveSeconds);
        _saveInterval = configuredInterval < OcspHandler.OcspResponseCacheMinimumSaveInterval
            ? OcspHandler.OcspResponseCacheMinimumSaveInterval
            : configuredInterval;
    }

    public byte[]? Get(string key) => Get(key, log: null);

    public byte[]? Get(string key, OcspLog? log)
    {
        _mu.EnterReadLock();
        try
        {
            if (!_online || string.IsNullOrEmpty(key))
            {
                return null;
            }

            if (!_cache.TryGetValue(key, out var item))
            {
                if (_stats is not null)
                {
                    _stats.Misses++;
                }
                return null;
            }

            if (_stats is not null)
            {
                _stats.Hits++;
            }

            var (decompressed, error) = Decompress(item.Resp);
            if (error != null)
            {
                log?.Errorf?.Invoke(OcspMessages.ErrResponseDecompressFail, [key, error]);
                return null;
            }

            return decompressed;
        }
        finally
        {
            _mu.ExitReadLock();
        }
    }

    public void Put(string key, byte[] response)
    {
        var status = OcspStatusAssertion.Unknown;
        var (parsed, error) = OcspHandler.ParseOcspResponse(response);
        if (error == null && parsed != null)
        {
            status = parsed.Status;
        }

        var ocspResponse = new OcspResponse
        {
            Status = status,
            ThisUpdate = DateTime.UtcNow,
            NextUpdate = DateTime.MinValue,
            Raw = [.. response],
        };

        Put(key, ocspResponse, string.Empty);
    }

    public void Put(string key, OcspResponse response, string subject, OcspLog? log = null)
    {
        if (response == null)
        {
            return;
        }

        _mu.EnterReadLock();
        try
        {
            if (!_online || string.IsNullOrEmpty(key))
            {
                return;
            }
        }
        finally
        {
            _mu.ExitReadLock();
        }

        var raw = response.Raw ?? [];
        var (compressed, error) = Compress(raw);
        if (error != null || compressed == null)
        {
            log?.Errorf?.Invoke(OcspMessages.ErrResponseCompressFail, [key, error ?? new InvalidOperationException("compression failed")]);
            return;
        }

        _mu.EnterWriteLock();
        try
        {
            if (_cache.TryGetValue(key, out var existing))
            {
                AdjustStats(-1, existing.RespStatus);
            }

            var item = new OcspResponseCacheItem
            {
                Subject = subject,
                CachedAt = DateTime.UtcNow,
                RespStatus = response.Status,
                RespExpires = response.NextUpdate,
                Resp = compressed,
            };

            _cache[key] = item;
            AdjustStats(1, item.RespStatus);
            _dirty = true;
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    internal void AdjustStatsHitToMiss()
    {
        if (_stats is null)
        {
            return;
        }

        _stats.Misses++;
        _stats.Hits--;
    }

    internal void AdjustStats(long delta, OcspStatusAssertion status)
    {
        if (delta == 0 || _stats is null)
        {
            return;
        }

        _stats.Responses += delta;

        switch (status)
        {
            case OcspStatusAssertion.Good:
                _stats.Goods += delta;
                break;
            case OcspStatusAssertion.Revoked:
                _stats.Revokes += delta;
                break;
            case OcspStatusAssertion.Unknown:
                _stats.Unknowns += delta;
                break;
        }
    }

    public void Remove(string key) => Delete(key, wasMiss: false, log: null);

    public void Delete(string key, bool wasMiss = false, OcspLog? log = null)
    {
        _mu.EnterWriteLock();
        try
        {
            if (!_online || string.IsNullOrEmpty(key))
            {
                return;
            }

            if (!_cache.TryGetValue(key, out var item))
            {
                return;
            }

            if (item.RespStatus == OcspStatusAssertion.Revoked && _config.PreserveRevoked)
            {
                if (wasMiss)
                {
                    AdjustStatsHitToMiss();
                }

                return;
            }

            _cache.Remove(key);
            AdjustStats(-1, item.RespStatus);

            if (wasMiss)
            {
                AdjustStatsHitToMiss();
            }

            _dirty = true;
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    public void Start(NatsServer? server = null)
    {
        if (server != null)
        {
            LoadCache(server);
        }

        InitStats();

        _mu.EnterWriteLock();
        try
        {
            _online = true;
            if (server != null)
            {
                _timer ??= new Timer(_ => SaveCache(server), null, _saveInterval, _saveInterval);
            }
            else
            {
                _timer ??= new Timer(_ => { }, null, _saveInterval, _saveInterval);
            }
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    public void Stop(NatsServer? server = null)
    {
        _mu.EnterWriteLock();
        try
        {
            _online = false;
            _timer?.Dispose();
            _timer = null;
        }
        finally
        {
            _mu.ExitWriteLock();
        }

        if (server != null)
        {
            SaveCache(server);
        }
    }

    internal void LoadCache(NatsServer server)
    {
        var storePath = CacheStorePath();
        Dictionary<string, OcspResponseCacheItem>? loaded;

        try
        {
            var bytes = File.ReadAllBytes(storePath);
            loaded = JsonSerializer.Deserialize<Dictionary<string, OcspResponseCacheItem>>(bytes);
        }
        catch (FileNotFoundException)
        {
            loaded = null;
        }
        catch (DirectoryNotFoundException)
        {
            loaded = null;
        }
        catch (Exception ex)
        {
            server.Warnf(OcspMessages.ErrLoadCacheFail, ex);
            _mu.EnterWriteLock();
            try
            {
                _cache.Clear();
                _dirty = true;
            }
            finally
            {
                _mu.ExitWriteLock();
            }
            return;
        }

        _mu.EnterWriteLock();
        try
        {
            _cache.Clear();
            if (loaded != null)
            {
                foreach (var (key, item) in loaded)
                {
                    _cache[key] = item;
                }
            }

            _dirty = false;
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    internal void SaveCache(NatsServer server)
    {
        bool dirty;

        _mu.EnterReadLock();
        try
        {
            dirty = _dirty;
        }
        finally
        {
            _mu.ExitReadLock();
        }

        if (!dirty)
        {
            return;
        }

        var storePath = CacheStorePath();
        var directory = Path.GetDirectoryName(storePath);
        if (string.IsNullOrEmpty(directory))
        {
            server.Errorf(OcspMessages.ErrSaveCacheFail, "cache directory path is invalid");
            return;
        }

        try
        {
            Directory.CreateDirectory(directory);
        }
        catch (Exception ex)
        {
            server.Errorf(OcspMessages.ErrSaveCacheFail, ex);
            return;
        }

        var tempPath = Path.Combine(directory, $"{OcspHandler.OcspResponseCacheDefaultTempFilePrefix}{Path.GetRandomFileName()}");

        try
        {
            _mu.EnterWriteLock();
            try
            {
                var payload = JsonSerializer.SerializeToUtf8Bytes(_cache, new JsonSerializerOptions { WriteIndented = true });
                File.WriteAllBytes(tempPath, payload);
                File.Move(tempPath, storePath, overwrite: true);
                _dirty = false;
            }
            finally
            {
                _mu.ExitWriteLock();
            }
        }
        catch (Exception ex)
        {
            server.Errorf(OcspMessages.ErrSaveCacheFail, ex);
        }
        finally
        {
            try
            {
                if (File.Exists(tempPath))
                {
                    File.Delete(tempPath);
                }
            }
            catch
            {
            }
        }
    }

    public bool Online()
    {
        _mu.EnterReadLock();
        try
        {
            return _online;
        }
        finally
        {
            _mu.ExitReadLock();
        }
    }

    public string Type()
    {
        _mu.EnterReadLock();
        try
        {
            return _config.Type;
        }
        finally
        {
            _mu.ExitReadLock();
        }
    }

    public OcspResponseCacheConfig Config()
    {
        _mu.EnterReadLock();
        try
        {
            return _config;
        }
        finally
        {
            _mu.ExitReadLock();
        }
    }

    public OcspResponseCacheStats? Stats()
    {
        _mu.EnterReadLock();
        try
        {
            if (_stats == null)
            {
                return null;
            }

            return new OcspResponseCacheStats
            {
                Responses = _stats.Responses,
                Hits = _stats.Hits,
                Misses = _stats.Misses,
                Revokes = _stats.Revokes,
                Goods = _stats.Goods,
                Unknowns = _stats.Unknowns,
            };
        }
        finally
        {
            _mu.ExitReadLock();
        }
    }

    internal void InitStats()
    {
        _mu.EnterWriteLock();
        try
        {
            _stats = new OcspResponseCacheStats
            {
                Responses = _cache.Count,
            };

            foreach (var entry in _cache.Values)
            {
                switch (entry.RespStatus)
                {
                    case OcspStatusAssertion.Good:
                        _stats.Goods++;
                        break;
                    case OcspStatusAssertion.Revoked:
                        _stats.Revokes++;
                        break;
                    case OcspStatusAssertion.Unknown:
                        _stats.Unknowns++;
                        break;
                }
            }
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    public (byte[]? compressed, Exception? error) Compress(ReadOnlySpan<byte> buffer)
    {
        try
        {
            using var output = new MemoryStream();
            using (var writer = new BrotliStream(output, CompressionLevel.Fastest, leaveOpen: true))
            {
                writer.Write(buffer);
            }

            return (output.ToArray(), null);
        }
        catch (Exception ex)
        {
            return (null, ex);
        }
    }

    public (byte[]? decompressed, Exception? error) Decompress(ReadOnlySpan<byte> buffer)
    {
        try
        {
            using var input = new MemoryStream(buffer.ToArray());
            using var reader = new BrotliStream(input, System.IO.Compression.CompressionMode.Decompress);
            using var output = new MemoryStream();
            reader.CopyTo(output);
            return (output.ToArray(), null);
        }
        catch (Exception ex)
        {
            return (null, ex);
        }
    }

    private string CacheFilePath(string key)
    {
        var hash = SHA256.HashData(Encoding.UTF8.GetBytes(key));
        var file = Convert.ToHexString(hash).ToLowerInvariant();
        return Path.Combine(_config.LocalStore, $"{file}.ocsp");
    }

    private string CacheStorePath()
    {
        var directory = string.IsNullOrEmpty(_config.LocalStore)
            ? OcspHandler.OcspResponseCacheDefaultDir
            : _config.LocalStore;

        return Path.Combine(directory, OcspHandler.OcspResponseCacheDefaultFilename);
    }
}

/// <summary>
/// Payload for the OCSP peer certificate rejection advisory event.
/// Mirrors Go <c>OCSPPeerRejectEventMsg</c> fields in server/events.go
/// and the OCSP peer reject logic in server/ocsp_peer.go.
/// </summary>
public sealed class OcspPeerRejectInfo
{
    [System.Text.Json.Serialization.JsonPropertyName("peer")]
    public string Peer   { get; set; } = string.Empty;

    [System.Text.Json.Serialization.JsonPropertyName("reason")]
    public string Reason { get; set; } = string.Empty;
}
