using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Queries.Catalog;
using Grand.Business.Core.Utilities.Catalog;
using Grand.Domain;
using Grand.Domain.Catalog;
using Grand.Domain.Customers;
using Grand.Data;
using Grand.Domain.Directory;
using Grand.Domain.Discounts;
using Grand.Infrastructure.Caching;
using Grand.Infrastructure.Caching.Constants;
using Grand.Infrastructure.Configuration;
using Grand.Infrastructure.Extensions;
using MediatR;

namespace Grand.Business.Catalog.Services.Discounts
{
    /// <summary>
    /// Discount service
    /// </summary>
    public class DiscountService : IDiscountService
    {

        #region Fields

        private readonly IRepository<Discount> _discountRepository;
        private readonly IRepository<DiscountCoupon> _discountCouponRepository;
        private readonly IRepository<DiscountUsageHistory> _discountUsageHistoryRepository;
        private readonly ICacheBase _cacheBase;
        private readonly IMediator _mediator;
        private readonly AccessControlConfig _accessControlConfig;
        
        #endregion

        #region Ctor

        /// <summary>
        /// Ctor
        /// </summary>
        public DiscountService(ICacheBase cacheBase,
            IRepository<Discount> discountRepository,
            IRepository<DiscountCoupon> discountCouponRepository,
            IRepository<DiscountUsageHistory> discountUsageHistoryRepository,
            IMediator mediator, AccessControlConfig accessControlConfig)
        {
            _cacheBase = cacheBase;
            _discountRepository = discountRepository;
            _discountCouponRepository = discountCouponRepository;
            _discountUsageHistoryRepository = discountUsageHistoryRepository;
            _mediator = mediator;
            _accessControlConfig = accessControlConfig;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets a discount
        /// </summary>
        /// <param name="discountId">Discount identifier</param>
        /// <returns>Discount</returns>
        public virtual Task<Discount> GetDiscountById(string discountId)
        {
            var key = string.Format(CacheKey.DISCOUNTS_BY_ID_KEY, discountId);
            return _cacheBase.GetAsync(key, () => _discountRepository.GetByIdAsync(discountId));
        }

        /// <summary>
        /// Gets active discounts by context
        /// </summary>       
        /// <returns>Discounts</returns>
        public virtual async Task<IList<Discount>> GetActiveDiscountsByContext(DiscountType discountType, string storeId = "", string currencyCode = "")
        {
            var key = string.Format(CacheKey.DISCOUNTS_CONTEXT_KEY, discountType, storeId, currencyCode);
            var allDiscounts = await _cacheBase.GetAsync(key, () => GetDiscountsQuery(discountType, storeId, currencyCode));
            
            var nowUtc = DateTime.UtcNow;
            var filteredDiscounts = 
                allDiscounts.Where(d => d.IsEnabled && 
                                        (!d.StartDateUtc.HasValue || d.StartDateUtc <= nowUtc) && 
                                        (!d.EndDateUtc.HasValue || d.EndDateUtc >= nowUtc));
            
            return filteredDiscounts.ToList();
        }
        public virtual async Task<IList<Discount>> GetDiscountsQuery(DiscountType? discountType, string storeId = "", string currencyCode = "",
            string couponCode = "", string discountName = "")
        {
            var query = _discountRepository.Table.AsQueryable();

            if (discountType.HasValue)
            {
                query = query.Where(d => d.DiscountTypeId == discountType);
            }

            if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations)
            {
                query = query.Where(d => !d.LimitedToStores || d.Stores.Contains(storeId));
            }

            if (!string.IsNullOrEmpty(couponCode))
            {
                var couponDiscountId = _discountCouponRepository.Table
                    .Where(x => x.CouponCode == couponCode)
                    .Select(x => x.DiscountId)
                    .FirstOrDefault();
                if (couponDiscountId != default)
                    query = query.Where(d => d.Id == couponDiscountId);
                else
                    query = query.Where(d => false);
            }

            if (!string.IsNullOrEmpty(discountName))
            {
                query = query.Where(d => d.Name != null && d.Name.Contains(discountName, StringComparison.CurrentCultureIgnoreCase));
            }

            if (!string.IsNullOrEmpty(currencyCode))
            {
                query = query.Where(d => d.CurrencyCode == currencyCode);
            }

            query = query.OrderBy(d => d.Name);

            return await Task.FromResult(query.ToList());
        }
        
