// ============================================================================
// mb_clouseau - CLR Metrics Collector
// Uncovering clues with MusicBee Clouseau
//
// Tracks .NET CLR internals: GC behavior, heap sizes, and memory pressure
// ============================================================================

using System;
using System.Diagnostics;
using NLog;

namespace MusicBeePlugin.Clouseau.Metrics
{
    /// <summary>
    /// Collects .NET CLR metrics including garbage collection statistics
    /// and heap sizes. Uses System.GC class and CLR performance counters.
    /// </summary>
    public class ClrMetrics : IDisposable
    {
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

        // CLR Performance Counters (for MusicBee process)
        private PerformanceCounter _gen0CollectionsCounter;
        private PerformanceCounter _gen1CollectionsCounter;
        private PerformanceCounter _gen2CollectionsCounter;
        private PerformanceCounter _timeInGCCounter;
        private PerformanceCounter _gen0HeapSizeCounter;
        private PerformanceCounter _gen1HeapSizeCounter;
        private PerformanceCounter _gen2HeapSizeCounter;
        private PerformanceCounter _lohSizeCounter;

        // Instance name for performance counters
        private string _instanceName;
        private bool _countersInitialized;
        private bool _countersFailed;

        // Previous GC counts for delta calculation
        private int _prevGen0Count;
        private int _prevGen1Count;
        private int _prevGen2Count;
        private long _prevManagedHeap;

        // GC pressure tracking
        private const int GCPressureWindowSize = 6; // 30 seconds at 5s intervals
        private readonly int[] _gen2CollectionDeltas = new int[GCPressureWindowSize];
        private readonly float[] _timeInGCValues = new float[GCPressureWindowSize];
        private int _gcTrackingIndex;
        private int _gcSampleCount;

        // Thresholds for GC pressure warning
        private const int Gen2CollectionsPerMinuteThreshold = 5;
        private const float TimeInGCPercentThreshold = 20f;

        private bool _initialized;
        private bool _disposed;

        /// <summary>
        /// Initializes the CLR metrics collector.
        /// </summary>
        public void Initialize()
        {
            if (_initialized) return;

            try
            {
                Logger.Debug("Initializing ClrMetrics collector...");

                // Prime the GC collection counts
                _prevGen0Count = GC.CollectionCount(0);
                _prevGen1Count = GC.CollectionCount(1);
                _prevGen2Count = GC.CollectionCount(2);
                _prevManagedHeap = GC.GetTotalMemory(false);

                // Try to initialize performance counters
                InitializePerformanceCounters();

                _initialized = true;
                Logger.Info("ClrMetrics initialized. GC counters: {0}",
                    _countersInitialized ? "available" : "unavailable");
            }
            catch (Exception ex)
            {
                Logger.Error(ex, "Failed to initialize ClrMetrics");
                _initialized = true;
            }
        }

        /// <summary>
        /// Initializes CLR performance counters for the MusicBee process.
        /// </summary>
        private void InitializePerformanceCounters()
        {
            try
            {
                // Get the instance name for MusicBee process
                _instanceName = GetClrInstanceName();

                if (string.IsNullOrEmpty(_instanceName))
                {
                    Logger.Warn("Could not determine CLR instance name for performance counters");
                    _countersFailed = true;
                    return;
                }

                Logger.Debug("Using CLR instance name: {0}", _instanceName);

                // Initialize counters - these may fail if the category doesn't exist
                // or if permissions are insufficient
                const string category = ".NET CLR Memory";

                _gen0CollectionsCounter = new PerformanceCounter(category, "# Gen 0 Collections", _instanceName, true);
                _gen1CollectionsCounter = new PerformanceCounter(category, "# Gen 1 Collections", _instanceName, true);
                _gen2CollectionsCounter = new PerformanceCounter(category, "# Gen 2 Collections", _instanceName, true);
                _timeInGCCounter = new PerformanceCounter(category, "% Time in GC", _instanceName, true);
                _gen0HeapSizeCounter = new PerformanceCounter(category, "Gen 0 heap size", _instanceName, true);
                _gen1HeapSizeCounter = new PerformanceCounter(category, "Gen 1 heap size", _instanceName, true);
                _gen2HeapSizeCounter = new PerformanceCounter(category, "Gen 2 heap size", _instanceName, true);
                _lohSizeCounter = new PerformanceCounter(category, "Large Object Heap size", _instanceName, true);

                // Prime the counters
                _gen0CollectionsCounter.NextValue();
                _gen1CollectionsCounter.NextValue();
                _gen2CollectionsCounter.NextValue();
                _timeInGCCounter.NextValue();
                _gen0HeapSizeCounter.NextValue();
                _gen1HeapSizeCounter.NextValue();
                _gen2HeapSizeCounter.NextValue();
                _lohSizeCounter.NextValue();

                _countersInitialized = true;
                Logger.Debug("CLR performance counters initialized successfully");
            }
            catch (Exception ex)
            {
                Logger.Warn(ex, "Failed to initialize CLR performance counters - will use System.GC fallback");
                _countersFailed = true;
                DisposeCounters();
            }
        }

