using System.Xml;
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Domain.Blogs;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Forums;
using Nop.Core.Domain.News;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Polls;
using Nop.Core.Domain.Shipping;
using Nop.Core.Domain.Tax;
using Nop.Core.Events;
using Nop.Core.Infrastructure;
using Nop.Data;
using Nop.Services.Common;
using Nop.Services.Localization;

namespace Nop.Services.Customers;

/// <summary>
/// Customer service
/// </summary>
public partial class CustomerService : ICustomerService
{
    #region Fields

    protected readonly CustomerSettings _customerSettings;
    protected readonly IEventPublisher _eventPublisher;
    protected readonly IGenericAttributeService _genericAttributeService;
    protected readonly INopDataProvider _dataProvider;
    protected readonly IRepository<Address> _customerAddressRepository;
    protected readonly IRepository<BlogComment> _blogCommentRepository;
    protected readonly IRepository<Customer> _customerRepository;
    protected readonly IRepository<CustomerAddressMapping> _customerAddressMappingRepository;
    protected readonly IRepository<CustomerCustomerRoleMapping> _customerCustomerRoleMappingRepository;
    protected readonly IRepository<CustomerPassword> _customerPasswordRepository;
    protected readonly IRepository<CustomerRole> _customerRoleRepository;
    protected readonly IRepository<ForumPost> _forumPostRepository;
    protected readonly IRepository<ForumTopic> _forumTopicRepository;
    protected readonly IRepository<GenericAttribute> _gaRepository;
    protected readonly IRepository<NewsComment> _newsCommentRepository;
    protected readonly IRepository<Order> _orderRepository;
    protected readonly IRepository<ProductReview> _productReviewRepository;
    protected readonly IRepository<ProductReviewHelpfulness> _productReviewHelpfulnessRepository;
    protected readonly IRepository<PollVotingRecord> _pollVotingRecordRepository;
    protected readonly IRepository<ShoppingCartItem> _shoppingCartRepository;
    protected readonly IShortTermCacheManager _shortTermCacheManager;
    protected readonly IStaticCacheManager _staticCacheManager;
    protected readonly IStoreContext _storeContext;
    protected readonly ShoppingCartSettings _shoppingCartSettings;
    protected readonly TaxSettings _taxSettings;

    #endregion

    #region Ctor

