using EonaCat.LogStack.Core;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using EonaCat.Json;
using System.Threading;
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>
/// Pushes log events to a Grafana Loki instance via the HTTP log-push API
/// (<c>POST /loki/api/v1/push</c>).
///
/// Features:
///   • Batching by count and/or flush interval
///   • Configurable static labels (stream selectors)
///   • Auto-label from event Level and Category
///   • Optional HTTP Basic / Bearer authentication
///   • Structured JSON line bodies (all properties serialised)
///   • Compatible with Loki 2.x and 3.x
///
/// <example>
/// <code>
/// new LogBuilder()
///     .WriteToLoki(
///         lokiUrl: "http://loki:3100",
///         labels: new() { ["app"] = "myapp", ["env"] = "prod" },
///         batchSize: 100,
///         batchIntervalMs: 2000)
/// </code>
/// </example>
/// </summary>
public sealed class LokiFlow : FlowBase
{
    private const string PushPath = "/loki/api/v1/push";

    private readonly HttpClient _http;
    private readonly bool _ownsHttpClient;
    private readonly string _lokiPushUrl;
    private readonly Dictionary<string, string> _staticLabels;
    private readonly int _batchSize;
    private readonly TimeSpan _batchInterval;
    private readonly ConcurrentQueue<(string Labels, string Line, long NanoTs)> _queue = new();
    private readonly CancellationTokenSource _cts = new();
    private readonly Task _flushTask;
    private readonly SemaphoreSlim _batchSignal = new(0, int.MaxValue);

