using System.DirectoryServices.AccountManagement;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Authentication;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;

using MesaFabApproval.API.Services;
using MesaFabApproval.Models;
using MesaFabApproval.Shared.Models;

using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens;

namespace MesaFabApprovalAPI.Services;

public interface IAuthenticationService {
    public Task<LoginResult> AuthenticateUser(AuthAttempt login);
    public Task<LoginResult> AttemptLocalUserAuth(WindowsIdentity identity);
    public AuthTokens GenerateAuthTokens(AuthAttempt authAttempt, IEnumerable<string> roles);
    public Task<LoginResult> RefreshAuthTokens(AuthAttempt authAttempt);
}

public class AuthenticationService : IAuthenticationService {
    private readonly ILogger<AuthenticationService> _logger;
    private readonly IMemoryCache _cache;
    private readonly IUserService _userService;

    private readonly string _jwtIssuer;
    private readonly string _jwtAudience;
    private readonly string _jwtKey;

    public AuthenticationService(ILogger<AuthenticationService> logger, IMemoryCache cache, IUserService userService, AppSettings appSettings) {
        _logger = logger ?? throw new ArgumentNullException("ILogger not injected");
        _cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
        _userService = userService ?? throw new ArgumentNullException("IUserService not injected");
        _jwtKey = appSettings.JwtKey;
        _jwtIssuer = appSettings.JwtIssuer;
        _jwtAudience = appSettings.JwtAudience;
    }

    public async Task<LoginResult> AuthenticateUser(AuthAttempt login) {
        try {
            _logger.LogInformation("Attempting to authenticate user");

            if (login is null) throw new ArgumentNullException("Login cannot be null");

            string domain = "infineon.com";

            using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domain)) {
                bool isValid = pc.ValidateCredentials(login.LoginID, login.Password);

                if (isValid) {
                    User? user = _cache.Get<User>($"user{login.LoginID}");

                    if (user is null) {
                        user = await _userService.GetUserByLoginId(login.LoginID);

                        _cache.Set<User>($"user{login.LoginID}", user, DateTimeOffset.Now.AddDays(1));
                    }

                    List<string> roles = new();

                    if (user.IsManager) roles.Add("manager");
                    if (user.IsAdmin) roles.Add("admin");

                    AuthTokens tokens = GenerateAuthTokens(login, roles);

                    return new LoginResult {
                        IsAuthenticated = true,
                        AuthTokens = tokens,
                        User = user
                    };
                } else {
                    return new LoginResult() {
                        IsAuthenticated = false,
                        AuthTokens = new() {
                            JwtToken = "",
                            RefreshToken = ""
                        },
                        User = null
                    };
                }
            }
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to authenticate user. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task<LoginResult> AttemptLocalUserAuth(WindowsIdentity identity) {
        try {
            _logger.LogInformation("Attempting to authenticate local Windows system user");

            if (identity is null) throw new ArgumentNullException("WindowsIdentity cannot be null");

            User user = await _userService.GetUserByLoginId(identity.Name);

            List<string> roles = new();

            if (user.IsManager) roles.Add("manager");
            if (user.IsAdmin) roles.Add("admin");

            AuthAttempt authAttempt = new() {
                LoginID = user.LoginID,
            };

            AuthTokens tokens = GenerateAuthTokens(authAttempt, roles);

            return new LoginResult {
                IsAuthenticated = true,
                AuthTokens = tokens,
                User = user
            };
        } catch (Exception ex) {
            _logger.LogError($"Unable to authenticate local Windows system user, because {ex.Message}");
            throw;
        }
    }

