using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using EonaCat.Json;
using EonaCat.Logger.Extensions;
using EonaCat.Logger.Servers.GrayLog;
using EonaCat.Logger.Servers.Splunk.Models;
using Microsoft.Extensions.Logging;

// 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.

namespace EonaCat.Logger.Managers;

public class ErrorMessage
{
    public Exception Exception { get; set; }
    public string Message { get; set; }
}

public static class LogHelper
{
    internal static event EventHandler<ErrorMessage> OnException;

    internal static event EventHandler<ErrorMessage> OnLogLevelDisabled;

    private static readonly string MachineName = Environment.MachineName;
    private static readonly string HostName = Dns.GetHostName();

    internal static string ConvertToSyslogRfc5424(LoggerSettings settings, ELogType logType, string currentMessage, DateTime dateTime, string category = null)
    {
        string formattedMessage = FormatMessageWithHeader(settings, logType, currentMessage, dateTime, category);

        if (string.IsNullOrWhiteSpace(formattedMessage))
        {
            return formattedMessage;
        }

        string hostname = HostName;

        int facility = (int)SyslogFacility.Local0;
        int severity = (int)logType;
        int pri = (facility * 8) + severity;
        string timestamp = dateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
        var syslogMessage = $"<{pri}>1 {timestamp} {hostname} - - {formattedMessage}";

        if (settings?.Id != null)
        {
            syslogMessage = $"<{pri}>1 {timestamp} {hostname} {settings?.Id} - - {formattedMessage}";
        }
        syslogMessage = $"<{pri}>1 {timestamp} {hostname} - - {formattedMessage}";
        return syslogMessage;
    }

    internal static string ConvertToSyslogRfc3164(LoggerSettings settings, ELogType logType, string currentMessage, DateTime dateTime, string category = null)
    {
        string formattedMessage = FormatMessageWithHeader(settings, logType, currentMessage, dateTime, category);

        if (string.IsNullOrWhiteSpace(formattedMessage))
        {
            return formattedMessage;
        }

        string hostname = HostName;
        int facility = (int)SyslogFacility.Local0;
        int severity = (int)logType;
        int pri = (facility * 8) + severity;
        string timestamp = dateTime.ToString("MMM dd HH:mm:ss");
        string syslogMessage = $"<{pri}>{timestamp} {hostname} {settings?.Id ?? DllInfo.ApplicationName}: {formattedMessage}";
        return syslogMessage;
    }


    internal static string FormatMessageWithHeader(LoggerSettings settings, ELogType logType, string currentMessage,
        DateTime dateTime, string category = null)
    {
        if (string.IsNullOrWhiteSpace(currentMessage))
        {
            return currentMessage;
        }

        category ??= "General";
        var sb = new StringBuilder(settings?.HeaderFormat);
        var timestamp = dateTime.ToString(settings?.TimestampFormat ?? "yyyy-MM-dd HH:mm:ss");
        var timeLabel = settings?.UseLocalTime ?? false ? "[LOCAL]" : "[UTC]";

        sb.Replace("{ts}", $"{timestamp} {timeLabel}")
            .Replace("{host}", $"[Host:{HostName}]")
            .Replace("{category}", $"[Category:{category}]")
            .Replace("{thread}", $"[ThreadId:{Environment.CurrentManagedThreadId}]")
            .Replace("{sev}", $"[{logType}]");

        if (sb.Length > 0)
        {
            sb.Append(" ");
        }
        sb.Append(currentMessage);
        return sb.ToString();
    }

    internal static void SendToConsole(LoggerSettings settings, ELogType logType, string message)
    {
        if (settings == null || string.IsNullOrWhiteSpace(message))
        {
            return;
        }

        if (settings.EnableColors && settings.Colors != null)
        {
            var prevForeground = Console.ForegroundColor;
            var prevBackground = Console.BackgroundColor;

            if (TryGetLogColor(settings.Colors, logType, out var foregroundColor, out var backgroundColor))
            {
                Console.ForegroundColor = foregroundColor;
                Console.BackgroundColor = backgroundColor;
            }

            Console.WriteLine(message);
            Console.ForegroundColor = prevForeground;
            Console.BackgroundColor = prevBackground;
        }
        else
        {
            Console.WriteLine(message);
        }
    }