        /// <summary>
        /// Gets the CLR performance counter instance name for the current process.
        /// This can be tricky because the instance name includes a suffix like "#1", "#2" etc.
        /// </summary>
        private string GetClrInstanceName()
        {
            try
            {
                var processName = Process.GetCurrentProcess().ProcessName;

                // Check if the simple name works
                if (InstanceExists(processName))
                    return processName;

                // Try with numbered suffix
                for (int i = 1; i <= 10; i++)
                {
                    var instanceName = $"{processName}#{i}";
                    if (InstanceExists(instanceName))
                        return instanceName;
                }

                // Fallback: try to find it by enumerating
                var category = new PerformanceCounterCategory(".NET CLR Memory");
                var instanceNames = category.GetInstanceNames();

                foreach (var name in instanceNames)
                {
                    if (name.StartsWith(processName, StringComparison.OrdinalIgnoreCase))
                    {
                        Logger.Trace("Found CLR instance by enumeration: {0}", name);
                        return name;
                    }
                }

                return null;
            }
            catch (Exception ex)
            {
                Logger.Trace(ex, "Error determining CLR instance name");
                return null;
            }
        }

        /// <summary>
        /// Checks if a CLR performance counter instance exists.
        /// </summary>
        private bool InstanceExists(string instanceName)
        {
            try
            {
                var category = new PerformanceCounterCategory(".NET CLR Memory");
                return category.InstanceExists(instanceName);
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Collects CLR metrics and populates the snapshot.
        /// </summary>
        /// <param name="snapshot">The snapshot to populate with CLR metrics.</param>
        public void Collect(MetricsSnapshot snapshot)
        {
            if (!_initialized)
            {
                Initialize();
            }

            try
            {
                // Always use System.GC for basic collection counts (most reliable)
                CollectFromSystemGC(snapshot);

                // Use performance counters for additional metrics if available
                if (_countersInitialized && !_countersFailed)
                {
                    CollectFromPerformanceCounters(snapshot);
                }

                // Calculate deltas
                CalculateDeltas(snapshot);

                // Track GC pressure
                TrackGCPressure(snapshot);

                // Detect GC pressure issues
                DetectGCPressure(snapshot);
            }
            catch (Exception ex)
            {
                Logger.Error(ex, "Error collecting CLR metrics");
            }
        }

        /// <summary>
        /// Collects basic GC metrics from System.GC.
        /// </summary>
        private void CollectFromSystemGC(MetricsSnapshot snapshot)
        {
            snapshot.GCGen0Collections = GC.CollectionCount(0);
            snapshot.GCGen1Collections = GC.CollectionCount(1);
            snapshot.GCGen2Collections = GC.CollectionCount(2);

            snapshot.ManagedHeapBytes = GC.GetTotalMemory(false);
            snapshot.ManagedHeapMB = snapshot.ManagedHeapBytes / (1024 * 1024);

            Logger.Trace("GC: Gen0={0}, Gen1={1}, Gen2={2}, Heap={3}MB",
                snapshot.GCGen0Collections, snapshot.GCGen1Collections,
                snapshot.GCGen2Collections, snapshot.ManagedHeapMB);
        }

        /// <summary>
        /// Collects additional metrics from CLR performance counters.
        /// </summary>
        private void CollectFromPerformanceCounters(MetricsSnapshot snapshot)
        {
            try
            {
                // % Time in GC
                snapshot.GCTimePercent = _timeInGCCounter?.NextValue() ?? 0;

                // Heap sizes
                snapshot.Gen0HeapSize = (long)(_gen0HeapSizeCounter?.NextValue() ?? 0);
                snapshot.Gen1HeapSize = (long)(_gen1HeapSizeCounter?.NextValue() ?? 0);
                snapshot.Gen2HeapSize = (long)(_gen2HeapSizeCounter?.NextValue() ?? 0);
                snapshot.LargeObjectHeapSize = (long)(_lohSizeCounter?.NextValue() ?? 0);

                Logger.Trace("GC perf counters: Time={0:F1}%, Gen0={1}MB, Gen1={2}MB, Gen2={3}MB, LOH={4}MB",
                    snapshot.GCTimePercent,
                    snapshot.Gen0HeapSize / (1024 * 1024),
                    snapshot.Gen1HeapSize / (1024 * 1024),
                    snapshot.Gen2HeapSize / (1024 * 1024),
                    snapshot.LargeObjectHeapSize / (1024 * 1024));
            }
            catch (Exception ex)
            {
                Logger.Trace(ex, "Error reading CLR performance counters");
                _countersFailed = true;
            }
        }

        /// <summary>
        /// Calculates deltas from previous sample.
        /// </summary>
        private void CalculateDeltas(MetricsSnapshot snapshot)
        {
            snapshot.ManagedHeapDelta = snapshot.ManagedHeapBytes - _prevManagedHeap;

            // Update previous values
            _prevGen0Count = snapshot.GCGen0Collections;
            _prevGen1Count = snapshot.GCGen1Collections;
            _prevGen2Count = snapshot.GCGen2Collections;
            _prevManagedHeap = snapshot.ManagedHeapBytes;
        }

        /// <summary>
        /// Tracks GC activity for pressure analysis.
        /// </summary>
        private void TrackGCPressure(MetricsSnapshot snapshot)
        {
            // Calculate Gen2 collections delta for this sample
            int gen2Delta = _gcSampleCount > 0
                ? snapshot.GCGen2Collections - _prevGen2Count
                : 0;

            _gen2CollectionDeltas[_gcTrackingIndex] = gen2Delta;
            _timeInGCValues[_gcTrackingIndex] = snapshot.GCTimePercent;

            _gcTrackingIndex = (_gcTrackingIndex + 1) % GCPressureWindowSize;
            _gcSampleCount++;
        }

        /// <summary>
        /// Detects GC pressure issues based on tracked metrics.
        /// </summary>
        private void DetectGCPressure(MetricsSnapshot snapshot)
        {
            if (_gcSampleCount < GCPressureWindowSize / 2)
                return;

            // Sum Gen2 collections over the window
            int totalGen2Collections = 0;
            float avgTimeInGC = 0;
            int samplesToUse = Math.Min(_gcSampleCount, GCPressureWindowSize);

            for (int i = 0; i < samplesToUse; i++)
            {
                totalGen2Collections += _gen2CollectionDeltas[i];
                avgTimeInGC += _timeInGCValues[i];
            }
            avgTimeInGC /= samplesToUse;

            // Check thresholds
            // Window represents 30 seconds, so double the threshold for per-minute rate
            bool highGen2Rate = totalGen2Collections > (Gen2CollectionsPerMinuteThreshold / 2);
            bool highTimeInGC = avgTimeInGC > TimeInGCPercentThreshold;

            if (highGen2Rate || highTimeInGC)
            {
                snapshot.GCPressureHigh = true;
                Logger.Warn("GC PRESSURE HIGH! Gen2 collections: {0} in last {1} samples, Avg time in GC: {2:F1}%",
                    totalGen2Collections, samplesToUse, avgTimeInGC);
            }
        }

        /// <summary>
        /// Forces a garbage collection and returns the memory delta.
        /// Use sparingly as this can affect performance!
        /// </summary>
        /// <returns>Bytes freed by the collection.</returns>
        public long ForceGCAndMeasure()
        {
            Logger.Info("Forcing full GC collection for measurement...");

            var beforeBytes = GC.GetTotalMemory(false);

            // Force full collection
            GC.Collect(2, GCCollectionMode.Forced, true, true);
            GC.WaitForPendingFinalizers();
            GC.Collect(2, GCCollectionMode.Forced, true, true);

            var afterBytes = GC.GetTotalMemory(true);
            var freedBytes = beforeBytes - afterBytes;

            Logger.Info("Forced GC complete. Before: {0:N0} bytes, After: {1:N0} bytes, Freed: {2:N0} bytes",
                beforeBytes, afterBytes, freedBytes);

            return freedBytes;
        }

        /// <summary>
        /// Gets a diagnostic summary of current CLR state.
        /// </summary>
        public string GetDiagnosticSummary()
        {
            try
            {
                return $@"CLR / GC Diagnostics
====================
GC Collection Counts:
  Generation 0: {GC.CollectionCount(0)}
  Generation 1: {GC.CollectionCount(1)}
  Generation 2: {GC.CollectionCount(2)}

Managed Heap: {GC.GetTotalMemory(false) / (1024 * 1024)}MB

Performance Counters Available: {_countersInitialized && !_countersFailed}
Instance Name: {_instanceName ?? "(not set)"}

% Time in GC: {(_countersInitialized ? (_timeInGCCounter?.NextValue() ?? 0f).ToString("F1") + "%" : "N/A")}

Heap Sizes (if available):
  Gen 0: {(_countersInitialized ? ((_gen0HeapSizeCounter?.NextValue() ?? 0f) / (1024 * 1024)).ToString("F1") + "MB" : "N/A")}
  Gen 1: {(_countersInitialized ? ((_gen1HeapSizeCounter?.NextValue() ?? 0f) / (1024 * 1024)).ToString("F1") + "MB" : "N/A")}
  Gen 2: {(_countersInitialized ? ((_gen2HeapSizeCounter?.NextValue() ?? 0f) / (1024 * 1024)).ToString("F1") + "MB" : "N/A")}
  LOH: {(_countersInitialized ? ((_lohSizeCounter?.NextValue() ?? 0f) / (1024 * 1024)).ToString("F1") + "MB" : "N/A")}

GC Latency Mode: {System.Runtime.GCSettings.LatencyMode}
Is Server GC: {System.Runtime.GCSettings.IsServerGC}";
            }
            catch (Exception ex)
            {
                return $"Error getting CLR diagnostics: {ex.Message}";
            }
        }

        /// <summary>
        /// Disposes performance counters.
        /// </summary>
        private void DisposeCounters()
        {
            _gen0CollectionsCounter?.Dispose();
            _gen1CollectionsCounter?.Dispose();
            _gen2CollectionsCounter?.Dispose();
            _timeInGCCounter?.Dispose();
            _gen0HeapSizeCounter?.Dispose();
            _gen1HeapSizeCounter?.Dispose();
            _gen2HeapSizeCounter?.Dispose();
            _lohSizeCounter?.Dispose();

            _gen0CollectionsCounter = null;
            _gen1CollectionsCounter = null;
            _gen2CollectionsCounter = null;
            _timeInGCCounter = null;
            _gen0HeapSizeCounter = null;
            _gen1HeapSizeCounter = null;
            _gen2HeapSizeCounter = null;
            _lohSizeCounter = null;
        }

        /// <summary>
        /// Disposes resources.
        /// </summary>
        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;

            try
            {
                DisposeCounters();
                Logger.Debug("ClrMetrics disposed");
            }
            catch (Exception ex)
            {
                Logger.Warn(ex, "Error disposing ClrMetrics");
            }
        }
    }
}
