using EonaCat.Logger.EonaCatCoreLogger;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;

namespace EonaCat.Logger.EonaCatCoreLogger
{
    public sealed class BatchingDatabaseLogger : ILogger, IDisposable
    {
        // 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 readonly string _categoryName;
        private readonly BatchingDatabaseLoggerOptions _options;
        private readonly LoggerScopedContext _context = new();

        private readonly BlockingCollection<LogEntry> _queue;
        private readonly CancellationTokenSource _cts = new();
        private readonly Task _processingTask;

        private volatile bool _disposed;

        private const int MaxQueueSize = 10_000;

        public bool IncludeCorrelationId { get; }
        public event EventHandler<Exception> OnException;

        public BatchingDatabaseLogger(string categoryName, BatchingDatabaseLoggerOptions options)
        {
            _categoryName = categoryName;
            _options = options;
            IncludeCorrelationId = options.IncludeCorrelationId;

            _queue = new BlockingCollection<LogEntry>(new ConcurrentQueue<LogEntry>(), MaxQueueSize);
            _processingTask = Task.Run(() => ProcessQueueAsync(_cts.Token));
        }

        public IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);

        public bool IsEnabled(LogLevel logLevel) => !_disposed && _options.IsEnabled;

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (_disposed || !IsEnabled(logLevel) || formatter == null)
            {
                return;
            }

            var message = formatter(state, exception);

            var correlationId = IncludeCorrelationId
                ? _context.Get("CorrelationId") ?? Guid.NewGuid().ToString()
                : null;

            if (correlationId != null)
            {
                _context.Set("CorrelationId", correlationId);
            }

            var entry = new LogEntry
            {
                Timestamp = DateTime.UtcNow,
                LogLevel = logLevel.ToString(),
                Category = _categoryName,
                Message = message,
                Exception = exception?.ToString(),
                CorrelationId = correlationId
            };

            // Drop oldest when full (non-blocking)
            while (!_queue.TryAdd(entry))
            {
                _queue.TryTake(out _); // discard oldest
            }
        }

        private async Task ProcessQueueAsync(CancellationToken token)
        {
            var batch = new List<LogEntry>(_options.BatchSize);

            try
            {
                foreach (var entry in _queue.GetConsumingEnumerable(token))
                {
                    batch.Add(entry);

                    if (batch.Count >= _options.BatchSize)
                    {
                        await FlushBatchAsync(batch);
                    }
                }
            }
            catch (OperationCanceledException) { }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
            finally
            {
                if (batch.Count > 0)
                {
                    await FlushBatchAsync(batch); // flush remaining
                }
            }
        }

        private async Task FlushBatchAsync(List<LogEntry> batch)
        {
            if (batch.Count == 0)
            {
                return;
            }

            try
            {
                await InsertBatchAsync(batch);
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
            finally
            {
                batch.Clear();
            }
        }

        private async Task InsertBatchAsync(List<LogEntry> batch)
        {
            using var connection = _options.DbProviderFactory.CreateConnection()
                ?? throw new InvalidOperationException("Failed to create database connection.");

            connection.ConnectionString = _options.ConnectionString;
            await connection.OpenAsync(_cts.Token);

            foreach (var entry in batch)
            {
                using var command = connection.CreateCommand();
                command.CommandText = _options.InsertCommand;

                command.Parameters.Add(CreateParameter(command, "Timestamp", entry.Timestamp));
                command.Parameters.Add(CreateParameter(command, "LogLevel", entry.LogLevel));
                command.Parameters.Add(CreateParameter(command, "Category", entry.Category));
                command.Parameters.Add(CreateParameter(command, "Message", entry.Message));
                command.Parameters.Add(CreateParameter(command, "Exception", entry.Exception));
                command.Parameters.Add(CreateParameter(command, "CorrelationId", entry.CorrelationId));

                await command.ExecuteNonQueryAsync(_cts.Token);
            }
        }

        private DbParameter CreateParameter(DbCommand command, string name, object value)
        {
            var param = command.CreateParameter();
            param.ParameterName = $"@{name}";
            param.Value = value ?? DBNull.Value;
            return param;
        }

        public void Dispose()
        {
            if (_disposed)
            {
                return;
            }

            _disposed = true;

            _cts.Cancel();
            _queue.CompleteAdding();

            try
            {
                _processingTask.Wait(_options.ShutdownTimeout);
            }
            catch { /* ignore */ }

            _queue.Dispose();
            _cts.Dispose();
        }

        private sealed class LogEntry
        {
            public DateTime Timestamp { get; init; }
            public string LogLevel { get; init; }
            public string Category { get; init; }
            public string Message { get; init; }
            public string Exception { get; init; }
            public string CorrelationId { get; init; }
        }
    }
}