    private static bool TryGetLogColor(ColorSchema colors, ELogType logType, out ConsoleColor foreground, out ConsoleColor background)
    {
        foreground = default;
        background = default;

        return logType switch
        {
            ELogType.DEBUG => TrySetColors(colors.Debug, out foreground, out background),
            ELogType.INFO => TrySetColors(colors.Info, out foreground, out background),
            ELogType.WARNING => TrySetColors(colors.Warning, out foreground, out background),
            ELogType.ERROR => TrySetColors(colors.Error, out foreground, out background),
            ELogType.TRAFFIC => TrySetColors(colors.Traffic, out foreground, out background),
            ELogType.CRITICAL => TrySetColors(colors.Critical, out foreground, out background),
            ELogType.TRACE => TrySetColors(colors.Trace, out foreground, out background),
            _ => false,
        };
    }

    private static bool TrySetColors(ColorScheme color, out ConsoleColor foreground, out ConsoleColor background)
    {
        foreground = color.Foreground;
        background = color.Background;
        return true;
    }

    internal static void SendToFile(ILogger logger, LoggerSettings settings, ELogType logType, string message)
    {
        if (logger == null || settings == null || !settings.EnableFileLogging || string.IsNullOrWhiteSpace(message))
        {
            return;
        }

        if (IsLogLevelEnabled(settings, logType))
        {
            Log(logger, logType.ToLogLevel(), message);
        }
    }

    private static bool IsLogLevelEnabled(LoggerSettings loggerSettings, ELogType logType)
    {
        if (logType == ELogType.NONE)
        {
            OnLogLevelDisabled?.Invoke(null, new ErrorMessage { Message = "Logging is disabled for 'NONE' log type." });
            return false;
        }

        var isEnabled = loggerSettings?.TypesToLog == null || loggerSettings.TypesToLog.Count == 0 || loggerSettings.TypesToLog.Contains(logType);
        if (!isEnabled)
        {
            OnLogLevelDisabled?.Invoke(null, new ErrorMessage { Message = $"Logtype '{logType}' is not enabled." });
            return false;
        }

        return true;
    }


    public static void AddLogLevel(LoggerSettings settings, ELogType logType)
    {
        if (logType == ELogType.NONE)
        {
            return;
        }

        if (settings.TypesToLog == null || settings.TypesToLog.Count == 0)
        {
            settings.TypesToLog = new List<ELogType>();
        }

        if (!settings.TypesToLog.Contains(logType))
        {
            settings.TypesToLog.Add(logType);
        }
    }

    public static void RemoveLogLevel(LoggerSettings settings, ELogType logType)
    {
        if (logType == ELogType.NONE)
        {
            return;
        }

        if (settings.TypesToLog == null || settings.TypesToLog.Count == 0)
        {
            settings.TypesToLog = new List<ELogType>();
        }

        if (!settings.TypesToLog.Contains(logType))
        {
            return;
        }
        settings.TypesToLog.Remove(logType);
    }

    private static void Log(ILogger logger, LogLevel logLevel, string message)
    {
        logger.Log(logLevel, message);
    }

    private static bool TryGetEnumValue<TEnum>(string value, out TEnum result) where TEnum : struct, Enum
    {
        return Enum.TryParse(value, true, out result);
    }

    private static async Task SendToSplunkServersAsync(LoggerSettings settings, SplunkPayload splunkPayload, string logType, bool disableSplunkSSL = false)
    {
        if (settings == null || splunkPayload == null)
        {
            return;
        }

        if (settings.SplunkServers == null || settings.SplunkServers.Count() == 0)
        {
            return;
        }

        var tasks = settings.SplunkServers?
            .Where(splunkServer => splunkServer.HasHecUrl && splunkServer.HasHecToken)
            .Select(async splunkServer =>
            {
                try
                {
                    if (TryGetEnumValue(logType, out ELogType currentLogType))
                    {
                        if (currentLogType == ELogType.NONE)
                        {
                            splunkServer?.DisposeHttpClient();
                            return;
                        }

                        var isEnabled = (splunkServer?.TypesToLog == null || splunkServer?.TypesToLog.Count == 0 || splunkServer.TypesToLog.Contains(currentLogType));
                        if (!isEnabled)
                        {
                            splunkServer?.DisposeHttpClient();
                            return;
                        }
                    }

                    var response = await splunkServer?.SendAsync(splunkPayload, disableSplunkSSL);
                    if (!response.IsSuccessStatusCode)
                    {
                        LogError($"Failed to send log to Splunk '{splunkServer?.SplunkHecUrl}'. Status code: {response.StatusCode}");
                    }
                }
                catch (Exception ex)
                {
                    LogError($"Error logging to Splunk '{splunkServer?.SplunkHecUrl}': {ex.Message}", ex);
                }
            }) ?? new List<Task>();

        await Task.WhenAll(tasks);
    }

