using EonaCat.Json;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace EonaCat.Logger.EonaCatCoreLogger
{
    // 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.

    public class DiscordLogger : ILogger, IDisposable
    {
        private readonly string _categoryName;
        private readonly DiscordLoggerOptions _options;
        private readonly LoggerScopedContext _context = new LoggerScopedContext();

        private readonly HttpClient _httpClient = new HttpClient();
        private readonly SemaphoreSlim _flushLock = new SemaphoreSlim(1, 1);
        private readonly ConcurrentQueue<string> _messageQueue = new ConcurrentQueue<string>();

        private CancellationTokenSource _cts;
        private Task _flushTask;
        private int _flushStarted;

        private const int MaxQueueSize = 1000;

        public bool IncludeCorrelationId { get; set; }
        public event EventHandler<Exception> OnException;

        public DiscordLogger(string categoryName, DiscordLoggerOptions options)
        {
            _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
            _options = options ?? throw new ArgumentNullException(nameof(options));
            IncludeCorrelationId = options.IncludeCorrelationId;

            // Start flush loop exactly once
            if (Interlocked.CompareExchange(ref _flushStarted, 1, 0) == 0)
            {
                _cts = new CancellationTokenSource();
                _flushTask = Task.Run(() => FlushLoopAsync(_cts.Token));
            }
        }

        public IDisposable BeginScope<TState>(TState state) => null;
        public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;

        public void SetContext(string key, string value) => _context.Set(key, value);
        public void ClearContext() => _context.Clear();
        public string GetContext(string key) => _context.Get(key);

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
            Exception exception, Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel) || formatter == null)
            {
                return;
            }

            string correlationId = null;
            if (IncludeCorrelationId)
            {
                correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
                _context.Set("CorrelationId", correlationId);
            }

            // Build message efficiently
            var sb = new StringBuilder();
            sb.AppendLine($"**[{DateTime.UtcNow:u}]**");
            sb.AppendLine($"**[{logLevel}]**");
            sb.AppendLine($"**[{_categoryName}]**");
            sb.AppendLine($"Message: {formatter(state, exception)}");

            foreach (var kvp in _context.GetAll())
            {
                sb.AppendLine($"`{kvp.Key}`: {kvp.Value}");
            }

            if (exception != null)
            {
                sb.AppendLine($"Exception: {exception}");
            }

            if (_messageQueue.Count < MaxQueueSize)
            {
                _messageQueue.Enqueue(sb.ToString());
            }
            else
            {
                OnException?.Invoke(this, new Exception("DiscordLogger queue overflow"));
            }
        }

        private async Task FlushLoopAsync(CancellationToken token)
        {
            try
            {
                while (!token.IsCancellationRequested)
                {
                    await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token)
                              .ConfigureAwait(false);
                    await FlushBufferAsync(token).ConfigureAwait(false);
                }
            }
            catch (OperationCanceledException) 
            {
                // Do nothing, expected on cancellation
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }

            await FlushBufferAsync(token);
        }

        private async Task FlushBufferAsync(CancellationToken token)
        {
            if (!await _flushLock.WaitAsync(0, token).ConfigureAwait(false))
            {
                return;
            }

            try
            {
                var batchBuilder = new StringBuilder();
                while (_messageQueue.TryDequeue(out var message))
                {
                    batchBuilder.AppendLine(message);
                }

                if (batchBuilder.Length == 0)
                {
                    return;
                }

                var payload = new { content = batchBuilder.ToString() };
                using var content = new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json");

                var response = await _httpClient.PostAsync(_options.WebhookUrl, content, token)
                                                .ConfigureAwait(false);

                if (!response.IsSuccessStatusCode)
                {
                    var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                    OnException?.Invoke(this, new Exception($"Discord webhook failed: {response.StatusCode} {error}"));
                }
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
            finally
            {
                _flushLock.Release();
            }
        }

        public void Dispose()
        {
            if (_cts == null)
            {
                return;
            }

            _cts.Cancel();
            try 
            {
                _flushTask?.Wait(); 
            }
            catch (AggregateException ae) 
            {
                ae.Handle(e => e is OperationCanceledException);
            }

            _cts.Dispose();
            _httpClient.Dispose();
            _flushLock.Dispose();
        }
    }
}