    public LokiFlow(
        string lokiUrl,
        Dictionary<string, string>? labels = null,
        int batchSize = 50,
        int batchIntervalMs = 1000,
        string? bearerToken = null,
        string? basicUser = null,
        string? basicPassword = null,
        HttpClient? httpClient = null,
        LogLevel minimumLevel = LogLevel.Trace)
        : base("Loki", minimumLevel)
    {
        if (string.IsNullOrWhiteSpace(lokiUrl))
        {
            throw new ArgumentNullException(nameof(lokiUrl));
        }

        _lokiPushUrl = lokiUrl.TrimEnd('/') + PushPath;
        _staticLabels = labels ?? new Dictionary<string, string>();
        _batchSize = batchSize > 0 ? batchSize : 50;
        _batchInterval = TimeSpan.FromMilliseconds(batchIntervalMs > 0 ? batchIntervalMs : 1000);

        if (httpClient != null)
        {
            _http = httpClient;
            _ownsHttpClient = false;
        }
        else
        {
            _http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
            _ownsHttpClient = true;
        }

        if (!string.IsNullOrEmpty(bearerToken))
        {
            _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
        }
        else if (!string.IsNullOrEmpty(basicUser))
        {
            var encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{basicUser}:{basicPassword}"));
            _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", encoded);
        }

        _flushTask = Task.Run(() => BackgroundFlushAsync(_cts.Token));
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public override Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
    {
        if (!IsEnabled || !IsLogLevelEnabled(logEvent))
        {
            return Task.FromResult(WriteResult.LevelFiltered);
        }

        var labels  = BuildLabels(logEvent);
        var line    = BuildLine(logEvent);
        var nanoTs  = logEvent.Timestamp * 100L; // ticks → nanoseconds

        _queue.Enqueue((labels, line, nanoTs));
        Interlocked.Increment(ref BlastedCount);

        if (_queue.Count >= _batchSize)
        {
            _batchSignal.Release(1);
        }

        return Task.FromResult(WriteResult.Success);
    }

    public override async Task FlushAsync(CancellationToken cancellationToken = default)
    {
        await SendBatchAsync(cancellationToken).ConfigureAwait(false);
    }

    public override async ValueTask DisposeAsync()
    {
        IsEnabled = false;
        _cts.Cancel();
        try { await _flushTask.ConfigureAwait(false); } catch { }
        await SendBatchAsync(default).ConfigureAwait(false);
        if (_ownsHttpClient)
        {
            _http.Dispose();
        }

        _cts.Dispose();
        GC.SuppressFinalize(this);
    }

    // Background flush loop
    private async Task BackgroundFlushAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            try
            {
                await _batchSignal.WaitAsync(_batchInterval, ct).ConfigureAwait(false);
                await SendBatchAsync(ct).ConfigureAwait(false);
            }
            catch (OperationCanceledException) { break; }
            catch { }
        }
        await SendBatchAsync(default).ConfigureAwait(false);
    }

    private async Task SendBatchAsync(CancellationToken ct)
    {
        if (_queue.IsEmpty)
        {
            return;
        }

        // Group by label-set (Loki stream key)
        var groups = new Dictionary<string, List<(string Line, long NanoTs)>>(StringComparer.Ordinal);
        while (_queue.TryDequeue(out var item))
        {
            if (!groups.TryGetValue(item.Labels, out var list))
            {
                list = new List<(string, long)>(16);
                groups[item.Labels] = list;
            }
            list.Add((item.Line, item.NanoTs));
        }

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

        var body = BuildPushJson(groups);
        try
        {
            var content = new StringContent(body, Encoding.UTF8, "application/json");
            using var response = await _http.PostAsync(_lokiPushUrl, content, ct).ConfigureAwait(false);
            if (!response.IsSuccessStatusCode)
            {
                Interlocked.Add(ref DroppedCount, SumGroups(groups));
            }
        }
        catch
        {
            Interlocked.Add(ref DroppedCount, SumGroups(groups));
        }
    }

    private string BuildLabels(LogEvent e)
    {
        var sb = new StringBuilder("{");
        bool first = true;

        foreach (var kv in _staticLabels)
        {
            if (!first)
            {
                sb.Append(", ");
            }

            sb.Append('"').Append(kv.Key).Append("\": \"").Append(kv.Value).Append('"');
            first = false;
        }

        // Auto-labels
        AppendLabel(sb, "level",    e.Level.ToString(), ref first);
        if (e.HasCategory)
        {
            AppendLabel(sb, "category", e.Category,        ref first);
        }

        sb.Append('}');
        return sb.ToString();
    }

    private static void AppendLabel(StringBuilder sb, string key, string value, ref bool first)
    {
        if (!first)
        {
            sb.Append(", ");
        }

        sb.Append('"').Append(key).Append("\": \"").Append(EscapeJson(value)).Append('"');
        first = false;
    }

    private string BuildLine(LogEvent e)
    {
        using var doc = new Utf8JsonWriter_Builder();
        var obj = new
        {
            level     = e.Level.ToString(),
            category  = e.Category,
            message   = e.Message.ToString(),
            exception = e.Exception?.ToString(),
            thread    = e.ThreadId,
            props     = e.HasProperties ? (object)e.Properties : null
        };
        return JsonHelper.ToJson(obj);
    }

    private static string BuildPushJson(Dictionary<string, List<(string Line, long NanoTs)>> groups)
    {
        var sb = new StringBuilder("{\"streams\":[");
        bool firstStream = true;

        foreach (var kv in groups)
        {
            if (!firstStream)
            {
                sb.Append(',');
            }

            firstStream = false;

            sb.Append("{\"stream\":").Append(kv.Key).Append(",\"values\":[");
            bool firstVal = true;
            foreach (var (line, nanoTs) in kv.Value)
            {
                if (!firstVal)
                {
                    sb.Append(',');
                }

                firstVal = false;
                sb.Append("[\"").Append(nanoTs).Append("\",")
                  .Append(JsonHelper.ToJson(line)).Append(']');
            }
            sb.Append("]}");
        }

        sb.Append("]}");
        return sb.ToString();
    }

    private static string EscapeJson(string s)
        => s.Replace("\\", "\\\\").Replace("\"", "\\\"");

    private static long SumGroups(Dictionary<string, List<(string, long)>> groups)
    {
        long n = 0;
        foreach (var l in groups.Values)
        {
            n += l.Count;
        }

        return n;
    }

    // Minimal helper class (unused, kept for future extensibility)
    private sealed class Utf8JsonWriter_Builder : IDisposable
    {
        public void Dispose() { }
    }
}
