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
{
    // 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.

    public sealed class DatabaseLogger : ILogger, IDisposable
    {
        private readonly string _categoryName;
        private readonly DatabaseLoggerOptions _options;
        private readonly LoggerScopedContext _context = new();

        private readonly ConcurrentQueue<LogEntry> _queue = new();
        private readonly SemaphoreSlim _flushLock = new(1, 1);
        private CancellationTokenSource _cts = new();
        private Task _flushTask;
        private int _refCount;

        private const int MaxQueueSize = 10_000;

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

        public DatabaseLogger(string categoryName, DatabaseLoggerOptions options)
        {
            _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
            _options = options ?? throw new ArgumentNullException(nameof(options));
            IncludeCorrelationId = options.IncludeCorrelationId;

            if (Interlocked.Increment(ref _refCount) == 1)
            {
                _flushTask = Task.Run(() => FlushLoopAsync(_cts.Token));
            }
        }

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

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

        public void SetContext(string key, string value) => _context.Set(key, value);
        public void ClearContext() => _context.Clear();
        public string GetContext(string key) => _context.Get(key);

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

            if (_queue.Count >= MaxQueueSize)
            {
                _queue.TryDequeue(out _);
            }

            string correlationId = null;
            if (IncludeCorrelationId)
            {
                correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
                _context.Set("CorrelationId", correlationId);
            }

            _queue.Enqueue(new LogEntry
            {
                Timestamp = DateTime.UtcNow,
                LogLevel = logLevel.ToString(),
                Category = _categoryName,
                Message = formatter(state, exception),
                Exception = exception?.ToString(),
                CorrelationId = correlationId
            });
        }

        private async Task FlushLoopAsync(CancellationToken token)
        {
            try
            {
                while (!token.IsCancellationRequested)
                {
                    await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
                    await FlushBufferAsync();
                }
            }
            catch (OperationCanceledException) 
            {
                // Do nothing
            }

            await FlushBufferAsync();
        }

        private async Task FlushBufferAsync()
        {
            if (!await _flushLock.WaitAsync(0))
            {
                return;
            }

            try
            {
                var batch = new List<LogEntry>();
                while (_queue.TryDequeue(out var entry))
                {
                    batch.Add(entry);
                }

                if (batch.Count == 0)
                {
                    return;
                }

                using (var connection = _options.DbProviderFactory.CreateConnection())
                {
                    if (connection == null)
                    {
                        throw new InvalidOperationException("Failed to create DB connection.");
                    }

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

                    using (var transaction = connection.BeginTransaction())
                    using (var command = connection.CreateCommand())
                    {
                        command.Transaction = transaction;
                        command.CommandText = _options.InsertCommand;

                        // Prepare parameters once
                        var timestampParam = CreateParameter(command, "Timestamp", null);
                        var logLevelParam = CreateParameter(command, "LogLevel", null);
                        var categoryParam = CreateParameter(command, "Category", null);
                        var messageParam = CreateParameter(command, "Message", null);
                        var exceptionParam = CreateParameter(command, "Exception", null);
                        var correlationParam = CreateParameter(command, "CorrelationId", null);

                        command.Parameters.Add(timestampParam);
                        command.Parameters.Add(logLevelParam);
                        command.Parameters.Add(categoryParam);
                        command.Parameters.Add(messageParam);
                        command.Parameters.Add(exceptionParam);
                        command.Parameters.Add(correlationParam);

                        foreach (var entry in batch)
                        {
                            timestampParam.Value = entry.Timestamp;
                            logLevelParam.Value = entry.LogLevel ?? (object)DBNull.Value;
                            categoryParam.Value = entry.Category ?? (object)DBNull.Value;
                            messageParam.Value = entry.Message ?? (object)DBNull.Value;
                            exceptionParam.Value = entry.Exception ?? (object)DBNull.Value;
                            correlationParam.Value = entry.CorrelationId ?? (object)DBNull.Value;

                            await command.ExecuteNonQueryAsync();
                        }

                        transaction.Commit();
                    }
                }
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
            finally
            {
                _flushLock.Release();
            }
        }

        private static 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 (Interlocked.Decrement(ref _refCount) == 0)
            {
                _cts.Cancel();
                try 
                {
                    _flushTask?.Wait(); 
                }
                catch 
                {
                    // Do nothing
                }
                _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; }
        }
    }
}
