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

namespace EonaCat.Logger.EonaCatCoreLogger
{
    public sealed class HttpLogger : ILogger, IDisposable
    {
        // 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.

        private readonly string _categoryName;
        private readonly HttpLoggerOptions _options;
        private readonly LoggerScopedContext _context = new();
        private readonly HttpClient _client;
        private readonly BlockingCollection<Dictionary<string, object>> _queue;
        private readonly CancellationTokenSource _cts;
        private readonly Task _processingTask;

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

        public HttpLogger(string categoryName, HttpLoggerOptions options)
        {
            _categoryName = categoryName;
            _options = options;
            IncludeCorrelationId = options.IncludeCorrelationId;

            _client = new HttpClient();
            _queue = new BlockingCollection<Dictionary<string, object>>(boundedCapacity: 10000);
            _cts = new CancellationTokenSource();
            _processingTask = Task.Run(() => ProcessQueueAsync(_cts.Token));
        }

        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 IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);
        public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;

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

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

            var payload = new Dictionary<string, object>
            {
                { "timestamp", DateTime.UtcNow },
                { "level", logLevel.ToString() },
                { "category", _categoryName },
                { "message", formatter(state, exception) },
                { "eventId", eventId.Id }
            };

            var contextData = _context.GetAll();
            if (contextData.Count > 0)
            {
                payload["context"] = contextData;
            }

            // Add to queue, drop oldest if full
            while (!_queue.TryAdd(payload))
            {
                _queue.TryTake(out _); // drop oldest
            }
        }

        private async Task ProcessQueueAsync(CancellationToken token)
        {
            try
            {
                foreach (var payload in _queue.GetConsumingEnumerable(token))
                {
                    try
                    {
                        var content = _options.SendAsJson
                            ? new StringContent(JsonHelper.ToJson(payload), Encoding.UTF8, "application/json")
                            : new StringContent(payload["message"].ToString(), Encoding.UTF8, "text/plain");

                        var response = await _client.PostAsync(_options.Endpoint, content, token);

                        if (!response.IsSuccessStatusCode)
                        {
                            OnInvalidStatusCode?.Invoke(this, response.StatusCode.ToString());
                        }
                    }
                    catch (Exception ex)
                    {
                        OnException?.Invoke(this, ex);
                    }
                }
            }
            catch (OperationCanceledException) { }
        }

        public void Dispose()
        {
            _cts.Cancel();
            _queue.CompleteAdding();
            try
            {
                _processingTask.Wait();
            }
            catch { /* ignore */ }

            _queue.Dispose();
            _cts.Dispose();
            _client.Dispose();
        }
    }
}
