using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knoks.Core.Data.Dao;
using Knoks.Core.Data.Interfaces;
using Knoks.Core.Entities;
using Knoks.Core.Entities.Settings;
using Knoks.Core.Logic.Interfaces;
using Microsoft.Extensions.Logging;
using Nethereum.Contracts;
using Nethereum.Util;
using Nethereum.Web3;
using AccountTransactionType = Knoks.Core.Entities.Args.AccountTransactions.AccountTransactionType;

namespace Knoks.TokenContractTracking
{
    #region InnerClass

    internal class TransferInfo
    {
        public string From;
        public User FromUser;
        public Account FromAccount;
        public string To;
        public User ToUser;
        public Account ToAccount;
        public decimal AmountKnoks;
        public string TransactionHash;
        public DateTime EventDate;
        public ulong BlockNumber;
    }

    #endregion

    public class TokenContractTracker : IDisposable
    {
        private readonly ILogger<TokenContractTracker> _logger;
        private readonly TokenContractTrackerSettings _trackerSettings;
        private readonly EthereumConnectionSettings _ethSettings;

        private readonly string _trackerName = nameof(TokenContractTracker);

        CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

        public bool IsInitialized { get; private set; }

        private Task _worker;

        private TokenContractService _tokenContractService;

        private ulong _lastCheckedBlockNumber = 0;

        private IAccountTransactionManager _accountTransactionManager;
        private IUserManager _userManager;

        private readonly IAccountDao _accountDao;

        public TokenContractTracker(TokenContractTrackerSettings trackerSettings, EthereumConnectionSettings ethSettings, ILogger<TokenContractTracker> logger, IAccountTransactionManager accountTransactionManager, IUserManager userManager, IAccountDao accountDao)
        {
            _accountTransactionManager = accountTransactionManager ?? throw new ArgumentNullException(nameof(accountTransactionManager));
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            _accountDao = accountDao ?? throw new ArgumentNullException(nameof(accountDao));
            _trackerSettings = trackerSettings ?? throw new ArgumentNullException(nameof(trackerSettings));
            _ethSettings = ethSettings ?? throw new ArgumentNullException(nameof(ethSettings));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        private void LogInfo(string info)
        {
            _logger.LogInformation($"{DateTime.UtcNow:G} {info}");
        }

        public async Task Init()
        {
            if (IsInitialized)
                throw new InvalidOperationException("Already initialized");

            LogInfo($"Initializing service {_trackerName}..");

            //load tickers dictionary
            var transaction = await _accountTransactionManager.GetLatestDepositEvent();

            if (transaction != null)
            {
                if(!ulong.TryParse(transaction.Comment, out _lastCheckedBlockNumber))
                    throw new InvalidOperationException("Wrong Deposit transaction in database: Comment does not contan block number. AccountTransactionId = " + transaction.AccountTransactionId);
            }

            _tokenContractService = new TokenContractService(new Web3(_ethSettings.Web3TargetAddress), _ethSettings.KnokTokenContractAddress, _ethSettings.KnokTokenContractAbi);

            IsInitialized = true;
        }

        private void DebugInfo(string info, bool timestamp = false)
        {
            if (timestamp)
                _logger.LogDebug($"{DateTime.UtcNow:G} {info}");
            else
                _logger.LogDebug($"{info}");
        }

        public void Start()
        {
            //if (!IsInitialized)
            //    throw new InvalidOperationException($"Service is not initialized");

            if (_worker != null)
                throw new InvalidOperationException($"Service is already started ({_worker.Status})");

            LogInfo($"Starting {_trackerName}..");

            _worker = Task.Run(() => CheckContractChanges_DoWork(_cancellationTokenSource.Token), _cancellationTokenSource.Token);

            LogInfo($"{_trackerName} started.");
        }

        public void Stop()
        {
            if (_worker == null)
                throw new InvalidOperationException("Workers are not started");

            LogInfo($"Signalling {_trackerName} to stop..");
            _cancellationTokenSource.Cancel();
            LogInfo($"{_trackerName} signalled to stop.");
        }

        private async Task CheckContractChanges_DoWork(CancellationToken ct)
        {
            if (ct.IsCancellationRequested)
            {
                LogInfo("Task was cancelled before it got started.");
                ct.ThrowIfCancellationRequested();
            }

            DateTime cycleBeginTime;
            do
            {
                cycleBeginTime = DateTime.UtcNow;
                List<EventLog<TransferEventDto>> resultEvents = await _tokenContractService.GetTransferEvents(_lastCheckedBlockNumber + 1);

                List<TransferInfo> infos = new List<TransferInfo>();

                foreach (var resultEvent in resultEvents)
                {
                    infos.Add(new TransferInfo()
                    {
                        From = resultEvent.Event.From,
                        To = resultEvent.Event.To,
                        AmountKnoks = UnitConversion.Convert.FromWei(resultEvent.Event.Value),
                        EventDate = DateTime.UtcNow,
                        TransactionHash = resultEvent.Log.TransactionHash,
                        BlockNumber = (ulong)resultEvent.Log.BlockNumber.Value
                    });
                }

                //TODO: change to batch processing inside single stored procedure (PASS datatable to it)
                foreach (var depositInfo in infos)
                {
                    var userFromInfo = await _userManager.GetUserInfo(depositInfo.From);
                    var userToInfo = await _userManager.GetUserInfo(depositInfo.To);

                    if (userFromInfo.User == null || userToInfo.User == null)
                    {
                        //TODO: support single account transaction  (use SetAccountBalance method)
                        LogInfo($"Balance TRANSFER processing FAILURE: at least one of wallet addresses are not exist. From: {depositInfo.From}; To: {depositInfo.To}");
                    }
                    else
                    {
                        await _accountTransactionManager.Transfer(userFromInfo.User, userFromInfo.Accounts.First(),
                            userToInfo.User, userToInfo.Accounts.First(), depositInfo.AmountKnoks, null,
                            depositInfo.TransactionHash, depositInfo.BlockNumber);
                    }

                    if (_lastCheckedBlockNumber < depositInfo.BlockNumber)
                        _lastCheckedBlockNumber = depositInfo.BlockNumber;
                }

            } while (!ct.WaitHandle.WaitOne(GetRemainRefreshDataIntervalMs(cycleBeginTime, _trackerSettings.RefreshDataIntervalMs)));

            LogInfo("Task_DoWork exited.");
        }

        private int GetRemainRefreshDataIntervalMs(DateTime cycleBeginTime, int intervalMs)
        {
            var cycleMS = (int)(DateTime.UtcNow - cycleBeginTime).TotalMilliseconds;
            if (cycleMS < intervalMs)
                return intervalMs - cycleMS;

            return 0;
        }

        public async Task WaitForCompletion()
        {
            LogInfo("Waiting for service to stop..");
            try
            {
                await Task.WhenAll(_worker);
                LogInfo($"{_trackerName} stopped. Really.");
            }
            catch (AggregateException e)
            {
                LogInfo("\nStopping failed: AggregateException thrown with the following inner exceptions:");
                // Display information about each exception. 
                foreach (var v in e.InnerExceptions)
                {
                    if (v is TaskCanceledException)
                        LogInfo("   TaskCanceledException: Task " + ((TaskCanceledException)v).Task.Id);
                    else
                        LogInfo("   Exception: " + v.GetType().Name);
                }
            }
        }

        public void Dispose()
        {
            _cancellationTokenSource?.Dispose();
            _worker?.Dispose();
        }
    }
}
