using System.Net;
using System.Text.RegularExpressions;
using Nop.Core.Domain.Common;
using Nop.Data;
using Nop.Services.Attributes;
using Nop.Services.Directory;
using Nop.Services.Localization;

namespace Nop.Services.Common;

/// <summary>
/// Address service
/// </summary>
public partial class AddressService : IAddressService
{
    #region Fields

    protected readonly AddressSettings _addressSettings;
    protected readonly IAttributeParser<AddressAttribute, AddressAttributeValue> _addressAttributeParser;
    protected readonly IAttributeService<AddressAttribute, AddressAttributeValue> _addressAttributeService;
    protected readonly ICountryService _countryService;
    protected readonly ILocalizationService _localizationService;
    protected readonly IRepository<Address> _addressRepository;
    protected readonly IStateProvinceService _stateProvinceService;

    #endregion

    #region Ctor

    public AddressService(AddressSettings addressSettings,
        IAttributeParser<AddressAttribute, AddressAttributeValue> addressAttributeParser,
        IAttributeService<AddressAttribute, AddressAttributeValue> addressAttributeService,
        ICountryService countryService,
        ILocalizationService localizationService,
        IRepository<Address> addressRepository,
        IStateProvinceService stateProvinceService)
    {
        _addressSettings = addressSettings;
        _addressAttributeParser = addressAttributeParser;
        _addressAttributeService = addressAttributeService;
        _countryService = countryService;
        _localizationService = localizationService;
        _addressRepository = addressRepository;
        _stateProvinceService = stateProvinceService;
    }

    #endregion

    #region Methods

    /// <summary>
    /// Deletes an address
    /// </summary>
    /// <param name="address">Address</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteAddressAsync(Address address)
    {
        await _addressRepository.DeleteAsync(address);
    }

    /// <summary>
    /// Gets total number of addresses by country identifier
    /// </summary>
    /// <param name="countryId">Country identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the number of addresses
    /// </returns>
    public virtual async Task<int> GetAddressTotalByCountryIdAsync(int countryId)
    {
        if (countryId == 0)
            return 0;

        var query = from a in _addressRepository.Table
            where a.CountryId == countryId
            select a;

        return await query.CountAsync();
    }

    /// <summary>
    /// Gets total number of addresses by state/province identifier
    /// </summary>
    /// <param name="stateProvinceId">State/province identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the number of addresses
    /// </returns>
    public virtual async Task<int> GetAddressTotalByStateProvinceIdAsync(int stateProvinceId)
    {
        if (stateProvinceId == 0)
            return 0;

        var query = from a in _addressRepository.Table
            where a.StateProvinceId == stateProvinceId
            select a;

        return await query.CountAsync();
    }

    /// <summary>
    /// Gets an address by address identifier
    /// </summary>
    /// <param name="addressId">Address identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the address
    /// </returns>
    public virtual async Task<Address> GetAddressByIdAsync(int addressId)
    {
        return await _addressRepository.GetByIdAsync(addressId, cache => default, useShortTermCache: true);
    }

    /// <summary>
    /// Inserts an address
    /// </summary>
    /// <param name="address">Address</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertAddressAsync(Address address)
    {
        ArgumentNullException.ThrowIfNull(address);

        address.CreatedOnUtc = DateTime.UtcNow;

        //some validation
        if (address.CountryId == 0)
            address.CountryId = null;
        if (address.StateProvinceId == 0)
            address.StateProvinceId = null;

        await _addressRepository.InsertAsync(address);
    }

    /// <summary>
    /// Updates the address
    /// </summary>
    /// <param name="address">Address</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateAddressAsync(Address address)
    {
        ArgumentNullException.ThrowIfNull(address);

        //some validation
        if (address.CountryId == 0)
            address.CountryId = null;
        if (address.StateProvinceId == 0)
            address.StateProvinceId = null;

        await _addressRepository.UpdateAsync(address);
    }