        /// <summary>
        /// Inserts a discount
        /// </summary>
        /// <param name="discount">Discount</param>
        public virtual async Task InsertDiscount(Discount discount)
        {
            ArgumentNullException.ThrowIfNull(discount);
            
            await _discountRepository.InsertAsync(discount);

            await _cacheBase.RemoveByPrefix(CacheKey.DISCOUNTS_PATTERN_KEY);

            //event notification
            await _mediator.EntityInserted(discount);
        }

        /// <summary>
        /// Updates the discount
        /// </summary>
        /// <param name="discount">Discount</param>
        public virtual async Task UpdateDiscount(Discount discount)
        {
            ArgumentNullException.ThrowIfNull(discount);

            await _discountRepository.UpdateAsync(discount);

            await _cacheBase.RemoveByPrefix(CacheKey.DISCOUNTS_PATTERN_KEY);

            //event notification
            await _mediator.EntityUpdated(discount);
        }

        /// <summary>
        /// Delete discount
        /// </summary>
        /// <param name="discount">Discount</param>
        public virtual async Task DeleteDiscount(Discount discount)
        {
            ArgumentNullException.ThrowIfNull(discount);

            await _discountRepository.DeleteAsync(discount);

            await _cacheBase.RemoveByPrefix(CacheKey.DISCOUNTS_PATTERN_KEY);

            //event notification
            await _mediator.EntityDeleted(discount);
        }


        /// <summary>
        /// Get discount by coupon code
        /// </summary>
        /// <param name="couponCode">Coupon code</param>
        /// <param name="showHidden">A value that indicates if it should shows hidden records</param>
        /// <returns>Discount</returns>
        public virtual async Task<Discount> GetDiscountByCouponCode(string couponCode, bool showHidden = false)
        {
            if (string.IsNullOrWhiteSpace(couponCode))
                return null;

            var coupon = await _discountCouponRepository.GetOneAsync(x => x.CouponCode == couponCode);
            if (coupon == null)
                return null;

            var discount = await GetDiscountById(coupon.DiscountId);
            return discount;
        }

        /// <summary>
        /// Get all coupon codes for discount
        /// </summary>
        /// <param name="discountId"></param>
        /// <param name="pageIndex"></param>
        /// <param name="pageSize"></param>
        /// <returns></returns>
        public virtual async Task<IPagedList<DiscountCoupon>> GetAllCouponCodesByDiscountId(string discountId, int pageIndex = 0, int pageSize = int.MaxValue)
        {
            var query = from d in _discountCouponRepository.Table
                        select d;

            if (!string.IsNullOrEmpty(discountId))
                query = query.Where(duh => duh.DiscountId == discountId);
            query = query.OrderByDescending(c => c.CouponCode);

            return await PagedList<DiscountCoupon>.Create(query, pageIndex, pageSize);
        }


        /// <summary>
        /// Gets a discount
        /// </summary>
        /// <returns>Discount</returns>
        public virtual Task<DiscountCoupon> GetDiscountCodeById(string id)
        {
            return _discountCouponRepository.GetByIdAsync(id);
        }

        /// <summary>
        /// Get discount code by discount code
        /// </summary>
        /// <param name="couponCode">Coupon code</param>
        /// <returns></returns>
        public virtual async Task<DiscountCoupon> GetDiscountCodeByCode(string couponCode)
        {
            return await _discountCouponRepository.GetOneAsync(x => x.CouponCode == couponCode);
        }


        /// <summary>
        /// Delete discount code
        /// </summary>
        /// <param name="coupon"></param>
        public virtual async Task DeleteDiscountCoupon(DiscountCoupon coupon)
        {
            await _discountCouponRepository.DeleteAsync(coupon);
            
            //clear cache
            await _cacheBase.RemoveByPrefix(CacheKey.DISCOUNTS_PATTERN_KEY);
        }

