using Nop.Core.Domain.Shipping;
using Nop.Services.Common;
using Nop.Services.Directory;
using Nop.Services.Localization;
using Nop.Services.Shipping;
using Nop.Services.Shipping.Date;
using Nop.Services.Shipping.Pickup;
using Nop.Web.Areas.Admin.Infrastructure.Mapper.Extensions;
using Nop.Web.Areas.Admin.Models.Directory;
using Nop.Web.Areas.Admin.Models.Shipping;
using Nop.Web.Framework.Factories;
using Nop.Web.Framework.Models.Extensions;

namespace Nop.Web.Areas.Admin.Factories;

/// <summary>
/// Represents the shipping model factory implementation
/// </summary>
public partial class ShippingModelFactory : IShippingModelFactory
{
    #region Fields

    protected readonly IAddressModelFactory _addressModelFactory;
    protected readonly IAddressService _addressService;
    protected readonly ICountryService _countryService;
    protected readonly IDateRangeService _dateRangeService;
    protected readonly ILocalizationService _localizationService;
    protected readonly ILocalizedModelFactory _localizedModelFactory;
    protected readonly IPickupPluginManager _pickupPluginManager;
    protected readonly IShippingPluginManager _shippingPluginManager;
    protected readonly IShippingMethodsService _shippingMethodsService;
    protected readonly IStateProvinceService _stateProvinceService;
    protected readonly IWarehouseService _warehouseService;

    #endregion

    #region Ctor