    /// <summary>
    /// Gets a value indicating whether address is valid (can be saved)
    /// </summary>
    /// <param name="address">Address to validate</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<bool> IsAddressValidAsync(Address address)
    {
        ArgumentNullException.ThrowIfNull(address);

        if (string.IsNullOrWhiteSpace(address.FirstName))
            return false;

        if (string.IsNullOrWhiteSpace(address.LastName))
            return false;

        if (string.IsNullOrWhiteSpace(address.Email))
            return false;

        if (_addressSettings.CompanyEnabled &&
            _addressSettings.CompanyRequired &&
            string.IsNullOrWhiteSpace(address.Company))
            return false;

        if (_addressSettings.StreetAddressEnabled &&
            _addressSettings.StreetAddressRequired &&
            string.IsNullOrWhiteSpace(address.Address1))
            return false;

        if (_addressSettings.StreetAddress2Enabled &&
            _addressSettings.StreetAddress2Required &&
            string.IsNullOrWhiteSpace(address.Address2))
            return false;

        if (_addressSettings.ZipPostalCodeEnabled &&
            _addressSettings.ZipPostalCodeRequired &&
            string.IsNullOrWhiteSpace(address.ZipPostalCode))
            return false;

        if (_addressSettings.CountryEnabled)
        {
            var country = await _countryService.GetCountryByAddressAsync(address);
            if (country == null)
                return false;

            if (_addressSettings.StateProvinceEnabled)
            {
                var states = await _stateProvinceService.GetStateProvincesByCountryIdAsync(country.Id);
                if (states.Any())
                {
                    if (address.StateProvinceId == null || address.StateProvinceId.Value == 0)
                        return false;

                    var state = states.FirstOrDefault(x => x.Id == address.StateProvinceId.Value);
                    if (state == null)
                        return false;
                }
            }
        }

        if (_addressSettings.CountyEnabled &&
            _addressSettings.CountyRequired &&
            string.IsNullOrWhiteSpace(address.County))
            return false;

        if (_addressSettings.CityEnabled &&
            _addressSettings.CityRequired &&
            string.IsNullOrWhiteSpace(address.City))
            return false;

        if (_addressSettings.PhoneEnabled &&
            _addressSettings.PhoneRequired &&
            string.IsNullOrWhiteSpace(address.PhoneNumber))
            return false;

        if (_addressSettings.FaxEnabled &&
            _addressSettings.FaxRequired &&
            string.IsNullOrWhiteSpace(address.FaxNumber))
            return false;

        var requiredAttributes = (await _addressAttributeService.GetAllAttributesAsync()).Where(x => x.IsRequired);

        foreach (var requiredAttribute in requiredAttributes)
        {
            var value = _addressAttributeParser.ParseValues(address.CustomAttributes, requiredAttribute.Id);

            if (!value.Any() || string.IsNullOrEmpty(value[0]))
                return false;
        }

        return true;
    }

    /// <summary>
    /// Find an address
    /// </summary>
    /// <param name="source">Source</param>
    /// <param name="firstName">First name</param>
    /// <param name="lastName">Last name</param>
    /// <param name="phoneNumber">Phone number</param>
    /// <param name="email">Email</param>
    /// <param name="faxNumber">Fax number</param>
    /// <param name="company">Company</param>
    /// <param name="address1">Address 1</param>
    /// <param name="address2">Address 2</param>
    /// <param name="city">City</param>
    /// <param name="county">County</param>
    /// <param name="stateProvinceId">State/province identifier</param>
    /// <param name="zipPostalCode">Zip postal code</param>
    /// <param name="countryId">Country identifier</param>
    /// <param name="customAttributes">Custom address attributes (XML format)</param>
    /// <returns>Address</returns>
    public virtual Address FindAddress(List<Address> source, string firstName, string lastName, string phoneNumber, string email,
        string faxNumber, string company, string address1, string address2, string city, string county, int? stateProvinceId,
        string zipPostalCode, int? countryId, string customAttributes)
    {
        return source.Find(a => ((string.IsNullOrEmpty(a.FirstName) && string.IsNullOrEmpty(firstName)) || a.FirstName == firstName) &&
                                ((string.IsNullOrEmpty(a.LastName) && string.IsNullOrEmpty(lastName)) || a.LastName == lastName) &&
                                ((string.IsNullOrEmpty(a.PhoneNumber) && string.IsNullOrEmpty(phoneNumber)) || a.PhoneNumber == phoneNumber) &&
                                ((string.IsNullOrEmpty(a.Email) && string.IsNullOrEmpty(email)) || a.Email == email) &&
                                ((string.IsNullOrEmpty(a.FaxNumber) && string.IsNullOrEmpty(faxNumber)) || a.FaxNumber == faxNumber) &&
                                ((string.IsNullOrEmpty(a.Company) && string.IsNullOrEmpty(company)) || a.Company == company) &&
                                ((string.IsNullOrEmpty(a.Address1) && string.IsNullOrEmpty(address1)) || a.Address1 == address1) &&
                                ((string.IsNullOrEmpty(a.Address2) && string.IsNullOrEmpty(address2)) || a.Address2 == address2) &&
                                ((string.IsNullOrEmpty(a.City) && string.IsNullOrEmpty(city)) || a.City == city) &&
                                ((string.IsNullOrEmpty(a.County) && string.IsNullOrEmpty(county)) || a.County == county) &&
                                ((a.StateProvinceId == null && (stateProvinceId == null || stateProvinceId == 0)) || (a.StateProvinceId != null && a.StateProvinceId == stateProvinceId)) &&
                                ((string.IsNullOrEmpty(a.ZipPostalCode) && string.IsNullOrEmpty(zipPostalCode)) || a.ZipPostalCode == zipPostalCode) &&
                                ((a.CountryId == null && countryId == null) || (a.CountryId != null && a.CountryId == countryId)) &&
                                //actually we should parse custom address attribute (in case if "Display order" is changed) and then compare
                                //bu we simplify this process and simply compare their values in XML
                                ((string.IsNullOrEmpty(a.CustomAttributes) && string.IsNullOrEmpty(customAttributes)) || a.CustomAttributes == customAttributes));
    }

