using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Nop.Core;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Localization;
using Nop.Core.Events;
using Nop.Core.Http;
using Nop.Core.Http.Extensions;
using Nop.Data;
using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Localization;
using Nop.Services.Messages;

namespace Nop.Services.Authentication.External;

/// <summary>
/// Represents external authentication service implementation
/// </summary>
public partial class ExternalAuthenticationService : IExternalAuthenticationService
{
    #region Fields

    protected readonly CustomerSettings _customerSettings;
    protected readonly ExternalAuthenticationSettings _externalAuthenticationSettings;
    protected readonly IActionContextAccessor _actionContextAccessor;
    protected readonly IAuthenticationPluginManager _authenticationPluginManager;
    protected readonly ICustomerRegistrationService _customerRegistrationService;
    protected readonly ICustomerService _customerService;
    protected readonly IEventPublisher _eventPublisher;
    protected readonly IGenericAttributeService _genericAttributeService;
    protected readonly IHttpContextAccessor _httpContextAccessor;
    protected readonly ILocalizationService _localizationService;
    protected readonly IRepository<ExternalAuthenticationRecord> _externalAuthenticationRecordRepository;
    protected readonly IStoreContext _storeContext;
    protected readonly IUrlHelperFactory _urlHelperFactory;
    protected readonly IWorkContext _workContext;
    protected readonly IWorkflowMessageService _workflowMessageService;
    protected readonly LocalizationSettings _localizationSettings;

    #endregion

    #region Ctor

