using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System;
using System.Linq;
using System.Collections.Generic;

namespace EonaCat.LogStack.Server
{
    // 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>
    /// Minimum log level the server will persist. Entries below this level are silently dropped.
    /// </summary>
    public enum ServerLogLevel { Trace = 0, Debug = 1, Information = 2, Warning = 3, Error = 4, Critical = 5 }

    /// <summary>
    /// Live counters exposed via <see cref="Server.Metrics"/>.
    /// </summary>
    public class ServerMetrics
    {
        public long TotalReceived;
        public long TotalWritten;
        public long TotalDropped;
        public long TotalBytes;
        public long ActiveTcpConnections;
        public DateTime StartedAt = DateTime.UtcNow;
        public TimeSpan Uptime => DateTime.UtcNow - StartedAt;
    }

    /// <summary>
    /// Full configuration for the log server. All fields have sensible defaults.
    /// </summary>
    public class ServerOptions
    {
        /// <summary>Enable UDP listener (default: true).</summary>
        public bool UseUdp { get; set; } = true;

        /// <summary>
        /// Enable TCP listener (default: true).
        /// Both TCP and UDP can run simultaneously on the same port.
        /// </summary>
        public bool UseTcp { get; set; } = true;

        /// <summary>
        /// Enable a minimal HTTP ingest endpoint (default: false).
        /// POST /ingest - accepts a single JSON entry or a JSON array.
        /// GET  /metrics - returns a JSON metrics snapshot.
        /// </summary>
        public bool UseHttp { get; set; } = false;

        /// <summary>Port for UDP/TCP listeners (default: 5555).</summary>
        public int Port { get; set; } = 5555;

        /// <summary>Port for the HTTP ingest endpoint (default: 5556).</summary>
        public int HttpPort { get; set; } = 5556;

        /// <summary>IP address to bind to (null = IPAddress.Any).</summary>
        public IPAddress? BindAddress { get; set; }

        /// <summary>
        /// Only persist entries at or above this level (default: Trace = everything).
        /// Plain-text entries (non-JSON) always pass the level filter.
        /// </summary>
        public ServerLogLevel MinimumLevel { get; set; } = ServerLogLevel.Trace;

        /// <summary>Max log file size before rolling over (default: 200 MB).</summary>
        public long MaxLogFileSize { get; set; } = 200L * 1024 * 1024;

        /// <summary>Days to keep daily log directories (default: 30).</summary>
        public int LogRetentionDays { get; set; } = 30;

        /// <summary>Maximum total size of the logs directory before oldest dirs are purged (default: 10 GB).</summary>
        public long MaxLogDirectorySize { get; set; } = 10L * 1024 * 1024 * 1024;

        /// <summary>
        /// Max messages accepted per second per remote endpoint (0 = disabled).
        /// Excess messages are dropped and counted in <see cref="ServerMetrics.TotalDropped"/>.
        /// </summary>
        public int RateLimitPerSecond { get; set; } = 0;

        /// <summary>Root directory where log files are written (default: "logs").</summary>
        public string LogsRootDirectory { get; set; } = "logs";
    }

    public class Server
    {
        private TcpListener? _tcpListener;
        private UdpClient? _udpListener;
        private HttpListener? _httpListener;

        private CancellationTokenSource _cts = new();
        private bool _isRunning;
        private readonly ServerOptions _options;

        private readonly ConcurrentDictionary<string, (DateTime window, int count)> _rateLimiter = new();

        private const int UdpBufferSize = 65507;
        private const int TcpReadBufferChars = 8192;

        /// <summary>Live connection and throughput counters.</summary>
        public ServerMetrics Metrics { get; } = new();

        /// <summary>Raised after a log line has been written to disk.</summary>
        public event Action<string>? LogWritten;

        /// <summary>Raised when a log line is dropped (level filter or rate limit).</summary>
        public event Action<string>? LogDropped;

        /// <summary>Create a server using explicit <see cref="ServerOptions"/>.</summary>
        public Server(ServerOptions options)
        {
            _options = options;
        }

