using System;
using System.Buffers;
using System.Collections.Generic;
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.

    public class Syslog : IDisposable
    {
        private const int MaxUdpPacketSize = 4096;
        private string _hostname = "127.0.0.1";
        private int _port = 514;
        private UdpClient _udp;
        private bool _disposed;

        public event EventHandler<Exception> OnException;

        public bool IsConnected { get; private set; }
        public string Nickname { get; set; }
        public string IpPort => $"{_hostname}:{_port}";
        public bool SupportsTcp { get; set; }
        public List<ELogType> TypesToLog { get; set; }
        public bool ConvertToRfc5424 { get; set; }
        public bool ConvertToRfc3164 { get; set; }

        public Syslog() { }

        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.IsNullOrWhiteSpace(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();
            }
        }

        internal void SetUdp()
        {
            if (_disposed)
            {
                return;
            }

            try
            {
                DisposeUdp();
                _udp = new UdpClient(_hostname, _port);
                IsConnected = true;
            }
            catch
            {
                IsConnected = false;
            }
        }

        internal void DisposeUdp()
        {
            if (_udp != null)
            {
                try
                {
                    IsConnected = false;
                    _udp.Close();
                    _udp.Dispose();
                }
                catch
                {
                    // Swallow cleanup exceptions
                }
                finally
                {
                    _udp = null;
                }
            }
        }

        public async Task WriteAsync(string message)
        {
            if (_disposed || string.IsNullOrWhiteSpace(message))
            {
                return;
            }

            var data = Encoding.UTF8.GetBytes(message);
            await SendAsync(data);
        }

        public async Task WriteAsync(byte[] data)
        {
            if (_disposed || data == null || data.Length == 0)
            {
                return;
            }

            await SendAsync(data);
        }

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

            if (data.Length <= MaxUdpPacketSize)
            {
                try
                {
                    await _udp.SendAsync(data, data.Length);
                }
                catch (Exception ex)
                {
                    OnException?.Invoke(this, ex);
                    IsConnected = false;
                }
            }
            else if (SupportsTcp)
            {
                await SendViaTcpAsync(data);
            }
            else
            {
                await SendUdpInChunksAsync(data, MaxUdpPacketSize);
            }
        }

        private async Task SendViaTcpAsync(byte[] data)
        {
            if (_disposed)
            {
                return;
            }

            try
            {
                using var tcpClient = new TcpClient();
                await tcpClient.ConnectAsync(_hostname, _port);

                var stream = tcpClient.GetStream();
                await stream.WriteAsync(data, 0, data.Length);
                await stream.FlushAsync();
            }
            catch
            {
                // TCP failure is silent
            }
        }

        private async Task SendUdpInChunksAsync(byte[] data, int chunkSize)
        {
            if (_disposed)
            {
                return;
            }

            int offset = 0;
            byte[] buffer = ArrayPool<byte>.Shared.Rent(chunkSize);

            try
            {
                while (offset < data.Length)
                {
                    int size = Math.Min(chunkSize, data.Length - offset);
                    Buffer.BlockCopy(data, offset, buffer, 0, size);
                    await _udp.SendAsync(buffer, size);
                    offset += size;
                }
            }
            catch
            {
                IsConnected = false;
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(buffer);
            }
        }

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

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }

            _disposed = true;

            if (disposing)
            {
                DisposeUdp();
            }
        }

        ~Syslog()
        {
            Dispose(false);
        }
    }
}