    /// <summary>
    /// Overload for sending a simple log message to Splunk.
    /// </summary>
    public static async Task SendToSplunkServersAsync(LoggerSettings settings, string logType, string message, bool disableSplunkSSL = false)
    {
        if (settings == null || string.IsNullOrWhiteSpace(message))
        {
            return;
        }

        if (settings.SplunkServers == null || settings.SplunkServers.Count() == 0)
        {
            return;
        }

        var splunkPayload = new SplunkPayload
        {
            Host = Environment.MachineName,
            EventData = message,
            SourceType = logType,
        };

        await SendToSplunkServersAsync(settings, splunkPayload, logType, disableSplunkSSL);
    }

    /// <summary>
    /// Logs an error using the OnException event.
    /// </summary>
    private static void LogError(string message, Exception ex = null)
    {
        OnException?.Invoke(null, new ErrorMessage { Message = message, Exception = ex });
    }


    internal static async Task SendToGrayLogServersAsync(LoggerSettings settings, string message, ELogType logType,
    string facility, string source, string version = "1.1")
    {
        if (settings == null || string.IsNullOrWhiteSpace(message))
        {
            return;
        }

        if (settings.GrayLogServers == null || settings.GrayLogServers.Count() == 0)
        {
            return;
        }

        const int MaxUdpPacketSize = 4096;

        var gelfMessage = new
        {
            version,
            host = MachineName,
            short_message = message,
            level = logType.ToGrayLogLevel(),
            facility,
            source,
            timestamp = DateTime.UtcNow.ToUnixTimestamp()
        };

        var messageBytes = Encoding.UTF8.GetBytes(JsonHelper.ToJson(gelfMessage));

        var tasks = settings.GrayLogServers?
            .Where(server => !string.IsNullOrWhiteSpace(server.Hostname) && server.Port >= 0)
            .Select(async grayLogServer =>
            {
                try
                {
                    if (logType == ELogType.NONE)
                    {
                        grayLogServer?.DisposeUdp();
                        return;
                    }

                    var isEnabled = (grayLogServer?.TypesToLog == null || grayLogServer.TypesToLog.Count == 0 || grayLogServer.TypesToLog.Contains(logType));
                    if (!isEnabled)
                    {
                        grayLogServer?.DisposeUdp();
                        return;
                    }

                    if (!grayLogServer.IsConnected)
                    {
                        grayLogServer?.SetUdp();
                    }

                    if (messageBytes.Length <= MaxUdpPacketSize)
                    {
                        // Send via UDP (single packet)
                        await grayLogServer?.Udp.SendAsync(messageBytes, messageBytes.Length);
                    }
                    else if (grayLogServer.SupportsTcp)
                    {
                        // Send via TCP if supported
                        await SendViaTcpAsync(grayLogServer, messageBytes);
                    }
                    else
                    {
                        // Chunk large messages for UDP
                        await SendUdpInChunksAsync(grayLogServer, messageBytes, MaxUdpPacketSize);
                    }
                }
                catch (Exception ex)
                {
                    OnException?.Invoke(null, new ErrorMessage
                    {
                        Exception = ex,
                        Message = $"Error logging to GrayLog Server '{grayLogServer.Hostname}': {ex.Message}"
                    });
                }
            }) ?? new List<Task>();

        await Task.WhenAll(tasks);
    }

    /// <summary>
    /// Sends a message via TCP to a GrayLog server.
    /// </summary>
    private static async Task SendViaTcpAsync(Graylog server, byte[] data)
    {
        if (server == null)
        {
            return;
        }
        using var tcpClient = new System.Net.Sockets.TcpClient();
        await tcpClient.ConnectAsync(server.Hostname, server.Port);
        using var stream = tcpClient.GetStream();
        await stream.WriteAsync(data, 0, data.Length);
        await stream.FlushAsync();
    }

    /// <summary>
    /// Sends large messages in chunks over UDP.
    /// </summary>
    private static async Task SendUdpInChunksAsync(Graylog server, byte[] data, int chunkSize)
    {
        if (server == null)
        {
            return;
        }
        for (int i = 0; i < data.Length; i += chunkSize)
        {
            var chunk = data.Skip(i).Take(chunkSize).ToArray();
            await server.Udp.SendAsync(chunk, chunk.Length);
        }
    }