    public CustomerService(CustomerSettings customerSettings,
        IEventPublisher eventPublisher,
        IGenericAttributeService genericAttributeService,
        INopDataProvider dataProvider,
        IRepository<Address> customerAddressRepository,
        IRepository<BlogComment> blogCommentRepository,
        IRepository<Customer> customerRepository,
        IRepository<CustomerAddressMapping> customerAddressMappingRepository,
        IRepository<CustomerCustomerRoleMapping> customerCustomerRoleMappingRepository,
        IRepository<CustomerPassword> customerPasswordRepository,
        IRepository<CustomerRole> customerRoleRepository,
        IRepository<ForumPost> forumPostRepository,
        IRepository<ForumTopic> forumTopicRepository,
        IRepository<GenericAttribute> gaRepository,
        IRepository<NewsComment> newsCommentRepository,
        IRepository<Order> orderRepository,
        IRepository<ProductReview> productReviewRepository,
        IRepository<ProductReviewHelpfulness> productReviewHelpfulnessRepository,
        IRepository<PollVotingRecord> pollVotingRecordRepository,
        IRepository<ShoppingCartItem> shoppingCartRepository,
        IShortTermCacheManager shortTermCacheManager,
        IStaticCacheManager staticCacheManager,
        IStoreContext storeContext,
        ShoppingCartSettings shoppingCartSettings,
        TaxSettings taxSettings)
    {
        _customerSettings = customerSettings;
        _eventPublisher = eventPublisher;
        _genericAttributeService = genericAttributeService;
        _dataProvider = dataProvider;
        _customerAddressRepository = customerAddressRepository;
        _blogCommentRepository = blogCommentRepository;
        _customerRepository = customerRepository;
        _customerAddressMappingRepository = customerAddressMappingRepository;
        _customerCustomerRoleMappingRepository = customerCustomerRoleMappingRepository;
        _customerPasswordRepository = customerPasswordRepository;
        _customerRoleRepository = customerRoleRepository;
        _forumPostRepository = forumPostRepository;
        _forumTopicRepository = forumTopicRepository;
        _gaRepository = gaRepository;
        _newsCommentRepository = newsCommentRepository;
        _orderRepository = orderRepository;
        _productReviewRepository = productReviewRepository;
        _productReviewHelpfulnessRepository = productReviewHelpfulnessRepository;
        _pollVotingRecordRepository = pollVotingRecordRepository;
        _shoppingCartRepository = shoppingCartRepository;
        _shortTermCacheManager = shortTermCacheManager;
        _staticCacheManager = staticCacheManager;
        _storeContext = storeContext;
        _shoppingCartSettings = shoppingCartSettings;
        _taxSettings = taxSettings;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Gets a dictionary of all customer roles mapped by ID.
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation and contains a dictionary of all customer roles mapped by ID.
    /// </returns>
    protected virtual async Task<IDictionary<int, CustomerRole>> GetAllCustomerRolesDictionaryAsync()
    {
        return await _staticCacheManager.GetAsync(
            _staticCacheManager.PrepareKeyForDefaultCache(NopEntityCacheDefaults<CustomerRole>.AllCacheKey),
            async () => await _customerRoleRepository.Table.ToDictionaryAsync(cr => cr.Id));
    }

    #endregion

    #region Methods

    #region Customers

    /// <summary>
    /// Gets all customers
    /// </summary>
    /// <param name="createdFromUtc">Created date from (UTC); null to load all records</param>
    /// <param name="createdToUtc">Created date to (UTC); null to load all records</param>
    /// <param name="lastActivityFromUtc">Last activity date from (UTC); null to load all records</param>
    /// <param name="lastActivityToUtc">Last activity date to (UTC); null to load all records</param>
    /// <param name="affiliateId">Affiliate identifier</param>
    /// <param name="vendorId">Vendor identifier</param>
    /// <param name="customerRoleIds">A list of customer role identifiers to filter by (at least one match); pass null or empty list in order to load all customers; </param>
    /// <param name="email">Email; null to load all customers</param>
    /// <param name="username">Username; null to load all customers</param>
    /// <param name="firstName">First name; null to load all customers</param>
    /// <param name="lastName">Last name; null to load all customers</param>
    /// <param name="dayOfBirth">Day of birth; 0 to load all customers</param>
    /// <param name="monthOfBirth">Month of birth; 0 to load all customers</param>
    /// <param name="company">Company; null to load all customers</param>
    /// <param name="phone">Phone; null to load all customers</param>
    /// <param name="zipPostalCode">Phone; null to load all customers</param>
    /// <param name="ipAddress">IP address; null to load all customers</param>
    /// <param name="isActive">Customer is active; null to load all customers</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <param name="getOnlyTotalCount">A value in indicating whether you want to load only total number of records. Set to "true" if you don't want to load data from database</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customers
    /// </returns>
    public virtual async Task<IPagedList<Customer>> GetAllCustomersAsync(DateTime? createdFromUtc = null, DateTime? createdToUtc = null,
        DateTime? lastActivityFromUtc = null, DateTime? lastActivityToUtc = null,
        int affiliateId = 0, int vendorId = 0, int[] customerRoleIds = null,
        string email = null, string username = null, string firstName = null, string lastName = null,
        int dayOfBirth = 0, int monthOfBirth = 0,
        string company = null, string phone = null, string zipPostalCode = null, string ipAddress = null,
        bool? isActive = null, int pageIndex = 0, int pageSize = int.MaxValue, bool getOnlyTotalCount = false)
    {
        var customers = await _customerRepository.GetAllPagedAsync(query =>
        {
            if (createdFromUtc.HasValue)
                query = query.Where(c => createdFromUtc.Value <= c.CreatedOnUtc);
            if (createdToUtc.HasValue)
                query = query.Where(c => createdToUtc.Value >= c.CreatedOnUtc);
            if (lastActivityFromUtc.HasValue)
                query = query.Where(c => lastActivityFromUtc.Value <= c.LastActivityDateUtc);
            if (lastActivityToUtc.HasValue)
                query = query.Where(c => lastActivityToUtc.Value >= c.LastActivityDateUtc);
            if (affiliateId > 0)
                query = query.Where(c => affiliateId == c.AffiliateId);
            if (vendorId > 0)
                query = query.Where(c => vendorId == c.VendorId);
            if (isActive.HasValue)
                query = query.Where(c => c.Active == isActive.Value);

            query = query.Where(c => !c.Deleted);

            if (customerRoleIds != null && customerRoleIds.Length > 0)
            {
                query = query.Join(_customerCustomerRoleMappingRepository.Table, x => x.Id, y => y.CustomerId,
                        (x, y) => new { Customer = x, Mapping = y })
                    .Where(z => customerRoleIds.Contains(z.Mapping.CustomerRoleId))
                    .Select(z => z.Customer)
                    .Distinct();
            }

            if (!string.IsNullOrWhiteSpace(email))
                query = query.Where(c => c.Email.Contains(email));
            if (!string.IsNullOrWhiteSpace(username))
                query = query.Where(c => c.Username.Contains(username));
            if (!string.IsNullOrWhiteSpace(firstName))
                query = query.Where(c => c.FirstName.Contains(firstName));
            if (!string.IsNullOrWhiteSpace(lastName))
                query = query.Where(c => c.LastName.Contains(lastName));
            if (!string.IsNullOrWhiteSpace(company))
                query = query.Where(c => c.Company.Contains(company));
            if (!string.IsNullOrWhiteSpace(phone))
                query = query.Where(c => c.Phone.Contains(phone));
            if (!string.IsNullOrWhiteSpace(zipPostalCode))
                query = query.Where(c => c.ZipPostalCode.Contains(zipPostalCode));

            if (dayOfBirth > 0 && monthOfBirth > 0)
                query = query.Where(c => c.DateOfBirth.HasValue && c.DateOfBirth.Value.Day == dayOfBirth &&
                                         c.DateOfBirth.Value.Month == monthOfBirth);
            else if (dayOfBirth > 0)
                query = query.Where(c => c.DateOfBirth.HasValue && c.DateOfBirth.Value.Day == dayOfBirth);
            else if (monthOfBirth > 0)
                query = query.Where(c => c.DateOfBirth.HasValue && c.DateOfBirth.Value.Month == monthOfBirth);

            //search by IpAddress
            if (!string.IsNullOrWhiteSpace(ipAddress) && CommonHelper.IsValidIpAddress(ipAddress))
            {
                query = query.Where(w => w.LastIpAddress == ipAddress);
            }

            query = query.OrderByDescending(c => c.CreatedOnUtc);

            return query;
        }, pageIndex, pageSize, getOnlyTotalCount);

        return customers;
    }

    /// <summary>
    /// Gets online customers
    /// </summary>
    /// <param name="lastActivityFromUtc">Customer last activity date (from)</param>
    /// <param name="customerRoleIds">A list of customer role identifiers to filter by (at least one match); pass null or empty list in order to load all customers; </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 customers
    /// </returns>
    public virtual async Task<IPagedList<Customer>> GetOnlineCustomersAsync(DateTime lastActivityFromUtc,
        int[] customerRoleIds, int pageIndex = 0, int pageSize = int.MaxValue)
    {
        var query = _customerRepository.Table;
        query = query.Where(c => lastActivityFromUtc <= c.LastActivityDateUtc);
        query = query.Where(c => !c.Deleted);

        if (customerRoleIds != null && customerRoleIds.Length > 0)
            query = query.Where(c => _customerCustomerRoleMappingRepository.Table.Any(ccrm => ccrm.CustomerId == c.Id && customerRoleIds.Contains(ccrm.CustomerRoleId)));

        query = query.OrderByDescending(c => c.LastActivityDateUtc);
        var customers = await query.ToPagedListAsync(pageIndex, pageSize);

        return customers;
    }

    /// <summary>
    /// Gets customers with shopping carts
    /// </summary>
    /// <param name="shoppingCartType">Shopping cart type; pass null to load all records</param>
    /// <param name="storeId">Store identifier; pass 0 to load all records</param>
    /// <param name="productId">Product identifier; pass null to load all records</param>
    /// <param name="createdFromUtc">Created date from (UTC); pass null to load all records</param>
    /// <param name="createdToUtc">Created date to (UTC); pass null to load all records</param>
    /// <param name="countryId">Billing country identifier; pass null to load all records</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 customers
    /// </returns>
    public virtual async Task<IPagedList<Customer>> GetCustomersWithShoppingCartsAsync(ShoppingCartType? shoppingCartType = null,
        int storeId = 0, int? productId = null,
        DateTime? createdFromUtc = null, DateTime? createdToUtc = null, int? countryId = null,
        int pageIndex = 0, int pageSize = int.MaxValue)
    {
        //get all shopping cart items
        var items = _shoppingCartRepository.Table;

        //filter by type
        if (shoppingCartType.HasValue)
            items = items.Where(item => item.ShoppingCartTypeId == (int)shoppingCartType.Value);

        //filter shopping cart items by store
        if (storeId > 0 && !_shoppingCartSettings.CartsSharedBetweenStores)
            items = items.Where(item => item.StoreId == storeId);

        //filter shopping cart items by product
        if (productId > 0)
            items = items.Where(item => item.ProductId == productId);

        //filter shopping cart items by date
        if (createdFromUtc.HasValue)
            items = items.Where(item => createdFromUtc.Value <= item.CreatedOnUtc);
        if (createdToUtc.HasValue)
            items = items.Where(item => createdToUtc.Value >= item.CreatedOnUtc);

        //get all active customers
        var customers = _customerRepository.Table.Where(customer => customer.Active && !customer.Deleted);

        //filter customers by billing country
        if (countryId > 0)
            customers = from c in customers
                join a in _customerAddressRepository.Table on c.BillingAddressId equals a.Id
                where a.CountryId == countryId
                select c;

        var customersWithCarts = from c in customers
            join item in items on c.Id equals item.CustomerId
            //we change ordering for the MySQL engine to avoid problems with the ONLY_FULL_GROUP_BY server property that is set by default since the 5.7.5 version
            orderby _dataProvider.ConfigurationName == "MySql" ? c.CreatedOnUtc : item.CreatedOnUtc descending
            select c;

        return await customersWithCarts.Distinct().ToPagedListAsync(pageIndex, pageSize);
    }

    /// <summary>
    /// Gets customer for shopping cart
    /// </summary>
    /// <param name="shoppingCart">Shopping cart</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<Customer> GetShoppingCartCustomerAsync(IList<ShoppingCartItem> shoppingCart)
    {
        var customerId = shoppingCart.FirstOrDefault()?.CustomerId;

        return customerId.HasValue && customerId != 0 ? await GetCustomerByIdAsync(customerId.Value) : null;
    }

    /// <summary>
    /// Delete a customer
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteCustomerAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        if (customer.IsSystemAccount)
            throw new NopException($"System customer account ({customer.SystemName}) could not be deleted");

        customer.Deleted = true;

        if (_customerSettings.SuffixDeletedCustomers)
        {
            if (!string.IsNullOrEmpty(customer.Email))
                customer.Email += NopCustomerServicesDefaults.CustomerDeletedSuffix;
            if (!string.IsNullOrEmpty(customer.Username))
                customer.Username += NopCustomerServicesDefaults.CustomerDeletedSuffix;
        }

        await _customerRepository.UpdateAsync(customer, false);
        await _customerRepository.DeleteAsync(customer);
    }

