using EonaCat.LogStack.Core;
using EonaCat.LogStack.EonaCatLogStackCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
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 SignalR hub via HTTP POST to the hub's /send endpoint.
    /// Works with ASP.NET SignalR (classic) and ASP.NET Core SignalR server-side REST API.
    ///
    /// A lightweight alternative to the SignalR client library
    ///
    /// On the server side you need a minimal hub endpoint that accepts POST:
    ///   POST {hubUrl}/send  body: { "target": "...", "arguments": [ { log json } ] }
    ///
    /// For live dashboards: the hub broadcasts to a "logs" group; clients subscribe and
    /// render events in real time.
    /// </summary>
    public sealed class SignalRFlow : FlowBase
    {
        private readonly string _hubUrl;
        private readonly string _hubMethod;
        private readonly HttpClient _http;
        private readonly bool _ownsHttpClient;
        private readonly int _batchSize;
        private readonly TimeSpan _batchInterval;

        private readonly List<string> _pending = new List<string>();
        private readonly object _lock = new object();
        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
        private readonly Thread _senderThread;

        private long _totalSent;

        public SignalRFlow(
            string hubUrl,
            string hubMethod = "ReceiveLog",
            HttpClient httpClient = null,
            int batchSize = 20,
            int batchIntervalMs = 500,
            LogLevel minimumLevel = LogLevel.Information)
            : base("SignalR:" + hubUrl, minimumLevel)
        {
            if (hubUrl == null)
            {
                throw new ArgumentNullException("hubUrl");
            }

            _hubUrl = hubUrl.TrimEnd('/');
            _hubMethod = hubMethod ?? "ReceiveLog";
            _batchSize = batchSize < 1 ? 1 : batchSize;
            _batchInterval = TimeSpan.FromMilliseconds(batchIntervalMs < 50 ? 50 : batchIntervalMs);

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

            _senderThread = new Thread(SenderLoop)
            {
                IsBackground = true,
                Name = "SignalRFlow.Sender",
                Priority = ThreadPriority.BelowNormal
            };
            _senderThread.Start();
        }

        public long TotalSent { get { return Interlocked.Read(ref _totalSent); } }

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

            string json = Serialize(logEvent);
            bool sendNow = false;

            lock (_lock)
            {
                _pending.Add(json);
                if (_pending.Count >= _batchSize)
                {
                    sendNow = true;
                }
            }

            if (sendNow)
            {
                DispatchBatch();
            }

            Interlocked.Increment(ref BlastedCount);
            return Task.FromResult(WriteResult.Success);
        }

        public override Task<WriteResult> BlastBatchAsync(
            ReadOnlyMemory<LogEvent> logEvents,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (!IsEnabled)
            {
                return Task.FromResult(WriteResult.FlowDisabled);
            }

            foreach (LogEvent e in logEvents.ToArray())
            {
                if (IsLogLevelEnabled(e))
                {
                    BlastAsync(e, cancellationToken);
                }
            }

            return Task.FromResult(WriteResult.Success);
        }

        public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            DispatchBatch();
            return Task.FromResult(0);
        }

        public override async ValueTask DisposeAsync()
        {
            IsEnabled = false;
            _cts.Cancel();
            _senderThread.Join(TimeSpan.FromSeconds(5));
            DispatchBatch();
            if (_ownsHttpClient)
            {
                _http.Dispose();
            }

            _cts.Dispose();
            await base.DisposeAsync().ConfigureAwait(false);
        }

        private void SenderLoop()
        {
            while (!_cts.Token.IsCancellationRequested)
            {
                try { Thread.Sleep(_batchInterval); }
                catch (ThreadInterruptedException) { break; }
                DispatchBatch();
            }
        }

        private void DispatchBatch()
        {
            List<string> batch;
            lock (_lock)
            {
                if (_pending.Count == 0)
                {
                    return;
                }

                batch = new List<string>(_pending);
                _pending.Clear();
            }

            ThreadPool.QueueUserWorkItem(_ =>
            {
                try { SendBatch(batch); }
                catch (Exception ex)
                {
                    Console.Error.WriteLine("[SignalRFlow] Send error: " + ex.Message);
                    Interlocked.Add(ref DroppedCount, batch.Count);
                }
            });
        }

        private void SendBatch(List<string> jsonEvents)
        {
            // Build envelope: { "target": "ReceiveLog", "arguments": [ [...events...] ] }
            var sb = new StringBuilder(jsonEvents.Count * 200 + 64);
            sb.Append("{\"target\":\"");
            JsonEscape(_hubMethod, sb);
            sb.Append("\",\"arguments\":[[");
            for (int i = 0; i < jsonEvents.Count; i++)
            {
                if (i > 0)
                {
                    sb.Append(',');
                }

                sb.Append(jsonEvents[i]);
            }
            sb.Append("]]}");

            string payload = sb.ToString();
            StringContent content = new StringContent(payload, Encoding.UTF8, "application/json");
            HttpResponseMessage resp = _http.PostAsync(_hubUrl + "/send", content).GetAwaiter().GetResult();

            if (resp.IsSuccessStatusCode)
            {
                Interlocked.Add(ref _totalSent, jsonEvents.Count);
            }
            else
            {
                Console.Error.WriteLine("[SignalRFlow] HTTP " + (int)resp.StatusCode + " from hub.");
                Interlocked.Add(ref DroppedCount, jsonEvents.Count);
            }
        }

        private static string Serialize(LogEvent log)
        {
            var sb = new StringBuilder(256);
            sb.Append('{');
            sb.Append("\"ts\":\"");
            sb.Append(LogEvent.GetDateTime(log.Timestamp).ToString("O"));
            sb.Append("\",\"level\":\"");
            sb.Append(LevelString(log.Level));
            sb.Append("\",\"category\":\"");
            JsonEscape(log.Category ?? string.Empty, sb);
            sb.Append("\",\"message\":\"");
            JsonEscape(log.Message.Length > 0 ? log.Message.ToString() : string.Empty, sb);
            sb.Append('"');

            if (log.Exception != null)
            {
                sb.Append(",\"exception\":\"");
                JsonEscape(log.Exception.GetType().Name + ": " + log.Exception.Message, sb);
                sb.Append('"');
            }

            if (log.Properties.Count > 0)
            {
                sb.Append(",\"props\":{");
                bool first = true;
                foreach (var kv in log.Properties.ToArray())
                {
                    if (!first)
                    {
                        sb.Append(',');
                    }

                    first = false;
                    sb.Append('"');
                    JsonEscape(kv.Key, sb);
                    sb.Append("\":\"");
                    JsonEscape(kv.Value != null ? kv.Value.ToString() : "null", sb);
                    sb.Append('"');
                }
                sb.Append('}');
            }

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

        private static void JsonEscape(string value, StringBuilder sb)
        {
            if (string.IsNullOrEmpty(value))
            {
                return;
            }

            foreach (char c in value)
            {
                switch (c)
                {
                    case '"': sb.Append("\\\""); break;
                    case '\\': sb.Append("\\\\"); break;
                    case '\n': sb.Append("\\n"); break;
                    case '\r': sb.Append("\\r"); break;
                    case '\t': sb.Append("\\t"); break;
                    default:
                        if (char.IsControl(c))
                        {
                            sb.Append("\\u");
                            sb.Append(((int)c).ToString("x4"));
                        }
                        else
                        {
                            sb.Append(c);
                        }

                        break;
                }
            }
        }

        private static string LevelString(LogLevel level)
        {
            switch (level)
            {
                case LogLevel.Trace: return "TRACE";
                case LogLevel.Debug: return "DEBUG";
                case LogLevel.Information: return "INFO";
                case LogLevel.Warning: return "WARN";
                case LogLevel.Error: return "ERROR";
                case LogLevel.Critical: return "CRITICAL";
                default: return level.ToString().ToUpperInvariant();
            }
        }
    }
}