using Nop.Core;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using Nop.Data;
using Nop.Services.Helpers;
using Nop.Services.Localization;

namespace Nop.Services.Orders;

/// <summary>
/// Reward point service
/// </summary>
public partial class RewardPointService : IRewardPointService
{
    #region Fields

    protected readonly IDateTimeHelper _dateTimeHelper;
    protected readonly ILocalizationService _localizationService;
    protected readonly IRepository<RewardPointsHistory> _rewardPointsHistoryRepository;
    protected readonly RewardPointsSettings _rewardPointsSettings;

    #endregion

    #region Ctor

    public RewardPointService(IDateTimeHelper dateTimeHelper,
        ILocalizationService localizationService,
        IRepository<RewardPointsHistory> rewardPointsHistoryRepository,
        RewardPointsSettings rewardPointsSettings)
    {
        _dateTimeHelper = dateTimeHelper;
        _localizationService = localizationService;
        _rewardPointsHistoryRepository = rewardPointsHistoryRepository;
        _rewardPointsSettings = rewardPointsSettings;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Get query to load reward points history
    /// </summary>
    /// <param name="customerId">Customer identifier; pass 0 to load all records</param>
    /// <param name="storeId">Store identifier; pass null to load all records</param>
    /// <param name="showNotActivated">Whether to load reward points that did not yet activated</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the query to load reward points history
    /// </returns>
    protected virtual async Task<IQueryable<RewardPointsHistory>> GetRewardPointsQueryAsync(int customerId, int? storeId, bool showNotActivated = false)
    {
        var query = _rewardPointsHistoryRepository.Table;

        //filter by customer
        if (customerId > 0)
            query = query.Where(historyEntry => historyEntry.CustomerId == customerId);

        //filter by store
        if (!_rewardPointsSettings.PointsAccumulatedForAllStores && storeId > 0)
            query = query.Where(historyEntry => historyEntry.StoreId == storeId);

        //whether to show only the points that already activated
        if (!showNotActivated)
            query = query.Where(historyEntry => historyEntry.CreatedOnUtc < DateTime.UtcNow);

        //update points balance
        await UpdateRewardPointsBalanceAsync(query);

        return query;
    }

    /// <summary>
    /// Update reward points balance if necessary
    /// </summary>
    /// <param name="query">Input query</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    protected virtual async Task UpdateRewardPointsBalanceAsync(IQueryable<RewardPointsHistory> query)
    {
        //get expired points
        var nowUtc = DateTime.UtcNow;
        var expiredPoints = query
            .Where(historyEntry => historyEntry.EndDateUtc < nowUtc && historyEntry.ValidPoints > 0)
            .OrderBy(historyEntry => historyEntry.CreatedOnUtc).ThenBy(historyEntry => historyEntry.Id).ToList();

        //reduce the balance for these points
        foreach (var historyEntry in expiredPoints)
        {
            await InsertRewardPointsHistoryEntryAsync(new RewardPointsHistory
            {
                CustomerId = historyEntry.CustomerId,
                StoreId = historyEntry.StoreId,
                Points = -historyEntry.ValidPoints.Value,
                Message = string.Format(await _localizationService.GetResourceAsync("RewardPoints.Expired"),
                    await _dateTimeHelper.ConvertToUserTimeAsync(historyEntry.CreatedOnUtc, DateTimeKind.Utc)),
                CreatedOnUtc = historyEntry.EndDateUtc.Value
            });

            historyEntry.ValidPoints = 0;
            await UpdateRewardPointsHistoryEntryAsync(historyEntry);
        }

        //get has not yet activated points, but it's time to do it
        var notActivatedPoints = query
            .Where(historyEntry => !historyEntry.PointsBalance.HasValue && historyEntry.CreatedOnUtc < nowUtc)
            .OrderBy(historyEntry => historyEntry.CreatedOnUtc).ThenBy(historyEntry => historyEntry.Id).ToList();
        if (!notActivatedPoints.Any())
            return;

        //get current points balance
        //LINQ to entities does not support Last method, thus order by desc and use First one
        var currentPointsBalance = query
            .OrderByDescending(historyEntry => historyEntry.CreatedOnUtc).ThenByDescending(historyEntry => historyEntry.Id)
            .FirstOrDefault(historyEntry => historyEntry.PointsBalance.HasValue)
            ?.PointsBalance ?? 0;

        //update appropriate records
        foreach (var historyEntry in notActivatedPoints)
        {
            currentPointsBalance += historyEntry.Points;
            historyEntry.PointsBalance = currentPointsBalance;
            await UpdateRewardPointsHistoryEntryAsync(historyEntry);
        }
    }

    /// <summary>
    /// Insert the reward point history entry
    /// </summary>
    /// <param name="rewardPointsHistory">Reward point history entry</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    protected virtual async Task InsertRewardPointsHistoryEntryAsync(RewardPointsHistory rewardPointsHistory)
    {
        await _rewardPointsHistoryRepository.InsertAsync(rewardPointsHistory);
    }

    #endregion

    #region Methods

    /// <summary>
    /// Load reward point history records
    /// </summary>
    /// <param name="customerId">Customer identifier; 0 to load all records</param>
    /// <param name="storeId">Store identifier; pass null to load all records</param>
    /// <param name="showNotActivated">A value indicating whether to show reward points that did not yet activated</param>
    /// <param name="orderGuid">Order Guid; pass null to load all record</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the reward point history records
    /// </returns>
    public virtual async Task<IPagedList<RewardPointsHistory>> GetRewardPointsHistoryAsync(int customerId = 0, int? storeId = null,
        bool showNotActivated = false, Guid? orderGuid = null, int pageIndex = 0, int pageSize = int.MaxValue)
    {
        var query = await GetRewardPointsQueryAsync(customerId, storeId, showNotActivated);

        if (orderGuid.HasValue)
            query = query.Where(historyEntry => historyEntry.UsedWithOrder == orderGuid.Value);

        query = query.OrderByDescending(historyEntry => historyEntry.CreatedOnUtc)
            .ThenByDescending(historyEntry => historyEntry.Id);

        //return paged reward points history
        return await query.ToPagedListAsync(pageIndex, pageSize);
    }

    /// <summary>
    /// Gets reward points balance
    /// </summary>
    /// <param name="customerId">Customer identifier</param>
    /// <param name="storeId">Store identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the balance
    /// </returns>
    public virtual async Task<int> GetRewardPointsBalanceAsync(int customerId, int storeId)
    {
        var query = (await GetRewardPointsQueryAsync(customerId, storeId))
            .OrderByDescending(historyEntry => historyEntry.CreatedOnUtc).ThenByDescending(historyEntry => historyEntry.Id);

        //return point balance of the first actual history entry
        return (await query.FirstOrDefaultAsync())?.PointsBalance ?? 0;
    }

    /// <summary>
    /// Add reward points history record
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="points">Number of points to add</param>
    /// <param name="storeId">Store identifier</param>
    /// <param name="message">Message</param>
    /// <param name="usedWithOrder">The order for which points were redeemed (spent) as a payment</param>
    /// <param name="usedAmount">Used amount</param>
    /// <param name="activatingDate">Date and time of activating reward points; pass null to immediately activating</param>
    /// <param name="endDate">Date and time when the reward points will no longer be valid; pass null to add date termless points</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the reward points history entry identifier
    /// </returns>
    public virtual async Task<int> AddRewardPointsHistoryEntryAsync(Customer customer, int points, int storeId, string message = "",
        Order usedWithOrder = null, decimal usedAmount = 0M, DateTime? activatingDate = null, DateTime? endDate = null)
    {
        ArgumentNullException.ThrowIfNull(customer);

        if (storeId == 0)
            throw new ArgumentException("Store ID should be valid");

        if (points < 0 && endDate.HasValue)
            throw new ArgumentException("End date is available only for positive points amount");

        //insert new history entry
        var newHistoryEntry = new RewardPointsHistory
        {
            CustomerId = customer.Id,
            StoreId = storeId,
            Points = points,
            PointsBalance = activatingDate.HasValue ? null : (int?)(await GetRewardPointsBalanceAsync(customer.Id, storeId) + points),
            UsedAmount = usedAmount,
            Message = message,
            CreatedOnUtc = activatingDate ?? DateTime.UtcNow,
            EndDateUtc = endDate,
            ValidPoints = points > 0 ? (int?)points : null,
            UsedWithOrder = usedWithOrder?.OrderGuid
        };
        await InsertRewardPointsHistoryEntryAsync(newHistoryEntry);

        //reduce valid points of previous entries
        if (points >= 0)
            return newHistoryEntry.Id;

        var withValidPoints = (await GetRewardPointsQueryAsync(customer.Id, storeId))
            .Where(historyEntry => historyEntry.ValidPoints > 0)
            .OrderBy(historyEntry => historyEntry.CreatedOnUtc).ThenBy(historyEntry => historyEntry.Id).ToList();
        foreach (var historyEntry in withValidPoints)
        {
            points += historyEntry.ValidPoints.Value;
            historyEntry.ValidPoints = Math.Max(points, 0);
            await UpdateRewardPointsHistoryEntryAsync(historyEntry);

            if (points >= 0)
                break;
        }

        return newHistoryEntry.Id;
    }

    /// <summary>
    /// Gets a reward point history entry
    /// </summary>
    /// <param name="rewardPointsHistoryId">Reward point history entry identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the reward point history entry
    /// </returns>
    public virtual async Task<RewardPointsHistory> GetRewardPointsHistoryEntryByIdAsync(int rewardPointsHistoryId)
    {
        return await _rewardPointsHistoryRepository.GetByIdAsync(rewardPointsHistoryId);
    }

    /// <summary>
    /// Update the reward point history entry
    /// </summary>
    /// <param name="rewardPointsHistory">Reward point history entry</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateRewardPointsHistoryEntryAsync(RewardPointsHistory rewardPointsHistory)
    {
        await _rewardPointsHistoryRepository.UpdateAsync(rewardPointsHistory);
    }

    /// <summary>
    /// Delete the reward point history entry
    /// </summary>
    /// <param name="rewardPointsHistory">Reward point history entry</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteRewardPointsHistoryEntryAsync(RewardPointsHistory rewardPointsHistory)
    {
        await _rewardPointsHistoryRepository.DeleteAsync(rewardPointsHistory);
    }

    #endregion
}