    internal static async Task SendToSysLogServersAsync(LoggerSettings settings, string message, ELogType logType, DateTime dateTime, string category)
    {
        if (settings == null || string.IsNullOrWhiteSpace(message))
        {
            return;
        }

        if (settings.SysLogServers == null || settings.SysLogServers.Count() == 0)
        {
            return;
        }

        var tasks = settings.SysLogServers?
            .Where(server => !string.IsNullOrWhiteSpace(server.Hostname) && server.Port >= 0)
            .Select(async server =>
            {
                try
                {
                    if (logType == ELogType.NONE)
                    {
                        server?.DisposeUdp();
                        return;
                    }

                    var isEnabled = (server?.TypesToLog == null || server.TypesToLog.Count == 0 || server.TypesToLog.Contains(logType));
                    if (!isEnabled)
                    {
                        server?.DisposeUdp();
                        return;
                    }

                    if (server.ConvertToRfc5424)
                    {
                        message = ConvertToSyslogRfc5424(settings, logType, message, dateTime, category);
                    }
                    else if (server.ConvertToRfc3164)
                    {
                        message = ConvertToSyslogRfc3164(settings, logType, message, dateTime, category);
                    }

                    if (!server.IsConnected)
                    {
                        server.SetUdp();
                    }
                    await server.WriteAsync(message);
                }
                catch (Exception ex)
                {
                    OnException?.Invoke(null, new ErrorMessage
                    {
                        Exception = ex,
                        Message = $"Error logging to SysLog Server '{server.Hostname}': {ex.Message}"
                    });
                }
            }) ?? new List<Task>();

        await Task.WhenAll(tasks);
    }

    internal static async Task SendToTcpLogServersAsync(LoggerSettings settings, string message, ELogType logType)
    {
        if (settings == null || string.IsNullOrWhiteSpace(message))
        {
            return;
        }

        if (settings.TcpServers == null || settings.TcpServers.Count() == 0)
        {
            return;
        }

        var tasks = settings.TcpServers?
            .Where(server => !string.IsNullOrWhiteSpace(server.Hostname) && server.Port >= 0)
            .Select(async server =>
            {
                try
                {
                    if (logType == ELogType.NONE)
                    {
                        server?.DisposeTcp();
                        return;
                    }

                    var isEnabled = (server?.TypesToLog == null || server.TypesToLog.Count == 0 || server.TypesToLog.Contains(logType));
                    if (!isEnabled)
                    {
                        server?.DisposeTcp();
                        return;
                    }

                    if (!server.IsConnected)
                    {
                        server.SetTcp();
                    }

                    var data = Encoding.UTF8.GetBytes(message);
                    await server.WriteAsync(data).ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    OnException?.Invoke(null, new ErrorMessage
                    {
                        Exception = ex,
                        Message = $"Error logging to Tcp Server '{server.Hostname}': {ex.Message}"
                    });
                }
            }) ?? new List<Task>();

        await Task.WhenAll(tasks);
    }

    internal static async Task SendToUdpLogServersAsync(LoggerSettings settings, string message, ELogType logType)
    {
        if (settings == null || string.IsNullOrWhiteSpace(message))
        {
            return;
        }

        if (settings.UdpServers == null || settings.UdpServers.Count() == 0)
        {
            return;
        }

        var tasks = settings.UdpServers?
            .Where(server => !string.IsNullOrWhiteSpace(server.Hostname) && server.Port >= 0)
            .Select(async server =>
            {
                try
                {
                    if (logType == ELogType.NONE)
                    {
                        server?.DisposeUdp();
                        return;
                    }

                    var isEnabled = (server?.TypesToLog == null || server.TypesToLog.Count == 0 || server.TypesToLog.Contains(logType));
                    if (!isEnabled)
                    {
                        server?.DisposeUdp();
                        return;
                    }

                    if (!server.IsConnected)
                    {
                        server.SetUdp();
                    }

                    var data = Encoding.UTF8.GetBytes(message);
                    await server.WriteAsync(data).ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    OnException?.Invoke(null, new ErrorMessage
                    {
                        Exception = ex,
                        Message = $"Error logging to Udp Server '{server.Hostname}': {ex.Message}"
                    });
                }
            }) ?? new List<Task>();

        await Task.WhenAll(tasks);
    }
    internal static string GetStartupMessage()
    {
        return $"{DllInfo.ApplicationName} started.";
    }

    internal static string GetStopMessage()
    {
        return $"{DllInfo.ApplicationName} stopped.";
    }
}