    public ExternalAuthenticationService(CustomerSettings customerSettings,
        ExternalAuthenticationSettings externalAuthenticationSettings,
        IActionContextAccessor actionContextAccessor,
        IAuthenticationPluginManager authenticationPluginManager,
        ICustomerRegistrationService customerRegistrationService,
        ICustomerService customerService,
        IEventPublisher eventPublisher,
        IGenericAttributeService genericAttributeService,
        IHttpContextAccessor httpContextAccessor,
        ILocalizationService localizationService,
        IRepository<ExternalAuthenticationRecord> externalAuthenticationRecordRepository,
        IStoreContext storeContext,
        IUrlHelperFactory urlHelperFactory,
        IWorkContext workContext,
        IWorkflowMessageService workflowMessageService,
        LocalizationSettings localizationSettings)
    {
        _customerSettings = customerSettings;
        _externalAuthenticationSettings = externalAuthenticationSettings;
        _actionContextAccessor = actionContextAccessor;
        _authenticationPluginManager = authenticationPluginManager;
        _customerRegistrationService = customerRegistrationService;
        _customerService = customerService;
        _eventPublisher = eventPublisher;
        _genericAttributeService = genericAttributeService;
        _httpContextAccessor = httpContextAccessor;
        _localizationService = localizationService;
        _externalAuthenticationRecordRepository = externalAuthenticationRecordRepository;
        _storeContext = storeContext;
        _urlHelperFactory = urlHelperFactory;
        _workContext = workContext;
        _workflowMessageService = workflowMessageService;
        _localizationSettings = localizationSettings;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Authenticate user with existing associated external account
    /// </summary>
    /// <param name="associatedUser">Associated with passed external authentication parameters user</param>
    /// <param name="currentLoggedInUser">Current logged-in user</param>
    /// <param name="returnUrl">URL to which the user will return after authentication</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result of an authentication
    /// </returns>
    protected virtual async Task<IActionResult> AuthenticateExistingUserAsync(Customer associatedUser, Customer currentLoggedInUser, string returnUrl)
    {
        //log in guest user
        if (currentLoggedInUser == null)
            return await _customerRegistrationService.SignInCustomerAsync(associatedUser, returnUrl);

        //account is already assigned to another user
        if (currentLoggedInUser.Id != associatedUser.Id)
            return await ErrorAuthenticationAsync(new[]
            {
                await _localizationService.GetResourceAsync("Account.AssociatedExternalAuth.AccountAlreadyAssigned")
            }, returnUrl);

        //or the user try to log in as himself. bit weird
        return SuccessfulAuthentication(returnUrl);
    }

    /// <summary>
    /// Authenticate current user and associate new external account with user
    /// </summary>
    /// <param name="currentLoggedInUser">Current logged-in user</param>
    /// <param name="parameters">Authentication parameters received from external authentication method</param>
    /// <param name="returnUrl">URL to which the user will return after authentication</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result of an authentication
    /// </returns>
    protected virtual async Task<IActionResult> AuthenticateNewUserAsync(Customer currentLoggedInUser, ExternalAuthenticationParameters parameters, string returnUrl)
    {
        //associate external account with logged-in user
        if (currentLoggedInUser != null)
        {
            await AssociateExternalAccountWithUserAsync(currentLoggedInUser, parameters);

            return SuccessfulAuthentication(returnUrl);
        }

        //or try to register new user
        if (_customerSettings.UserRegistrationType != UserRegistrationType.Disabled)
            return await RegisterNewUserAsync(parameters, returnUrl);

        //registration is disabled
        return await ErrorAuthenticationAsync(new[] { "Registration is disabled" }, returnUrl);
    }

    /// <summary>
    /// Register new user
    /// </summary>
    /// <param name="parameters">Authentication parameters received from external authentication method</param>
    /// <param name="returnUrl">URL to which the user will return after authentication</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result of an authentication
    /// </returns>
    protected virtual async Task<IActionResult> RegisterNewUserAsync(ExternalAuthenticationParameters parameters, string returnUrl)
    {
        //check whether the specified email has been already registered
        if (await _customerService.GetCustomerByEmailAsync(parameters.Email) != null)
        {
            var alreadyExistsError = string.Format(await _localizationService.GetResourceAsync("Account.AssociatedExternalAuth.EmailAlreadyExists"),
                !string.IsNullOrEmpty(parameters.ExternalDisplayIdentifier) ? parameters.ExternalDisplayIdentifier : parameters.ExternalIdentifier);
            return await ErrorAuthenticationAsync(new[] { alreadyExistsError }, returnUrl);
        }

        //registration is approved if validation isn't required
        var registrationIsApproved = _customerSettings.UserRegistrationType == UserRegistrationType.Standard ||
                                     (_customerSettings.UserRegistrationType == UserRegistrationType.EmailValidation && !_externalAuthenticationSettings.RequireEmailValidation);

        //create registration request
        var customer = await _workContext.GetCurrentCustomerAsync();
        var store = await _storeContext.GetCurrentStoreAsync();
        var registrationRequest = new CustomerRegistrationRequest(customer,
            parameters.Email, parameters.Email,
            CommonHelper.GenerateRandomDigitCode(20),
            PasswordFormat.Hashed,
            store.Id,
            registrationIsApproved);

        //whether registration request has been completed successfully
        var registrationResult = await _customerRegistrationService.RegisterCustomerAsync(registrationRequest);
        if (!registrationResult.Success)
            return await ErrorAuthenticationAsync(registrationResult.Errors, returnUrl);

        //allow to save other customer values by consuming this event
        await _eventPublisher.PublishAsync(new CustomerAutoRegisteredByExternalMethodEvent(customer, parameters));

        //raise customer registered event
        await _eventPublisher.PublishAsync(new CustomerRegisteredEvent(customer));

        //store owner notifications
        if (_customerSettings.NotifyNewCustomerRegistration)
            await _workflowMessageService.SendCustomerRegisteredStoreOwnerNotificationMessageAsync(customer, _localizationSettings.DefaultAdminLanguageId);

        //associate external account with registered user
        await AssociateExternalAccountWithUserAsync(customer, parameters);

        //authenticate
        var currentLanguage = await _workContext.GetWorkingLanguageAsync();
        if (registrationIsApproved)
        {
            await _workflowMessageService.SendCustomerWelcomeMessageAsync(customer, currentLanguage.Id);

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

            return await _customerRegistrationService.SignInCustomerAsync(customer, returnUrl, true);
        }

        //registration is succeeded but isn't activated
        if (_customerSettings.UserRegistrationType == UserRegistrationType.EmailValidation)
        {
            //email validation message
            await _genericAttributeService.SaveAttributeAsync(customer, NopCustomerDefaults.AccountActivationTokenAttribute, Guid.NewGuid().ToString());
            await _workflowMessageService.SendCustomerEmailValidationMessageAsync(customer, currentLanguage.Id);

            return new RedirectToRouteResult(NopRouteNames.Standard.REGISTER_RESULT, new { resultId = (int)UserRegistrationType.EmailValidation, returnUrl });
        }

        //registration is succeeded but isn't approved by admin
        if (_customerSettings.UserRegistrationType == UserRegistrationType.AdminApproval)
            return new RedirectToRouteResult(NopRouteNames.Standard.REGISTER_RESULT, new { resultId = (int)UserRegistrationType.AdminApproval, returnUrl });

        return await ErrorAuthenticationAsync(new[] { "Error on registration" }, returnUrl);
    }

    /// <summary>
    /// Add errors that occurred during authentication
    /// </summary>
    /// <param name="errors">Collection of errors</param>
    /// <param name="returnUrl">URL to which the user will return after authentication</param>
    /// <returns>Result of an authentication</returns>
    protected virtual async Task<IActionResult> ErrorAuthenticationAsync(IEnumerable<string> errors, string returnUrl)
    {
        var session = _httpContextAccessor.HttpContext?.Session;

        if (session != null)
        {
            var existsErrors = (await session.GetAsync<IList<string>>(NopAuthenticationDefaults.ExternalAuthenticationErrorsSessionKey))?.ToList() ?? new List<string>();

            existsErrors.AddRange(errors);

            await session.SetAsync(NopAuthenticationDefaults.ExternalAuthenticationErrorsSessionKey, existsErrors);
        }

        return new RedirectToActionResult("Login", "Customer", !string.IsNullOrEmpty(returnUrl) ? new { ReturnUrl = returnUrl } : null);
    }

    /// <summary>
    /// Redirect the user after successful authentication
    /// </summary>
    /// <param name="returnUrl">URL to which the user will return after authentication</param>
    /// <returns>Result of an authentication</returns>
    protected virtual IActionResult SuccessfulAuthentication(string returnUrl)
    {
        var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);

        //redirect to the return URL if it's specified
        if (!string.IsNullOrEmpty(returnUrl) && urlHelper.IsLocalUrl(returnUrl))
            return new RedirectResult(returnUrl);

        return new RedirectToRouteResult(NopRouteNames.General.HOMEPAGE, null);
    }

