using EonaCat.LogStack.Core;
using EonaCat.LogStack.EonaCatLogStackCore;
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace EonaCat.LogStack.Flows
{
    // This file is part of the EonaCat project(s) which is released under the Apache License.
    // See the LICENSE file or go to https://EonaCat.com/License for full license details.

    /// <summary>
    /// A decorator flow that applies per-level rate limiting (token bucket) to any
    /// inner flow.  Prevents log storms from overwhelming downstream sinks (e.g. Slack,
    /// HTTP, email) while ensuring that at least one event of each pattern gets through.
    ///
    /// Also supports deduplication: identical messages within a window are collapsed
    /// into a single entry with a repeat-count.
    /// </summary>
    public sealed class ThrottledFlow : FlowBase
    {
        private sealed class Bucket
        {
            public double Tokens;
            public DateTime LastRefill;
            public readonly double Capacity;
            public readonly double RefillPerSecond;

            public Bucket(double capacity, double refillPerSecond)
            {
                Capacity = capacity;
                RefillPerSecond = refillPerSecond;
                Tokens = capacity;
                LastRefill = DateTime.UtcNow;
            }

            /// Returns true and consumes a token if available.
            public bool TryConsume()
            {
                DateTime now = DateTime.UtcNow;
                double elapsed = (now - LastRefill).TotalSeconds;
                Tokens = Math.Min(Capacity, Tokens + elapsed * RefillPerSecond);
                LastRefill = now;

                if (Tokens >= 1.0) { Tokens -= 1.0; return true; }
                return false;
            }
        }

        private sealed class DedupEntry
        {
            public int Count;
            public DateTime FirstSeen;
            public LogEvent LastEvent;
        }

        private readonly IFlow _inner;
        private readonly int _burstCapacity;
        private readonly double _refillPerSecond;
        private readonly bool _deduplicate;
        private readonly TimeSpan _dedupWindow;
        private readonly int _dedupMaxKeys;

        private readonly Dictionary<LogLevel, Bucket> _buckets
            = new Dictionary<LogLevel, Bucket>();
        private readonly Dictionary<string, DedupEntry> _dedupMap
            = new Dictionary<string, DedupEntry>(StringComparer.Ordinal);
        private readonly object _lock = new object();
        private long _throttledCount;

        /// <param name="inner">The downstream flow to protect.</param>
        /// <param name="burstCapacity">
        ///   Max events that can be emitted in a burst per level (token bucket capacity).
        /// </param>
        /// <param name="refillPerSecond">
        ///   How many tokens are added per second per level. E.g. 5.0 = 5 events/second steady state.
        /// </param>
        /// <param name="deduplicate">
        ///   If true, identical messages within <paramref name="dedupWindow"/> are collapsed.
        ///   The suppressed count is appended to the message when the window expires.
        /// </param>
        /// <param name="dedupWindow">Deduplication window (default 60 s).</param>
        /// <param name="dedupMaxKeys">Maximum number of distinct messages tracked (default 1000).</param>
        /// <param name="minimumLevel">Minimum level this flow processes.</param>
        public ThrottledFlow(
            IFlow inner,
            int burstCapacity = 10,
            double refillPerSecond = 1.0,
            bool deduplicate = false,
            TimeSpan dedupWindow = default(TimeSpan),
            int dedupMaxKeys = 1000,
            LogLevel minimumLevel = LogLevel.Trace)
            : base("Throttled:" + (inner != null ? inner.GetType().Name : "null"), minimumLevel)
        {
            if (inner == null)
            {
                throw new ArgumentNullException("inner");
            }

            _inner = inner;
            _burstCapacity = burstCapacity < 1 ? 1 : burstCapacity;
            _refillPerSecond = refillPerSecond <= 0 ? 1.0 : refillPerSecond;
            _deduplicate = deduplicate;
            _dedupWindow = dedupWindow == default(TimeSpan) ? TimeSpan.FromSeconds(60) : dedupWindow;
            _dedupMaxKeys = dedupMaxKeys < 1 ? 1 : dedupMaxKeys;

            // Pre-create buckets for all defined levels
            foreach (LogLevel level in Enum.GetValues(typeof(LogLevel)))
            {
                _buckets[level] = new Bucket(_burstCapacity, _refillPerSecond);
            }
        }


        /// <summary>Events throttled (dropped by rate limit or dedup) so far.</summary>
        public long ThrottledCount { get { return Interlocked.Read(ref _throttledCount); } }

        public override async Task<WriteResult> BlastAsync(
            LogEvent logEvent,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (!IsEnabled || !IsLogLevelEnabled(logEvent))
            {
                return WriteResult.LevelFiltered;
            }

            lock (_lock)
            {
                // deduplication pass
                if (_deduplicate)
                {
                    string key = MakeDedupKey(logEvent);
                    DedupEntry entry;

                    // Flush expired entries to avoid unbounded growth
                    if (_dedupMap.Count >= _dedupMaxKeys)
                    {
                        PurgeExpiredDedupEntries();
                    }

                    if (_dedupMap.TryGetValue(key, out entry))
                    {
                        TimeSpan age = DateTime.UtcNow - entry.FirstSeen;
                        if (age < _dedupWindow)
                        {
                            entry.Count++;
                            entry.LastEvent = logEvent;
                            Interlocked.Increment(ref _throttledCount);
                            return WriteResult.Dropped;
                        }
                        else
                        {
                            // Window expired: flush the suppressed count as a synthetic event
                            if (entry.Count > 1)
                            {
                                FlushDedupEntry(key, entry);
                            }

                            _dedupMap.Remove(key);
                        }
                    }

                    // First occurrence
                    _dedupMap[key] = new DedupEntry
                    {
                        Count = 1,
                        FirstSeen = DateTime.UtcNow,
                        LastEvent = logEvent
                    };
                }

                // token bucket pass
                Bucket bucket;
                if (!_buckets.TryGetValue(logEvent.Level, out bucket))
                {
                    bucket = new Bucket(_burstCapacity, _refillPerSecond);
                    _buckets[logEvent.Level] = bucket;
                }

                if (!bucket.TryConsume())
                {
                    Interlocked.Increment(ref _throttledCount);
                    Interlocked.Increment(ref DroppedCount);
                    return WriteResult.Dropped;
                }
            }

            WriteResult result = await _inner.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
            Interlocked.Increment(ref BlastedCount);
            return result;
        }

        public override async Task<WriteResult> BlastBatchAsync(
            ReadOnlyMemory<LogEvent> logEvents,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (!IsEnabled)
            {
                return WriteResult.FlowDisabled;
            }

            WriteResult result = WriteResult.Success;
            foreach (LogEvent e in logEvents.ToArray())
            {
                WriteResult r = await BlastAsync(e, cancellationToken).ConfigureAwait(false);
                if (r == WriteResult.Dropped)
                {
                    result = WriteResult.Dropped;
                }
            }
            return result;
        }

        public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            // Flush all pending dedup entries
            lock (_lock)
            {
                List<string> keys = new List<string>(_dedupMap.Keys);
                foreach (string key in keys)
                {
                    DedupEntry entry;
                    if (_dedupMap.TryGetValue(key, out entry) && entry.Count > 1)
                    {
                        FlushDedupEntry(key, entry);
                    }

                    _dedupMap.Remove(key);
                }
            }
            return _inner.FlushAsync(cancellationToken);
        }

        public override async ValueTask DisposeAsync()
        {
            IsEnabled = false;
            await FlushAsync().ConfigureAwait(false);
            await base.DisposeAsync().ConfigureAwait(false);
        }

        private static string MakeDedupKey(LogEvent log)
        {
            // Key = level + category + first 200 chars of message (ignore dynamic parts like timestamps)
            string msg = log.Message.Length > 0 ? log.Message.ToString() : string.Empty;
            if (msg.Length > 200)
            {
                msg = msg.Substring(0, 200);
            }

            return log.Level + "|" + (log.Category ?? string.Empty) + "|" + msg;
        }

        private void FlushDedupEntry(string key, DedupEntry entry)
        {
            // Build a synthetic event that summarises the suppressed repeats
            string original = entry.LastEvent.Message.Length > 0
                ? entry.LastEvent.Message.ToString()
                : string.Empty;

            string summary = original + " [repeated " + (entry.Count - 1) + " more times in "
                + (int)_dedupWindow.TotalSeconds + "s window]";

            LogEvent synth = new LogEvent
            {
                Level = entry.LastEvent.Level,
                Category = entry.LastEvent.Category,
                Timestamp = entry.LastEvent.Timestamp,
                Message = new StringSegment(summary),
                Exception = entry.LastEvent.Exception
            };

            try { _inner.BlastAsync(synth).GetAwaiter().GetResult(); }
            catch { /* best-effort */ }
        }

        private void PurgeExpiredDedupEntries()
        {
            List<string> expired = new List<string>();
            DateTime cutoff = DateTime.UtcNow - _dedupWindow;

            foreach (KeyValuePair<string, DedupEntry> kv in _dedupMap)
            {
                if (kv.Value.FirstSeen < cutoff)
                {
                    expired.Add(kv.Key);
                }
            }

            foreach (string k in expired)
            {
                DedupEntry entry;
                if (_dedupMap.TryGetValue(k, out entry) && entry.Count > 1)
                {
                    FlushDedupEntry(k, entry);
                }

                _dedupMap.Remove(k);
            }
        }
    }
}