using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

namespace IOB_UT_NEXT
{
    public static class CallMetricsCollector
    {
        #region Public Methods

        /// <summary>
        /// Registra una chiamata tracciata. Thread-safe.
        /// </summary>
        public static void AddCall(string key, TimeSpan elapsed)
        {
            _metrics.AddOrUpdate(key,
                _ => new CallStats(1, elapsed),
                (_, existing) => new CallStats(existing.Count + 1, existing.TotalElapsed.Add(elapsed)));
        }

        /// <summary>
        /// Carica lo stato persistente da Redis. Thread-safe.
        /// </summary>
        public static async Task LoadFromRedisAsync(IDatabase redisDb, string redisKey = "app:callmetrics")
        {
            if (redisDb == null) throw new ArgumentNullException(nameof(redisDb));
            ;
            var json = await redisDb.StringGetAsync(redisKey);

            if (!json.HasValue) return;

            Dictionary<string, CallStats> parsed;
            try
            {
                parsed = JsonConvert.DeserializeObject<Dictionary<string, CallStats>>(json.ToString());
            }
            catch (Exception ex)
            {
                // Log: dati Redis corrotti o schema incompatibile. Si ignora per sicurezza.
                return;
            }

            if (parsed == null || parsed.Count == 0) return;

            // 5. Applicazione dati sotto lock
            lock (_serializationLock)
            {
                _metrics.Clear();
                foreach (var kvp in parsed)
                {
                    _metrics[kvp.Key] = kvp.Value;
                }
            }
        }

        /// <summary>
        /// Metodo per estrarre il report Pareto e reset del contatore.
        /// </summary>
        /// <param name="keys">Se null o vuoto, elabora tutte le chiavi. Altrimenti filtra solo quelle indicate.</param>
        public static List<ParetoEntry> LogPareto(IEnumerable<string> keys = null)
        {
            List<ParetoEntry> result;

            lock (_extractLock)
            {
                // 1. Snapshot atomico + reset
                var snapshot = _metrics.ToDictionary(
                    kvp => kvp.Key,
                    kvp => new CallStats(kvp.Value.Count, kvp.Value.TotalElapsed));
                _metrics.Clear();

                // 2. Filtro opzionale (compatibile .NET 4.7.0)
                HashSet<string> filter = null;
                if (keys != null)
                {
                    filter = new HashSet<string>(keys);
                }

                var filtered = (filter != null && filter.Any())
                    ? snapshot.Where(kvp => filter.Contains(kvp.Key))
                    : snapshot;

                double totalCalls = filtered.Sum(kvp => kvp.Value.Count);
                if (totalCalls == 0) return new List<ParetoEntry>();

                // 3. Ordinamento decrescente e calcolo Pareto cumulativo
                var sorted = filtered
                    .OrderByDescending(kvp => kvp.Value.Count)
                    .ThenByDescending(kvp => kvp.Value.TotalElapsed)
                    .ToList();

                double cumulative = 0;
                result = sorted.Select(kvp =>
                {
                    double countShare = kvp.Value.Count / totalCalls;
                    cumulative += countShare;
                    double avgMs = (100 * kvp.Value.TotalElapsed.TotalMilliseconds / kvp.Value.Count) / 100;

                    return new ParetoEntry(
                        key: kvp.Key,
                        count: kvp.Value.Count,
                        totalElapsed: kvp.Value.TotalElapsed,
                        avgElapsed: TimeSpan.FromMilliseconds(avgMs),
                        cumulativeParetoPct: cumulative);
                }).ToList();
            }

            return result;
        }

        /// <summary>
        /// Salva lo stato corrente in Redis. Thread-safe.
        /// </summary>
        /// <param name="redisDb">Istanza di IDatabase</param>
        /// <param name="redisKey">Chiave Redis dove salvare i dati</param>
        /// <param name="clearAfterSave">Se true, resetta i contatori dopo il salvataggio</param>
        public static async Task SaveToRedisAsync(IDatabase redisDb, string redisKey = "app:callmetrics", bool clearAfterSave = true)
        {
            if (redisDb == null) throw new ArgumentNullException(nameof(redisDb));

            // 1. Snapshot atomico sotto lock
            Dictionary<string, CallStats> snapshot;
            lock (_serializationLock)
            {
                snapshot = _metrics
                    .OrderByDescending(x => x.Value.Count)
                    .ToDictionary(
                    kvp => kvp.Key,
                    kvp => new CallStats(kvp.Value.Count, kvp.Value.TotalElapsed));
            }

            if (snapshot.Count == 0) return;

            // 2. Serializzazione JSON (fuori dal lock)
            var json = JsonConvert.SerializeObject(snapshot);

            // 3. Scrittura su Redis (fuori dal lock)
            await redisDb.StringSetAsync(redisKey, json);

            // 4. Reset opzionale (sotto lock per sicurezza)
            if (clearAfterSave)
            {
                lock (_serializationLock)
                {
                    _metrics.Clear();
                }
            }
        }

        #endregion Public Methods

        #region Public Classes

        /// <summary>
        /// Risultato dell'analisi Pareto
        /// </summary>
        public class ParetoEntry
        {
            #region Public Constructors

            public ParetoEntry(string key, int count, TimeSpan totalElapsed, TimeSpan avgElapsed, double cumulativeParetoPct)
            {
                Key = key;
                Count = count;
                TotalElapsed = totalElapsed;
                AvgElapsed = avgElapsed;
                CumulativeParetoPct = cumulativeParetoPct;
            }

            #endregion Public Constructors

            #region Public Properties

            public TimeSpan AvgElapsed { get; }
            public int Count { get; }
            public double CumulativeParetoPct { get; }
            public string Key { get; }
            public TimeSpan TotalElapsed { get; }

            #endregion Public Properties
        }

        #endregion Public Classes

        #region Private Fields

        // Lock dedicato solo alla fase di snapshot e reset
        private static readonly object _extractLock = new object();

        // Accumulatore thread-safe
        private static readonly ConcurrentDictionary<string, CallStats> _metrics = new ConcurrentDictionary<string, CallStats>();

        // lock x serializzazione su redisDb
        private static readonly object _serializationLock = new object();

        #endregion Private Fields

        #region Private Classes

        /// <summary>
        /// Struttura interna per l'accumulo
        /// </summary>
        private class CallStats
        {
            #region Public Constructors

            public CallStats(int count, TimeSpan totalElapsed)
            {
                Count = count;
                TotalElapsed = totalElapsed;
            }

            #endregion Public Constructors

            #region Public Properties

            public int Count { get; }
            public TimeSpan TotalElapsed { get; }

            #endregion Public Properties
        }

        #endregion Private Classes
    }
}