    #endregion

    #region Methods

    #region Authentication

    /// <summary>
    /// Authenticate user by passed parameters
    /// </summary>
    /// <param name="parameters">External authentication parameters</param>
    /// <param name="returnUrl">URL to which the user will return after authentication</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result of an authentication
    /// </returns>
    public virtual async Task<IActionResult> AuthenticateAsync(ExternalAuthenticationParameters parameters, string returnUrl = null)
    {
        ArgumentNullException.ThrowIfNull(parameters);

        var customer = await _workContext.GetCurrentCustomerAsync();
        var store = await _storeContext.GetCurrentStoreAsync();
        if (!await _authenticationPluginManager.IsPluginActiveAsync(parameters.ProviderSystemName, customer, store.Id))
            return await ErrorAuthenticationAsync(new[] { "External authentication method cannot be loaded" }, returnUrl);

        //get current logged-in user
        var currentLoggedInUser = await _customerService.IsRegisteredAsync(customer) ? customer : null;

        //authenticate associated user if already exists
        var associatedUser = await GetUserByExternalAuthenticationParametersAsync(parameters);
        if (associatedUser != null)
            return await AuthenticateExistingUserAsync(associatedUser, currentLoggedInUser, returnUrl);

        //or associate and authenticate new user
        return await AuthenticateNewUserAsync(currentLoggedInUser, parameters, returnUrl);
    }

    #endregion

