using Knoks.Core.Entities;
using Knoks.Core.Logic.Interfaces;
using Knoks.Framework.Content;
using Knoks.Framework.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Knoks.Core.Logic.Managers
{
    public class EventManager : IEventManager, IInitializable, IDisposable
    {
        private class EventInfo
        {
            public object Source { get; set; }
            public EventAction EventAction { get; set; }
            public object[] EventData { get; set; }
            public DateTime Timestamp { get; set; }
            public JObject EventArgs { get; set; }

            public EventInfo(object source, EventAction eventAction, object[] eventData, DateTime timestamp)
            {
                Source = source;
                EventAction = eventAction;
                EventData = eventData;
                Timestamp = timestamp;
            }
        }

        private readonly Dictionary<Type, Action<object, JObject>> _ojectParsers = new Dictionary<Type, Action<object, JObject>>();
        private readonly Dictionary<EventAction, IList<Action<JObject>>> _eventDelegates = new Dictionary<EventAction, IList<Action<JObject>>>();
        private readonly BlockingCollection<EventInfo> _eventQueue = new BlockingCollection<EventInfo>();

        private Task _eventConsumingTask;

        private readonly ILogger<EventManager> _logger;

        private readonly IServiceProvider _serviceProvider;
        private T Get<T>() { return _serviceProvider.GetRequiredService<T>(); }

        public EventManager(ILogger<EventManager> logger, IServiceProvider serviceProvider)
        {
            _logger = logger;

            //Due to cross references (for example: UserManager) was decided to using DI on demand via IServiceProvider.
            //So, don't try to call IServiceProvider.GetService<T> in constructot,
            //It will lead you to "Process is terminated due to StackOverflowException."
            //Instead use T Get<T>()
            _serviceProvider = serviceProvider;
        }

        public Task Initialize()
        {
            return Task.Run(() =>
            {
                InitObjectParsers();
                InitEventDelegates();

                _eventConsumingTask = Task.Factory.StartNew(() =>
                {
                    foreach (var eventInfo in _eventQueue.GetConsumingEnumerable())
                    {
                        ProcessEvent(eventInfo);
                    }
                }, TaskCreationOptions.LongRunning);
            });
        }

        private void ProcessEvent(EventInfo eventInfo)
        {
            try
            {
                if (_eventDelegates.TryGetValue(eventInfo.EventAction, out IList<Action<JObject>> actions))
                {
                    if (actions.Count > 0)
                    {
                        eventInfo.EventArgs = JObject.FromObject(new
                        {
                            EventActionId = (int)eventInfo.EventAction,
                            EventActionName = eventInfo.EventAction.ToString(),
                            eventInfo.Timestamp
                        });

                        foreach (var obj in eventInfo.EventData)
                        {
                            //if (!_ojectParsers.Keys.Contains(obj.GetType()))
                            //{
                            //    _ojectParsers.Add(obj.GetType(), null);
                            //}
                            //var o = (obj, eventInfo.EventArgs);
                            _ojectParsers[obj.GetType()](obj, eventInfo.EventArgs);
                        }
                    }

                    foreach (var action in actions)
                    {
                        Task.Run(() =>
                        {
                            try
                            {
                                action(eventInfo.EventArgs);
                            }
                            catch (Exception ex)
                            {
                                _logger.LogError($"Process action - {eventInfo.EventAction} error.{Environment.NewLine}{ex}");
                            }
                        });
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"Process event - {eventInfo.EventAction} error.{Environment.NewLine}{ex}");
            }
        }

        public void Dispose()
        {
            try
            {
                if (_eventQueue != null)
                    _eventQueue.CompleteAdding();

                if (_eventConsumingTask != null)
                    _eventConsumingTask.Wait();
            }
            finally
            {
                if (_eventQueue != null)
                    _eventQueue.Dispose();
            }
        }

        public async Task FireЕvent(object source, EventAction eventAction, params object[] eventData)
        {
            await Task.Run(() =>
            {
                var timestamp = Get<ISystemConfigurationManager>().SystemDateTime;

                if (!FireЕventParams[eventAction].SequenceEqual(eventData.Select(i => i.GetType())))
                    throw new InvalidOperationException($"Unexpected parameters sequence or type(s). The expected format for '{eventAction}' is [{ string.Join(", ", FireЕventParams[eventAction].Select(i => i.FullName)) }]");

                _eventQueue.Add(new EventInfo(source, eventAction, eventData, timestamp));
            });
        }

        //-- Example for multiple methods per EventAction variation --
        //private static Dictionary<EventAction, Type[][]> FireЕventParams = new Dictionary<EventAction, Type[][]>
        //{
        //    [EventAction.LeadCreated] = new[]
        //    {
        //        new [] { typeof(LoanRequest), typeof(LoanRequest) },
        //        new [] { typeof(LoanRequest), typeof(LoanRequest) }
        //    }
        //};

        private static Dictionary<EventAction, Type[]> FireЕventParams = new Dictionary<EventAction, Type[]>
        {
            [EventAction.UserCreated] = new[] { typeof(User) },
            [EventAction.ResetPassword] = new[] { typeof(UserPasswordReset) }
        };

        private void ParseUser(long userId, JObject eventArgs)
        {
            var user = Get<IUserManager>().GetUsers(userId).Result.Single();
            _ojectParsers[user.GetType()](user, eventArgs);
        }

        private void ParseUser(string email, JObject eventArgs)
        {
            var user = Get<IUserManager>().GetUserByEmail(email).Result;
            _ojectParsers[user.GetType()](user, eventArgs);
        }

        private void InitObjectParsers()
        {
            _ojectParsers.Add(typeof(User), (obj, eventArgs) =>
            {
                var user = (User)obj;
                eventArgs["User"] = new JObject
                {
                    ["UserId"] = user.UserId,
                    ["Email"] = user.Email,
                    ["FirstName"] = user.FirstName,
                    ["LastName"] = user.LastName,
                    ["UserEmail"] = user.Email,
                    ["Phone"] = user.Phone,
                    ["CountryId"] = user.CountryId,
                    ["LanguageId"] = user.LanguageId,
                    ["LanguageCode"] = "en"
                };
            });

            _ojectParsers.Add(typeof(UserPasswordReset), (obj, eventArgs) =>
            {
                var userPasswordReset = (UserPasswordReset)obj;
                if (!string.IsNullOrEmpty(userPasswordReset.Email))
                {
                    ParseUser(userPasswordReset.Email, eventArgs);
                }
                eventArgs["UserPasswordReset"] = new JObject
                {
                    ["Email"] = userPasswordReset.Email,
                    ["Link"] = userPasswordReset.Link
                };
            });
        }

        private void InitEventDelegates()
        {
            //_eventDelegates.Add(EventAction.UserCreated, new List<Action<JObject>>
            //{
            //    (eventArgs) => //send email welcome
            //    {
            //        _logger.LogInformation($"############### Event type: {eventArgs["EventActionName"]} ##################");
            //        dynamic args = eventArgs;
            //        var contentResult = Get<IContentGenerator>().GenerateContent(new ContentSettings
            //        {
            //            Category = "Email",
            //            Group = "welcome",
            //            LanguageCode = args.User.LanguageCode,
            //            Data = eventArgs
            //        });
            //    }
            //});

            _eventDelegates.Add(EventAction.ResetPassword, new List<Action<JObject>>
            {
                (eventArgs) => //send password reset email
                {
                    _logger.LogInformation($"############### Event type: {eventArgs["EventActionName"]} ##################");
                    dynamic args = eventArgs;
                    var contentResult = Get<IContentGenerator>().GenerateContent(new ContentSettings
                    {
                        Category = "Email",
                        Group = "resetPassword",
                        LanguageCode = args.User.LanguageCode,
                        Data = eventArgs
                    });
                    Get<IEmailService>().SendHtml(
                        contentResult.Model["Subject"], 
                        contentResult.Content,
                        null,
                        eventArgs["UserPasswordReset"]["Email"].Value<string>());
                }
            });
        }
    }
}
