using EonaCat.LogStack.Core;
using EonaCat.LogStack.EonaCatLogStackCore;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Net.Sockets;
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>
    /// Publishes log events to a Redis channel using the PUBLISH command (Pub/Sub)
    /// and optionally appends them to a Redis List (LPUSH) for persistence.
    ///
    /// Uses raw TCP + RESP protocol, so there arent additional dependencies
    ///
    /// Features:
    ///  - Reconnect with exponential back-off on connection failure
    ///  - Optional LPUSH to a list key with LTRIM to cap list length
    ///  - Optional password authentication (AUTH command)
    ///  - Optional DB selection (SELECT command)
    ///  - Background writer thread (non-blocking callers)
    /// </summary>
    public sealed class RedisFlow : FlowBase
    {
        private readonly string _host;
        private readonly int _port;
        private readonly string _password;
        private readonly int _database;
        private readonly string _channel;       // PUBLISH channel (Pub/Sub)
        private readonly string _listKey;       // LPUSH list key (null = disabled)
        private readonly int _maxListLength; // LTRIM cap (0 = unlimited)

        private readonly BlockingCollection<string> _queue;
        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
        private readonly Thread _writerThread;

        private TcpClient _tcp;
        private NetworkStream _stream;
        private readonly object _connLock = new object();

        private long _totalPublished;

        public RedisFlow(
            string host = "localhost",
            int port = 6379,
            string password = null,
            int database = 0,
            string channel = "eonacat:logs",
            string listKey = null,
            int maxListLength = 10000,
            LogLevel minimumLevel = LogLevel.Trace)
            : base("Redis:" + host + ":" + port, minimumLevel)
        {
            if (host == null)
            {
                throw new ArgumentNullException("host");
            }

            if (channel == null)
            {
                throw new ArgumentNullException("channel");
            }

            _host = host;
            _port = port;
            _password = password;
            _database = database;
            _channel = channel;
            _listKey = listKey;
            _maxListLength = maxListLength;

            _queue = new BlockingCollection<string>(new ConcurrentQueue<string>(), 16384);

            _writerThread = new Thread(WriterLoop)
            {
                IsBackground = true,
                Name = "RedisFlow.Writer",
                Priority = ThreadPriority.AboveNormal
            };
            _writerThread.Start();
        }

        public long TotalPublished { get { return Interlocked.Read(ref _totalPublished); } }

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

            string json = Serialize(logEvent);
            if (_queue.TryAdd(json))
            {
                Interlocked.Increment(ref BlastedCount);
                return Task.FromResult(WriteResult.Success);
            }

            Interlocked.Increment(ref DroppedCount);
            return Task.FromResult(WriteResult.Dropped);
        }

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

            WriteResult result = WriteResult.Success;
            foreach (LogEvent e in logEvents.ToArray())
            {
                if (e.Level < MinimumLevel)
                {
                    continue;
                }

                if (BlastAsync(e, cancellationToken).GetAwaiter().GetResult() == WriteResult.Dropped)
                {
                    result = WriteResult.Dropped;
                }
            }
            return Task.FromResult(result);
        }

        public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
            while (_queue.Count > 0 && sw.Elapsed < TimeSpan.FromSeconds(5))
            {
                Thread.Sleep(5);
            }

            return Task.FromResult(0);
        }

        public override async ValueTask DisposeAsync()
        {
            IsEnabled = false;
            _queue.CompleteAdding();
            _cts.Cancel();
            _writerThread.Join(TimeSpan.FromSeconds(5));
            Disconnect();
            _cts.Dispose();
            _queue.Dispose();
            await base.DisposeAsync().ConfigureAwait(false);
        }

        private void WriterLoop()
        {
            int backoff = 500;

            while (!_queue.IsCompleted)
            {
                try
                {
                    EnsureConnected();
                    backoff = 500; // reset on successful connect

                    while (!_queue.IsCompleted)
                    {
                        string msg;
                        try { msg = _queue.Take(_cts.Token); }
                        catch (OperationCanceledException) { return; }
                        catch (InvalidOperationException) { return; }

                        SendToRedis(msg);

                        string extra;
                        int batch = 0;
                        while (batch < 64 && _queue.TryTake(out extra))
                        {
                            SendToRedis(extra);
                            batch++;
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.Error.WriteLine("[RedisFlow] Error: " + ex.Message + ". Reconnecting in " + backoff + "ms.");
                    Disconnect();
                    try { Thread.Sleep(backoff); } catch (ThreadInterruptedException) { return; }
                    backoff = Math.Min(backoff * 2, 30000);
                }
            }
        }

        private void SendToRedis(string json)
        {
            // PUBLISH channel message
            SendCommand("PUBLISH", _channel, json);

            // Optional list persistence
            if (!string.IsNullOrEmpty(_listKey))
            {
                SendCommand("LPUSH", _listKey, json);
                if (_maxListLength > 0)
                {
                    SendCommand("LTRIM", _listKey, "0", (_maxListLength - 1).ToString());
                }
            }

            Interlocked.Increment(ref _totalPublished);
        }

        private void EnsureConnected()
        {
            lock (_connLock)
            {
                if (_tcp != null && _tcp.Connected)
                {
                    return;
                }

                Disconnect();
                _tcp = new TcpClient();
                _tcp.Connect(_host, _port);
                _stream = _tcp.GetStream();

                if (!string.IsNullOrEmpty(_password))
                {
                    SendCommandRaw("AUTH", _password);
                    ReadResp(); // +OK
                }

                if (_database != 0)
                {
                    SendCommandRaw("SELECT", _database.ToString());
                    ReadResp(); // +OK
                }
            }
        }

        private void Disconnect()
        {
            lock (_connLock)
            {
                if (_stream != null) { try { _stream.Dispose(); } catch { } _stream = null; }
                if (_tcp != null) { try { _tcp.Dispose(); } catch { } _tcp = null; }
            }
        }

        // Build RESP array and write + read response (inline, synchronous)
        private void SendCommand(params string[] args)
        {
            SendCommandRaw(args);
            ReadResp(); // discard but catch errors
        }

        private void SendCommandRaw(params string[] args)
        {
            var sb = new StringBuilder();
            sb.Append('*').Append(args.Length).Append("\r\n");
            foreach (string arg in args)
            {
                byte[] bytes = Encoding.UTF8.GetBytes(arg);
                sb.Append('$').Append(bytes.Length).Append("\r\n");
                // Append raw bytes via stream after the header
                byte[] header = Encoding.ASCII.GetBytes(sb.ToString());
                sb.Clear();
                _stream.Write(header, 0, header.Length);
                _stream.Write(bytes, 0, bytes.Length);
                _stream.WriteByte(0x0D); // \r
                _stream.WriteByte(0x0A); // \n
            }
        }

        private void ReadResp()
        {
            // Read one RESP line we only need to consume the response,
            // error checking is minimal (connection drop will be caught upstream)
            int b = _stream.ReadByte();
            if (b == -1)
            {
                throw new Exception("Redis connection closed.");
            }

            if ((char)b == '-') // error line
            {
                StringBuilder err = new StringBuilder();
                int c;
                while ((c = _stream.ReadByte()) != -1 && (char)c != '\r')
                {
                    err.Append((char)c);
                }

                _stream.ReadByte(); // consume \n
                throw new Exception("Redis error: " + err);
            }
            // Consume remainder of the line
            while (true)
            {
                int c = _stream.ReadByte();
                if (c == -1 || (char)c == '\n')
                {
                    break;
                }
            }
        }

        private static string Serialize(LogEvent log)
        {
            var sb = new StringBuilder(256);
            sb.Append("{\"ts\":\"");
            sb.Append(LogEvent.GetDateTime(log.Timestamp).ToString("O"));
            sb.Append("\",\"level\":\"");
            sb.Append(LevelString(log.Level));
            sb.Append("\",\"host\":\"");
            JsonEscape(Environment.MachineName, sb);
            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.ToString(), 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();
            }
        }
    }
}