using Insight.Agent.Enums;
using Insight.Agent.Interfaces;
using Insight.Agent.Messages;
using Insight.Infrastructure;
using Insight.Infrastructure.Entities;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;

namespace Insight.Server.Network.Handlers.Agent
{
    public class AgentHandler : IAgentMessageHandler<AgentSession>
    {
        private readonly IMongoDatabase _database;
        private readonly ILogger<AgentHandler> _logger;

        public AgentHandler(IMongoDatabase database, ILogger<AgentHandler> logger)
        {
            _database = database;
            _logger = logger;
        }

        public async ValueTask HandleAsync<TMessage>(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage
        {
            if (message is AuthenticationRequest authenticationRequest)
            {
                await AuthenticationRequestAsync(sender, authenticationRequest, cancellationToken);
            }

            if (message is Authentication authentication)
            {
                await AuthenticationAsync(sender, authentication, cancellationToken);
            }
        }

        private async ValueTask AuthenticationRequestAsync(AgentSession session, AuthenticationRequest message, CancellationToken cancellationToken)
        {
            await session.SendAsync(message, cancellationToken);

            for (int i = 0; i < 200; i++)
            {
                if (session.Id is not null)
                {
                    _logger.LogInformation("Agent ({ep?}) authenticated", session.RemoteEndPoint);
                    return;
                }

                await Task.Delay(50, cancellationToken).ConfigureAwait(false);
            }

            _logger.LogError("Authentication Timeout ({ep?})", session.RemoteEndPoint);
            session.Disconnect();
        }

        private async ValueTask AuthenticationAsync(AgentSession session, Authentication authentication, CancellationToken cancellationToken)
        {
            if (authentication is null)
            {
                throw new NullReferenceException($"authentication failed (empty response)");
            }

            if (authentication.Serial == default)
            {
                throw new InvalidDataException($"authentication failed ({nameof(authentication.Serial)})");
            }

            //if (authentication.Version == default || authentication.Version < Domain.Constants.Configuration.Version)
            //{
            //    throw new InvalidDataException($"authentication failed ({nameof(authentication.Version)})");
            //}

            // upsert agent
            await _database.Agent().UpdateOneAsync(Builders<AgentEntity>
                .Filter
                .Eq(p => p.Serial, authentication.Serial.ToString()), Builders<AgentEntity>.Update
                .SetOnInsert(p => p.Insert, DateTime.Now)
                .SetOnInsert(p => p.Serial, authentication.Serial.ToString())
                .Set(p => p.Update, DateTime.Now)
                .Set(p => p.Connected, DateTime.Now)
                .Set(p => p.Version, authentication.Version)
                .Set(p => p.Endpoint, session.RemoteEndPoint?.ToString())
                .Set(p => p.Hostname, authentication.Hostname), new UpdateOptions
                {
                    IsUpsert = true
                }, cancellationToken)
                .ConfigureAwait(false);

            // get agent
            var agentEntity = await _database.Agent()
                .Find(Builders<AgentEntity>
                .Filter
                .Eq(p => p.Serial, authentication.Serial.ToString()))
                .FirstOrDefaultAsync(cancellationToken)
                .ConfigureAwait(false);

            // set session id
            session.Id = agentEntity.Id;
        }

        public async ValueTask ConnectedAsync(AgentSession session, CancellationToken cancellationToken)
        {
            if (session.Id is null) return;

            // insert connect log
            await _database.AgentLog()
                .InsertOneAsync(new AgentLogEntity
                {
                    Insert = DateTime.Now,
                    Agent = session.Id,
                    Category = CategoryEnum.Network.ToString(),
                    Status = StatusEnum.Information.ToString(),
                    Message = $"Connected ({session.RemoteEndPoint})",
                    Timestamp = DateTime.Now
                }, cancellationToken: cancellationToken)
                .ConfigureAwait(false);
        }

        public async ValueTask DisconnectedAsync(AgentSession session, CancellationToken cancellationToken)
        {
            if (session.Id is null) return;

            // insert disconnect log
            await _database.AgentLog()
                .InsertOneAsync(new AgentLogEntity
                {
                    Insert = DateTime.Now,
                    Agent = session.Id,
                    Category = CategoryEnum.Network.ToString(),
                    Status = StatusEnum.Information.ToString(),
                    Message = $"Disconnected ({session.RemoteEndPoint})",
                    Timestamp = DateTime.Now
                }, cancellationToken: cancellationToken)
                .ConfigureAwait(false);
        }

        public async ValueTask StatisticUpdateAsync(AgentSession session, CancellationToken cancellationToken)
        {
            if (session.Id is null) return;

            // update agents stats
            await _database.Agent().UpdateOneAsync(Builders<AgentEntity>
                .Filter
                .Eq(p => p.Id, session.Id), Builders<AgentEntity>
                .Update
                .Set(p => p.Update, DateTime.Now)
                .Set(p => p.Activity, session.Activity)
                .Set(p => p.SentBytes, session.TotalOutputBytes)
                .Set(p => p.ReceivedBytes, session.TotalInputBytes)
                .Set(p => p.SentPackets, session.TotalOutputPackets)
                .Set(p => p.ReceivedPackets, session.TotalInputPackets), null, cancellationToken)
            .ConfigureAwait(false);
        }

        public async ValueTask LogAsync(AgentSession session, AgentLogEntity log, CancellationToken cancellationToken)
        {
            if (session.Id is null) return;

            await _database.AgentLog()
                .InsertOneAsync(log, cancellationToken: cancellationToken)
                .ConfigureAwait(false);
        }
    }
}