    /// <summary>
    /// Clone address
    /// </summary>
    /// <returns>A deep copy of address</returns>
    public virtual Address CloneAddress(Address address)
    {
        var addr = new Address
        {
            FirstName = address.FirstName,
            LastName = address.LastName,
            Email = address.Email,
            Company = address.Company,
            CountryId = address.CountryId,
            StateProvinceId = address.StateProvinceId,
            County = address.County,
            City = address.City,
            Address1 = address.Address1,
            Address2 = address.Address2,
            ZipPostalCode = address.ZipPostalCode,
            PhoneNumber = address.PhoneNumber,
            FaxNumber = address.FaxNumber,
            CustomAttributes = address.CustomAttributes,
            CreatedOnUtc = address.CreatedOnUtc
        };

        return addr;
    }

    /// <summary>
    /// Address format
    /// </summary>
    /// <param name="address">Address</param>
    /// <param name="languageId">Language identifier</param>
    /// <param name="separator">Separator</param>
    /// <param name="htmlEncode">Encode to HTML</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// Address line, array address fields
    /// </returns>      
    public virtual async Task<(string, KeyValuePair<AddressField, string>[])> FormatAddressAsync(Address address, int languageId = 0, string separator = ", ", bool htmlEncode = false)
    {
        var fieldsList = new KeyValuePair<AddressField, string>[7];

        if (address == null)
            return (string.Empty, fieldsList);

        var format = await _localizationService.GetResourceAsync("Address.LineFormat", languageId, true, "{0}{1}{2}{3}{4}{5}{6}");
        var indexArray = Regex.Matches(format, @"{\d}").Select(x => Convert.ToInt32(Regex.Match(x.Value, @"\d").Value)).ToArray();

        var indexItem = 0;
        foreach (var item in indexArray)
        {
            switch ((AddressField)item)
            {
                case AddressField.Country:
                    var country = await _countryService.GetCountryByAddressAsync(address);
                    var countryName = country != null ? await _localizationService.GetLocalizedAsync(country, x => x.Name, languageId) : string.Empty;

                    if (_addressSettings.CountryEnabled && !string.IsNullOrWhiteSpace(countryName))
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.Country, htmlEncode ? WebUtility.HtmlEncode(countryName) : countryName);
                    else
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.Country, string.Empty);

                    break;

                case AddressField.StateProvince:
                    var stateProvince = await _stateProvinceService.GetStateProvinceByAddressAsync(address);
                    var stateProvinceName = stateProvince != null ? await _localizationService.GetLocalizedAsync(stateProvince, x => x.Name, languageId) : string.Empty;

                    if (_addressSettings.StateProvinceEnabled && !string.IsNullOrWhiteSpace(stateProvinceName))
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.StateProvince, htmlEncode ? WebUtility.HtmlEncode(stateProvinceName) : stateProvinceName);
                    else
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.StateProvince, string.Empty);

                    break;

                case AddressField.City:
                    if (_addressSettings.CityEnabled && !string.IsNullOrWhiteSpace(address.City))
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.City, htmlEncode ? WebUtility.HtmlEncode(address.City) : address.City);
                    else
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.City, string.Empty);
                    break;

                case AddressField.County:
                    if (_addressSettings.CountyEnabled && !string.IsNullOrWhiteSpace(address.County))
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.County, htmlEncode ? WebUtility.HtmlEncode(address.County) : address.County);
                    else
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.County, string.Empty);
                    break;

                case AddressField.Address1:
                    if (_addressSettings.StreetAddressEnabled && !string.IsNullOrWhiteSpace(address.Address1))
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.Address1, htmlEncode ? WebUtility.HtmlEncode(address.Address1) : address.Address1);
                    else
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.Address1, string.Empty);
                    break;

                case AddressField.Address2:
                    if (_addressSettings.StreetAddress2Enabled && !string.IsNullOrWhiteSpace(address.Address2))
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.Address2, htmlEncode ? WebUtility.HtmlEncode(address.Address2) : address.Address2);
                    else
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.Address2, string.Empty);
                    break;

                case AddressField.ZipPostalCode:
                    if (_addressSettings.ZipPostalCodeEnabled && !string.IsNullOrWhiteSpace(address.ZipPostalCode))
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.ZipPostalCode, htmlEncode ? WebUtility.HtmlEncode(address.ZipPostalCode) : address.ZipPostalCode);
                    else
                        fieldsList[indexItem] = new KeyValuePair<AddressField, string>(AddressField.ZipPostalCode, string.Empty);
                    break;
                default:
                    break;
            }
            indexItem++;
        }

        var formatString = string.Join("", fieldsList.Select(x => !string.IsNullOrEmpty(x.Value) ? $"{x.Value}{separator}" : x.Value).ToArray())
            .TrimEnd(separator.ToArray());

        return (formatString, fieldsList);
    }
    #endregion
}