    /// <summary>
    /// Gets a customer
    /// </summary>
    /// <param name="customerId">Customer identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains a customer
    /// </returns>
    public virtual async Task<Customer> GetCustomerByIdAsync(int customerId)
    {
        return await _customerRepository.GetByIdAsync(customerId, cache => default, useShortTermCache: true);
    }

    /// <summary>
    /// Get customers by identifiers
    /// </summary>
    /// <param name="customerIds">Customer identifiers</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customers
    /// </returns>
    public virtual async Task<IList<Customer>> GetCustomersByIdsAsync(int[] customerIds)
    {
        return await _customerRepository.GetByIdsAsync(customerIds, includeDeleted: false);
    }

    /// <summary>
    /// Get customers by guids
    /// </summary>
    /// <param name="customerGuids">Customer guids</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customers
    /// </returns>
    public virtual async Task<IList<Customer>> GetCustomersByGuidsAsync(Guid[] customerGuids)
    {
        if (customerGuids == null)
            return null;

        var query = from c in _customerRepository.Table
            where customerGuids.Contains(c.CustomerGuid)
            select c;
        var customers = await query.ToListAsync();

        return customers;
    }

    /// <summary>
    /// Gets a customer by GUID
    /// </summary>
    /// <param name="customerGuid">Customer GUID</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains a customer
    /// </returns>
    public virtual async Task<Customer> GetCustomerByGuidAsync(Guid customerGuid)
    {
        if (customerGuid == Guid.Empty)
            return null;

        var query = from c in _customerRepository.Table
            where c.CustomerGuid == customerGuid
            orderby c.Id
            select c;

        return await _shortTermCacheManager.GetAsync(async () => await query.FirstOrDefaultAsync(), NopCustomerServicesDefaults.CustomerByGuidCacheKey, customerGuid);
    }

    /// <summary>
    /// Get customer by email
    /// </summary>
    /// <param name="email">Email</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer
    /// </returns>
    public virtual async Task<Customer> GetCustomerByEmailAsync(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            return null;

        var query = from c in _customerRepository.Table
            orderby c.Id
            where c.Email == email
            select c;
        var customer = await query.FirstOrDefaultAsync();

        return customer;
    }

    /// <summary>
    /// Get customer by system name
    /// </summary>
    /// <param name="systemName">System name</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer
    /// </returns>
    public virtual async Task<Customer> GetCustomerBySystemNameAsync(string systemName)
    {
        if (string.IsNullOrWhiteSpace(systemName))
            return null;

        var query = from c in _customerRepository.Table
            orderby c.Id
            where c.SystemName == systemName
            select c;

        var customer = await _shortTermCacheManager.GetAsync(async () => await query.FirstOrDefaultAsync(), NopCustomerServicesDefaults.CustomerBySystemNameCacheKey, systemName);

        return customer;
    }

    /// <summary>
    /// Gets built-in system record used for background tasks
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains a customer object
    /// </returns>
    public virtual async Task<Customer> GetOrCreateBackgroundTaskUserAsync()
    {
        var backgroundTaskUser = await GetCustomerBySystemNameAsync(NopCustomerDefaults.BackgroundTaskCustomerName);

        if (backgroundTaskUser is null)
        {
            var store = await _storeContext.GetCurrentStoreAsync();
            //If for any reason the system user isn't in the database, then we add it
            backgroundTaskUser = new Customer
            {
                Email = "builtin@background-task-record.com",
                CustomerGuid = Guid.NewGuid(),
                AdminComment = "Built-in system record used for background tasks.",
                Active = true,
                IsSystemAccount = true,
                SystemName = NopCustomerDefaults.BackgroundTaskCustomerName,
                CreatedOnUtc = DateTime.UtcNow,
                LastActivityDateUtc = DateTime.UtcNow,
                RegisteredInStoreId = store.Id
            };

            await InsertCustomerAsync(backgroundTaskUser);

            var guestRole = await GetCustomerRoleBySystemNameAsync(NopCustomerDefaults.GuestsRoleName) ?? throw new NopException("'Guests' role could not be loaded");

            await AddCustomerRoleMappingAsync(new CustomerCustomerRoleMapping { CustomerRoleId = guestRole.Id, CustomerId = backgroundTaskUser.Id });
        }

        return backgroundTaskUser;
    }

