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

namespace EonaCat.Logger.EonaCatCoreLogger
{
    public sealed class JsonFileLogger : ILogger, IDisposable
    {
        private readonly string _categoryName;
        private readonly JsonFileLoggerOptions _options;
        private readonly string _filePath;
        private readonly LoggerScopedContext _context = new();

        private readonly BlockingCollection<string> _queue;
        private readonly CancellationTokenSource _cts = new();
        private readonly Task _workerTask;

        private const int MaxQueueSize = 10_000;

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

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

            _filePath = Path.Combine(_options.LogDirectory, _options.FileName);
            _queue = new BlockingCollection<string>(MaxQueueSize);

            try
            {
                Directory.CreateDirectory(_options.LogDirectory);
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }

            _workerTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
        }

        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;
            }

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

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

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

                string json = JsonHelper.ToJson(logObject);

                // Drop oldest if full
                while (!_queue.TryAdd(json))
                {
                    _queue.TryTake(out _);
                }
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
        }

        private void ProcessQueue()
        {
            try
            {
                foreach (var log in _queue.GetConsumingEnumerable(_cts.Token))
                {
                    try
                    {
                        File.AppendAllText(_filePath, log + Environment.NewLine, Encoding.UTF8);
                    }
                    catch (Exception ex)
                    {
                        OnException?.Invoke(this, ex);
                    }
                }
            }
            catch (OperationCanceledException) 
            {
                // normal shutdown
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
        }

        /// <summary>
        /// Flush all pending logs to file.
        /// </summary>
        public void Flush()
        {
            while (_queue.Count > 0)
            {
                Thread.Sleep(50);
            }
        }

        public void Dispose()
        {
            _cts.Cancel();
            _queue.CompleteAdding();

            try
            {
                _workerTask.Wait();
            }
            catch 
            {
                // Do nothing
            }

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