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;

/// <summary>
/// HTTP flow for sending logs to remote endpoints with batching and retry logic
/// </summary>
public sealed class HttpFlow : FlowBase
{
    // 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.

    private const int ChannelCapacity = 2048;
    private readonly int _batchSize;
    private const int MaxRetries = 3;

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

    private readonly HttpClient _httpClient;
    private readonly string _endpoint;
    private readonly bool _ownHttpClient;
    private readonly TimeSpan _batchInterval;
    private readonly Dictionary<string, string>? _headers;

    public HttpFlow(
        string endpoint,
        HttpClient? httpClient = null,
        int batchSize = 1,
        LogLevel minimumLevel = LogLevel.Information,
        TimeSpan? batchInterval = null,
        Dictionary<string, string>? headers = null)
        : base($"Http:{endpoint}", minimumLevel)
    {
        _endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
        _batchInterval = batchInterval ?? TimeSpan.FromSeconds(5);
        _headers = headers;

        if (httpClient == null)
        {
            _httpClient = new HttpClient
            {
                Timeout = TimeSpan.FromSeconds(30)
            };
            _ownHttpClient = true;
        }
        else
        {
            _httpClient = httpClient;
            _ownHttpClient = false;
        }

        _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();
        _writerTask = Task.Run(() => ProcessLogEventsAsync(_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);
    }

    public override async Task FlushAsync(CancellationToken cancellationToken = default)
    {
        _channel.Writer.Complete();

        try
        {
            await _writerTask.ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            // Expected
        }
    }

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

        try
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                var hasMore = true;

                // Collect batch
                while (batch.Count < _batchSize && hasMore)
                {
                    if (_channel.Reader.TryRead(out var logEvent))
                    {
                        batch.Add(logEvent);
                    }
                    else
                    {
                        hasMore = false;
                    }
                }

                // Send batch if we have events
                if (batch.Count > 0)
                {
                    await SendBatchWithRetryAsync(batch, cancellationToken).ConfigureAwait(false);
                    batch.Clear();
                }

                // Wait for either new events or batch interval
                if (_channel.Reader.Count == 0)
                {
                    try
                    {
                        await Task.Delay(_batchInterval, cancellationToken).ConfigureAwait(false);
                    }
                    catch (OperationCanceledException)
                    {
                        // Expected if cancellation was requested during delay
                        break;
                    }
                }
            }
        }
        catch (OperationCanceledException)
        {
            // Expected when shutting down
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"HttpFlow error: {ex.Message}");
        }
    }

    private async Task SendBatchWithRetryAsync(List<LogEvent> batch, CancellationToken cancellationToken)
    {
        var payload = SerializeBatch(batch);

        // Serialize payload to JSON string
        var jsonPayload = JsonHelper.ToJson(payload);

        for (int retry = 0; retry < MaxRetries; retry++)
        {
            try
            {
                using (var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"))
                using (var request = new HttpRequestMessage(HttpMethod.Post, _endpoint) { Content = content })
                {
                    if (_headers != null)
                    {
                        foreach (var header in _headers)
                        {
                            request.Headers.TryAddWithoutValidation(header.Key, header.Value);
                        }
                    }

                    var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);

                    if (response.IsSuccessStatusCode)
                    {
                        return; // Success
                    }

                    // Last retry: mark as dropped
                    if (retry == MaxRetries - 1)
                    {
                        Interlocked.Add(ref DroppedCount, batch.Count);
                    }
                }
            }
            catch
            {
                if (retry == MaxRetries - 1)
                {
                    Interlocked.Add(ref DroppedCount, batch.Count);
                }
            }

            // Exponential backoff
            if (retry < MaxRetries - 1)
            {
                await Task.Delay(TimeSpan.FromMilliseconds(100 * Math.Pow(2, retry)), cancellationToken).ConfigureAwait(false);
            }
        }
    }

    private object[] SerializeBatch(List<LogEvent> batch)
    {
        var payload = new object[batch.Count];

        for (int i = 0; i < batch.Count; i++)
        {
            var logEvent = batch[i];
            var dto = new Dictionary<string, object?>
            {
                ["timestamp"] = LogEvent.GetDateTime(logEvent.Timestamp).ToString("O"),
                ["level"] = logEvent.Level.ToString(),
                ["message"] = logEvent.Message.ToString(),
                ["category"] = logEvent.Category,
                ["threadId"] = logEvent.ThreadId
            };

            if (logEvent.TraceId != default)
            {
                dto["traceId"] = logEvent.TraceId.ToString();
            }

            if (logEvent.SpanId != default)
            {
                dto["spanId"] = logEvent.SpanId.ToString();
            }

            if (logEvent.Exception != null)
            {
                dto["exception"] = new
                {
                    type = logEvent.Exception.GetType().FullName,
                    message = logEvent.Exception.Message,
                    stackTrace = logEvent.Exception.StackTrace
                };
            }

            if (logEvent.Properties.Count > 0)
            {
                var props = new Dictionary<string, object?>();
                foreach (var prop in logEvent.Properties)
                {
                    props[prop.Key] = prop.Value;
                }
                dto["properties"] = props;
            }

            payload[i] = dto;
        }

        return payload;
    }

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

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

        if (_ownHttpClient)
        {
            _httpClient.Dispose();
        }

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