        /// <summary>
        /// Insert discount code
        /// </summary>
        /// <param name="coupon"></param>
        public virtual async Task InsertDiscountCoupon(DiscountCoupon coupon)
        {
            await _discountCouponRepository.InsertAsync(coupon);
            
            //clear cache
            await _cacheBase.RemoveByPrefix(CacheKey.DISCOUNTS_PATTERN_KEY);
        }

        /// <summary>
        /// Update discount code - set as used or not
        /// </summary>
        /// <param name="couponCode"></param>
        /// <param name="used"></param>
        public virtual async Task DiscountCouponSetAsUsed(string couponCode, bool used)
        {
            if (string.IsNullOrEmpty(couponCode))
                return;

            var coupon = await GetDiscountCodeByCode(couponCode);
            if (coupon != null)
            {
                if (used)
                {
                    coupon.Used = true;
                    coupon.Qty++;
                }
                else
                {
                    coupon.Qty -= 1;
                    coupon.Used = coupon.Qty > 0;
                }
                await _discountCouponRepository.UpdateAsync(coupon);
            }
        }

        /// <summary>
        /// Cancel discount if order was canceled or deleted
        /// </summary>
        /// <param name="orderId"></param>
        public virtual async Task CancelDiscount(string orderId)
        {
            var discountUsage = _discountUsageHistoryRepository.Table.Where(x => x.OrderId == orderId).ToList();
            foreach (var item in discountUsage)
            {
                await DiscountCouponSetAsUsed(item.CouponCode, false);
                item.Canceled = true;
                await UpdateDiscountUsageHistory(item);
            }
        }

        
        /// <summary>
        /// Gets a discount usage history record
        /// </summary>
        /// <param name="discountUsageHistoryId">Discount usage history record identifier</param>
        /// <returns>Discount usage history</returns>
        public virtual Task<DiscountUsageHistory> GetDiscountUsageHistoryById(string discountUsageHistoryId)
        {
            return _discountUsageHistoryRepository.GetByIdAsync(discountUsageHistoryId);
        }
        
        /// <summary>
        /// Insert discount usage history item
        /// </summary>
        /// <param name="discountUsageHistory">Discount usage history item</param>
        public virtual async Task InsertDiscountUsageHistory(DiscountUsageHistory discountUsageHistory)
        {
            ArgumentNullException.ThrowIfNull(discountUsageHistory);

            await _discountUsageHistoryRepository.InsertAsync(discountUsageHistory);

            //Support for coupon code
            await DiscountCouponSetAsUsed(discountUsageHistory.CouponCode, true);
            //clear cache
            await _cacheBase.RemoveByPrefix(CacheKey.DISCOUNTS_PATTERN_KEY);
            //event notification
            await _mediator.EntityInserted(discountUsageHistory);
        }


        /// <summary>
        /// Update discount usage history item
        /// </summary>
        /// <param name="discountUsageHistory">Discount usage history item</param>
        public virtual async Task UpdateDiscountUsageHistory(DiscountUsageHistory discountUsageHistory)
        {
            ArgumentNullException.ThrowIfNull(discountUsageHistory);

            await _discountUsageHistoryRepository.UpdateAsync(discountUsageHistory);

            await _cacheBase.RemoveByPrefix(CacheKey.DISCOUNTS_PATTERN_KEY);

            //event notification
            await _mediator.EntityUpdated(discountUsageHistory);
        }

        /// <summary>
        /// Delete discount usage history record
        /// </summary>
        /// <param name="discountUsageHistory">Discount usage history record</param>
        public virtual async Task DeleteDiscountUsageHistory(DiscountUsageHistory discountUsageHistory)
        {
            ArgumentNullException.ThrowIfNull(discountUsageHistory);

            await _discountUsageHistoryRepository.DeleteAsync(discountUsageHistory);

            await _cacheBase.RemoveByPrefix(CacheKey.DISCOUNTS_PATTERN_KEY);

            //event notification
            await _mediator.EntityDeleted(discountUsageHistory);
        }

