using Knoks.Core.Entities;
using Knoks.Core.Entities.Args;
using Knoks.Core.Logic.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Linq;
using Knoks.Core.Data.Interfaces;
using Microsoft.AspNetCore.Http;
using System.IO;
using Knoks.Core.Data.Dao;

namespace Knoks.Core.Logic.Managers
{
    public class KnokManager : IKnokManager
    {
        #region Fields
        private readonly IKnokSettings settings;
        private readonly IKnokDao knokDao;
        private readonly IImageManager imageManager;
        private readonly ITagDao tagDao;
        private readonly Framework.Services.CommonSettings _commonSettings;
        private readonly IKnokPricelManager _priceManager;
        private readonly IGeneralManager _generalManager;
        #endregion

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="commonSettings"></param>
        /// <param name="settings"></param>
        /// <param name="knokDao"></param>
        /// <param name="tagDao"></param>
        /// <param name="imageManager"></param>
        /// <param name="generalManager"></param>
        public KnokManager(Framework.Services.CommonSettings commonSettings, IKnokSettings settings, IKnokDao knokDao, ITagDao tagDao, IImageManager imageManager, IKnokPricelManager priceManager, IGeneralManager generalManager)
        {
            this.settings = settings;
            this._priceManager = priceManager;
            this.knokDao = knokDao;
            this.tagDao = tagDao;
            this.imageManager = imageManager;
            this._commonSettings = commonSettings;
            this._generalManager = generalManager;
        }

        public IDictionary<string, string> Settings
        {
            get
            {
                var accessor = FastMember.TypeAccessor.Create(typeof(KnokSettings));
                return accessor.GetMembers().ToDictionary(it => it.Name, it => accessor[settings, it.Name]?.ToString());
            }
        }

        public async Task<Knok> Process(CreateKnokArgs knok, decimal currentPrice)
        {
            if (await Validate(knok))
            {
                var result = await knokDao.CreateKnok(knok, currentPrice);
                await UpdateTags(knok, result);
                await SaveImage(knok, result);
                InitializeImage(result.KnokId.Value, result?.Technical?.Image);
                return result;
            }
            return null;
        }

        public async Task<Knok> GetKnok(long knokId, long? userId)
        {
            var result = await knokDao.GetKnokDetails(knokId, (settings as IKnokFeedsSettings).KnokFeedExpirationHours);
            if (result.CreatorUserId != userId)
            {
                throw new ArgumentException("Knok does not exists.", nameof(userId));
            }
            await LoadTags(result);
            await knokDao.LoadImage(result.KnokId.Value, result);
            InitializeImage(knokId, result?.Technical?.Image);
            return result;
        }

        public async Task<KnokFullData> GetKnokFull(long knokId, long userId)
        {
            var result = await knokDao.GetKnokFullDetails(knokId, userId, (settings as IKnokFeedsSettings).KnokFeedExpirationHours, settings.DurationCRTAggrItems);
            if (result.Knoker.HasAvatar.HasValue)
            {
                result.Knoker.AvatarUrl = _generalManager.GetAvatarUrl(result.Knoker.UserId);
            }
            await LoadTags(result.Knok);
            await knokDao.LoadImage(result.Knok.KnokId.Value, result?.Knok);
            InitializeImage(knokId, result?.Knok.Technical?.Image);
            return result;
        }