    public AuthTokens GenerateAuthTokens(AuthAttempt authAttempt, IEnumerable<string> roles) {
        try {
            _logger.LogInformation("Attempting to generate JWT");

            if (authAttempt is null) throw new ArgumentNullException("AuthAttempt cannot be null");
            if (string.IsNullOrWhiteSpace(authAttempt.LoginID)) throw new ArgumentException("UserName cannot be null or empty");
            if (roles is null) throw new ArgumentNullException("roles cannot be null");

            byte[] key = Encoding.ASCII.GetBytes(_jwtKey);

            List<Claim> claims = new() {
                new Claim(nameof(authAttempt.LoginID), authAttempt.LoginID)
            };

            foreach (string role in roles) {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }

            ClaimsIdentity identity = new ClaimsIdentity(claims);

            SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor {
                Issuer = _jwtIssuer,
                Audience = _jwtAudience,
                Subject = identity,
                NotBefore = DateTime.Now,
                Expires = DateTime.Now.AddHours(8),
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
            };

            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();

            JwtSecurityToken token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);

            string jwt = tokenHandler.WriteToken(token);

            string refreshToken = GenerateRefreshToken();

            List<string>? refreshTokensForUser = _cache.Get<List<string>>(authAttempt.LoginID);

            if (refreshTokensForUser is null)
                refreshTokensForUser = new List<string>();

            if (refreshTokensForUser.Count > 9)
                refreshTokensForUser.RemoveRange(9, refreshTokensForUser.Count - 9);

            refreshTokensForUser.Insert(0, refreshToken);

            _cache.Set<List<string>>(authAttempt.LoginID, refreshTokensForUser, DateTimeOffset.Now.AddHours(4));

            return new AuthTokens {
                JwtToken = jwt,
                RefreshToken = refreshToken
            };
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to generate JWT. Exception: {ex.Message}");
            throw;
        }
    }

    public async Task<LoginResult> RefreshAuthTokens(AuthAttempt authAttempt) {
        try {
            _logger.LogInformation("Attempting to refresh auth tokens");

            if (authAttempt is null) throw new ArgumentNullException("AuthAttempt cannot be null");
            if (authAttempt.AuthTokens is null) throw new ArgumentNullException("AuthTokens cannot be null");

            bool refreshTokenIsValid = IsRefreshTokenValid(authAttempt.LoginID, authAttempt.AuthTokens.RefreshToken);

            if (refreshTokenIsValid) {
                User? user = _cache.Get<User>($"user{authAttempt.LoginID}");

                if (user is null) {
                    user = await _userService.GetUserByLoginId(authAttempt.LoginID);

                    _cache.Set<User>($"user{authAttempt.LoginID}", user, DateTimeOffset.Now.AddDays(1));
                }

                List<string> roles = new();

                if (user.IsManager) roles.Add("manager");
                if (user.IsAdmin) roles.Add("admin");

                AuthTokens refreshedTokens = GenerateAuthTokens(authAttempt, roles);

                LoginResult loginResult = new LoginResult() {
                    IsAuthenticated = true,
                    AuthTokens = refreshedTokens,
                    User = user
                };

                return loginResult;
            } else {
                throw new AuthenticationException("Invalid refresh token");
            }
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to refresh auth tokens. Exception: {ex.Message}");
            throw;
        }
    }

    private string GenerateRefreshToken() {
        byte[] randomNumber = new byte[32];
        using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) {
            rng.GetBytes(randomNumber);
            return Convert.ToBase64String(randomNumber);
        }
    }

    private bool IsRefreshTokenValid(string loginId, string refreshToken) {
        try {
            _logger.LogInformation("Attempting to determine if refresh token is valid");

            if (string.IsNullOrWhiteSpace(loginId)) throw new ArgumentNullException("LoginID cannot be null or empty");
            if (string.IsNullOrWhiteSpace(refreshToken))
                throw new ArgumentNullException("Refresh token cannot be null or empty");

            List<string>? cachedRefreshTokensForUser = _cache.Get<List<string>>(loginId);

            if (cachedRefreshTokensForUser is null || !cachedRefreshTokensForUser.Contains(refreshToken)) {
                _logger.LogInformation($"Could not find cached refresh tokens for user {loginId}");
                return false;
            }

            return true;
        } catch (Exception ex) {
            _logger.LogError($"An exception occurred when attempting to validate refresh token. Exception: {ex.Message}");
            throw;
        }
    }
}