﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using JetBrains.Annotations;
using LessGravity.Core.Logging;

namespace LessGravity.Core.Infrastructure
{
    [UsedImplicitly]
    public sealed class MessageBus : IMessageBus
    {
        private readonly ILogger _logger;
        private readonly ConcurrentQueue<Subscription> _subscriptionRequests = new ConcurrentQueue<Subscription>();
        private readonly ConcurrentQueue<Guid> _unsubscribeRequests = new ConcurrentQueue<Guid>();
        private readonly ActionBlock<SendMessageRequest> _messageProcessor;
        private readonly List<Subscription> _subscriptions;

        private async Task ProcessMessageRequest(SendMessageRequest request)
        {
            while (_unsubscribeRequests.TryDequeue(out var subscriptionId))
            {
                var subId = subscriptionId;
                _logger.Info($"Removing subscription '{subId}'.", Strings.Logging.Categories.MessageBus);
                _subscriptions.RemoveAll(s => s.Id == subId);
            }

            while (_subscriptionRequests.TryDequeue(out var newSubscription))
            {
                _logger.Info($"Adding subscription '{newSubscription.Id}'.", Strings.Logging.Categories.MessageBus);
                _subscriptions.Add(newSubscription);
            }

            var result = true;

            _logger.Info($"Processing message type '{request.Payload.GetType().FullName}'.", Strings.Logging.Categories.MessageBus);

            foreach (var subscription in _subscriptions)
            {
                if (request.CancellationToken.IsCancellationRequested)
                {
                    _logger.Warn("Cancellation request recieved. Processing stopped.", Strings.Logging.Categories.MessageBus);
                    result = false;
                    break;
                }

                try
                {
                    _logger.Info($"Executing subscription '{subscription.Id}' handler.", Strings.Logging.Categories.MessageBus);
                    await subscription.Handler.Invoke(request.Payload, request.CancellationToken);
                }
                catch (Exception ex)
                {
                    _logger.Error($"There was a problem executing subscription '{subscription.Id}' handler. Exception message: {ex.Message}", Strings.Logging.Categories.MessageBus);
                    result = false;
                }
            }

            request.OnSendComplete(result);

        }

        public MessageBus(ILogger logger)
        {
            _logger = logger;
            _subscriptions = new List<Subscription>();
            _messageProcessor = new ActionBlock<SendMessageRequest>(async request => await ProcessMessageRequest(request));
        }

        public Task SendAsync<TMessage>([NotNull] TMessage message)
        {
            return SendAsync(message, CancellationToken.None);
        }

        public Task SendAsync<TMessage>([NotNull] TMessage message, [NotNull] CancellationToken cancellationToken)
        {
            if (message == null)
            {
                throw new ArgumentNullException(nameof(message));
            }
            if (cancellationToken == null)
            {
                throw new ArgumentNullException(nameof(cancellationToken));
            }

            var tcs = new TaskCompletionSource<bool>();
            _messageProcessor.Post(new SendMessageRequest(message, cancellationToken, result => tcs.SetResult(result)));
            return tcs.Task;
        }

        public Guid Subscribe<TMessage>([NotNull] Action<TMessage> handler)
        {
            return Subscribe<TMessage>((message, cancellationToken) =>
            {
                handler.Invoke(message);
                return Task.FromResult(0);
            });
        }

        public Guid Subscribe<TMessage>(Func<TMessage, CancellationToken, Task> handler)
        {
            if (handler == null)
            {
                throw new ArgumentNullException(nameof(handler));
            }

            var subscription = Subscription.Create(handler);
            _subscriptionRequests.Enqueue(subscription);
            return subscription.Id;
        }

        public void Unsubscribe(Guid subscriptionId)
        {
            _unsubscribeRequests.Enqueue(subscriptionId);
        }
    }
}