using Core.Abstractions;
using DomainObjects;
using CryptoProvider;
using DataProviders.Collections;
using DomainObjects.Documents;
using DomainResults.Common;
using JWTService;
using Microsoft.Extensions.Options;
using WeatherForecast.Models.Account.Requests;

namespace WeatherForecast.Services {


  public interface IAccountPolicyService {
    /// <summary>
    /// 
    /// </summary>
    /// <param name="user"></param>
    /// <param name="token"></param>
    /// <returns></returns>
    IDomainResult Authenticate(UserDocument user, string token);
  }


  /// <summary>
  /// 
  /// </summary>
  public interface IAccountService {
    /// <summary>
    /// 
    /// </summary>
    /// <param name="requestData"></param>
    /// <returns></returns>
    (string?, IDomainResult) Authenticate(AuthenticationRequestModel requestData);

    (Guid?, IDomainResult) PasswordChange(UserDocument user, string newPassword);
  }

  /// <summary>
  /// 
  /// </summary>
  public class AccountService : ServiceBase<AccountService>, IAccountService, IAccountPolicyService {

    private readonly IAesKey? _aesKey;
    private readonly IUserDataProvider _userDataProvider;
    private readonly IJWTService _jwtService;

    /// <summary>
    /// 
    /// </summary>
    /// <param name="logger"></param>
    /// <param name="options"></param>
    /// <param name="userDataProvider"></param>
    /// <param name="jwtService"></param>
    public AccountService(
      ILogger<AccountService> logger,
      IOptions<Configuration> options,
      IUserDataProvider userDataProvider,
      IJWTService jwtService
    ) : base(logger) {
      _aesKey = options.Value.JwtTokenEncryption;
      _userDataProvider = userDataProvider;
      _jwtService = jwtService;
    }


    /// <summary>
    /// 
    /// </summary>
    /// <param name="requestData"></param>
    /// <returns></returns>
    public (string?, IDomainResult) Authenticate(AuthenticationRequestModel requestData) {

      if (_aesKey?.IV == null || _aesKey?.Key == null)
        return IDomainResult.Failed<string?>("IV or Key are not set");

      // Retrieve user from database by userName
      var (user, getUserResult) = _userDataProvider.GetByUsername(requestData.Username);
      if (!getUserResult.IsSuccess || user == null)
        return (null, getUserResult);

      if (user.Passwords.Password == null)
        return IDomainResult.Failed<string?>("Password is not set, create new password.");

      // Check provided password hash with the stored one
      var (salt, hash) = HashService.CreateSaltedHash(requestData.Password);
      if (!HashService.ValidateHash(requestData.Password, salt, hash))
        return IDomainResult.Unauthorized<string?>();

      // Check password expiration if enabled
      if (user.Passwords.Expiration.Enabled && DateTime.UtcNow > user.Passwords.Password.Created.AddDays(user.Passwords.Expiration.Days)) {
        user.Passwords.Expired.Add(user.Passwords.Password.Prototype());

        user.Passwords.Password = null;
        user.Tokens = new List<Token>();

        return IDomainResult.Failed<string?>("Password is expired, create new password.");
      }

      // Creating JWT token
      var claims = new List<KeyValuePair<string, string>> {
        new KeyValuePair<string, string>("unique_name", $"{user.Id}"),

        // (JWT ID): Unique identifier; can be used to prevent the JWT from being replayed (allows a token to be used only once)
        new KeyValuePair<string, string>("jti", $"{Guid.NewGuid()}")
      };

      var created = DateTime.UtcNow;
      var expires = created.AddDays(365);

      var token = _jwtService.CreateJwtToken(expires, claims);

      user.Tokens.Add(new Token {
        Value = AesService.EncryptString(_aesKey.IV, _aesKey.Key, token),
        Created = created,
        Expires = expires,
      });

      var (_, usdateUserResult) = _userDataProvider.Update(user);
      if (!usdateUserResult.IsSuccess)
        return IDomainResult.Failed<string?>();

      return IDomainResult.Success(token);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="user"></param>
    /// <param name="token"></param>
    /// <returns></returns>
    public IDomainResult Authenticate(UserDocument user, string token) {
      if (_aesKey?.IV == null || _aesKey?.Key == null)
        return IDomainResult.Failed("IV or Key are not set");

      #region Tokens cleanup
      var userTokens = user.Tokens.Where(x => x.Expires > DateTime.UtcNow).OrderByDescending(x => x.Expires).Take(10).ToList();

      if (user.Tokens.Count != userTokens.Count) {
        user.Tokens = userTokens;
        _userDataProvider.Update(user);
      }
      #endregion

      // Check if whitelisted
      if (!userTokens.Select(x => AesService.DecryptString(_aesKey.IV, _aesKey.Key, x.Value)).Any(x => string.Compare(x, token) == 0))
        return IDomainResult.Unauthorized();

      return IDomainResult.Success();
    }

    /// <summary>
    /// Passing the Username in the request body is a more secure alternative to passing it as a GET param
    /// </summary>
    /// <param name="requestData"></param>
    /// <returns></returns>
    /// <exception cref="NotImplementedException"></exception>
    public IDomainResult PasswordRecovery(PasswordRecoveryRequestModel requestData) {
      var (user, getUserResult) = _userDataProvider.GetByUsername(requestData.Username);
      if (!getUserResult.IsSuccess || user == null)
        return getUserResult;

      // 

      // send an email

      throw new NotImplementedException();

    }

    /// <summary>
    /// When the form is submitted with the new password and the token as inputs the reset password process will take place.
    /// The form data will be sent with a PUT request again but this time including the token and we will replace the resource password with a new value
    /// </summary>
    /// <param name="requestData"></param>
    /// <returns></returns>
    public IDomainResult PasswordReset(PasswordResetRequestModel requestData) {


      var (salt, hash) = HashService.CreateSaltedHash(requestData.Password);


      return IDomainResult.Success();
    }

    
    public (Guid?, IDomainResult) PasswordChange(UserDocument user, string newPassword) {

      user.SetPassword(newPassword);

      return IDomainResult.Success(user.Id);
    }
  }
}
