using System.Collections.Generic;
using System.Diagnostics.Metrics;

namespace StellaOps.Feedser.Source.Ghsa.Internal;

public sealed class GhsaDiagnostics : IDisposable
{
    private const string MeterName = "StellaOps.Feedser.Source.Ghsa";
    private const string MeterVersion = "1.0.0";

    private readonly Meter _meter;
    private readonly Counter<long> _fetchAttempts;
    private readonly Counter<long> _fetchDocuments;
    private readonly Counter<long> _fetchFailures;
    private readonly Counter<long> _fetchUnchanged;
    private readonly Counter<long> _parseSuccess;
    private readonly Counter<long> _parseFailures;
    private readonly Counter<long> _parseQuarantine;
    private readonly Counter<long> _mapSuccess;
    private readonly Histogram<long> _rateLimitRemaining;
    private readonly Histogram<long> _rateLimitLimit;
    private readonly Histogram<double> _rateLimitResetSeconds;
    private readonly Histogram<double> _rateLimitHeadroomPct;
    private readonly ObservableGauge<double> _rateLimitHeadroomGauge;
    private readonly Counter<long> _rateLimitExhausted;
    private readonly Counter<long> _canonicalMetricFallbacks;
    private readonly object _rateLimitLock = new();
    private GhsaRateLimitSnapshot? _lastRateLimitSnapshot;
    private readonly Dictionary<(string Phase, string? Resource), GhsaRateLimitSnapshot> _rateLimitSnapshots = new();

    public GhsaDiagnostics()
    {
        _meter = new Meter(MeterName, MeterVersion);
        _fetchAttempts = _meter.CreateCounter<long>("ghsa.fetch.attempts", unit: "operations");
        _fetchDocuments = _meter.CreateCounter<long>("ghsa.fetch.documents", unit: "documents");
        _fetchFailures = _meter.CreateCounter<long>("ghsa.fetch.failures", unit: "operations");
        _fetchUnchanged = _meter.CreateCounter<long>("ghsa.fetch.unchanged", unit: "operations");
        _parseSuccess = _meter.CreateCounter<long>("ghsa.parse.success", unit: "documents");
        _parseFailures = _meter.CreateCounter<long>("ghsa.parse.failures", unit: "documents");
        _parseQuarantine = _meter.CreateCounter<long>("ghsa.parse.quarantine", unit: "documents");
        _mapSuccess = _meter.CreateCounter<long>("ghsa.map.success", unit: "advisories");
        _rateLimitRemaining = _meter.CreateHistogram<long>("ghsa.ratelimit.remaining", unit: "requests");
        _rateLimitLimit = _meter.CreateHistogram<long>("ghsa.ratelimit.limit", unit: "requests");
        _rateLimitResetSeconds = _meter.CreateHistogram<double>("ghsa.ratelimit.reset_seconds", unit: "s");
        _rateLimitHeadroomPct = _meter.CreateHistogram<double>("ghsa.ratelimit.headroom_pct", unit: "percent");
        _rateLimitHeadroomGauge = _meter.CreateObservableGauge("ghsa.ratelimit.headroom_pct_current", ObserveHeadroom, unit: "percent");
        _rateLimitExhausted = _meter.CreateCounter<long>("ghsa.ratelimit.exhausted", unit: "events");
        _canonicalMetricFallbacks = _meter.CreateCounter<long>("ghsa.map.canonical_metric_fallbacks", unit: "advisories");
    }

    public void FetchAttempt() => _fetchAttempts.Add(1);

    public void FetchDocument() => _fetchDocuments.Add(1);

    public void FetchFailure() => _fetchFailures.Add(1);

    public void FetchUnchanged() => _fetchUnchanged.Add(1);

    public void ParseSuccess() => _parseSuccess.Add(1);

    public void ParseFailure() => _parseFailures.Add(1);

    public void ParseQuarantine() => _parseQuarantine.Add(1);

    public void MapSuccess(long count) => _mapSuccess.Add(count);

    internal void RecordRateLimit(GhsaRateLimitSnapshot snapshot)
    {
        var tags = new KeyValuePair<string, object?>[]
        {
            new("phase", snapshot.Phase),
            new("resource", snapshot.Resource ?? "unknown")
        };

        if (snapshot.Limit.HasValue)
        {
            _rateLimitLimit.Record(snapshot.Limit.Value, tags);
        }

        if (snapshot.Remaining.HasValue)
        {
            _rateLimitRemaining.Record(snapshot.Remaining.Value, tags);
        }

        if (snapshot.ResetAfter.HasValue)
        {
            _rateLimitResetSeconds.Record(snapshot.ResetAfter.Value.TotalSeconds, tags);
        }

        if (TryCalculateHeadroom(snapshot, out var headroom))
        {
            _rateLimitHeadroomPct.Record(headroom, tags);
        }

        lock (_rateLimitLock)
        {
            _lastRateLimitSnapshot = snapshot;
            _rateLimitSnapshots[(snapshot.Phase, snapshot.Resource)] = snapshot;
        }
    }

    internal void RateLimitExhausted(string phase)
        => _rateLimitExhausted.Add(1, new KeyValuePair<string, object?>("phase", phase));

    public void CanonicalMetricFallback(string canonicalMetricId, string severity)
        => _canonicalMetricFallbacks.Add(
            1,
            new KeyValuePair<string, object?>("canonical_metric_id", canonicalMetricId),
            new KeyValuePair<string, object?>("severity", severity),
            new KeyValuePair<string, object?>("reason", "no_cvss"));

    internal GhsaRateLimitSnapshot? GetLastRateLimitSnapshot()
    {
        lock (_rateLimitLock)
        {
            return _lastRateLimitSnapshot;
        }
    }

    private IEnumerable<Measurement<double>> ObserveHeadroom()
    {
        lock (_rateLimitLock)
        {
            if (_rateLimitSnapshots.Count == 0)
            {
                yield break;
            }

            foreach (var snapshot in _rateLimitSnapshots.Values)
            {
                if (TryCalculateHeadroom(snapshot, out var headroom))
                {
                    yield return new Measurement<double>(
                        headroom,
                        new KeyValuePair<string, object?>("phase", snapshot.Phase),
                        new KeyValuePair<string, object?>("resource", snapshot.Resource ?? "unknown"));
                }
            }
        }
    }

    private static bool TryCalculateHeadroom(in GhsaRateLimitSnapshot snapshot, out double headroomPct)
    {
        headroomPct = 0;
        if (!snapshot.Limit.HasValue || !snapshot.Remaining.HasValue)
        {
            return false;
        }

        var limit = snapshot.Limit.Value;
        if (limit <= 0)
        {
            return false;
        }

        headroomPct = (double)snapshot.Remaining.Value / limit * 100d;
        return true;
    }

    public void Dispose()
    {
        _meter.Dispose();
    }
}
