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>
    /// Elasticsearch logging flow using HTTP bulk API (without NEST)
    /// </summary>
    public sealed class ElasticSearchFlow : 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 _elasticsearchUrl;
        private readonly string _indexName;

        public ElasticSearchFlow(
            string elasticsearchUrl,
            string indexName = "logs",
            int batchSize = 1,
            LogLevel minimumLevel = LogLevel.Trace)
            : base($"Elasticsearch:{indexName}", minimumLevel)
        {
            _elasticsearchUrl = elasticsearchUrl?.TrimEnd('/') ?? throw new ArgumentNullException(nameof(elasticsearchUrl));
            _indexName = indexName;
            _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(_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(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 SendBulkAsync(batch, cancellationToken);
                            batch.Clear();
                        }
                    }

                    if (batch.Count > 0)
                    {
                        await SendBulkAsync(batch, cancellationToken);
                        batch.Clear();
                    }
                }

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

        private async Task SendBulkAsync(List<LogEvent> batch, CancellationToken cancellationToken)
        {
            var sb = new StringBuilder();

            foreach (var logEvent in batch)
            {
                // Action metadata
                sb.AppendLine(JsonHelper.ToJson(new { index = new { _index = _indexName } }));

                // Document
                var doc = new Dictionary<string, object?>
                {
                    ["timestamp"] = LogEvent.GetDateTime(logEvent.Timestamp).ToString("O"),
                    ["level"] = logEvent.Level.ToString(),
                    ["category"] = logEvent.Category ?? string.Empty,
                    ["message"] = logEvent.Message.ToString(),
                    ["threadId"] = logEvent.ThreadId
                };

                if (logEvent.Exception != null)
                {
                    doc["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;
                    }

                    doc["properties"] = props;
                }

                sb.AppendLine(JsonHelper.ToJson(doc));
            }

            var content = new StringContent(sb.ToString(), Encoding.UTF8, "application/x-ndjson");
            using var response = await _httpClient.PostAsync($"{_elasticsearchUrl}/_bulk", content, cancellationToken);

            if (!response.IsSuccessStatusCode)
            {
                var respText = await response.Content.ReadAsStringAsync();
                Console.Error.WriteLine($"ElasticSearchFlow bulk insert failed: {response.StatusCode} {respText}");
            }
        }

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