    /// <summary>
    /// Gets built-in system guest record used for requests from search engines
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains a customer object
    /// </returns>
    public virtual async Task<Customer> GetOrCreateSearchEngineUserAsync()
    {
        var searchEngineUser = await GetCustomerBySystemNameAsync(NopCustomerDefaults.SearchEngineCustomerName);

        if (searchEngineUser is null)
        {
            var store = await _storeContext.GetCurrentStoreAsync();
            //If for any reason the system user isn't in the database, then we add it
            searchEngineUser = new Customer
            {
                Email = "builtin@search_engine_record.com",
                CustomerGuid = Guid.NewGuid(),
                AdminComment = "Built-in system guest record used for requests from search engines.",
                Active = true,
                IsSystemAccount = true,
                SystemName = NopCustomerDefaults.SearchEngineCustomerName,
                CreatedOnUtc = DateTime.UtcNow,
                LastActivityDateUtc = DateTime.UtcNow,
                RegisteredInStoreId = store.Id
            };

            await InsertCustomerAsync(searchEngineUser);

            var guestRole = await GetCustomerRoleBySystemNameAsync(NopCustomerDefaults.GuestsRoleName) ?? throw new NopException("'Guests' role could not be loaded");

            await AddCustomerRoleMappingAsync(new CustomerCustomerRoleMapping { CustomerRoleId = guestRole.Id, CustomerId = searchEngineUser.Id });
        }

        return searchEngineUser;
    }

    /// <summary>
    /// Get customer by username
    /// </summary>
    /// <param name="username">Username</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer
    /// </returns>
    public virtual async Task<Customer> GetCustomerByUsernameAsync(string username)
    {
        if (string.IsNullOrWhiteSpace(username))
            return null;

        var query = from c in _customerRepository.Table
            orderby c.Id
            where c.Username == username
            select c;
        var customer = await query.FirstOrDefaultAsync();

        return customer;
    }

    /// <summary>
    /// Insert a guest customer
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer
    /// </returns>
    public virtual async Task<Customer> InsertGuestCustomerAsync()
    {
        var customer = new Customer
        {
            CustomerGuid = Guid.NewGuid(),
            Active = true,
            CreatedOnUtc = DateTime.UtcNow,
            LastActivityDateUtc = DateTime.UtcNow
        };

        //add to 'Guests' role
        var guestRole = await GetCustomerRoleBySystemNameAsync(NopCustomerDefaults.GuestsRoleName) ?? throw new NopException("'Guests' role could not be loaded");

        await _customerRepository.InsertAsync(customer);

        await AddCustomerRoleMappingAsync(new CustomerCustomerRoleMapping { CustomerId = customer.Id, CustomerRoleId = guestRole.Id });

        return customer;
    }

    /// <summary>
    /// Insert a customer
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertCustomerAsync(Customer customer)
    {
        await _customerRepository.InsertAsync(customer);
    }

    /// <summary>
    /// Updates the customer
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateCustomerAsync(Customer customer)
    {
        await _customerRepository.UpdateAsync(customer);
    }

