using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;

namespace EonaCat.Logger.EonaCatCoreLogger
{
    public sealed class TcpLogger : ILogger, IDisposable
    {
        private readonly string _categoryName;
        private readonly TcpLoggerOptions _options;
        private readonly LoggerScopedContext _context = new();
        private readonly Channel<string> _logChannel;
        private readonly CancellationTokenSource _cts = new();
        private readonly Task _processingTask;

        private const int MaxQueueSize = 1000;

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

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

            _logChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(MaxQueueSize)
            {
                FullMode = BoundedChannelFullMode.DropOldest,
                SingleReader = true,
                SingleWriter = false
            });

            _processingTask = Task.Run(() => ProcessLogQueueAsync(_cts.Token), _cts.Token);
        }

        public IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);
        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;
            }

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

                string message = formatter(state, exception);

                // Build message
                var sb = new StringBuilder(256);
                sb.Append('[').Append(DateTime.UtcNow.ToString("u")).Append("] | ");
                sb.Append('[').Append(logLevel).Append("] | ");
                sb.Append('[').Append(_categoryName).Append("] | ");
                sb.Append("Message: ").Append(message);

                var contextData = _context.GetAll();
                if (contextData.Count > 0)
                {
                    sb.Append(" | Context: ");
                    foreach (var kvp in contextData)
                    {
                        sb.Append(kvp.Key).Append("=").Append(kvp.Value).Append("; ");
                    }
                }

                if (exception != null)
                {
                    sb.Append(" | Exception: ").Append(exception);
                }

                _logChannel.Writer.TryWrite(sb.ToString());
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
        }

        private async Task ProcessLogQueueAsync(CancellationToken token)
        {
            TcpClient client = null;
            StreamWriter writer = null;

            try
            {
                client = new TcpClient();
                await client.ConnectAsync(_options.Host, _options.Port).ConfigureAwait(false);

                writer = new StreamWriter(client.GetStream(), Encoding.UTF8) { AutoFlush = true };

                await foreach (var log in _logChannel.Reader.ReadAllAsync(token))
                {
                    try
                    {
                        await writer.WriteLineAsync(log).ConfigureAwait(false);
                    }
                    catch (IOException ioEx)
                    {
                        OnException?.Invoke(this, ioEx);
                        
                        // Attempt to reconnect
                        writer.Dispose();
                        client.Dispose();
                        client = new TcpClient();
                        await client.ConnectAsync(_options.Host, _options.Port).ConfigureAwait(false);
                        writer = new StreamWriter(client.GetStream(), Encoding.UTF8) { AutoFlush = true };
                    }
                    catch (Exception ex)
                    {
                        OnException?.Invoke(this, ex);
                    }
                }
            }
            catch (OperationCanceledException) 
            {
                // normal shutdown
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
            finally
            {
                writer?.Dispose();
                client?.Dispose();
            }
        }

        public void Dispose()
        {
            _cts.Cancel();
            _logChannel.Writer.Complete();

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

            _cts.Dispose();
        }
    }
}