        /// <summary>
        /// Backwards-compatible constructor matching the original API.
        /// When <paramref name="useUdp"/> is true UDP is enabled; TCP is also enabled by default.
        /// </summary>
        public Server(bool useUdp = true, int logRetentionDays = 30, long maxLogDirectorySize = 10L * 1024 * 1024 * 1024)
        {
            _options = new ServerOptions
            {
                UseUdp = useUdp,
                UseTcp = true,
                LogRetentionDays = logRetentionDays,
                MaxLogDirectorySize = maxLogDirectorySize
            };
        }

        /// <summary>Start all configured transports and begin accepting log entries.</summary>
        public async Task Start(IPAddress? ipAddress = null, int port = 5555)
        {
            if (_isRunning)
            {
                Console.WriteLine("[EonaCat Server] Already running.");
                return;
            }

            _cts = new CancellationTokenSource();
            _isRunning = true;
            Metrics.StartedAt = DateTime.UtcNow;

            var bind = ipAddress ?? _options.BindAddress ?? IPAddress.Any;
            var tasks = new List<Task>();

            if (_options.UseTcp)
            {
                _tcpListener = new TcpListener(bind, port);
                _tcpListener.Start();
                Console.WriteLine($"[EonaCat Server] TCP listener on {bind}:{port}");
                tasks.Add(ListenTcpAsync(_cts.Token));
            }

            if (_options.UseUdp)
            {
                _udpListener = new UdpClient(port);
                Console.WriteLine($"[EonaCat Server] UDP listener on port {port}");
                tasks.Add(ListenUdpAsync(_cts.Token));
            }

            if (_options.UseHttp)
            {
                _httpListener = new HttpListener();
                _httpListener.Prefixes.Add($"http://*:{_options.HttpPort}/");
                _httpListener.Start();
                Console.WriteLine($"[EonaCat Server] HTTP ingest on port {_options.HttpPort}");
                tasks.Add(ListenHttpAsync(_cts.Token));
            }

            if (tasks.Count == 0)
            {
                Console.WriteLine("[EonaCat Server] WARNING: No transports are enabled. Check ServerOptions.");
                _isRunning = false;
                return;
            }

            await Task.WhenAll(tasks).ConfigureAwait(false);
        }

        /// <summary>Gracefully stop all transports.</summary>
        public void Stop()
        {
            if (!_isRunning)
            {
                return;
            }

            _cts.Cancel();
            _tcpListener?.Stop();
            _udpListener?.Close();
            _httpListener?.Stop();
            _cts.Dispose();
            _isRunning = false;

            Console.WriteLine(
                $"[EonaCat Server] Stopped. " +
                $"Received={Metrics.TotalReceived} Written={Metrics.TotalWritten} " +
                $"Dropped={Metrics.TotalDropped} Bytes={Metrics.TotalBytes} " +
                $"Uptime={Metrics.Uptime:hh\\:mm\\:ss}");
        }

        private async Task ListenTcpAsync(CancellationToken ct)
        {
            try
            {
                while (!ct.IsCancellationRequested)
                {
                    var client = await _tcpListener!.AcceptTcpClientAsync().ConfigureAwait(false);
                    Interlocked.Increment(ref Metrics.ActiveTcpConnections);
                    _ = Task.Run(() => HandleTcpClientAsync(client, ct), ct);
                }
            }
            catch (OperationCanceledException) 
            {
                // Do nothing
            }
            catch (Exception ex) { Console.WriteLine($"[TCP] Fatal: {ex.Message}"); }
            finally { Console.WriteLine("[EonaCat Server] TCP listener stopped."); }
        }