        public KnokRate KnokRatingCalc(CreateKnokArgs create)
        {
            var knok = create;
            var result = new KnokRate();
            if (knok == null)
            {
                throw new ArgumentNullException("Please provide data");
            }

            result.Entry = (create.EntryPriceTo > 0 && create.EntryPriceFrom > 0) ?
                (1 - (create.EntryPriceTo - create.EntryPriceFrom) / (create.EntryPriceFrom * (settings.MAREntry / 100))) * 100 : 0;

            result.Exit = (create.ExitPriceTo > 0 && create.ExitPriceFrom > 0) ?
                (1 - (create.ExitPriceTo - create.ExitPriceFrom) / (create.ExitPriceFrom * (settings.MARExit / 100))) * 100 : 0;

            result.EntryToSL = (create.StopLoss > 0 && create.EntryPriceFrom > 0) ?
                (1 - (create.EntryPriceFrom - create.StopLoss) / (create.StopLoss * (settings.MaxSL / 100m))) * 100 : 0;

            result.Duration = create.Duration > 0 ?
                (1 - ((decimal)create.Duration / (decimal)settings.MAD)) * 100 : 0;

            var midEntry = (create.EntryPriceTo + create.EntryPriceFrom) / 2;
            var midExit = (create.ExitPriceTo + create.ExitPriceFrom) / 2;
            result.PotentialProfitValue = (midExit - midEntry) / midEntry * 100;
            if (result.PotentialProfitValue <= Constants.potentialProfitLow * 100)
            {
                result.PotentialProfit = PotentialProfit.Low;
            }
            else if (result.PotentialProfitValue <= Constants.potentialProfitMedium * 100)
            {
                result.PotentialProfit = PotentialProfit.Medium;
            }
            else if (result.PotentialProfitValue <= Constants.potentialProfitHigh * 100)
            {
                result.PotentialProfit = PotentialProfit.High;
            }
            else
            {
                result.PotentialProfit = PotentialProfit.VeryHigh;
            }

            //Calculation total depends on weights. 50% max
            result.Total = result.Entry * (Constants.entryWeight * 0.5m) +
                    result.Exit * (Constants.exitWeight * 0.5m) +
                    result.PotentialProfitValue * (Constants.entryToExitWeight * 0.5m) +
                    result.EntryToSL * (Constants.entryToStopWeight * 0.5m) +
                    result.Duration * (Constants.durationWeight * 0.5m);

            // Add additional 50%
            if (create.Technical != null && !string.IsNullOrEmpty( create.Technical.Header) && (create.Technical.Image != null && (create.Technical.Image.Content != null || !string.IsNullOrEmpty(create.Technical.Image.Url))))
            {
                result.Total += Constants.analysisInfoRate;
            }

            if (create.Fundamental != null && !string.IsNullOrEmpty(create.Fundamental.Header) && !string.IsNullOrEmpty(create.Fundamental.References))
            {
                result.Total += Constants.analysisInfoRate;
            }

            //TODO: That is not part of the knok rating calculating
            result.Price = _priceManager.TotalPrice(knok, create.UserId).GetAwaiter().GetResult();
            result.PriceUSD = result.Price;

            return result;
        }

        public Task<bool> Validate(CreateKnokArgs create)
        {
            var knok = create;
            if (knok == null)
            {
                throw new ArgumentNullException("Please provide data");
            }
            Dictionary<string, string> errors = new Dictionary<string, string>();
            if (knok.EntryPriceFrom >= knok.EntryPriceTo)
            {
                errors.Add(nameof(knok.EntryPriceTo), $"To value should be greater than From");
            }
            else
            {
                if (knok.EntryPriceFrom > 0 && (knok.EntryPriceTo - knok.EntryPriceFrom) / knok.EntryPriceFrom > settings.MAREntry * 0.01m)
                {
                    errors.Add(nameof(knok.EntryPriceTo), $"Maximum allowed range between Entry From and To is {settings.MAREntry} percent");
                }
            }
            if (knok.ExitPriceFrom >= knok.ExitPriceTo)
            {
                errors.Add(nameof(knok.ExitPriceTo), $"To value should be greater than From");
            }
            else
            {
                if (knok.ExitPriceFrom > 0 && (knok.ExitPriceTo - knok.ExitPriceFrom) / knok.ExitPriceFrom > settings.MARExit * 0.01m)
                {
                    errors.Add(nameof(knok.ExitPriceTo), $"Maximum allowed range between Exit From and To is {settings.MARExit} percent");
                }
            }
            if (knok.StopLoss >= knok.EntryPriceFrom)
            {
                errors.Add(nameof(knok.StopLoss), $"Stop Loss value should be less than Entry From");
            }
            if (errors.Any())
            {
                throw new ValidationException(errors.First().Value, errors);
            }

            if (knok.StopLoss > 0 && (knok.EntryPriceFrom - knok.StopLoss) / knok.StopLoss > settings.MaxSL * 0.01m)
            {
                errors.Add(nameof(knok.StopLoss), $"Maximum allowed range between Stop Loss and Entry Price From is {settings.MaxSL} percent");
            }
            if (knok.Duration > settings.MAD)
            {
                errors.Add(nameof(knok.Duration), $"Duration should be less than {settings.MAD} days");
            }
            if (knok.EntryPriceTo > 0)
            {
                var potentialProfitValue = (knok.ExitPriceFrom - knok.EntryPriceTo) / knok.EntryPriceTo;
                if (Math.Round(potentialProfitValue, 2) < settings.MinAP * 0.01m)
                {
                    errors.Add(nameof(knok.ExitPriceFrom), $"Minimum allowed profit is {settings.MinAP} percent");
                }
                if (Math.Round(potentialProfitValue, 2) > settings.MaxAP * 0.01m)
                {
                    errors.Add(nameof(knok.ExitPriceFrom), $"Maximum allowed profit is {settings.MaxAP} percent");
                }
                ////Low: up to 30 % potential profit.
                //if (knok.PotentialProfitValue < 0.30m)
                //{
                //    knok.PotentialProfit = PotentialProfit.Low;
                //}
                ////Medium: 30 - 100 % potential profit.
                //else if (knok.PotentialProfitValue < 1m)
                //{
                //    knok.PotentialProfit = PotentialProfit.Medium;
                //}
                ////High: More than 100 % potential profit.
                //else
                //{
                //    knok.PotentialProfit = PotentialProfit.High;
                //}
            }
            if (errors.Any())
            {
                throw new ValidationException(errors.First().Value, errors);
            }
            return Task.FromResult(true);
        }

