using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
using ROLAC.API.Entities.Logging;
using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;

namespace ROLAC.API.Services.Logging;

/// <summary>
/// A singleton <see cref="ILoggerProvider"/> that turns framework/app log events into
/// <see cref="SystemLog"/> rows enqueued onto <see cref="SystemLogQueue"/>. It depends only on
/// singletons (queue, options, IHttpContextAccessor) and NEVER touches a DbContext — that is
/// what makes the enqueue-only design safe from the singleton logging pipeline.
/// </summary>
[ProviderAlias("Database")]
public sealed class DbLoggerProvider : ILoggerProvider
{
    private readonly SystemLogQueue          _queue;
    private readonly DatabaseLoggerOptions   _options;
    private readonly IHttpContextAccessor    _http;
    private readonly ConcurrentDictionary<string, DbLogger> _loggers = new();

    public DbLoggerProvider(
        SystemLogQueue queue, IOptions<DatabaseLoggerOptions> options, IHttpContextAccessor http)
    {
        _queue   = queue;
        _options = options.Value;
        _http    = http;
    }

    public ILogger CreateLogger(string categoryName) =>
        _loggers.GetOrAdd(categoryName, name => new DbLogger(name, _queue, _options, _http));

    public void Dispose() => _loggers.Clear();
}

/// <summary>The per-category logger. Drops events below the floor or in excluded categories.</summary>
internal sealed class DbLogger : ILogger
{
    private readonly string                _category;
    private readonly SystemLogQueue        _queue;
    private readonly DatabaseLoggerOptions _options;
    private readonly IHttpContextAccessor  _http;
    private readonly bool                  _excluded;

    public DbLogger(
        string category, SystemLogQueue queue, DatabaseLoggerOptions options, IHttpContextAccessor http)
    {
        _category = category;
        _queue    = queue;
        _options  = options;
        _http     = http;
        _excluded = options.ExcludedCategories.Any(prefix =>
            category.StartsWith(prefix, StringComparison.Ordinal));
    }

    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;

    public bool IsEnabled(MsLogLevel logLevel) =>
        !_excluded && logLevel != MsLogLevel.None && logLevel >= _options.MinimumLevel;

    public void Log<TState>(
        MsLogLevel logLevel, EventId eventId, TState state, Exception? exception,
        Func<TState, Exception?, string> formatter)
    {
        if (!IsEnabled(logLevel))
            return;

        var context = _http.HttpContext;

        var log = new SystemLog
        {
            Timestamp     = DateTimeOffset.UtcNow,
            Level         = LogLevelMap.FromMs(logLevel),
            Category      = _category,
            EventId       = eventId.Id == 0 ? null : eventId.Id,
            Message       = formatter(state, exception),
            Exception     = exception?.ToString(),
            RequestPath   = context?.Request.Path.Value,
            HttpMethod    = context?.Request.Method,
            UserId        = context?.User.FindFirst("sub")?.Value,
            IpAddress     = context?.Connection.RemoteIpAddress?.ToString(),
            CorrelationId = context?.TraceIdentifier,
        };

        _queue.TryEnqueue(log);
    }
}
