using EonaCat.LogStack.Core;
using System;
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 UdpFlow : FlowBase
{
    private readonly int _batchSize;
    private const int ChannelCapacity = 4096;

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

    private readonly string _host;
    private readonly int _port;
    private readonly UdpClient _udpClient;
    private readonly BackpressureStrategy _backpressureStrategy;
    private readonly TimeSpan _flushInterval;
    private readonly Task _flushTask;

    public UdpFlow(
       string host,
       int port,
       int flushIntervalInMilliseconds = 2000,
       int batchSize = 1,
       LogLevel minimumLevel = LogLevel.Trace,
       BackpressureStrategy backpressureStrategy = BackpressureStrategy.DropOldest)
       : base($"UDP:{host}:{port}", minimumLevel)
    {
        _host = host ?? throw new ArgumentNullException(nameof(host));
        _port = port;
        _backpressureStrategy = backpressureStrategy;
        _flushInterval = TimeSpan.FromMilliseconds(flushIntervalInMilliseconds);

        _batchSize = batchSize <= 0 ? 1 : batchSize;

        _udpClient = new UdpClient();

        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();

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

        if (flushIntervalInMilliseconds > 0)
        {
            _flushTask = Task.Run(() => PeriodicFlushAsync(_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);
        var sb = new StringBuilder(8192);

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

                if (batch.Count >= _batchSize || _channel.Reader.Count == 0)
                {
                    await SendBatchAsync(batch, sb, cancellationToken);
                    batch.Clear();
                    sb.Clear();
                }
            }

            if (batch.Count > 0)
            {
                await SendBatchAsync(batch, sb, CancellationToken.None);
            }
        }
        catch (OperationCanceledException) { }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"UdpFlow error: {ex.Message}");
        }
    }

    private async Task SendBatchAsync(List<LogEvent> batch, StringBuilder sb, CancellationToken cancellationToken)
    {
        foreach (var logEvent in batch)
        {
            FormatLogEvent(logEvent, sb);
            sb.AppendLine();
        }

        var data = Encoding.UTF8.GetBytes(sb.ToString());
        await _udpClient.SendAsync(data, data.Length, _host, _port);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void FormatLogEvent(LogEvent logEvent, StringBuilder sb)
    {
        var dt = LogEvent.GetDateTime(logEvent.Timestamp);
        sb.Append(dt.ToString("yyyy-MM-dd HH:mm:ss.fff"));
        sb.Append(" [");
        sb.Append(logEvent.Level.ToString().ToUpperInvariant());
        sb.Append("] ");
        if (!string.IsNullOrEmpty(logEvent.Category))
        {
            sb.Append(logEvent.Category);
            sb.Append(": ");
        }
        sb.Append(logEvent.Message);
    }

    private async Task PeriodicFlushAsync(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            await Task.Delay(_flushInterval, token);
        }
    }

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

        _udpClient.Dispose();
        _cts.Dispose();

        await base.DisposeAsync();
    }
}