    /// <summary>
    /// Reset data required for checkout
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="storeId">Store identifier</param>
    /// <param name="clearCouponCodes">A value indicating whether to clear coupon code</param>
    /// <param name="clearCheckoutAttributes">A value indicating whether to clear selected checkout attributes</param>
    /// <param name="clearRewardPoints">A value indicating whether to clear "Use reward points" flag</param>
    /// <param name="clearShippingMethod">A value indicating whether to clear selected shipping method</param>
    /// <param name="clearPaymentMethod">A value indicating whether to clear selected payment method</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task ResetCheckoutDataAsync(Customer customer, int storeId,
        bool clearCouponCodes = false, bool clearCheckoutAttributes = false,
        bool clearRewardPoints = true, bool clearShippingMethod = true,
        bool clearPaymentMethod = true)
    {
        ArgumentNullException.ThrowIfNull(customer);

        //clear entered coupon codes
        if (clearCouponCodes)
        {
            await _genericAttributeService.SaveAttributeAsync<string>(customer, NopCustomerDefaults.DiscountCouponCodeAttribute, null);
            await _genericAttributeService.SaveAttributeAsync<string>(customer, NopCustomerDefaults.GiftCardCouponCodesAttribute, null);
        }

        //clear checkout attributes
        if (clearCheckoutAttributes)
            await _genericAttributeService.SaveAttributeAsync<string>(customer, NopCustomerDefaults.CheckoutAttributes, null, storeId);

        //clear reward points flag
        if (clearRewardPoints)
            await _genericAttributeService.SaveAttributeAsync(customer, NopCustomerDefaults.UseRewardPointsDuringCheckoutAttribute, false, storeId);

        //clear selected shipping method
        if (clearShippingMethod)
        {
            await _genericAttributeService.SaveAttributeAsync<ShippingOption>(customer, NopCustomerDefaults.SelectedShippingOptionAttribute, null, storeId);
            await _genericAttributeService.SaveAttributeAsync<ShippingOption>(customer, NopCustomerDefaults.OfferedShippingOptionsAttribute, null, storeId);
            await _genericAttributeService.SaveAttributeAsync<PickupPoint>(customer, NopCustomerDefaults.SelectedPickupPointAttribute, null, storeId);
        }

        //clear selected payment method
        if (clearPaymentMethod)
            await _genericAttributeService.SaveAttributeAsync<string>(customer, NopCustomerDefaults.SelectedPaymentMethodAttribute, null, storeId);

        await _eventPublisher.PublishAsync(new ResetCheckoutDataEvent(customer, storeId));
    }

    /// <summary>
    /// Delete guest customer records
    /// </summary>
    /// <param name="createdFromUtc">Created date from (UTC); null to load all records</param>
    /// <param name="createdToUtc">Created date to (UTC); null to load all records</param>
    /// <param name="onlyWithoutShoppingCart">A value indicating whether to delete customers only without shopping cart</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the number of deleted customers
    /// </returns>
    public virtual async Task<int> DeleteGuestCustomersAsync(DateTime? createdFromUtc, DateTime? createdToUtc, bool onlyWithoutShoppingCart)
    {
        var guestRole = await GetCustomerRoleBySystemNameAsync(NopCustomerDefaults.GuestsRoleName);

        var allGuestCustomers = from guest in _customerRepository.Table
            join ccm in _customerCustomerRoleMappingRepository.Table on guest.Id equals ccm.CustomerId
            where ccm.CustomerRoleId == guestRole.Id
            select guest;

        var guestsToDelete = from guest in _customerRepository.Table
            join g in allGuestCustomers on guest.Id equals g.Id
            from sCart in _shoppingCartRepository.Table.Where(sci => sci.CustomerId == guest.Id).DefaultIfEmpty()
            from order in _orderRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
            from blogComment in _blogCommentRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
            from newsComment in _newsCommentRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
            from productReview in _productReviewRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
            from productReviewHelpfulness in _productReviewHelpfulnessRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
            from pollVotingRecord in _pollVotingRecordRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
            from forumTopic in _forumTopicRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
            from forumPost in _forumPostRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
            where (!onlyWithoutShoppingCart || sCart == null) &&
                  order == null && blogComment == null && newsComment == null && productReview == null && productReviewHelpfulness == null &&
                  pollVotingRecord == null && forumTopic == null && forumPost == null &&
                  !guest.IsSystemAccount &&
                  (createdFromUtc == null || guest.CreatedOnUtc > createdFromUtc) &&
                  (createdToUtc == null || guest.CreatedOnUtc < createdToUtc)
            select new { CustomerId = guest.Id };

        await using var tmpGuests = await _dataProvider.CreateTempDataStorageAsync("tmp_guestsToDelete", guestsToDelete);
        await using var tmpAddresses = await _dataProvider.CreateTempDataStorageAsync("tmp_guestsAddressesToDelete",
            _customerAddressMappingRepository.Table
                .Where(ca => tmpGuests.Any(c => c.CustomerId == ca.CustomerId))
                .Select(ca => new { AddressId = ca.AddressId }));

        //delete guests
        var totalRecordsDeleted = await _customerRepository.DeleteAsync(c => tmpGuests.Any(tmp => tmp.CustomerId == c.Id));

        //delete attributes
        await _gaRepository.DeleteAsync(ga => tmpGuests.Any(c => c.CustomerId == ga.EntityId) && ga.KeyGroup == nameof(Customer));

        //delete m -> m addresses
        await _customerAddressRepository.DeleteAsync(a => tmpAddresses.Any(tmp => tmp.AddressId == a.Id));

        return totalRecordsDeleted;
    }

    /// <summary>
    /// Gets a tax display type for the customer
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the tax display type
    /// </returns>
    public virtual async Task<TaxDisplayType> GetCustomerTaxDisplayTypeAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        //default tax display type
        var taxDisplayType = _taxSettings.TaxDisplayType;

        //whether customers are allowed to select tax display type and the customer has previously saved one
        if (_taxSettings.AllowCustomersToSelectTaxDisplayType && customer.TaxDisplayTypeId.HasValue)
            taxDisplayType = (TaxDisplayType)customer.TaxDisplayTypeId.Value;
        else
        {
            //default tax type by customer roles
            var defaultRoleTaxDisplayType = await GetCustomerDefaultTaxDisplayTypeAsync(customer);
            if (defaultRoleTaxDisplayType.HasValue)
                taxDisplayType = defaultRoleTaxDisplayType.Value;
        }

        return taxDisplayType;
    }

    /// <summary>
    /// Gets a default tax display type (if configured)
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<TaxDisplayType?> GetCustomerDefaultTaxDisplayTypeAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var roleWithOverriddenTaxType = (await GetCustomerRolesAsync(customer)).FirstOrDefault(cr => cr.Active && cr.OverrideTaxDisplayType);
        if (roleWithOverriddenTaxType == null)
            return null;

        return (TaxDisplayType)roleWithOverriddenTaxType.DefaultTaxDisplayTypeId;
    }

    /// <summary>
    /// Get full name
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer full name
    /// </returns>
    public virtual async Task<string> GetCustomerFullNameAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var firstName = customer.FirstName;
        var lastName = customer.LastName;

        var fullName = string.Empty;
        if (!string.IsNullOrWhiteSpace(firstName) && !string.IsNullOrWhiteSpace(lastName))
        {
            //do not inject ILocalizationService via constructor because it'll cause circular references
            var format = await EngineContext.Current.Resolve<ILocalizationService>().GetResourceAsync("Customer.FullNameFormat");

            fullName = string.Format(format, firstName, lastName);
        }
        else
        {
            if (!string.IsNullOrWhiteSpace(firstName))
                fullName = firstName;

            if (!string.IsNullOrWhiteSpace(lastName))
                fullName = lastName;
        }

        return fullName;
    }

    /// <summary>
    /// Formats the customer name
    /// </summary>
    /// <param name="customer">Source</param>
    /// <param name="stripTooLong">Strip too long customer name</param>
    /// <param name="maxLength">Maximum customer name length</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the formatted text
    /// </returns>
    public virtual async Task<string> FormatUsernameAsync(Customer customer, bool stripTooLong = false, int maxLength = 0)
    {
        if (customer == null)
            return string.Empty;

        if (await IsGuestAsync(customer))
            //do not inject ILocalizationService via constructor because it'll cause circular references
            return await EngineContext.Current.Resolve<ILocalizationService>().GetResourceAsync("Customer.Guest");

        var result = string.Empty;
        switch (_customerSettings.CustomerNameFormat)
        {
            case CustomerNameFormat.ShowEmails:
                result = customer.Email;
                break;
            case CustomerNameFormat.ShowUsernames:
                result = customer.Username;
                break;
            case CustomerNameFormat.ShowFullNames:
                result = await GetCustomerFullNameAsync(customer);
                break;
            case CustomerNameFormat.ShowFirstName:
                result = customer.FirstName;
                break;
            default:
                break;
        }

        if (stripTooLong && maxLength > 0)
            result = CommonHelper.EnsureMaximumLength(result, maxLength);

        return result ?? string.Empty;
    }

    /// <summary>
    /// Gets coupon codes
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the coupon codes
    /// </returns>
    public virtual async Task<string[]> ParseAppliedDiscountCouponCodesAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var existingCouponCodes = await _genericAttributeService.GetAttributeAsync<string>(customer, NopCustomerDefaults.DiscountCouponCodeAttribute);

        var couponCodes = new List<string>();
        if (string.IsNullOrEmpty(existingCouponCodes))
            return couponCodes.ToArray();

        try
        {
            var xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(existingCouponCodes);

            var nodeList1 = xmlDoc.SelectNodes(@"//DiscountCouponCodes/CouponCode");
            foreach (XmlNode node1 in nodeList1)
            {
                if (node1.Attributes?["Code"] == null)
                    continue;
                var code = node1.Attributes["Code"].InnerText.Trim();
                couponCodes.Add(code);
            }
        }
        catch
        {
            // ignored
        }

        return couponCodes.ToArray();
    }

    /// <summary>
    /// Adds a coupon code
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="couponCode">Coupon code</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the new coupon codes document
    /// </returns>
    public virtual async Task ApplyDiscountCouponCodeAsync(Customer customer, string couponCode)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var result = string.Empty;
        try
        {
            var existingCouponCodes = await _genericAttributeService.GetAttributeAsync<string>(customer, NopCustomerDefaults.DiscountCouponCodeAttribute);

            couponCode = couponCode.Trim().ToLowerInvariant();

            var xmlDoc = new XmlDocument();
            if (string.IsNullOrEmpty(existingCouponCodes))
            {
                var element1 = xmlDoc.CreateElement("DiscountCouponCodes");
                xmlDoc.AppendChild(element1);
            }
            else
                xmlDoc.LoadXml(existingCouponCodes);

            var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//DiscountCouponCodes");

            XmlElement gcElement = null;
            //find existing
            var nodeList1 = xmlDoc.SelectNodes(@"//DiscountCouponCodes/CouponCode");
            foreach (XmlNode node1 in nodeList1)
            {
                if (node1.Attributes?["Code"] == null)
                    continue;

                var couponCodeAttribute = node1.Attributes["Code"].InnerText.Trim();

                if (couponCodeAttribute.ToLowerInvariant() != couponCode.ToLowerInvariant())
                    continue;

                gcElement = (XmlElement)node1;
                break;
            }

            //create new one if not found
            if (gcElement == null)
            {
                gcElement = xmlDoc.CreateElement("CouponCode");
                gcElement.SetAttribute("Code", couponCode);
                rootElement.AppendChild(gcElement);
            }

            result = xmlDoc.OuterXml;
        }
        catch
        {
            // ignored
        }

        //apply new value
        await _genericAttributeService.SaveAttributeAsync(customer, NopCustomerDefaults.DiscountCouponCodeAttribute, result);
    }

    /// <summary>
    /// Removes a coupon code
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="couponCode">Coupon code to remove</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the new coupon codes document
    /// </returns>
    public virtual async Task RemoveDiscountCouponCodeAsync(Customer customer, string couponCode)
    {
        ArgumentNullException.ThrowIfNull(customer);

        //get applied coupon codes
        var existingCouponCodes = await ParseAppliedDiscountCouponCodesAsync(customer);

        //clear them
        await _genericAttributeService.SaveAttributeAsync<string>(customer, NopCustomerDefaults.DiscountCouponCodeAttribute, null);

        //save again except removed one
        foreach (var existingCouponCode in existingCouponCodes)
            if (!existingCouponCode.Equals(couponCode, StringComparison.InvariantCultureIgnoreCase))
                await ApplyDiscountCouponCodeAsync(customer, existingCouponCode);
    }

    /// <summary>
    /// Gets coupon codes
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the coupon codes
    /// </returns>
    public virtual async Task<string[]> ParseAppliedGiftCardCouponCodesAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var existingCouponCodes = await _genericAttributeService.GetAttributeAsync<string>(customer, NopCustomerDefaults.GiftCardCouponCodesAttribute);

        var couponCodes = new List<string>();
        if (string.IsNullOrEmpty(existingCouponCodes))
            return couponCodes.ToArray();

        try
        {
            var xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(existingCouponCodes);

            var nodeList1 = xmlDoc.SelectNodes(@"//GiftCardCouponCodes/CouponCode");
            foreach (XmlNode node1 in nodeList1)
            {
                if (node1.Attributes?["Code"] == null)
                    continue;

                var code = node1.Attributes["Code"].InnerText.Trim();
                couponCodes.Add(code);
            }
        }
        catch
        {
            // ignored
        }

        return couponCodes.ToArray();
    }

    /// <summary>
    /// Adds a coupon code
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="couponCode">Coupon code</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the new coupon codes document
    /// </returns>
    public virtual async Task ApplyGiftCardCouponCodeAsync(Customer customer, string couponCode)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var result = string.Empty;
        try
        {
            var existingCouponCodes = await _genericAttributeService.GetAttributeAsync<string>(customer, NopCustomerDefaults.GiftCardCouponCodesAttribute);

            couponCode = couponCode.Trim().ToLowerInvariant();

            var xmlDoc = new XmlDocument();
            if (string.IsNullOrEmpty(existingCouponCodes))
            {
                var element1 = xmlDoc.CreateElement("GiftCardCouponCodes");
                xmlDoc.AppendChild(element1);
            }
            else
                xmlDoc.LoadXml(existingCouponCodes);

            var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//GiftCardCouponCodes");

            XmlElement gcElement = null;
            //find existing
            var nodeList1 = xmlDoc.SelectNodes(@"//GiftCardCouponCodes/CouponCode");
            foreach (XmlNode node1 in nodeList1)
            {
                if (node1.Attributes?["Code"] == null)
                    continue;

                var couponCodeAttribute = node1.Attributes["Code"].InnerText.Trim();
                if (couponCodeAttribute.ToLowerInvariant() != couponCode.ToLowerInvariant())
                    continue;

                gcElement = (XmlElement)node1;
                break;
            }

            //create new one if not found
            if (gcElement == null)
            {
                gcElement = xmlDoc.CreateElement("CouponCode");
                gcElement.SetAttribute("Code", couponCode);
                rootElement.AppendChild(gcElement);
            }

            result = xmlDoc.OuterXml;
        }
        catch
        {
            // ignored
        }

        //apply new value
        await _genericAttributeService.SaveAttributeAsync(customer, NopCustomerDefaults.GiftCardCouponCodesAttribute, result);
    }

    /// <summary>
    /// Removes a coupon code
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="couponCode">Coupon code to remove</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the new coupon codes document
    /// </returns>
    public virtual async Task RemoveGiftCardCouponCodeAsync(Customer customer, string couponCode)
    {
        ArgumentNullException.ThrowIfNull(customer);

        //get applied coupon codes
        var existingCouponCodes = await ParseAppliedGiftCardCouponCodesAsync(customer);

        //clear them
        await _genericAttributeService.SaveAttributeAsync<string>(customer, NopCustomerDefaults.GiftCardCouponCodesAttribute, null);

        //save again except removed one
        foreach (var existingCouponCode in existingCouponCodes)
            if (!existingCouponCode.Equals(couponCode, StringComparison.InvariantCultureIgnoreCase))
                await ApplyGiftCardCouponCodeAsync(customer, existingCouponCode);
    }

    /// <summary>
    /// Returns a list of guids of not existing customers
    /// </summary>
    /// <param name="guids">The guids of the customers to check</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the list of guids not existing customers
    /// </returns>
    public virtual async Task<Guid[]> GetNotExistingCustomersAsync(Guid[] guids)
    {
        ArgumentNullException.ThrowIfNull(guids);

        var query = _customerRepository.Table;
        var queryFilter = guids.Distinct().ToArray();
        //filtering by guid
        var filter = await query.Select(c => c.CustomerGuid)
            .Where(c => queryFilter.Contains(c))
            .ToListAsync();

        return queryFilter.Except(filter).ToArray();
    }

    #endregion

    #region Customer roles

    /// <summary>
    /// Add a customer-customer role mapping
    /// </summary>
    /// <param name="roleMapping">Customer-customer role mapping</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task AddCustomerRoleMappingAsync(CustomerCustomerRoleMapping roleMapping)
    {
        await _customerCustomerRoleMappingRepository.InsertAsync(roleMapping);
    }

    /// <summary>
    /// Remove a customer-customer role mapping
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="role">Customer role</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task RemoveCustomerRoleMappingAsync(Customer customer, CustomerRole role)
    {
        ArgumentNullException.ThrowIfNull(customer);

        ArgumentNullException.ThrowIfNull(role);

        var mapping = await _customerCustomerRoleMappingRepository.Table
            .SingleOrDefaultAsync(ccrm => ccrm.CustomerId == customer.Id && ccrm.CustomerRoleId == role.Id);

        if (mapping != null)
            await _customerCustomerRoleMappingRepository.DeleteAsync(mapping);
    }

    /// <summary>
    /// Delete a customer role
    /// </summary>
    /// <param name="customerRole">Customer role</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteCustomerRoleAsync(CustomerRole customerRole)
    {
        ArgumentNullException.ThrowIfNull(customerRole);

        if (customerRole.IsSystemRole)
            throw new NopException("System role could not be deleted");

        await _customerRoleRepository.DeleteAsync(customerRole);
    }

    /// <summary>
    /// Gets a customer role
    /// </summary>
    /// <param name="customerRoleId">Customer role identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer role
    /// </returns>
    public virtual async Task<CustomerRole> GetCustomerRoleByIdAsync(int customerRoleId)
    {
        var allRolesById = await GetAllCustomerRolesDictionaryAsync();

        return allRolesById.TryGetValue(customerRoleId, out var role) ? role : null;
    }

    /// <summary>
    /// Gets a customer role
    /// </summary>
    /// <param name="systemName">Customer role system name</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer role
    /// </returns>
    public virtual async Task<CustomerRole> GetCustomerRoleBySystemNameAsync(string systemName)
    {
        if (string.IsNullOrWhiteSpace(systemName))
            return null;

        var key = _staticCacheManager.PrepareKeyForDefaultCache(NopCustomerServicesDefaults.CustomerRolesBySystemNameCacheKey, systemName);

        var query = from cr in _customerRoleRepository.Table
            orderby cr.Id
            where cr.SystemName == systemName
            select cr;

        var customerRole = await _staticCacheManager.GetAsync(key, async () => await query.FirstOrDefaultAsync());

        return customerRole;
    }

    /// <summary>
    /// Get customer role identifiers
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="showHidden">A value indicating whether to load hidden records</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer role identifiers
    /// </returns>
    public virtual async Task<int[]> GetCustomerRoleIdsAsync(Customer customer, bool showHidden = false)
    {
        ArgumentNullException.ThrowIfNull(customer);

        return (await GetCustomerRolesAsync(customer, showHidden: showHidden))
            .Select(cr => cr.Id)
            .ToArray();
    }

    /// <summary>
    /// Gets list of customer roles
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="showHidden">A value indicating whether to load hidden records</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<IList<CustomerRole>> GetCustomerRolesAsync(Customer customer, bool showHidden = false)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var allRolesById = await GetAllCustomerRolesDictionaryAsync();

        var mappings = await _shortTermCacheManager.GetAsync(
            async () => await _customerCustomerRoleMappingRepository.GetAllAsync(query => query.Where(crm => crm.CustomerId == customer.Id)), NopCustomerServicesDefaults.CustomerRolesCacheKey, customer);

        return mappings.Select(mapping => allRolesById.TryGetValue(mapping.CustomerRoleId, out var role) ? role : null)
            .Where(cr => cr != null && (showHidden || cr.Active))
            .ToList();
    }

    /// <summary>
    /// Gets all customer roles
    /// </summary>
    /// <param name="showHidden">A value indicating whether to show hidden records</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer roles
    /// </returns>
    public virtual async Task<IList<CustomerRole>> GetAllCustomerRolesAsync(bool showHidden = false)
    {
        var allRolesById = await GetAllCustomerRolesDictionaryAsync();

        return allRolesById.Values
            .Where(cr => showHidden || cr.Active)
            .ToList();
    }

    /// <summary>
    /// Inserts a customer role
    /// </summary>
    /// <param name="customerRole">Customer role</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertCustomerRoleAsync(CustomerRole customerRole)
    {
        await _customerRoleRepository.InsertAsync(customerRole);
    }

    /// <summary>
    /// Gets a value indicating whether customer is in a certain customer role
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="customerRoleSystemName">Customer role system name</param>
    /// <param name="onlyActiveCustomerRoles">A value indicating whether we should look only in active customer roles</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<bool> IsInCustomerRoleAsync(Customer customer,
        string customerRoleSystemName, bool onlyActiveCustomerRoles = true)
    {
        ArgumentNullException.ThrowIfNull(customer);
        ArgumentException.ThrowIfNullOrEmpty(customerRoleSystemName);

        var customerRoles = await GetCustomerRolesAsync(customer, !onlyActiveCustomerRoles);

        return customerRoles?.Any(cr => cr.SystemName == customerRoleSystemName) ?? false;
    }

    /// <summary>
    /// Gets a value indicating whether customer is administrator
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="onlyActiveCustomerRoles">A value indicating whether we should look only in active customer roles</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<bool> IsAdminAsync(Customer customer, bool onlyActiveCustomerRoles = true)
    {
        return await IsInCustomerRoleAsync(customer, NopCustomerDefaults.AdministratorsRoleName, onlyActiveCustomerRoles);
    }

    /// <summary>
    /// Gets a value indicating whether customer is a forum moderator
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="onlyActiveCustomerRoles">A value indicating whether we should look only in active customer roles</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<bool> IsForumModeratorAsync(Customer customer, bool onlyActiveCustomerRoles = true)
    {
        return await IsInCustomerRoleAsync(customer, NopCustomerDefaults.ForumModeratorsRoleName, onlyActiveCustomerRoles);
    }

    /// <summary>
    /// Gets a value indicating whether customer is registered
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="onlyActiveCustomerRoles">A value indicating whether we should look only in active customer roles</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<bool> IsRegisteredAsync(Customer customer, bool onlyActiveCustomerRoles = true)
    {
        return await IsInCustomerRoleAsync(customer, NopCustomerDefaults.RegisteredRoleName, onlyActiveCustomerRoles);
    }

    /// <summary>
    /// Gets a value indicating whether customer is guest
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="onlyActiveCustomerRoles">A value indicating whether we should look only in active customer roles</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<bool> IsGuestAsync(Customer customer, bool onlyActiveCustomerRoles = true)
    {
        return await IsInCustomerRoleAsync(customer, NopCustomerDefaults.GuestsRoleName, onlyActiveCustomerRoles);
    }

    /// <summary>
    /// Gets a value indicating whether customer is vendor
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="onlyActiveCustomerRoles">A value indicating whether we should look only in active customer roles</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<bool> IsVendorAsync(Customer customer, bool onlyActiveCustomerRoles = true)
    {
        return await IsInCustomerRoleAsync(customer, NopCustomerDefaults.VendorsRoleName, onlyActiveCustomerRoles);
    }

    /// <summary>
    /// Updates the customer role
    /// </summary>
    /// <param name="customerRole">Customer role</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateCustomerRoleAsync(CustomerRole customerRole)
    {
        await _customerRoleRepository.UpdateAsync(customerRole);
    }

    #endregion

    #region Customer passwords

    /// <summary>
    /// Gets customer passwords
    /// </summary>
    /// <param name="customerId">Customer identifier; pass null to load all records</param>
    /// <param name="passwordFormat">Password format; pass null to load all records</param>
    /// <param name="passwordsToReturn">Number of returning passwords; pass null to load all records</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the list of customer passwords
    /// </returns>
    public virtual async Task<IList<CustomerPassword>> GetCustomerPasswordsAsync(int? customerId = null,
        PasswordFormat? passwordFormat = null, int? passwordsToReturn = null)
    {
        var query = _customerPasswordRepository.Table;

        //filter by customer
        if (customerId.HasValue)
            query = query.Where(password => password.CustomerId == customerId.Value);

        //filter by password format
        if (passwordFormat.HasValue)
            query = query.Where(password => password.PasswordFormatId == (int)passwordFormat.Value);

        //get the latest passwords
        if (passwordsToReturn.HasValue)
            query = query.OrderByDescending(password => password.CreatedOnUtc).Take(passwordsToReturn.Value);

        return await query.ToListAsync();
    }

    /// <summary>
    /// Get current customer password
    /// </summary>
    /// <param name="customerId">Customer identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer password
    /// </returns>
    public virtual async Task<CustomerPassword> GetCurrentPasswordAsync(int customerId)
    {
        if (customerId == 0)
            return null;

        //return the latest password
        return (await GetCustomerPasswordsAsync(customerId, passwordsToReturn: 1)).FirstOrDefault();
    }

    /// <summary>
    /// Insert a customer password
    /// </summary>
    /// <param name="customerPassword">Customer password</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertCustomerPasswordAsync(CustomerPassword customerPassword)
    {
        await _customerPasswordRepository.InsertAsync(customerPassword);
    }

    /// <summary>
    /// Update a customer password
    /// </summary>
    /// <param name="customerPassword">Customer password</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateCustomerPasswordAsync(CustomerPassword customerPassword)
    {
        await _customerPasswordRepository.UpdateAsync(customerPassword);
    }

    /// <summary>
    /// Check whether password recovery token is valid
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="token">Token to validate</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<bool> IsPasswordRecoveryTokenValidAsync(Customer customer, string token)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var cPrt = await _genericAttributeService.GetAttributeAsync<string>(customer, NopCustomerDefaults.PasswordRecoveryTokenAttribute);
        if (string.IsNullOrEmpty(cPrt))
            return false;

        if (!cPrt.Equals(token, StringComparison.InvariantCultureIgnoreCase))
            return false;

        return true;
    }

    /// <summary>
    /// Check whether password recovery link is expired
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<bool> IsPasswordRecoveryLinkExpiredAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        if (_customerSettings.PasswordRecoveryLinkDaysValid == 0)
            return false;

        var generatedDate = await _genericAttributeService.GetAttributeAsync<DateTime?>(customer, NopCustomerDefaults.PasswordRecoveryTokenDateGeneratedAttribute);
        if (!generatedDate.HasValue)
            return false;

        var daysPassed = (DateTime.UtcNow - generatedDate.Value).TotalDays;
        if (daysPassed > _customerSettings.PasswordRecoveryLinkDaysValid)
            return true;

        return false;
    }

    /// <summary>
    /// Check whether customer password is expired 
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains true if password is expired; otherwise false
    /// </returns>
    public virtual async Task<bool> IsPasswordExpiredAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        //the guests don't have a password
        if (await IsGuestAsync(customer))
            return false;

        //password lifetime is disabled for user
        if (!(await GetCustomerRolesAsync(customer)).Any(role => role.Active && role.EnablePasswordLifetime))
            return false;

        //setting disabled for all
        if (_customerSettings.PasswordLifetime == 0)
            return false;

        //get current password usage time
        var currentLifetime = await _shortTermCacheManager.GetAsync(async () =>
        {
            var customerPassword = await GetCurrentPasswordAsync(customer.Id);
            //password is not found, so return max value to force customer to change password
            if (customerPassword == null)
                return int.MaxValue;

            return (DateTime.UtcNow - customerPassword.CreatedOnUtc).Days;
        }, NopCustomerServicesDefaults.CustomerPasswordLifetimeCacheKey, customer);

        return currentLifetime >= _customerSettings.PasswordLifetime;
    }

    #endregion

    #region Customer address mapping

    /// <summary>
    /// Remove a customer-address mapping record
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="address">Address</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task RemoveCustomerAddressAsync(Customer customer, Address address)
    {
        ArgumentNullException.ThrowIfNull(customer);

        if (await _customerAddressMappingRepository.Table
                .FirstOrDefaultAsync(m => m.AddressId == address.Id && m.CustomerId == customer.Id)
            is CustomerAddressMapping mapping)
        {
            if (customer.BillingAddressId == address.Id)
                customer.BillingAddressId = null;
            if (customer.ShippingAddressId == address.Id)
                customer.ShippingAddressId = null;

            await _customerAddressMappingRepository.DeleteAsync(mapping);
        }
    }

    /// <summary>
    /// Inserts a customer-address mapping record
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="address">Address</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertCustomerAddressAsync(Customer customer, Address address)
    {
        ArgumentNullException.ThrowIfNull(customer);

        ArgumentNullException.ThrowIfNull(address);

        if (await _customerAddressMappingRepository.Table
                .FirstOrDefaultAsync(m => m.AddressId == address.Id && m.CustomerId == customer.Id)
            is null)
        {
            var mapping = new CustomerAddressMapping
            {
                AddressId = address.Id,
                CustomerId = customer.Id
            };

            await _customerAddressMappingRepository.InsertAsync(mapping);
        }
    }

    /// <summary>
    /// Gets a list of addresses mapped to customer
    /// </summary>
    /// <param name="customerId">Customer identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<IList<Address>> GetAddressesByCustomerIdAsync(int customerId)
    {
        var query = from address in _customerAddressRepository.Table
            join cam in _customerAddressMappingRepository.Table on address.Id equals cam.AddressId
            where cam.CustomerId == customerId
            select address;

        return await _shortTermCacheManager.GetAsync(async () => await query.ToListAsync(), NopCustomerServicesDefaults.CustomerAddressesCacheKey, customerId);
    }

    /// <summary>
    /// Gets a address mapped to customer
    /// </summary>
    /// <param name="customerId">Customer identifier</param>
    /// <param name="addressId">Address identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<Address> GetCustomerAddressAsync(int customerId, int addressId)
    {
        if (customerId == 0 || addressId == 0)
            return null;

        var query = from address in _customerAddressRepository.Table
            join cam in _customerAddressMappingRepository.Table on address.Id equals cam.AddressId
            where cam.CustomerId == customerId && address.Id == addressId
            select address;

        return await _shortTermCacheManager.GetAsync(async () => await query.FirstOrDefaultAsync(), NopCustomerServicesDefaults.CustomerAddressCacheKey, customerId, addressId);
    }

    /// <summary>
    /// Gets a customer billing address
    /// </summary>
    /// <param name="customer">Customer identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<Address> GetCustomerBillingAddressAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        return await GetCustomerAddressAsync(customer.Id, customer.BillingAddressId ?? 0);
    }

    /// <summary>
    /// Gets a customer shipping address
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<Address> GetCustomerShippingAddressAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        return await GetCustomerAddressAsync(customer.Id, customer.ShippingAddressId ?? 0);
    }

    #endregion

    #endregion
}