        private async Task HandleTcpClientAsync(TcpClient client, CancellationToken ct)
        {
            var remote = client.Client.RemoteEndPoint?.ToString() ?? "unknown";
            try
            {
                using (client)
                using (var stream = client.GetStream())
                using (var reader = new StreamReader(stream, Encoding.UTF8))
                {
                    var buffer = new char[TcpReadBufferChars];
                    var sb = new StringBuilder();
                    int bytesRead;

                    while ((bytesRead = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
                    {
                        sb.Append(buffer, 0, bytesRead);
                    }

                    var raw = sb.ToString();
                    Interlocked.Add(ref Metrics.TotalBytes, Encoding.UTF8.GetByteCount(raw));
                    await DispatchAsync(raw, remote).ConfigureAwait(false);
                }
            }
            catch (Exception ex) when (!ct.IsCancellationRequested)
            {
                Console.WriteLine($"[TCP] {remote}: {ex.Message}");
            }
            finally
            {
                Interlocked.Decrement(ref Metrics.ActiveTcpConnections);
            }
        }

        private async Task ListenUdpAsync(CancellationToken ct)
        {
            try
            {
                while (!ct.IsCancellationRequested)
                {
                    var result = await _udpListener!.ReceiveAsync().ConfigureAwait(false);
                    var remote = result.RemoteEndPoint.ToString();
                    Interlocked.Add(ref Metrics.TotalBytes, result.Buffer.Length);
                    var raw = Encoding.UTF8.GetString(result.Buffer);
                    await DispatchAsync(raw, remote).ConfigureAwait(false);
                }
            }
            catch (OperationCanceledException) 
            {
                // Do nothing
            }
            catch (Exception ex) { Console.WriteLine($"[UDP] Fatal: {ex.Message}"); }
            finally { Console.WriteLine("[EonaCat Server] UDP listener stopped."); }
        }

        private async Task ListenHttpAsync(CancellationToken ct)
        {
            try
            {
                while (!ct.IsCancellationRequested)
                {
                    var context = await _httpListener!.GetContextAsync().ConfigureAwait(false);
                    _ = Task.Run(() => HandleHttpContextAsync(context, ct), ct);
                }
            }
            catch (HttpListenerException) when (ct.IsCancellationRequested) { }
            catch (Exception ex) { Console.WriteLine($"[HTTP] Fatal: {ex.Message}"); }
            finally { Console.WriteLine("[EonaCat Server] HTTP listener stopped."); }
        }

        private async Task HandleHttpContextAsync(HttpListenerContext ctx, CancellationToken ct)
        {
            var req = ctx.Request;
            var res = ctx.Response;
            var remote = req.RemoteEndPoint?.ToString() ?? "http";
            var path = req.Url?.AbsolutePath.TrimEnd('/') ?? "";

            try
            {
                // GET /metrics
                if (req.HttpMethod == "GET" && path == "/metrics")
                {
                    var json = JsonSerializer.Serialize(new
                    {
                        totalReceived = Metrics.TotalReceived,
                        totalWritten = Metrics.TotalWritten,
                        totalDropped = Metrics.TotalDropped,
                        totalBytes = Metrics.TotalBytes,
                        activeTcpConnections = Metrics.ActiveTcpConnections,
                        uptimeSeconds = (long)Metrics.Uptime.TotalSeconds,
                        startedAt = Metrics.StartedAt.ToString("o")
                    });
                    await WriteJsonResponseAsync(res, json, 200, ct).ConfigureAwait(false);
                    return;
                }

                // POST /ingest
                if (req.HttpMethod == "POST" && path == "/ingest")
                {
                    using var reader = new StreamReader(req.InputStream, req.ContentEncoding);
                    var body = await reader.ReadToEndAsync().ConfigureAwait(false);
                    Interlocked.Add(ref Metrics.TotalBytes, Encoding.UTF8.GetByteCount(body));
                    await DispatchAsync(body, remote).ConfigureAwait(false);
                    await WriteJsonResponseAsync(res, "{\"accepted\":true}", 202, ct).ConfigureAwait(false);
                    return;
                }

                res.StatusCode = 404;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[HTTP] Handler error: {ex.Message}");
                try 
                {
                    res.StatusCode = 500; 
                }
                catch 
                {
                    // Do nothing
                }
            }
            finally
            {
                try { res.Close(); } catch { }
            }
        }

        private static async Task WriteJsonResponseAsync(HttpListenerResponse res, string json, int statusCode, CancellationToken ct)
        {
            var bytes = Encoding.UTF8.GetBytes(json);
            res.StatusCode = statusCode;
            res.ContentType = "application/json; charset=utf-8";
            res.ContentLength64 = bytes.Length;
            await res.OutputStream.WriteAsync(bytes, 0, bytes.Length, ct).ConfigureAwait(false);
        }


        private async Task DispatchAsync(string raw, string remote)
        {
            if (string.IsNullOrWhiteSpace(raw))
            {
                return;
            }

            Interlocked.Increment(ref Metrics.TotalReceived);

            // Rate limiting
            if (_options.RateLimitPerSecond > 0 && IsRateLimited(remote))
            {
                Interlocked.Increment(ref Metrics.TotalDropped);
                LogDropped?.Invoke(raw);
                return;
            }

            // Try structured JSON path first
            var entries = TryParseStructured(raw);
            if (entries.Count == 0)
            {
                // Plain text - always passes level filter
                await ProcessLogAsync(raw).ConfigureAwait(false);
                return;
            }

            foreach (var (line, level) in entries)
            {
                if (level < _options.MinimumLevel)
                {
                    Interlocked.Increment(ref Metrics.TotalDropped);
                    LogDropped?.Invoke(line);
                    continue;
                }
                await ProcessLogAsync(line).ConfigureAwait(false);
            }
        }

        private List<(string line, ServerLogLevel level)> TryParseStructured(string raw)
        {
            var trimmed = raw.Trim();
            if (trimmed.Length == 0 || (trimmed[0] != '{' && trimmed[0] != '['))
            {
                return new List<(string, ServerLogLevel)>();
            }

            var result = new List<(string, ServerLogLevel)>();
            try
            {
                using var doc = JsonDocument.Parse(trimmed);
                if (doc.RootElement.ValueKind == JsonValueKind.Array)
                {
                    foreach (var el in doc.RootElement.EnumerateArray())
                    {
                        result.Add(ParseJsonEntry(el));
                    }
                }
                else
                {
                    result.Add(ParseJsonEntry(doc.RootElement));
                }
            }
            catch 
            {
                // if parsing fails, treat as plain text
            }
            return result;
        }

        private static (string line, ServerLogLevel level) ParseJsonEntry(JsonElement jsonElement)
        {
            var level = ExtractLevel(jsonElement);
            var timestamp = jsonElement.TryGetProperty("timestamp", out var tsProp) ? tsProp.GetString() ?? "" : DateTime.UtcNow.ToString("o");
            var message = jsonElement.TryGetProperty("message", out var mProp) ? mProp.GetString() ?? ""
                    : jsonElement.TryGetProperty("Message", out var mProp2) ? mProp2.GetString() ?? "" : jsonElement.GetRawText();
            var source = jsonElement.TryGetProperty("source", out var sProp) ? sProp.GetString() ?? ""
                    : jsonElement.TryGetProperty("application", out var aProp) ? aProp.GetString() ?? "" : "";
            var host = jsonElement.TryGetProperty("host", out var hProp) ? hProp.GetString() ?? "" : "";
            var exception = jsonElement.TryGetProperty("exception", out var eProp) ? eProp.GetString() : null;
            var traceId = jsonElement.TryGetProperty("traceId", out var trProp) ? trProp.GetString() : null;

            var stringBuilder = new StringBuilder();
            stringBuilder.Append($"[{timestamp}] [{level.ToString().ToUpper()}]");
            if (!string.IsNullOrEmpty(source))
            {
                stringBuilder.Append($" [{source}]");
            }

            if (!string.IsNullOrEmpty(host))
            {
                stringBuilder.Append($" host={host}");
            }

            if (!string.IsNullOrEmpty(traceId))
            {
                stringBuilder.Append($" trace={traceId}");
            }

            stringBuilder.Append($" {message}");
            if (!string.IsNullOrEmpty(exception))
            {
                stringBuilder.Append($"\n  EXCEPTION: {exception}");
            }

            return (stringBuilder.ToString(), level);
        }

        private static ServerLogLevel ExtractLevel(JsonElement element)
        {
            string? level = null;
            foreach (var key in new[] { "level", "Level", "severity", "Severity" })
            {
                if (element.TryGetProperty(key, out var p)) 
                {
                    level = p.GetString(); 
                    break; 
                }
            }

            return level?.ToLowerInvariant() switch
            {
                "trace" or "verbose" => ServerLogLevel.Trace,
                "debug" => ServerLogLevel.Debug,
                "info" or "information" => ServerLogLevel.Information,
                "warn" or "warning" => ServerLogLevel.Warning,
                "error" => ServerLogLevel.Error,
                "fatal" or "critical" => ServerLogLevel.Critical,
                _ => ServerLogLevel.Information
            };
        }


        private bool IsRateLimited(string remote)
        {
            var now = DateTime.UtcNow;
            var entry = _rateLimiter.GetOrAdd(remote, _ => (now, 0));

            if ((now - entry.window).TotalSeconds >= 1.0)
            {
                _rateLimiter[remote] = (now, 1);
                return false;
            }

            if (entry.count >= _options.RateLimitPerSecond)
            {
                return true;
            }

            _rateLimiter[remote] = (entry.window, entry.count + 1);
            return false;
        }

        protected virtual async Task ProcessLogAsync(string logData)
        {
            var root = _options.LogsRootDirectory;
            Directory.CreateDirectory(root);

            var daily = Path.Combine(root, DateTime.Now.ToString("yyyyMMdd"));
            Directory.CreateDirectory(daily);

            var basePath = Path.Combine(daily, "EonaCatLogs");
            var filePath = basePath + ".log";

            int idx = 1;
            while (File.Exists(filePath) && new FileInfo(filePath).Length > _options.MaxLogFileSize)
            {
                filePath = $"{basePath}_{idx++}.log";
            }

            File.AppendAllText(filePath, logData + Environment.NewLine);
            Interlocked.Increment(ref Metrics.TotalWritten);
            LogWritten?.Invoke(logData);

            CleanUpOldLogs();
        }

        private void CleanUpOldLogs()
        {
            var root = _options.LogsRootDirectory;
            if (!Directory.Exists(root))
            {
                return;
            }

            foreach (var dir in Directory.GetDirectories(root))
            {
                try
                {
                    if (new DirectoryInfo(dir).CreationTime < DateTime.Now.AddDays(-_options.LogRetentionDays))
                    {
                        Directory.Delete(dir, true);
                        Console.WriteLine($"[Retention] Deleted expired directory: {dir}");
                    }
                }
                catch (Exception ex) { Console.WriteLine($"[Retention] Error: {ex.Message}"); }
            }

            long total = GetDirectorySize(root);
            if (total <= _options.MaxLogDirectorySize)
            {
                return;
            }

            Console.WriteLine($"[Retention] Directory {total / (1024 * 1024)} MB over limit. Purging oldest...");
            foreach (var dir in Directory.GetDirectories(root).OrderBy(d => new DirectoryInfo(d).CreationTime))
            {
                try
                {
                    total -= GetDirectorySize(dir);
                    Directory.Delete(dir, true);
                    Console.WriteLine($"[Retention] Purged: {dir}");
                    if (total <= _options.MaxLogDirectorySize)
                    {
                        break;
                    }
                }
                catch (Exception ex) { Console.WriteLine($"[Retention] Error purging {dir}: {ex.Message}"); }
            }
        }

        private static long GetDirectorySize(string directory)
        {
            long size = 0;
            try
            {
                size += Directory.GetFiles(directory).Sum(f => new FileInfo(f).Length);
                foreach (var sub in Directory.GetDirectories(directory))
                {
                    size += GetDirectorySize(sub);
                }
            }
            catch 
            {
                // ignored
            }
            return size;
        }
    }
}
