using EonaCat.Json;
using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Data.Common;
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>
    /// database flow with batched inserts for any ADO.NET database
    /// </summary>
    public sealed class DatabaseFlow : FlowBase
    {
        private const int ChannelCapacity = 4096;
        private readonly int _batchSize;

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

        private readonly Func<DbConnection> _connectionFactory;
        private readonly string _tableName;

        public DatabaseFlow(
            Func<DbConnection> connectionFactory,
            string tableName = "Logs",
            int batchSize = 1,
            LogLevel minimumLevel = LogLevel.Trace)
            : base($"Database:{tableName}", minimumLevel)
        {
            _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
            _tableName = tableName;

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

        private async Task ProcessLogEventsAsync(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 WriteBatchAsync(batch, cancellationToken).ConfigureAwait(false);
                            batch.Clear();
                        }
                    }

                    if (batch.Count > 0)
                    {
                        await WriteBatchAsync(batch, cancellationToken).ConfigureAwait(false);
                        batch.Clear();
                    }
                }

                if (batch.Count > 0)
                {
                    await WriteBatchAsync(batch, cancellationToken).ConfigureAwait(false);
                    batch.Clear();
                }
            }
            catch (OperationCanceledException) { }
            catch (Exception ex)
            {
                Console.Error.WriteLine($"DatabaseFlow error: {ex.Message}");
            }
        }

        private async Task WriteBatchAsync(List<LogEvent> batch, CancellationToken cancellationToken)
        {
            using var connection = _connectionFactory();
            await connection.OpenAsync(cancellationToken);

            using var transaction = connection.BeginTransaction();

            // Build a single SQL command with multiple inserts
            var sb = new StringBuilder();
            var parameters = new List<DbParameter>();
            int paramIndex = 0;

            foreach (var logEvent in batch)
            {
                sb.Append($"INSERT INTO {_tableName} (Timestamp, Level, Category, Message, ThreadId, Exception, Properties) VALUES (");

                // Timestamp
                var timestampParam = CreateParameter(connection, $"@p{paramIndex++}", LogEvent.GetDateTime(logEvent.Timestamp).ToString("O"));
                parameters.Add(timestampParam);
                sb.Append(timestampParam.ParameterName).Append(", ");

                // Level
                var levelParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.Level.ToString());
                parameters.Add(levelParam);
                sb.Append(levelParam.ParameterName).Append(", ");

                // Category
                var categoryParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.Category ?? string.Empty);
                parameters.Add(categoryParam);
                sb.Append(categoryParam.ParameterName).Append(", ");

                // Message
                var messageParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.Message.ToString());
                parameters.Add(messageParam);
                sb.Append(messageParam.ParameterName).Append(", ");

                // ThreadId
                var threadParam = CreateParameter(connection, $"@p{paramIndex++}", logEvent.ThreadId);
                parameters.Add(threadParam);
                sb.Append(threadParam.ParameterName).Append(", ");

                // Exception
                object exValue = logEvent.Exception != null
                    ? JsonHelper.ToJson(new
                    {
                        type = logEvent.Exception.GetType().FullName,
                        message = logEvent.Exception.Message,
                        stackTrace = logEvent.Exception.StackTrace
                    })
                    : DBNull.Value;

                var exParam = CreateParameter(connection, $"@p{paramIndex++}", exValue);
                parameters.Add(exParam);
                sb.Append(exParam.ParameterName).Append(", ");

                // Properties
                object propsValue = logEvent.Properties.Count > 0
                    ? JsonHelper.ToJson(logEvent.Properties)
                    : DBNull.Value;

                var propsParam = CreateParameter(connection, $"@p{paramIndex++}", propsValue);
                parameters.Add(propsParam);
                sb.Append(propsParam.ParameterName).Append(");");
            }

            using var command = connection.CreateCommand();
            command.Transaction = transaction;
            command.CommandText = sb.ToString();

            foreach (var p in parameters)
            {
                command.Parameters.Add(p);
            }

            await command.ExecuteNonQueryAsync(cancellationToken);
            transaction.Commit();
        }

        private static Dictionary<string, object?> ToDictionary(ReadOnlyMemory<KeyValuePair<string, object?>> properties)
        {
            var dict = new Dictionary<string, object?>();
            foreach (var prop in properties.Span)
            {
                dict[prop.Key] = prop.Value;
            }

            return dict;
        }

        private static DbParameter CreateParameter(DbConnection connection, string name, object value)
        {
            var p = connection.CreateCommand().CreateParameter();
            p.ParameterName = name;
            p.Value = value ?? DBNull.Value;
            return p;
        }

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

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

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