using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Text.Json;

using MesaFabApproval.Shared.Models;

using Microsoft.Extensions.Caching.Memory;

namespace MesaFabApproval.Client.Services;

public interface IAuthenticationService {
    Task<ClaimsPrincipal> SendAuthenticationRequest(string loginId,  string password);
    Task<ClaimsPrincipal> AttemptLocalUserAuth();
    Task<ClaimsPrincipal> FetchAuthState();
    Task ClearTokens();
    Task ClearCurrentUser();
    Task SetTokens(string jwt, string refreshToken);
    Task SetLoginId(string loginId);
    Task SetCurrentUser(User user);
    Task<User> GetCurrentUser();
    Task<AuthTokens> GetAuthTokens();
    Task<string> GetLoginId();
    ClaimsPrincipal GetClaimsPrincipalFromJwt(string jwt);
}

public class AuthenticationService : IAuthenticationService {
    private readonly ILocalStorageService _localStorageService;
    private readonly IMemoryCache _cache;
    private readonly IHttpClientFactory _httpClientFactory;

    public AuthenticationService(ILocalStorageService localStorageService,
                                 IMemoryCache cache,
                                 IHttpClientFactory httpClientFactory) {
        _localStorageService = localStorageService ??
            throw new ArgumentNullException("ILocalStorageService not injected");
        _cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException("IHttpClientFactory not injected");
    }

    public async Task<ClaimsPrincipal> SendAuthenticationRequest(string loginId, string password) {
        if (string.IsNullOrWhiteSpace(loginId)) throw new ArgumentException("loginId cannot be null or empty");
        if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("password cannot be null or empty");

        HttpClient httpClient = _httpClientFactory.CreateClient("API");

        AuthAttempt authAttempt = new() { 
            LoginID = loginId,
            Password = password
        };

        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "auth/login");

        request.Content = new StringContent(JsonSerializer.Serialize(authAttempt),
                                            Encoding.UTF8,
                                            "application/json");

        HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(request);

