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

namespace EonaCat.Logger.Servers.Syslog;
// 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>
///     Syslog server.
/// </summary>
public class Syslog : IDisposable
{
    const int MaxUdpPacketSize = 4096;
    private string _Hostname = "127.0.0.1";
    private int _Port = 514;
    public bool IsConnected { get; private set; }

    internal UdpClient Udp;

    public Syslog() { }

    /// <summary>
    /// Syslog server
    /// </summary>
    /// <param name="hostname"></param>
    /// <param name="port"></param>
    /// <param name="nickName"></param>
    /// <param name="typesToLog"></param>
    /// <param name="convertToRfc5424"></param>
    /// <param name="convertToRfc3164"></param>
    public Syslog(string hostname = "127.0.0.1", int port = 514, string nickName = null, List<ELogType> typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false)
    {
        Hostname = hostname;
        Port = port;
        Nickname = nickName ?? IpPort;
        TypesToLog = typesToLog;
        ConvertToRfc5424 = convertToRfc5424;
        ConvertToRfc3164 = convertToRfc3164;
    }

    public string Hostname
    {
        get => _Hostname;
        set
        {
            if (string.IsNullOrEmpty(value))
                throw new ArgumentNullException(nameof(Hostname));

            _Hostname = value;
            SetUdp();
        }
    }

    public int Port
    {
        get => _Port;
        set
        {
            if (value < 0)
                throw new ArgumentException("Port must be zero or greater.");

            _Port = value;
            SetUdp();
        }
    }

    public string IpPort => _Hostname + ":" + _Port;
    public bool SupportsTcp { get; set; }
    public string Nickname { get; set; }
    public List<ELogType> TypesToLog { get; set; }
    public bool ConvertToRfc5424 { get; set; }
    public bool ConvertToRfc3164 { get; set; }

    internal void SetUdp()
    {
        try
        {
            DisposeUdp();
            Udp = new UdpClient(_Hostname, _Port);
            IsConnected = true;
        }
        catch
        {
            IsConnected = false;
        }
    }

    internal void DisposeUdp()
    {
        if (Udp != null)
        {
            IsConnected = false;

            try
            {
                Udp?.Close();
                Udp?.Dispose();
            }
            catch
            {
                // Do nothing
            }
            Udp = null;
        }
    }

    public void Dispose()
    {
        DisposeUdp();
        GC.SuppressFinalize(this);
    }

    public async Task WriteAsync(string message)
    {
        var data = Encoding.UTF8.GetBytes(message);
        await SendAsync(data);
    }

    public async Task WriteAsync(byte[] data)
    {
        await SendAsync(data);
    }

    private async Task SendAsync(byte[] data)
    {
        if (data == null)
        {
            return;
        }

        if (Udp == null)
        {
            return;
        }

        if (data.Length <= MaxUdpPacketSize)
        {
            // Send via UDP (single packet)
            await Udp?.SendAsync(data, data.Length);
        }
        else if (SupportsTcp)
        {
            // Send via TCP if supported
            await SendViaTcpAsync(this, data);
        }
        else
        {
            // Chunk large messages for UDP
            await SendUdpInChunksAsync(this, data, MaxUdpPacketSize);
        }
    }

    /// <summary>
    /// Sends a message via TCP to a syslog server.
    /// </summary>
    private static async Task SendViaTcpAsync(Servers.Syslog.Syslog 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(Servers.Syslog.Syslog server, byte[] data, int chunkSize)
    {
        for (int i = 0; i < data.Length; i += chunkSize)
        {
            var chunk = data.Skip(i).Take(chunkSize).ToArray();
            await server.Udp.SendAsync(chunk, chunk.Length);
        }
    }

    ~Syslog()
    {
        Dispose();
    }
}