using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Nop.Core;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.Tax;
using Nop.Core.Domain.Vendors;
using Nop.Core.Events;
using Nop.Core.Http;
using Nop.Core.Security;
using Nop.Services.Authentication;
using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Directory;
using Nop.Services.Helpers;
using Nop.Services.Localization;
using Nop.Services.ScheduleTasks;
using Nop.Services.Vendors;
using Nop.Web.Framework.Globalization;

namespace Nop.Web.Framework;

/// <summary>
/// Represents work context for web application
/// </summary>
public partial class WebWorkContext : IWorkContext
{
    #region Fields

    protected readonly CookieSettings _cookieSettings;
    protected readonly CurrencySettings _currencySettings;
    protected readonly IAuthenticationService _authenticationService;
    protected readonly ICurrencyService _currencyService;
    protected readonly ICustomerService _customerService;
    protected readonly IEventPublisher _eventPublisher;
    protected readonly IGenericAttributeService _genericAttributeService;
    protected readonly IHttpContextAccessor _httpContextAccessor;
    protected readonly ILanguageService _languageService;
    protected readonly IStoreContext _storeContext;
    protected readonly IUserAgentHelper _userAgentHelper;
    protected readonly IVendorService _vendorService;
    protected readonly IWebHelper _webHelper;
    protected readonly LocalizationSettings _localizationSettings;
    protected readonly TaxSettings _taxSettings;

    protected Customer _cachedCustomer;
    protected Customer _originalCustomerIfImpersonated;
    protected Vendor _cachedVendor;
    protected Language _cachedLanguage;
    protected Currency _cachedCurrency;
    protected TaxDisplayType? _cachedTaxDisplayType;

    #endregion

    #region Ctor

