using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Blazored.LocalStorage;
using DiplomovaPrace.Contracts.Security;
using DiplomovaPrace.Contracts.Security.ViewModels;
using Microsoft.AspNetCore.Components.Authorization;

namespace DiplomovaPrace.Web.Admin.Infrastructure
{
	public class JwtAuthenticationStateProvider : AuthenticationStateProvider
	{
		private readonly ILocalStorageService localStorageService;
		private readonly ISecurityServiceContract securityServiceContract;

		public JwtAuthenticationStateProvider(ILocalStorageService localStorageService, ISecurityServiceContract securityServiceContract)
		{
			this.localStorageService = localStorageService;
			this.securityServiceContract = securityServiceContract;
		}

		public override async Task<AuthenticationState> GetAuthenticationStateAsync()
		{
			string token = await GetTokenAsync().ConfigureAwait(false);

			ClaimsIdentity identity = string.IsNullOrEmpty(token)
				? new ClaimsIdentity()
				: new ClaimsIdentity(ParseClaimsFromJWT(token), "jwt");

			return new AuthenticationState(new ClaimsPrincipal(identity));
		}

		public async Task SetTokenAsync(string token)
		{
			if (token is null)
			{
				await localStorageService.RemoveItemAsync(nameof(LocalStorageType.AuthorizationToken)).ConfigureAwait(false);
			}
			else
			{
				await localStorageService.SetItemAsync(nameof(LocalStorageType.AuthorizationToken), token).ConfigureAwait(false);
			}

			NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
		}

		public async Task<JwtVM> GetJwtTokenVMAsync()
		{
			string token = await localStorageService.GetItemAsync<string>(nameof(LocalStorageType.AuthorizationToken)).ConfigureAwait(false);

			if (token is not null)
			{
				IEnumerable<Claim> identity = ParseClaimsFromJWT(token);
				JwtVM jwtVM = new();

				foreach (Claim claim in identity)
				{
					switch (Enum.Parse<JwtType>(new DirectoryInfo(claim.Type).Name, true))
					{
						case JwtType.Name:
							jwtVM.Name = claim.Value;
							break;

						case JwtType.Role:
							jwtVM.Role = claim.Value;
							break;

						case JwtType.FirstName:
							jwtVM.FirstName = claim.Value;
							break;

						case JwtType.LastName:
							jwtVM.LastName = claim.Value;
							break;

						case JwtType.Exp:
							jwtVM.Expiry = DateTimeOffset.FromUnixTimeSeconds(long.Parse(claim.Value));
							break;
					}
				}
				return jwtVM;
			}
			return null;
		}

		private async Task<string> GetTokenAsync()
		{
			string token = await localStorageService.GetItemAsync<string>(nameof(LocalStorageType.AuthorizationToken)).ConfigureAwait(false);
			if (token is not null)
			{
				IEnumerable<Claim> identity = ParseClaimsFromJWT(token);
				string expiry = identity.First(i => i.Type.Equals(nameof(JwtType.Exp), StringComparison.InvariantCultureIgnoreCase)).Value;

				if (Convert.ToInt64(expiry) > DateTimeOffset.Now.ToUnixTimeSeconds())
				{
					RefreshToken((int)((Convert.ToInt64(expiry) - DateTimeOffset.Now.ToUnixTimeSeconds()) * 0.95));

					return await localStorageService.GetItemAsync<string>(nameof(LocalStorageType.AuthorizationToken)).ConfigureAwait(false);
				}
				else
				{
					await SetTokenAsync(null).ConfigureAwait(false);
				}
			}

			return null;
		}

		private static IEnumerable<Claim> ParseClaimsFromJWT(string jwt)
		{
			string payload = jwt.Split('.')[1];
			byte[] jsonBytes = ParseBase64WithoutPadding(payload);
			Dictionary<string, object> keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);

			return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
		}

		private static byte[] ParseBase64WithoutPadding(string base64)
		{
			switch (base64.Length % 4)
			{
				case 2: base64 += "=="; break;
				case 3: base64 += "="; break;
			}

			return Convert.FromBase64String(base64);
		}

		private async void RefreshToken(int secondsToRefresh)
		{
			await Task.Delay(secondsToRefresh * 1000).ConfigureAwait(false);

			AuthenticationResult result = await securityServiceContract.RefreshTokenAsync().ConfigureAwait(false);

			if (string.IsNullOrWhiteSpace(result.Token))
			{
				await SetTokenAsync(result.Token).ConfigureAwait(false);
			}
		}
	}
}