        public string GetImageUrl(long knokId, string fileName)
        {
            string divider = fileName?.StartsWith("/") == true ? "" : "/";
            return $"/knok/{knokId}/files{divider}{fileName}";
        }

        public FileResponse GetFile(long knokId, string fileName)
        {
            var task = knokDao.GetKnokDetails(knokId, (settings as IKnokFeedsSettings).KnokFeedExpirationHours);
            task.Wait();
            var knok = task.Result;
            var stream = imageManager.LoadKnokImage(knok.CreatorUserId.Value, knokId, fileName, out var modified);
            return new FileResponse() { File = stream, FileDownloadName = fileName, LastModified = modified };
        }

        public async Task<IEnumerable<KnokFeed>> GetPublicKnoks(KnokFeedArgs args)
        {
            var knoks = await knokDao.GetPublicKnoks(args, (settings as IKnokFeedsSettings).KnokFeedExpirationHours);
            return knoks.Select(it =>
            {
                if (it.Knoker.HasAvatar)
                {
                    it.Knoker.Href = _generalManager.GetAvatarUrl(it.Knoker.UserId.Value);
                }
                return it;
            });
        }

        public async Task<IEnumerable<KnokOngoing>> GetAliveKnoks(KnokOngoingArgs args)
        {
            var knoks = await knokDao.GetAliveKnoks(args, settings.DurationCRTAggrItems);
            return knoks.Select(it =>
            {
                if (it.Knokser.HasAvatar)
                {
                    it.Knokser.Href = _generalManager.GetAvatarUrl(it.Knokser.UserId.Value);
                }
                return it;
            });
        }

        public async Task<IEnumerable<KnokEnded>> GetEndedKnoks(KnokEndedArgs args)
        {
            var knoks = await knokDao.GetEndedKnoks(args, settings.DurationCRTAggrItems);
            return knoks.Select(it =>
            {
                if (it.Knokser.HasAvatar)
                {
                    it.Knokser.Href = _generalManager.GetAvatarUrl(it.Knokser.UserId.Value);
                }
                return it;
            });
        }

        public async Task<IEnumerable<KnokOngoing>> GetCurrentlyPublishedKnoks(KnokCurrentlyPublishedArgs args, long userId)
        {
            var knoks = await knokDao.GetCurrentlyPublishedKnoks(args, userId, (settings as IKnokFeedsSettings).KnokFeedExpirationHours, settings.DurationCRTAggrItems);
            return knoks.Select(it =>
            {
                if (it.Knokser.HasAvatar)
                {
                    it.Knokser.Href = _generalManager.GetAvatarUrl(it.Knokser.UserId.Value);
                }
                return it;
            });
        }

        public async Task<IEnumerable<KnokEnded>> GetPastPublishedKnoks(KnokPastPublishedArgs args, long userId)
        {
            var knoks = await knokDao.GetPastPublishedKnoks(args, userId, settings.DurationCRTAggrItems);
            return knoks.Select(it =>
            {
                if (it.Knokser.HasAvatar)
                {
                    it.Knokser.Href = _generalManager.GetAvatarUrl(it.Knokser.UserId.Value);
                }
                return it;
            });
        }

        public async Task<KnokOwnData> GetOwnData(long userId)
        {
            var data = await knokDao.GetOwnData(userId);
            if (data.Knoker.HasAvatar)
            {
                data.Knoker.Href = _generalManager.GetAvatarUrl(data.Knoker.UserId.Value);
            }
            return data;
        }