    public ShippingModelFactory(IAddressModelFactory addressModelFactory,
        IAddressService addressService,
        ICountryService countryService,
        IDateRangeService dateRangeService,
        ILocalizationService localizationService,
        ILocalizedModelFactory localizedModelFactory,
        IPickupPluginManager pickupPluginManager,
        IShippingPluginManager shippingPluginManager,
        IShippingMethodsService shippingMethodsService,
        IStateProvinceService stateProvinceService,
        IWarehouseService warehouseService)
    {
        _addressModelFactory = addressModelFactory;
        _addressService = addressService;
        _countryService = countryService;
        _dateRangeService = dateRangeService;
        _localizationService = localizationService;
        _localizedModelFactory = localizedModelFactory;
        _pickupPluginManager = pickupPluginManager;
        _shippingPluginManager = shippingPluginManager;
        _shippingMethodsService = shippingMethodsService;
        _stateProvinceService = stateProvinceService;
        _warehouseService = warehouseService;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Prepare delivery date search model
    /// </summary>
    /// <param name="searchModel">Delivery date search model</param>
    /// <returns>Delivery date search model</returns>
    protected virtual DeliveryDateSearchModel PrepareDeliveryDateSearchModel(DeliveryDateSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare product availability range search model
    /// </summary>
    /// <param name="searchModel">Product availability range search model</param>
    /// <returns>Product availability range search model</returns>
    protected virtual ProductAvailabilityRangeSearchModel PrepareProductAvailabilityRangeSearchModel(ProductAvailabilityRangeSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    #endregion

    #region Methods

    /// <summary>
    /// Prepare shipping provider search model
    /// </summary>
    /// <param name="searchModel">Shipping provider search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the shipping provider search model
    /// </returns>
    public virtual Task<ShippingProviderSearchModel> PrepareShippingProviderSearchModelAsync(ShippingProviderSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //prepare page parameters
        searchModel.SetGridPageSize();

        return Task.FromResult(searchModel);
    }

    /// <summary>
    /// Prepare paged shipping provider list model
    /// </summary>
    /// <param name="searchModel">Shipping provider search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the shipping provider list model
    /// </returns>
    public virtual async Task<ShippingProviderListModel> PrepareShippingProviderListModelAsync(ShippingProviderSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //get shipping providers
        var shippingProviders = (await _shippingPluginManager.LoadAllPluginsAsync()).ToPagedList(searchModel);

        //prepare grid model
        var model = await new ShippingProviderListModel().PrepareToGridAsync(searchModel, shippingProviders, () =>
        {
            return shippingProviders.SelectAwait(async provider =>
            {
                //fill in model values from the entity
                var shippingProviderModel = provider.ToPluginModel<ShippingProviderModel>();

                //fill in additional values (not existing in the entity)
                shippingProviderModel.IsActive = _shippingPluginManager.IsPluginActive(provider);
                shippingProviderModel.ConfigurationUrl = provider.GetConfigurationPageUrl();

                shippingProviderModel.LogoUrl = await _shippingPluginManager.GetPluginLogoUrlAsync(provider);

                return shippingProviderModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare pickup point provider search model
    /// </summary>
    /// <param name="searchModel">Pickup point provider search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the pickup point provider search model
    /// </returns>
    public virtual Task<PickupPointProviderSearchModel> PreparePickupPointProviderSearchModelAsync(PickupPointProviderSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //prepare page parameters
        searchModel.SetGridPageSize();

        return Task.FromResult(searchModel);
    }

    /// <summary>
    /// Prepare paged pickup point provider list model
    /// </summary>
    /// <param name="searchModel">Pickup point provider search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the pickup point provider list model
    /// </returns>
    public virtual async Task<PickupPointProviderListModel> PreparePickupPointProviderListModelAsync(PickupPointProviderSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //get pickup point providers
        var pickupPointProviders = (await _pickupPluginManager.LoadAllPluginsAsync()).ToPagedList(searchModel);

        //prepare grid model
        var model = await new PickupPointProviderListModel().PrepareToGridAsync(searchModel, pickupPointProviders, () =>
        {
            return pickupPointProviders.SelectAwait(async provider =>
            {
                //fill in model values from the entity
                var pickupPointProviderModel = provider.ToPluginModel<PickupPointProviderModel>();

                //fill in additional values (not existing in the entity)
                pickupPointProviderModel.IsActive = _pickupPluginManager.IsPluginActive(provider);
                pickupPointProviderModel.ConfigurationUrl = provider.GetConfigurationPageUrl();

                pickupPointProviderModel.LogoUrl = await _pickupPluginManager.GetPluginLogoUrlAsync(provider);

                return pickupPointProviderModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare shipping method search model
    /// </summary>
    /// <param name="searchModel">Shipping method search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the shipping method search model
    /// </returns>
    public virtual Task<ShippingMethodSearchModel> PrepareShippingMethodSearchModelAsync(ShippingMethodSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //prepare page parameters
        searchModel.SetGridPageSize();

        return Task.FromResult(searchModel);
    }

    /// <summary>
    /// Prepare paged shipping method list model
    /// </summary>
    /// <param name="searchModel">Shipping method search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the shipping method list model
    /// </returns>
    public virtual async Task<ShippingMethodListModel> PrepareShippingMethodListModelAsync(ShippingMethodSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //get shipping methods
        var shippingMethods = (await _shippingMethodsService.GetAllShippingMethodsAsync()).ToPagedList(searchModel);

        //prepare grid model
        var model = new ShippingMethodListModel().PrepareToGrid(searchModel, shippingMethods, () =>
        {
            return shippingMethods.Select(method => method.ToModel<ShippingMethodModel>());
        });

        return model;
    }

    /// <summary>
    /// Prepare shipping method model
    /// </summary>
    /// <param name="model">Shipping method model</param>
    /// <param name="shippingMethod">Shipping method</param>
    /// <param name="excludeProperties">Whether to exclude populating of some properties of model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the shipping method model
    /// </returns>
    public virtual async Task<ShippingMethodModel> PrepareShippingMethodModelAsync(ShippingMethodModel model,
        ShippingMethod shippingMethod, bool excludeProperties = false)
    {
        Func<ShippingMethodLocalizedModel, int, Task> localizedModelConfiguration = null;

        if (shippingMethod != null)
        {
            //fill in model values from the entity
            model ??= shippingMethod.ToModel<ShippingMethodModel>();

            //define localized model configuration action
            localizedModelConfiguration = async (locale, languageId) =>
            {
                locale.Name = await _localizationService.GetLocalizedAsync(shippingMethod, entity => entity.Name, languageId, false, false);
                locale.Description = await _localizationService.GetLocalizedAsync(shippingMethod, entity => entity.Description, languageId, false, false);
            };
        }

        //prepare localized models
        if (!excludeProperties)
            model.Locales = await _localizedModelFactory.PrepareLocalizedModelsAsync(localizedModelConfiguration);

        return model;
    }

    /// <summary>
    /// Prepare dates and ranges search model
    /// </summary>
    /// <param name="searchModel">Dates and ranges search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the dates and ranges search model
    /// </returns>
    public virtual Task<DatesRangesSearchModel> PrepareDatesRangesSearchModelAsync(DatesRangesSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //prepare nested search models
        PrepareDeliveryDateSearchModel(searchModel.DeliveryDateSearchModel);
        PrepareProductAvailabilityRangeSearchModel(searchModel.ProductAvailabilityRangeSearchModel);

        return Task.FromResult(searchModel);
    }

    /// <summary>
    /// Prepare paged delivery date list model
    /// </summary>
    /// <param name="searchModel">Delivery date search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the delivery date list model
    /// </returns>
    public virtual async Task<DeliveryDateListModel> PrepareDeliveryDateListModelAsync(DeliveryDateSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //get delivery dates
        var deliveryDates = (await _dateRangeService.GetAllDeliveryDatesAsync()).ToPagedList(searchModel);

        //prepare grid model
        var model = new DeliveryDateListModel().PrepareToGrid(searchModel, deliveryDates, () =>
        {
            //fill in model values from the entity
            return deliveryDates.Select(date => date.ToModel<DeliveryDateModel>());
        });

        return model;
    }

    /// <summary>
    /// Prepare delivery date model
    /// </summary>
    /// <param name="model">Delivery date model</param>
    /// <param name="deliveryDate">Delivery date</param>
    /// <param name="excludeProperties">Whether to exclude populating of some properties of model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the delivery date model
    /// </returns>
    public virtual async Task<DeliveryDateModel> PrepareDeliveryDateModelAsync(DeliveryDateModel model, DeliveryDate deliveryDate, bool excludeProperties = false)
    {
        Func<DeliveryDateLocalizedModel, int, Task> localizedModelConfiguration = null;

        if (deliveryDate != null)
        {
            //fill in model values from the entity
            model ??= deliveryDate.ToModel<DeliveryDateModel>();

            //define localized model configuration action
            localizedModelConfiguration = async (locale, languageId) =>
            {
                locale.Name = await _localizationService.GetLocalizedAsync(deliveryDate, entity => entity.Name, languageId, false, false);
            };
        }

        //prepare localized models
        if (!excludeProperties)
            model.Locales = await _localizedModelFactory.PrepareLocalizedModelsAsync(localizedModelConfiguration);

        return model;
    }

    /// <summary>
    /// Prepare paged product availability range list model
    /// </summary>
    /// <param name="searchModel">Product availability range search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product availability range list model
    /// </returns>
    public virtual async Task<ProductAvailabilityRangeListModel> PrepareProductAvailabilityRangeListModelAsync(ProductAvailabilityRangeSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //get product availability ranges
        var productAvailabilityRanges = (await _dateRangeService.GetAllProductAvailabilityRangesAsync()).ToPagedList(searchModel);

        //prepare grid model
        var model = new ProductAvailabilityRangeListModel().PrepareToGrid(searchModel, productAvailabilityRanges, () =>
        {
            //fill in model values from the entity
            return productAvailabilityRanges.Select(range => range.ToModel<ProductAvailabilityRangeModel>());
        });

        return model;
    }

    /// <summary>
    /// Prepare product availability range model
    /// </summary>
    /// <param name="model">Product availability range model</param>
    /// <param name="productAvailabilityRange">Product availability range</param>
    /// <param name="excludeProperties">Whether to exclude populating of some properties of model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product availability range model
    /// </returns>
    public virtual async Task<ProductAvailabilityRangeModel> PrepareProductAvailabilityRangeModelAsync(ProductAvailabilityRangeModel model,
        ProductAvailabilityRange productAvailabilityRange, bool excludeProperties = false)
    {
        Func<ProductAvailabilityRangeLocalizedModel, int, Task> localizedModelConfiguration = null;

        if (productAvailabilityRange != null)
        {
            //fill in model values from the entity
            model ??= productAvailabilityRange.ToModel<ProductAvailabilityRangeModel>();

            //define localized model configuration action
            localizedModelConfiguration = async (locale, languageId) =>
            {
                locale.Name = await _localizationService.GetLocalizedAsync(productAvailabilityRange, entity => entity.Name, languageId, false, false);
            };
        }

        //prepare localized models
        if (!excludeProperties)
            model.Locales = await _localizedModelFactory.PrepareLocalizedModelsAsync(localizedModelConfiguration);

        return model;
    }

    /// <summary>
    /// Prepare warehouse search model
    /// </summary>
    /// <param name="searchModel">Warehouse search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the warehouse search model
    /// </returns>
    public virtual Task<WarehouseSearchModel> PrepareWarehouseSearchModelAsync(WarehouseSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //prepare page parameters
        searchModel.SetGridPageSize();

        return Task.FromResult(searchModel);
    }

    /// <summary>
    /// Prepare paged warehouse list model
    /// </summary>
    /// <param name="searchModel">Warehouse search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the warehouse list model
    /// </returns>
    public virtual async Task<WarehouseListModel> PrepareWarehouseListModelAsync(WarehouseSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //get warehouses
        var warehouses = (await _warehouseService.GetAllWarehousesAsync(
                name: searchModel.SearchName))
            .ToPagedList(searchModel);

        //prepare list model
        var model = new WarehouseListModel().PrepareToGrid(searchModel, warehouses, () =>
        {
            //fill in model values from the entity
            return warehouses.Select(warehouse => warehouse.ToModel<WarehouseModel>());
        });

        return model;
    }

    /// <summary>
    /// Prepare warehouse model
    /// </summary>
    /// <param name="model">Warehouse model</param>
    /// <param name="warehouse">Warehouse</param>
    /// <param name="excludeProperties">Whether to exclude populating of some properties of model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the warehouse model
    /// </returns>
    public virtual async Task<WarehouseModel> PrepareWarehouseModelAsync(WarehouseModel model, Warehouse warehouse, bool excludeProperties = false)
    {
        if (warehouse != null)
        {
            //fill in model values from the entity
            if (model == null)
            {
                model = warehouse.ToModel<WarehouseModel>();
            }
        }

        //prepare address model
        var address = await _addressService.GetAddressByIdAsync(warehouse?.AddressId ?? 0);
        if (!excludeProperties && address != null)
            model.Address = address.ToModel(model.Address);
        await _addressModelFactory.PrepareAddressModelAsync(model.Address, address);
        model.Address.CountryRequired = true;
        model.Address.ZipPostalCodeRequired = true;

        return model;
    }

    /// <summary>
    /// Prepare shipping method restriction model
    /// </summary>
    /// <param name="model">Shipping method restriction model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the shipping method restriction model
    /// </returns>
    public virtual async Task<ShippingMethodRestrictionModel> PrepareShippingMethodRestrictionModelAsync(ShippingMethodRestrictionModel model)
    {
        ArgumentNullException.ThrowIfNull(model);

        var countries = await _countryService.GetAllCountriesAsync(showHidden: true);
        model.AvailableCountries = await countries.SelectAwait(async country =>
        {
            var countryModel = country.ToModel<CountryModel>();
            countryModel.NumberOfStates = (await _stateProvinceService.GetStateProvincesByCountryIdAsync(country.Id))?.Count ?? 0;

            return countryModel;
        }).ToListAsync();

        foreach (var shippingMethod in await _shippingMethodsService.GetAllShippingMethodsAsync())
        {
            model.AvailableShippingMethods.Add(shippingMethod.ToModel<ShippingMethodModel>());
            foreach (var country in countries)
            {
                if (!model.Restricted.ContainsKey(country.Id))
                    model.Restricted[country.Id] = new Dictionary<int, bool>();

                model.Restricted[country.Id][shippingMethod.Id] = await _shippingMethodsService.CountryRestrictionExistsAsync(shippingMethod, country.Id);
            }
        }

        return model;
    }

    #endregion
}