        /// <summary>
        /// Get discount amount 
        /// </summary>
        /// <param name="discount">Discount</param>
        /// <param name="amount">Amount</param>
        /// <param name="currency">currency</param>
        /// <param name="customer">Customer</param>
        /// <param name="product">Product</param>
        public virtual async Task<double> GetDiscountAmount(Discount discount, Customer customer, Currency currency, Product product, double amount)
        {
            ArgumentNullException.ThrowIfNull(discount);

            //calculate discount amount
            double result;
            if (!discount.CalculateByPlugin)
            {
                if (discount.UsePercentage)
                    result = (float)amount * (float)discount.DiscountPercentage / 100f;
                else
                {
                    result = discount.DiscountAmount;
                }
            }
            else
            {
                result = await _mediator.Send(new GetDiscountAmountProvider(discount, customer, product, currency, amount));
            }

            //validate maximum discount amount
            if (discount.UsePercentage &&
                discount.MaximumDiscountAmount.HasValue &&
                result > discount.MaximumDiscountAmount.Value)
                result = discount.MaximumDiscountAmount.Value;

            if (result < 0)
                result = 0;

            return result;
        }

        /// <summary>
        /// Get preferred discount (with maximum discount value)
        /// </summary>
        /// <param name="discounts">A list of discounts to check</param>
        /// <param name="customer">customer</param>
        /// <param name="currency">currency</param>
        /// <param name="product"></param>
        /// <param name="amount">Amount</param>
        /// <returns>Preferred discount</returns>
        public virtual async Task<(List<ApplyDiscount> appliedDiscount, double discountAmount)> GetPreferredDiscount(
            IList<ApplyDiscount> discounts, Customer customer, Currency currency, Product product,
            double amount)
        {
            ArgumentNullException.ThrowIfNull(discounts);

            var appliedDiscount = new List<ApplyDiscount>();
            double discountAmount = 0;
            if (!discounts.Any())
                return (appliedDiscount, discountAmount);

            //check simple discounts
            foreach (var applieddiscount in discounts)
            {
                var discount = await GetDiscountById(applieddiscount.DiscountId);
                var currentDiscountValue = await GetDiscountAmount(discount, customer, currency, product, amount);
                if (!(currentDiscountValue > discountAmount)) continue;
                discountAmount = currentDiscountValue;
                appliedDiscount.Clear();
                appliedDiscount.Add(applieddiscount);
            }
            //cumulative discounts
            var cumulativeDiscounts = discounts.Where(x => x.IsCumulative).ToList();
            if (cumulativeDiscounts.Count <= 1) return (appliedDiscount, discountAmount);
            {
                double cumulativeDiscountAmount = 0;
                foreach (var item in cumulativeDiscounts)
                {
                    var discount = await GetDiscountById(item.DiscountId);
                    cumulativeDiscountAmount += await GetDiscountAmount(discount, customer, currency, product, amount);
                }

                if (!(cumulativeDiscountAmount > discountAmount)) return (appliedDiscount, discountAmount);
                discountAmount = cumulativeDiscountAmount;

                appliedDiscount.Clear();
                appliedDiscount.AddRange(cumulativeDiscounts);
            }

            return (appliedDiscount, discountAmount);
        }

        /// <summary>
        /// Get preferred discount (with maximum discount value)
        /// </summary>
        /// <param name="discounts">A list of discounts to check</param>
        /// <param name="customer"></param>
        /// <param name="currency">currency</param>
        /// <param name="amount">Amount</param>
        /// <returns>Preferred discount</returns>
        public virtual async Task<(List<ApplyDiscount> appliedDiscount, double discountAmount)> GetPreferredDiscount(
            IList<ApplyDiscount> discounts,
            Customer customer,
            Currency currency,
            double amount)
        {
            return await GetPreferredDiscount(discounts, customer, currency, null, amount);
        }

        #endregion
    }
}