        public Task<float> GetKnokPrice(long userId)
        {
            return knokDao.GetKnokPrice(userId);
        }

        public async Task<Knok> PublishKnok(long knokId, User user, Account userAccount, decimal publishPrice, CreateKnokArgs args) //  decimal publishPrice, decimal knokRating)
        {
            var knokRating = 0m;
            var knok = await knokDao.GetKnokDetails(knokId, (settings as IKnokFeedsSettings).KnokFeedExpirationHours);
            if (knok != null)
            {
                var knokargs = new CreateKnokArgs
                {
                    Currency1 = knok.Currency1,
                    Currency2 = knok.Currency2,
                    Duration = knok.Duration,
                    EntryPriceFrom = knok.EntryPriceFrom,
                    EntryPriceTo = knok.EntryPriceTo,
                    ExchangeId = knok.ExchangeId,
                    ExitPriceFrom = knok.ExitPriceFrom,
                    ExitPriceTo = knok.ExitPriceTo,
                    Fundamental = knok.Fundamental,
                    PotentialProfit = knok.PotentialProfit,
                    PotentialProfitValue = knok.PotentialProfitValue,
                    StopLoss = knok.StopLoss,
                    UserId = knok.CreatorUserId.Value
                };
                var rate = KnokRatingCalc(knokargs);
                knokRating = rate.Total;
            }
            else {
                knokRating = KnokRatingCalc(args).Total;
            }
            //var publishPrice = _priceManager.TotalPrice(args, user.UserId).GetAwaiter().GetResult();
            return await knokDao.PublishKnok(knokId, user.UserId, userAccount.AccountId, publishPrice, knokRating);
        }

        public Task PurchaseKnok(long knokId, User user, Account account)
        {
            return knokDao.PurchaseKnok(knokId, user.UserId, account.AccountId);
        }

        public async Task<KnokserProfile> GetKnokserProfile(long knokserId)
        {
            var knokserProfile = await knokDao.GetKnokserProfile(knokserId);
            knokserProfile.Href = knokserProfile.HasAvatar ? _generalManager.GetAvatarUrl(knokserProfile.UserId.Value) : null;
            return knokserProfile;
        }

        #region Private Methods
        private async Task SaveImage(CreateKnokArgs knok, Knok result)
        {
            var image = knok?.Technical?.Image;
            if (image != null)
            {
                Stream stream = image.ContentStream ?? (image.Content != null ? new MemoryStream(image.Content) : image.ContentBase64 != null ? Framework.Extentions.FileInfoExtentions.ParseImage(image.ContentBase64) : null);
                if (stream == null && !string.IsNullOrWhiteSpace(image.Url))
                {
                    try
                    {
                        image.FileName = image.FileName ?? new Uri(image.Url).AbsolutePath;
                    }
                    catch { }
                    stream = await new System.Net.Http.HttpClient().GetStreamAsync(image.Url);
                }
                if (stream != null)
                {
                    var fileName = await imageManager.SaveKnokImage(result.CreatorUserId.Value, result.KnokId.Value, image.FileName ?? "image.png", stream);
                    image.FileName = fileName;
                }
                if (!string.IsNullOrWhiteSpace(image.FileName) || !string.IsNullOrWhiteSpace(image.Url))
                {
                    result.Technical.Image = await knokDao.CreateImage(result.KnokId.Value, image);
                }
            }
        }

        private async Task UpdateTags(CreateKnokArgs knok, Knok result)
        {
            if (knok?.Technical?.Tags != null)
            {
                result.Technical.Tags = await tagDao.SaveKnokTags((int)result.KnokId, knok?.Technical?.Tags, 1);
            }
            if (knok?.Fundamental?.Tags != null)
            {
                result.Fundamental.Tags = await tagDao.SaveKnokTags((int)result.KnokId, knok?.Fundamental?.Tags, 2);
            }
        }

        private async Task LoadTags(Knok knok)
        {
            if (knok?.Technical != null)
            {
                knok.Technical.Tags = await tagDao.GetKnokTags((int)knok.KnokId, 1);
            }
            if (knok?.Fundamental != null)
            {
                knok.Fundamental.Tags = await tagDao.GetKnokTags((int)knok.KnokId, 2);
            }
        }

        private void InitializeImage(long knokId, KnokImage image)
        {
            if (image != null)
            {
                image.Href = image.FileName != null ? GetImageUrl(knokId, image.FileName) : image.Url;
            }
        }
        #endregion
    }
}
