using EonaCat.Json;
using EonaCat.LogStack.Core;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Channels;
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.

    public sealed class GraylogFlow : FlowBase
    {
        private readonly int _batchSize;
        private const int ChannelCapacity = 4096;
        private const int MaxUdpPacketSize = 8192;

        private readonly Channel<LogEvent> _channel;
        private readonly Task _senderTask;
        private readonly CancellationTokenSource _cts;

        private readonly string _host;
        private readonly int _port;
        private readonly bool _useTcp;
        private TcpClient? _tcpClient;
        private NetworkStream? _tcpStream;
        private UdpClient? _udpClient;
        private readonly BackpressureStrategy _backpressureStrategy;
        private readonly string _graylogHostName;

        public GraylogFlow(
            string host,
            int port = 12201,
            bool useTcp = false,
            string graylogHostName = null,
            int batchSize = 1,
            LogLevel minimumLevel = LogLevel.Trace,
            BackpressureStrategy backpressureStrategy = BackpressureStrategy.DropOldest)
            : base($"Graylog:{host}:{port}", minimumLevel)
        {
            _host = host ?? throw new ArgumentNullException(nameof(host));
            _port = port;
            _useTcp = useTcp;
            _backpressureStrategy = backpressureStrategy;
            _graylogHostName = graylogHostName ?? Environment.MachineName;
            _batchSize = batchSize <= 0 ? 1 : batchSize;

            var channelOptions = new BoundedChannelOptions(ChannelCapacity)
            {
                FullMode = backpressureStrategy switch
                {
                    BackpressureStrategy.Wait => BoundedChannelFullMode.Wait,
                    BackpressureStrategy.DropNewest => BoundedChannelFullMode.DropWrite,
                    BackpressureStrategy.DropOldest => BoundedChannelFullMode.DropOldest,
                    _ => BoundedChannelFullMode.Wait
                },
                SingleReader = true,
                SingleWriter = false
            };

            _channel = Channel.CreateBounded<LogEvent>(channelOptions);
            _cts = new CancellationTokenSource();

            if (!_useTcp)
            {
                _udpClient = new UdpClient();
            }

            _senderTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token));
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public override Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
        {
            if (!IsEnabled || !IsLogLevelEnabled(logEvent))
            {
                return Task.FromResult(WriteResult.LevelFiltered);
            }

            if (_channel.Writer.TryWrite(logEvent))
            {
                Interlocked.Increment(ref BlastedCount);
                return Task.FromResult(WriteResult.Success);
            }

            Interlocked.Increment(ref DroppedCount);
            return Task.FromResult(WriteResult.Dropped);
        }

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

            var result = WriteResult.Success;
            foreach (var logEvent in logEvents.Span)
            {
                if (!IsLogLevelEnabled(logEvent))
                {
                    continue;
                }

                if (_channel.Writer.TryWrite(logEvent))
                {
                    Interlocked.Increment(ref BlastedCount);
                }
                else
                {
                    Interlocked.Increment(ref DroppedCount);
                    result = WriteResult.Dropped;
                }
            }

            return result;
        }

        private async Task ProcessLogEventsAsync(CancellationToken cancellationToken)
        {
            var batch = new List<LogEvent>(_batchSize);

            while (!cancellationToken.IsCancellationRequested)
            {
                try
                {
                    if (_useTcp)
                    {
                        await EnsureTcpConnectedAsync(cancellationToken);
                    }

                    await foreach (var logEvent in _channel.Reader.ReadAllAsync(cancellationToken))
                    {
                        batch.Add(logEvent);

                        if (batch.Count >= _batchSize || _channel.Reader.Count == 0)
                        {
                            await SendBatchAsync(batch, cancellationToken);
                            batch.Clear();
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.Error.WriteLine($"GraylogFlow error: {ex.Message}");
                    await Task.Delay(1000, cancellationToken);
                    _tcpClient?.Dispose();
                    _tcpClient = null;
                }
            }
        }

        private async Task EnsureTcpConnectedAsync(CancellationToken cancellationToken)
        {
            if (_tcpClient != null && _tcpClient.Connected)
            {
                return;
            }

            _tcpClient?.Dispose();
            _tcpClient = new TcpClient();
            await _tcpClient.ConnectAsync(_host, _port);
            _tcpStream = _tcpClient.GetStream();
        }

        private static double ToUnixTimeSeconds(DateTime dt)
        {
            // Make sure the DateTime is UTC
            var utc = dt.ToUniversalTime();
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return (utc - epoch).TotalSeconds;
        }


        private async Task SendBatchAsync(List<LogEvent> batch, CancellationToken cancellationToken)
        {
            foreach (var logEvent in batch)
            {
                var dt = LogEvent.GetDateTime(logEvent.Timestamp);
                var unixTimestamp = ToUnixTimeSeconds(dt);

                var gelfMessage = new
                {
                    version = "1.1",
                    host = _graylogHostName,
                    short_message = logEvent.Message,
                    timestamp = unixTimestamp,
                    level = MapLogLevelToSyslogSeverity(logEvent.Level),
                    _category = logEvent.Category
                };

                string json = JsonHelper.ToJson(gelfMessage);
                byte[] data = Encoding.UTF8.GetBytes(json);

                if (_useTcp)
                {
                    if (_tcpStream != null)
                    {
                        await _tcpStream.WriteAsync(data, 0, data.Length, cancellationToken);
                        await _tcpStream.FlushAsync(cancellationToken);
                    }
                }
                else
                {
                    if (_udpClient != null)
                    {
                        if (data.Length <= MaxUdpPacketSize)
                        {
                            await _udpClient.SendAsync(data, data.Length, _host, _port);
                        }
                        else
                        {
                            await SendUdpInChunksAsync(data, MaxUdpPacketSize, cancellationToken);
                        }
                    }
                }
            }
        }

        private async Task SendUdpInChunksAsync(byte[] data, int chunkSize, CancellationToken cancellationToken)
        {
            int offset = 0;
            byte[] buffer = ArrayPool<byte>.Shared.Rent(chunkSize);

            try
            {
                while (offset < data.Length)
                {
                    int size = Math.Min(chunkSize, data.Length - offset);
                    Buffer.BlockCopy(data, offset, buffer, 0, size);
                    if (_udpClient != null)
                    {
                        await _udpClient.SendAsync(buffer, size, _host, _port);
                    }

                    offset += size;
                }
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(buffer);
            }
        }

        private int MapLogLevelToSyslogSeverity(LogLevel level)
        {
            return level switch
            {
                LogLevel.Trace => 7,
                LogLevel.Debug => 7,
                LogLevel.Information => 6,
                LogLevel.Warning => 4,
                LogLevel.Error => 3,
                LogLevel.Critical => 2,
                _ => 6
            };
        }

        public override async Task FlushAsync(CancellationToken cancellationToken = default)
        {
            _channel.Writer.Complete();
            try { await _senderTask.ConfigureAwait(false); } catch { }
        }

        public override async ValueTask DisposeAsync()
        {
            IsEnabled = false;
            _channel.Writer.Complete();
            _cts.Cancel();

            try { await _senderTask.ConfigureAwait(false); } catch { }

            _tcpStream?.Dispose();
            _tcpClient?.Dispose();
            _udpClient?.Dispose();
            _cts.Dispose();

            await base.DisposeAsync();
        }
    }
}
