using EonaCat.LogStack.Core;
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 flow that detects and suppresses duplicate log events.
    /// Uses content-based hashing to identify duplicates within a rolling time window.
    /// </summary>
    public sealed class DuplicateDetectionFlow : FlowBase
    {
        private readonly IFlow _innerFlow;
        private readonly int _windowSizeMs;
        private readonly Dictionary<int, DuplicateEntry> _seenHashes;
        private readonly Timer _pruneTimer;
        private readonly object _hashLock = new();
        private bool _disposed;

        private class DuplicateEntry
        {
            public DateTime FirstSeen { get; set; }
            public int Count { get; set; }
        }

        public DuplicateDetectionFlow(
            IFlow innerFlow,
            int windowSizeMs = 300000,
            int pruneIntervalMs = 60000,
            LogLevel minimumLevel = LogLevel.Trace)
            : base(innerFlow?.Name + "_DuplicateDetection" ?? "DuplicateDetectionFlow", minimumLevel)
        {
            _innerFlow = innerFlow ?? throw new ArgumentNullException(nameof(innerFlow));
            _windowSizeMs = windowSizeMs > 0 ? windowSizeMs : 300000;
            _seenHashes = new Dictionary<int, DuplicateEntry>();
            _pruneTimer = new Timer(PruneOldEntriesCallback, null, pruneIntervalMs > 0 ? pruneIntervalMs : 60000, pruneIntervalMs > 0 ? pruneIntervalMs : 60000);
        }

        private int ComputeHash(LogEvent logEvent)
        {
            unchecked
            {
                int hash = 17;
                hash = hash * 31 + logEvent.Message.ToString().GetHashCode();
                hash = hash * 31 + (logEvent.Category?.GetHashCode() ?? 0);
                hash = hash * 31 + logEvent.Level.GetHashCode();
                hash = hash * 31 + (logEvent.Exception?.GetType().Name.GetHashCode() ?? 0);
                return hash;
            }
        }

        private bool IsDuplicate(LogEvent logEvent, out bool isNewException)
        {
            isNewException = false;
            var hash = ComputeHash(logEvent);

            lock (_hashLock)
            {
                var now = DateTime.UtcNow;

                if (_seenHashes.TryGetValue(hash, out var entry))
                {
                    var age = (now - entry.FirstSeen).TotalMilliseconds;
                    if (age < _windowSizeMs)
                    {
                        entry.Count++;
                        return true;
                    }
                    else
                    {
                        _seenHashes[hash] = new DuplicateEntry { FirstSeen = now, Count = 1 };
                        return false;
                    }
                }
                else
                {
                    _seenHashes[hash] = new DuplicateEntry { FirstSeen = now, Count = 1 };
                    return false;
                }
            }
        }

        private void PruneOldEntriesCallback(object? state)
        {
            if (!_disposed)
            {
                lock (_hashLock)
                {
                    var now = DateTime.UtcNow;
                    var keysToRemove = new List<int>();

                    foreach (var kvp in _seenHashes)
                    {
                        var age = (now - kvp.Value.FirstSeen).TotalMilliseconds;
                        if (age >= _windowSizeMs)
                        {
                            keysToRemove.Add(kvp.Key);
                        }
                    }

                    foreach (var key in keysToRemove)
                    {
                        _seenHashes.Remove(key);
                    }
                }
            }
        }

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

            if (IsDuplicate(logEvent, out _))
            {
                Interlocked.Increment(ref DroppedCount);
                return WriteResult.Success;
            }

            var result = await _innerFlow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
            if (result == WriteResult.Success)
            {
                Interlocked.Increment(ref BlastedCount);
            }
            else
            {
                Interlocked.Increment(ref DroppedCount);
            }

            return result;
        }

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

            var uniqueEvents = new List<LogEvent>();
            foreach (var logEvent in logEvents.Span)
            {
                if (IsLogLevelEnabled(logEvent) && !IsDuplicate(logEvent, out _))
                {
                    uniqueEvents.Add(logEvent);
                    Interlocked.Increment(ref BlastedCount);
                }
                else
                {
                    Interlocked.Increment(ref DroppedCount);
                }
            }

            if (uniqueEvents.Count > 0)
            {
                return await _innerFlow.BlastBatchAsync(uniqueEvents.ToArray(), cancellationToken).ConfigureAwait(false);
            }

            return WriteResult.Success;
        }

        public override async Task FlushAsync(CancellationToken cancellationToken = default)
        {
            await _innerFlow.FlushAsync(cancellationToken).ConfigureAwait(false);
        }

        public override async ValueTask DisposeAsync()
        {
            _disposed = true;
            _pruneTimer?.Dispose();
            await _innerFlow.DisposeAsync().ConfigureAwait(false);
            await base.DisposeAsync().ConfigureAwait(false);
        }
    }
}