    public WebWorkContext(CookieSettings cookieSettings,
        CurrencySettings currencySettings,
        IAuthenticationService authenticationService,
        ICurrencyService currencyService,
        ICustomerService customerService,
        IEventPublisher eventPublisher,
        IGenericAttributeService genericAttributeService,
        IHttpContextAccessor httpContextAccessor,
        ILanguageService languageService,
        IStoreContext storeContext,
        IUserAgentHelper userAgentHelper,
        IVendorService vendorService,
        IWebHelper webHelper,
        LocalizationSettings localizationSettings,
        TaxSettings taxSettings)
    {
        _cookieSettings = cookieSettings;
        _currencySettings = currencySettings;
        _authenticationService = authenticationService;
        _currencyService = currencyService;
        _customerService = customerService;
        _eventPublisher = eventPublisher;
        _genericAttributeService = genericAttributeService;
        _httpContextAccessor = httpContextAccessor;
        _languageService = languageService;
        _storeContext = storeContext;
        _userAgentHelper = userAgentHelper;
        _vendorService = vendorService;
        _webHelper = webHelper;
        _localizationSettings = localizationSettings;
        _taxSettings = taxSettings;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Get nop customer cookie
    /// </summary>
    /// <returns>String value of cookie</returns>
    protected virtual string GetCustomerCookie()
    {
        var cookieName = $"{NopCookieDefaults.Prefix}{NopCookieDefaults.CustomerCookie}";
        return _httpContextAccessor.HttpContext?.Request?.Cookies[cookieName];
    }

    /// <summary>
    /// Set nop customer cookie
    /// </summary>
    /// <param name="customerGuid">Guid of the customer</param>
    protected virtual void SetCustomerCookie(Guid customerGuid)
    {
        if (_httpContextAccessor.HttpContext?.Response.HasStarted ?? true)
            return;

        //delete current cookie value
        var cookieName = $"{NopCookieDefaults.Prefix}{NopCookieDefaults.CustomerCookie}";
        _httpContextAccessor.HttpContext.Response.Cookies.Delete(cookieName);

        //get date of cookie expiration
        var cookieExpires = _cookieSettings.CustomerCookieExpires;
        var cookieExpiresDate = DateTime.Now.AddHours(cookieExpires);

        //if passed guid is empty set cookie as expired
        if (customerGuid == Guid.Empty)
            cookieExpiresDate = DateTime.Now.AddMonths(-1);

        //set new cookie value
        var options = new CookieOptions
        {
            HttpOnly = true,
            Expires = cookieExpiresDate,
            Secure = _webHelper.IsCurrentConnectionSecured()
        };
        _httpContextAccessor.HttpContext.Response.Cookies.Append(cookieName, customerGuid.ToString(), options);
    }

    /// <summary>
    /// Set language culture cookie
    /// </summary>
    /// <param name="language">Language</param>
    protected virtual void SetLanguageCookie(Language language)
    {
        if (_httpContextAccessor.HttpContext?.Response.HasStarted ?? true)
            return;

        //delete current cookie value
        var cookieName = $"{NopCookieDefaults.Prefix}{NopCookieDefaults.CultureCookie}";
        _httpContextAccessor.HttpContext.Response.Cookies.Delete(cookieName);

        if (string.IsNullOrEmpty(language?.LanguageCulture))
            return;

        //set new cookie value
        var value = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(language.LanguageCulture));
        var options = new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) };
        _httpContextAccessor.HttpContext.Response.Cookies.Append(cookieName, value, options);
    }

    /// <summary>
    /// Get language from the request
    /// </summary>
    /// <param name="storeId">Get a language that is only allowed for the specified store; pass 0 to ignore limitations</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the found language
    /// </returns>
    protected virtual async Task<Language> GetLanguageFromRequestAsync(int storeId = 0)
    {
        var requestCultureFeature = _httpContextAccessor.HttpContext?.Features.Get<IRequestCultureFeature>();
        if (requestCultureFeature is null)
            return null;

        //whether we should detect the current language by customer settings
        if (requestCultureFeature.Provider is not NopSeoUrlCultureProvider && !_localizationSettings.AutomaticallyDetectLanguage)
            return null;

        //get request culture
        if (requestCultureFeature.RequestCulture is null)
            return null;

        //try to get language by culture name
        var store = await _storeContext.GetCurrentStoreAsync();
        var requestLanguage = (await _languageService.GetAllLanguagesAsync(storeId: storeId)).FirstOrDefault(language =>
            language.LanguageCulture.Equals(requestCultureFeature.RequestCulture.Culture.Name, StringComparison.InvariantCultureIgnoreCase));

        return requestLanguage;
    }

    #endregion

    #region Properties

    /// <summary>
    /// Gets the current customer
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task<Customer> GetCurrentCustomerAsync()
    {
        //whether there is a cached value
        if (_cachedCustomer != null)
            return _cachedCustomer;

        await SetCurrentCustomerAsync();

        return _cachedCustomer;
    }

    /// <summary>
    /// Sets the current customer
    /// </summary>
    /// <param name="customer">Current customer</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task SetCurrentCustomerAsync(Customer customer = null)
    {
        if (customer == null)
        {
            //check whether request is made by a background (schedule) task
            if (_httpContextAccessor.HttpContext?.Request
                    ?.Path.Equals(new PathString($"/{NopTaskDefaults.ScheduleTaskPath}"), StringComparison.InvariantCultureIgnoreCase)
                ?? true)
            {
                //in this case return built-in customer record for background task
                customer = await _customerService.GetOrCreateBackgroundTaskUserAsync();
            }

            if (customer == null || customer.Deleted || !customer.Active || customer.RequireReLogin)
            {
                //check whether request is made by a search engine, in this case return built-in customer record for search engines
                if (_userAgentHelper.IsSearchEngine())
                    customer = await _customerService.GetOrCreateSearchEngineUserAsync();
            }

            if (customer == null || customer.Deleted || !customer.Active || customer.RequireReLogin)
            {
                //try to get registered user
                customer = await _authenticationService.GetAuthenticatedCustomerAsync();
            }

            if (customer != null && !customer.Deleted && customer.Active && !customer.RequireReLogin)
            {
                //get impersonate user if required
                var impersonatedCustomerId = await _genericAttributeService
                    .GetAttributeAsync<int?>(customer, NopCustomerDefaults.ImpersonatedCustomerIdAttribute);
                if (impersonatedCustomerId.HasValue && impersonatedCustomerId.Value > 0)
                {
                    var impersonatedCustomer = await _customerService.GetCustomerByIdAsync(impersonatedCustomerId.Value);
                    if (impersonatedCustomer != null && !impersonatedCustomer.Deleted &&
                        impersonatedCustomer.Active &&
                        !impersonatedCustomer.RequireReLogin)
                    {
                        //set impersonated customer
                        _originalCustomerIfImpersonated = customer;
                        customer = impersonatedCustomer;
                    }
                }
            }

            if (customer == null || customer.Deleted || !customer.Active || customer.RequireReLogin)
            {
                //get guest customer
                var customerCookie = GetCustomerCookie();
                if (Guid.TryParse(customerCookie, out var customerGuid))
                {
                    //get customer from cookie (should not be registered)
                    var customerByCookie = await _customerService.GetCustomerByGuidAsync(customerGuid);
                    if (customerByCookie != null && !await _customerService.IsRegisteredAsync(customerByCookie))
                        customer = customerByCookie;
                }
            }

            if (customer == null || customer.Deleted || !customer.Active || customer.RequireReLogin)
            {
                //create guest if not exists
                customer = await _customerService.InsertGuestCustomerAsync();
            }
        }

        if (!customer.Deleted && customer.Active && !customer.RequireReLogin)
        {
            //set customer cookie
            SetCustomerCookie(customer.CustomerGuid);

            //cache the found customer
            _cachedCustomer = customer;
        }
    }

    /// <summary>
    /// Gets the original customer (in case the current one is impersonated)
    /// </summary>
    public virtual Customer OriginalCustomerIfImpersonated => _originalCustomerIfImpersonated;

    /// <summary>
    /// Gets the current vendor (logged-in manager)
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task<Vendor> GetCurrentVendorAsync()
    {
        //whether there is a cached value
        if (_cachedVendor != null)
            return _cachedVendor;

        var customer = await GetCurrentCustomerAsync();
        if (customer == null)
            return null;

        //check vendor availability
        var vendor = await _vendorService.GetVendorByIdAsync(customer.VendorId);
        if (vendor == null || vendor.Deleted || !vendor.Active)
            return null;

        //cache the found vendor
        _cachedVendor = vendor;

        return _cachedVendor;
    }

    /// <summary>
    /// Sets current user working language
    /// </summary>
    /// <param name="language">Language</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task SetWorkingLanguageAsync(Language language)
    {
        //save passed language identifier
        var customer = await GetCurrentCustomerAsync();

        if (!customer.IsSystemAccount)
        {
            customer.LanguageId = language?.Id;
            await _customerService.UpdateCustomerAsync(customer);

            //raise event
            await _eventPublisher.PublishAsync(new CustomerChangeWorkingLanguageEvent(customer));
        }

        //set cookie
        SetLanguageCookie(language);

        //then reset the cached value
        _cachedLanguage = null;
    }

    /// <summary>
    /// Gets current user working language
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task<Language> GetWorkingLanguageAsync()
    {
        //whether there is a cached value
        if (_cachedLanguage != null)
            return _cachedLanguage;

        var customer = await GetCurrentCustomerAsync();
        var store = await _storeContext.GetCurrentStoreAsync();

        //whether we should detect the language from the request
        var detectedLanguage = await GetLanguageFromRequestAsync(store.Id);

        //get current saved language identifier
        var currentLanguageId = customer.LanguageId;

        //if the language is detected we need to save it
        if (detectedLanguage != null)
        {
            //save the detected language identifier if it differs from the current one
            if (detectedLanguage.Id != currentLanguageId)
                await SetWorkingLanguageAsync(detectedLanguage);
        }
        else
        {
            var allStoreLanguages = await _languageService.GetAllLanguagesAsync(storeId: store.Id);

            //check customer language availability
            detectedLanguage = allStoreLanguages.FirstOrDefault(language => language.Id == currentLanguageId);

            //it not found, then try to get the default language for the current store (if specified)
            detectedLanguage ??= allStoreLanguages.FirstOrDefault(language => language.Id == store.DefaultLanguageId);

            //if the default language for the current store not found, then try to get the first one
            detectedLanguage ??= allStoreLanguages.FirstOrDefault();

            //if there are no languages for the current store try to get the first one regardless of the store
            detectedLanguage ??= (await _languageService.GetAllLanguagesAsync()).FirstOrDefault();

            SetLanguageCookie(detectedLanguage);
        }

        //cache the found language
        _cachedLanguage = detectedLanguage;

        return _cachedLanguage;
    }

    /// <summary>
    /// Gets current user working currency
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task<Currency> GetWorkingCurrencyAsync()
    {
        //whether there is a cached value
        if (_cachedCurrency != null)
            return _cachedCurrency;

        var adminAreaUrl = $"{_webHelper.GetStoreLocation()}admin";

        //return primary store currency when we're in admin area/mode
        if (_webHelper.GetThisPageUrl(false).StartsWith(adminAreaUrl, StringComparison.InvariantCultureIgnoreCase))
        {
            var primaryStoreCurrency = await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId);
            if (primaryStoreCurrency != null)
            {
                _cachedCurrency = primaryStoreCurrency;
                return primaryStoreCurrency;
            }
        }

        var customer = await GetCurrentCustomerAsync();
        var store = await _storeContext.GetCurrentStoreAsync();

        if (customer.IsSearchEngineAccount())
        {
            _cachedCurrency = await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId)
                              ?? (await _currencyService.GetAllCurrenciesAsync(storeId: store.Id)).FirstOrDefault();

            return _cachedCurrency;
        }

        var allStoreCurrencies = await _currencyService.GetAllCurrenciesAsync(storeId: store.Id);

        //check customer currency availability
        var customerCurrency = allStoreCurrencies.FirstOrDefault(currency => currency.Id == customer.CurrencyId);
        if (customerCurrency == null)
        {
            //it not found, then try to get the default currency for the current language (if specified)
            var language = await GetWorkingLanguageAsync();
            customerCurrency = allStoreCurrencies
                .FirstOrDefault(currency => currency.Id == language.DefaultCurrencyId);
        }

        //if the default currency for the current store not found, then try to get the first one
        customerCurrency ??= allStoreCurrencies.FirstOrDefault();

        //if there are no currencies for the current store try to get the first one regardless of the store
        customerCurrency ??= (await _currencyService.GetAllCurrenciesAsync()).FirstOrDefault();

        //cache the found currency
        _cachedCurrency = customerCurrency;

        return _cachedCurrency;
    }

    /// <summary>
    /// Sets current user working currency
    /// </summary>
    /// <param name="currency">Currency</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task SetWorkingCurrencyAsync(Currency currency)
    {
        //save passed currency identifier
        var customer = await GetCurrentCustomerAsync();
        if (customer.IsSearchEngineAccount())
            return;

        customer.CurrencyId = currency?.Id;
        await _customerService.UpdateCustomerAsync(customer);

        //then reset the cached value
        _cachedCurrency = null;
    }

    /// <summary>
    /// Gets or sets current tax display type
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task<TaxDisplayType> GetTaxDisplayTypeAsync()
    {
        //whether there is a cached value
        if (_cachedTaxDisplayType.HasValue)
            return _cachedTaxDisplayType.Value;

        var customer = await GetCurrentCustomerAsync();
        var taxDisplayType = await _customerService.GetCustomerTaxDisplayTypeAsync(customer);

        //cache the value
        _cachedTaxDisplayType = taxDisplayType;

        return _cachedTaxDisplayType.Value;
    }

    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task SetTaxDisplayTypeAsync(TaxDisplayType taxDisplayType)
    {
        //whether customers are allowed to select tax display type
        if (!_taxSettings.AllowCustomersToSelectTaxDisplayType)
            return;

        //save passed value
        var customer = await GetCurrentCustomerAsync();
        customer.TaxDisplayType = taxDisplayType;
        await _customerService.UpdateCustomerAsync(customer);

        //then reset the cached value
        _cachedTaxDisplayType = null;
    }

    /// <summary>
    /// Gets or sets value indicating whether we're in admin area
    /// </summary>
    public virtual bool IsAdmin { get; set; }

    #endregion
}