    /// <summary>
    /// Get the external authentication records by identifier
    /// </summary>
    /// <param name="externalAuthenticationRecordId">External authentication record identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<ExternalAuthenticationRecord> GetExternalAuthenticationRecordByIdAsync(int externalAuthenticationRecordId)
    {
        return await _externalAuthenticationRecordRepository.GetByIdAsync(externalAuthenticationRecordId, cache => default, useShortTermCache: true);
    }

    /// <summary>
    /// Get list of the external authentication records by customer
    /// </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<IList<ExternalAuthenticationRecord>> GetCustomerExternalAuthenticationRecordsAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var associationRecords = _externalAuthenticationRecordRepository.Table.Where(ear => ear.CustomerId == customer.Id);

        return await associationRecords.ToListAsync();
    }

    /// <summary>
    /// Delete the external authentication record
    /// </summary>
    /// <param name="externalAuthenticationRecord">External authentication record</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteExternalAuthenticationRecordAsync(ExternalAuthenticationRecord externalAuthenticationRecord)
    {
        ArgumentNullException.ThrowIfNull(externalAuthenticationRecord);

        await _externalAuthenticationRecordRepository.DeleteAsync(externalAuthenticationRecord, false);
    }

    /// <summary>
    /// Get the external authentication record
    /// </summary>
    /// <param name="parameters">External authentication parameters</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<ExternalAuthenticationRecord> GetExternalAuthenticationRecordByExternalAuthenticationParametersAsync(ExternalAuthenticationParameters parameters)
    {
        ArgumentNullException.ThrowIfNull(parameters);

        var associationRecord = await _externalAuthenticationRecordRepository.Table.FirstOrDefaultAsync(record =>
            record.ExternalIdentifier.Equals(parameters.ExternalIdentifier) && record.ProviderSystemName.Equals(parameters.ProviderSystemName));

        return associationRecord;
    }

    /// <summary>
    /// Associate external account with customer
    /// </summary>
    /// <param name="customer">Customer</param>
    /// <param name="parameters">External authentication parameters</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task AssociateExternalAccountWithUserAsync(Customer customer, ExternalAuthenticationParameters parameters)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var externalAuthenticationRecord = new ExternalAuthenticationRecord
        {
            CustomerId = customer.Id,
            Email = parameters.Email,
            ExternalIdentifier = parameters.ExternalIdentifier,
            ExternalDisplayIdentifier = parameters.ExternalDisplayIdentifier,
            OAuthAccessToken = parameters.AccessToken,
            ProviderSystemName = parameters.ProviderSystemName
        };

        await _externalAuthenticationRecordRepository.InsertAsync(externalAuthenticationRecord, false);
    }

    /// <summary>
    /// Get the particular user with specified parameters
    /// </summary>
    /// <param name="parameters">External authentication parameters</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the customer
    /// </returns>
    public virtual async Task<Customer> GetUserByExternalAuthenticationParametersAsync(ExternalAuthenticationParameters parameters)
    {
        ArgumentNullException.ThrowIfNull(parameters);

        var associationRecord = await _externalAuthenticationRecordRepository.Table.FirstOrDefaultAsync(record =>
            record.ExternalIdentifier.Equals(parameters.ExternalIdentifier) && record.ProviderSystemName.Equals(parameters.ProviderSystemName));
        if (associationRecord == null)
            return null;

        return await _customerService.GetCustomerByIdAsync(associationRecord.CustomerId);
    }

    /// <summary>
    /// Remove the association
    /// </summary>
    /// <param name="parameters">External authentication parameters</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task RemoveAssociationAsync(ExternalAuthenticationParameters parameters)
    {
        ArgumentNullException.ThrowIfNull(parameters);

        var associationRecord = await _externalAuthenticationRecordRepository.Table.FirstOrDefaultAsync(record =>
            record.ExternalIdentifier.Equals(parameters.ExternalIdentifier) && record.ProviderSystemName.Equals(parameters.ProviderSystemName));

        if (associationRecord != null)
            await _externalAuthenticationRecordRepository.DeleteAsync(associationRecord, false);
    }


    #endregion
}