using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ClaudeDo.Data.Models;
using Serilog.Core;
using Serilog.Events;

namespace ClaudeDo.Worker.Logging;

/// <summary>
/// Serilog sink that (a) buffers every event (all levels) into <see cref="LogRingBuffer"/>
/// for the Log Visualizer overlay, and (b) broadcasts Warn/Error events to the UI footer
/// via the attached delegate — deduped within a rate-limit window, with a loop guard so
/// SignalR's own log output cannot feed back into another broadcast.
/// </summary>
public sealed class BroadcastLogSink : ILogEventSink
{
    private static readonly TimeSpan RateLimitWindow = TimeSpan.FromSeconds(120);

    private readonly LogRingBuffer _buffer;
    private readonly Func<DateTime> _utcNow;
    private readonly Dictionary<string, DateTime> _lastBroadcast = new();
    private readonly object _gate = new();
    private Func<string, WorkerLogLevel, DateTime, Task>? _broadcast;

    public BroadcastLogSink(LogRingBuffer buffer, Func<DateTime>? utcNow = null)
    {
        _buffer = buffer;
        _utcNow = utcNow ?? (() => DateTime.UtcNow);
    }

    /// <summary>Wired post-build, once the SignalR hub context exists.</summary>
    public void Attach(Func<string, WorkerLogLevel, DateTime, Task> broadcast) => _broadcast = broadcast;

    public void Emit(LogEvent logEvent)
    {
        var level = Map(logEvent.Level);
        var message = Render(logEvent);
        var tsUtc = logEvent.Timestamp.UtcDateTime;

        _buffer.Append(new WorkerLogRecord(message, level, tsUtc));

        if (level is not (WorkerLogLevel.Warn or WorkerLogLevel.Error))
            return;
        if (IsPlumbing(logEvent))
            return;

        var broadcast = _broadcast;
        if (broadcast is null)
            return;

        lock (_gate)
        {
            if (_lastBroadcast.TryGetValue(message, out var last) && _utcNow() - last < RateLimitWindow)
                return;
            _lastBroadcast[message] = _utcNow();
            Prune();
        }

        // Fire-and-forget. A broadcast failure itself logs (re-entering this sink), so it
        // must never throw out of Emit nor leave an unobserved faulted task.
        try { _ = broadcast(message, level, tsUtc).ContinueWith(t => { _ = t.Exception; }, TaskScheduler.Default); }
        catch { /* swallow — logging must not crash on a transport hiccup */ }
    }

    private static WorkerLogLevel Map(LogEventLevel level) => level switch
    {
        LogEventLevel.Warning => WorkerLogLevel.Warn,
        LogEventLevel.Error or LogEventLevel.Fatal => WorkerLogLevel.Error,
        _ => WorkerLogLevel.Info,
    };

    private static string Render(LogEvent e)
    {
        var msg = e.RenderMessage();
        if (e.Exception is { } ex)
        {
            var first = ex.Message.Split('\n', 2)[0].Trim();
            msg = $"{msg}: {ex.GetType().Name}: {first}";
        }
        return msg;
    }

    private static bool IsPlumbing(LogEvent e)
    {
        if (!e.Properties.TryGetValue("SourceContext", out var v) || v is not ScalarValue { Value: string sc })
            return false;
        return sc.StartsWith("Microsoft.AspNetCore.SignalR", StringComparison.Ordinal)
            || sc.StartsWith("Microsoft.AspNetCore.Http.Connections", StringComparison.Ordinal);
    }

    private void Prune()
    {
        if (_lastBroadcast.Count <= 256) return;
        var cutoff = _utcNow() - RateLimitWindow;
        foreach (var key in _lastBroadcast.Where(kv => kv.Value < cutoff).Select(kv => kv.Key).ToList())
            _lastBroadcast.Remove(key);
    }
}
