using Insight.Agent.Messages;
using Insight.Agent.Network;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Snmp;
using System.Net;
using System.Net.Sockets;
using System.Threading.Channels;
using Vaitr.Network;

namespace Insight.Agent.Services
{
    public class TrapService : BackgroundService
    {
        private static IPEndPoint EndpointCache { get; } = new(IPAddress.Any, 0);
        private readonly Channel<Trap> _queue;


        private readonly int _port;
        private readonly TcpSessionPool<AgentSession, MemPackSerializer, IAgentMessage> _pool;
        private readonly ILogger<TrapService> _logger;

        public TrapService(TcpSessionPool<AgentSession, MemPackSerializer, IAgentMessage> pool, IConfiguration configuration, ILogger<TrapService> logger)
        {
            _port = configuration.GetValue<int?>(Appsettings.TrapPort) ?? throw new Exception($"{Appsettings.TrapPort} value not set (appsettings)");
            _pool = pool;
            _logger = logger;

            _queue = Channel.CreateBounded<Trap>(new BoundedChannelOptions(100)
            {
                SingleReader = false,
                SingleWriter = true,
                AllowSynchronousContinuations = false,
                FullMode = BoundedChannelFullMode.DropOldest
            });
        }

        protected override async Task ExecuteAsync(CancellationToken cancellationToken)
        {
            _logger.LogTrace("ExecuteAsync");

            while (cancellationToken.IsCancellationRequested is false)
            {
                try
                {
                    var tasks = new List<Task>
                    {
                        HandleQueueAsync(cancellationToken),
                        ListenAsync(cancellationToken)
                    };

                    await Task.WhenAll(tasks);
                }
                catch (OperationCanceledException) { }
                catch (Exception ex)
                {
                    _logger.LogError("{ex}", ex);
                }
            }
        }

        private async Task HandleQueueAsync(CancellationToken cancellationToken)
        {
            while (await _queue.Reader.WaitToReadAsync(cancellationToken))
            {
                var session = _pool.FirstOrDefault();

                if (session.Value is null)
                {
                    await Task.Delay(10000, cancellationToken);
                    continue;
                }

                if (_queue.Reader.TryRead(out var item) is false)
                {
                    await Task.Delay(1000, cancellationToken);
                    continue;
                }

                try
                {
                    await session.Value.SendAsync(item, cancellationToken);
                }
                catch (OperationCanceledException) { }
                catch (Exception ex)
                {
                    _logger.LogError("{ex}", ex);
                    await Task.Delay(10000, cancellationToken);
                }
            }
        }

        private async Task ListenAsync(CancellationToken cancellationToken)
        {
            using var udpSocket = new Socket(SocketType.Dgram, ProtocolType.Udp);

            cancellationToken.Register(udpSocket.Dispose);

            udpSocket.Bind(new IPEndPoint(IPAddress.Any, _port));

            var buffer = GC.AllocateArray<byte>(length: 65527, pinned: true);
            var bufferMem = buffer.AsMemory();

            while (cancellationToken.IsCancellationRequested is false)
            {
                try
                {
                    var result = await udpSocket.ReceiveFromAsync(bufferMem, SocketFlags.None, EndpointCache, cancellationToken);

                    if (result.ReceivedBytes == 0) continue;

                    var actualBytes = bufferMem[..result.ReceivedBytes].ToArray();

                    var ep = (IPEndPoint)result.RemoteEndPoint;

                    await ProcessAsync(ep, actualBytes, cancellationToken);
                }
                catch (SocketException) { continue; }
                catch (IOException) { continue; }
                catch (Exception) { continue; }
            }
        }

        private async ValueTask ProcessAsync(IPEndPoint endpoint, byte[] buffer, CancellationToken cancellationToken)
        {
            var trap = new Trap
            {
                Timestamp = DateTime.Now,
                Endpoint = endpoint.ToString(),
            };

            try
            {
                var protocol = (SnmpVersion)SnmpPacket.GetProtocolVersion(buffer, buffer.Length);

                trap.Version = protocol.ToString();
                trap.Hostname = Dns.GetHostEntry(endpoint.Address)?.HostName;

                if (protocol == SnmpVersion.Ver1)
                {
                    var packet = new SnmpV1TrapPacket();
                    packet.decode(buffer, buffer.Length);

                    trap.Community = packet?.Community?.ToString();

                    if (packet?.Pdu?.VbList is not null)
                    {
                        trap.Data = ConvertVbs(packet.Pdu.VbList);
                    }
                }

                if (protocol == SnmpVersion.Ver2)
                {
                    var packet = new SnmpV2Packet();
                    packet.decode(buffer, buffer.Length);

                    trap.Community = packet?.Community?.ToString();

                    if (packet?.Pdu?.VbList is not null)
                    {
                        trap.Data = ConvertVbs(packet.Pdu.VbList);
                    }
                }

                if (protocol == SnmpVersion.Ver3)
                {
                    var packet = new SnmpV3Packet();
                }
            }
            catch (Exception ex)
            {
                _logger.LogError("{ex}", ex);
            }

            await _queue.Writer.WriteAsync(trap, cancellationToken);
        }

        private List<KeyValuePair<string, string?>>? ConvertVbs(VbCollection vbs)
        {
            var data = new List<KeyValuePair<string, string?>>();

            try
            {
                foreach (var item in vbs)
                {
                    if (item.Oid is null) continue;

                    data.Add(new KeyValuePair<string, string?>(item.Oid.ToString(), item?.Value?.ToString()));
                }
            }
            catch (Exception ex)
            {
                _logger.LogError("{ex}", ex);
            }

            return data;
        }
    }
}