using EonaCat.Json;
using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Net.Http;
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.

    /// <summary>
    /// logging flow that sends messages to a Discord channel via webhook.
    /// </summary>
    public sealed class DiscordFlow : FlowBase, IAsyncDisposable
    {
        private const int ChannelCapacity = 4096;
        private readonly int _batchSize;

        private readonly Channel<LogEvent> _channel;
        private readonly Task _workerTask;
        private readonly CancellationTokenSource _cts;
        private readonly HttpClient _httpClient;
        private readonly string _webhookUrl;

        public DiscordFlow(
            string webhookUrl,
            string botName,
             int batchSize = 1,
            LogLevel minimumLevel = LogLevel.Information)
            : base("Discord", minimumLevel)
        {
            _webhookUrl = webhookUrl ?? throw new ArgumentNullException(nameof(webhookUrl));
            _httpClient = new HttpClient();
            _batchSize = batchSize <= 0 ? 1 : batchSize;

            var channelOptions = new BoundedChannelOptions(ChannelCapacity)
            {
                FullMode = BoundedChannelFullMode.DropOldest,
                SingleReader = true,
                SingleWriter = false
            };

            _channel = Channel.CreateBounded<LogEvent>(channelOptions);
            _cts = new CancellationTokenSource();
            _workerTask = Task.Run(() => ProcessQueueAsync(botName, _cts.Token));
        }

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

        private async Task ProcessQueueAsync(string botName, CancellationToken cancellationToken)
        {
            var batch = new List<LogEvent>(_batchSize);

            try
            {
                while (await _channel.Reader.WaitToReadAsync(cancellationToken))
                {
                    while (_channel.Reader.TryRead(out var logEvent))
                    {
                        batch.Add(logEvent);

                        if (batch.Count >= _batchSize)
                        {
                            await SendBatchAsync(botName, batch, cancellationToken);
                            batch.Clear();
                        }
                    }

                    if (batch.Count > 0)
                    {
                        await SendBatchAsync(botName, batch, cancellationToken);
                        batch.Clear();
                    }
                }
            }
            catch (OperationCanceledException) { }
            catch (Exception ex)
            {
                Console.Error.WriteLine($"DiscordFlow error: {ex.Message}");
            }
        }

        private async Task SendBatchAsync(string botName, List<LogEvent> batch, CancellationToken cancellationToken)
        {
            foreach (var logEvent in batch)
            {
                var content = new
                {
                    username = botName,
                    embeds = new[]
                    {
                        new
                        {
                            title = logEvent.Level.ToString(),
                            description = logEvent.Message,
                            color = GetDiscordColor(logEvent.Level),
                            timestamp = LogEvent.GetDateTime(logEvent.Timestamp).ToString("O"),
                            fields = logEvent.Properties.Count > 0
                                ? GetFields(logEvent)
                                : Array.Empty<object>()
                        }
                    }
                };

                var json = JsonHelper.ToJson(content);
                using var stringContent = new StringContent(json, Encoding.UTF8, "application/json");
                await _httpClient.PostAsync(_webhookUrl, stringContent, cancellationToken);

                if (logEvent.Exception != null)
                {
                    var exContent = new
                    {
                        username = botName,
                        content = $"**Exception:** {logEvent.Exception.GetType().FullName}\n```{logEvent.Exception.Message}\n{logEvent.Exception.StackTrace}```"
                    };
                    var exJson = JsonHelper.ToJson(exContent);
                    using var exStringContent = new StringContent(exJson, Encoding.UTF8, "application/json");
                    await _httpClient.PostAsync(_webhookUrl, exStringContent, cancellationToken);
                }
            }
        }

        private static int GetDiscordColor(LogLevel level)
        {
            return level switch
            {
                LogLevel.Trace => 0x00FFFF,       // Cyan
                LogLevel.Debug => 0x00FF00,       // Green
                LogLevel.Information => 0xFFFFFF, // White
                LogLevel.Warning => 0xFFFF00,     // Yellow
                LogLevel.Error => 0xFF0000,       // Red
                LogLevel.Critical => 0x800000,    // Dark Red
                _ => 0x808080,                    // Gray
            };
        }

        private static object[] GetFields(LogEvent logEvent)
        {
            var fields = new List<object>();
            foreach (var prop in logEvent.Properties)
            {
                fields.Add(new
                {
                    name = prop.Key,
                    value = prop.Value?.ToString() ?? "null",
                    inline = true
                });
            }
            return fields.ToArray();
        }

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

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

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

            _httpClient.Dispose();
            _cts.Dispose();
            await base.DisposeAsync();
        }
    }
}