        if (httpResponseMessage.IsSuccessStatusCode) {
            string responseContent = await httpResponseMessage.Content.ReadAsStringAsync();

            JsonSerializerOptions jsonSerializerOptions = new() {
                PropertyNameCaseInsensitive = true
            };

            LoginResult loginResult = JsonSerializer.Deserialize<LoginResult>(responseContent, jsonSerializerOptions) ??
                throw new Exception("Unable to parse login result from API response");

            if (!loginResult.IsAuthenticated) throw new Exception($"User with Login ID {loginId} not authorized");

            await SetLoginId(loginId);

            await SetTokens(loginResult.AuthTokens.JwtToken, loginResult.AuthTokens.RefreshToken);

            await SetCurrentUser(loginResult.User);

            ClaimsPrincipal principal = GetClaimsPrincipalFromJwt(loginResult.AuthTokens.JwtToken);

            return principal;
        } else {
            throw new Exception($"Login API request failed for {loginId}, because {httpResponseMessage.ReasonPhrase}");
        }
    }

    public async Task<ClaimsPrincipal> AttemptLocalUserAuth() {
        WindowsIdentity? user = WindowsIdentity.GetCurrent();

        if (user is null)
            throw new Exception("no authenticated user found");

        if (!user.IsAuthenticated)
            throw new Exception("you are not authenticated");

        HttpClient httpClient = _httpClientFactory.CreateClient("API");

        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "auth/login/localWindows");

        request.Content = new StringContent(JsonSerializer.Serialize(user),
                                            Encoding.UTF8,
                                            "application/json");

        HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(request);

        if (httpResponseMessage.IsSuccessStatusCode) {
            string responseContent = await httpResponseMessage.Content.ReadAsStringAsync();

            JsonSerializerOptions jsonSerializerOptions = new() {
                PropertyNameCaseInsensitive = true
            };

            LoginResult loginResult = JsonSerializer.Deserialize<LoginResult>(responseContent, jsonSerializerOptions) ??
                throw new Exception("Unable to parse login result from API response");

            if (!loginResult.IsAuthenticated) throw new Exception($"User with Login ID {user.Name} not authorized");

            await SetLoginId(loginResult.User.LoginID);

            await SetTokens(loginResult.AuthTokens.JwtToken, loginResult.AuthTokens.RefreshToken);

            await SetCurrentUser(loginResult.User);

            ClaimsPrincipal principal = GetClaimsPrincipalFromJwt(loginResult.AuthTokens.JwtToken);

            return principal;
        } else {
            throw new Exception($"Login API request failed for {user.Name}, because {httpResponseMessage.ReasonPhrase}");
        }
    }

    public async Task<ClaimsPrincipal> FetchAuthState() {
        string? jwt = _cache.Get<string>("MesaFabApprovalJwt");

        if (jwt is null) jwt = await _localStorageService.GetItem("MesaFabApprovalJwt");

        if (jwt is null) throw new Exception("Unable to find JWT");

        ClaimsPrincipal principal = GetClaimsPrincipalFromJwt(jwt);

        return principal;
    }

    public async Task ClearTokens() {
        _cache.Remove("MesaFabApprovalJwt");
        await _localStorageService.RemoveItem("MesaFabApprovalJwt");
        _cache.Remove("MesaFabApprovalRefreshToken");
        await _localStorageService.RemoveItem("MesaFabApprovalRefreshToken");
    }

    public async Task SetTokens(string jwt, string refreshToken) {
        if (string.IsNullOrWhiteSpace(jwt)) throw new ArgumentNullException("JWT cannot be null or empty");
        if (string.IsNullOrWhiteSpace(refreshToken)) throw new ArgumentNullException("Refresh token cannot be null or empty");

        _cache.Set<string>("MesaFabApprovalJwt", jwt);
        await _localStorageService.AddItem("MesaFabApprovalJwt", jwt);
        _cache.Set<string>("MesaFabApprovalRefreshToken", refreshToken);
        await _localStorageService.AddItem("MesaFabApprovalRefreshToken", refreshToken);
    }

    public async Task SetLoginId(string loginId) {
        if (string.IsNullOrWhiteSpace(loginId)) throw new ArgumentNullException("LoginId cannot be null or empty");

        _cache.Set<string>("MesaFabApprovalUserId", loginId);
        await _localStorageService.AddItem("MesaFabApprovalUserId", loginId);
    }

    public async Task SetCurrentUser(User? user) {
        _cache.Set<User>("MesaFabApprovalCurrentUser", user);
        await _localStorageService.AddItem<User>("MesaFabApprovalCurrentUser", user);
    }

    public async Task ClearCurrentUser() {
        _cache.Remove("MesaFabApprovalCurrentUser");
        await _localStorageService.RemoveItem("MesaFabApprovalCurrentUser");
        _cache.Remove("MesaFabApprovalUserId");
        await _localStorageService.RemoveItem("MesaFabApprovalUserId");
    }

    public async Task<User> GetCurrentUser() {
        User? currentUser = null;

        currentUser = _cache.Get<User>("MesaFabApprovalCurrentUser");

        if (currentUser is null)
            currentUser = await _localStorageService.GetItem<User>("MesaFabApprovalCurrentUser");

        return currentUser;
    }

    public async Task<AuthTokens> GetAuthTokens() {
        AuthTokens? authTokens = null;

        string? jwt = _cache.Get<string>("MesaFabApprovalJwt");
        if (jwt is null) jwt = await _localStorageService.GetItem("MesaFabApprovalJwt");

        string? refreshToken = _cache.Get<string>("MesaFabApprovalRefreshToken");
        if (refreshToken is null) refreshToken = await _localStorageService.GetItem("MesaFabApprovalRefreshToken");

        if (!string.IsNullOrWhiteSpace(jwt) && !string.IsNullOrWhiteSpace(refreshToken)) {
            authTokens = new() { 
                JwtToken = jwt,
                RefreshToken = refreshToken
            };
        }

        return authTokens;
    }

    public async Task<string> GetLoginId() {
        string? loginId = _cache.Get<string>("MesaFabApprovalUserId");
        if (loginId is null) loginId = await _localStorageService.GetItem("MesaFabApprovalUserId");

        return loginId;
    }

    public ClaimsPrincipal GetClaimsPrincipalFromJwt(string jwt) {
        if (string.IsNullOrWhiteSpace(jwt)) throw new ArgumentException("JWT cannot be null or empty");

        JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();

        if (!tokenHandler.CanReadToken(jwt)) {
            throw new Exception("Unable to parse JWT from API");
        }

        JwtSecurityToken jwtSecurityToken = tokenHandler.ReadJwtToken(jwt);
        
        ClaimsIdentity identity = new ClaimsIdentity(jwtSecurityToken.Claims, "MesaFabApprovalWasm");

        return new(identity);
    }
}