@page "/Account/ExternalLogin"

@using System.ComponentModel.DataAnnotations
@using System.Security.Claims
@using System.Text
@using System.Text.Encodings.Web
@using Humanizer
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using Wave.Data

@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<IdentityRole> RoleManager
@inject IUserStore<ApplicationUser> UserStore
@inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ExternalLogin> Logger
@inject IStringLocalizer<ExternalLogin> Localizer

<PageTitle>@Localizer["Title"]</PageTitle>

<StatusMessage Message="@message" />

<BoardComponent CenterContent="true">
	<BoardCardComponent Heading="@Localizer["Title"]">
		<Alert CanRemove="false" Type="Alert.MessageType.Information">
			<p>
				@string.Format(Localizer["Message"], ProviderDisplayName)
			</p>
		</Alert>
		<EditForm Model="Input" OnValidSubmit="OnValidSubmitAsync" FormName="confirmation" method="post">
			<DataAnnotationsValidator/>
			<InputLabelComponent LabelText="@Localizer["Email_Label"]" For="() => Input.Email">
				<InputText @bind-Value="Input.Email" class="input input-bordered w-full"
				           autocomplete="email" placeholder="@Localizer["Email_Placeholder"]" />
			</InputLabelComponent>

			<button type="submit" class="btn btn-primary w-full">
				@Localizer["Submit"]
			</button>
		</EditForm>
	</BoardCardComponent>
</BoardComponent>

@code {
	public const string LoginCallbackAction = "LoginCallback";

	private string? message;
	private ExternalLoginInfo externalLoginInfo = default!;

	[CascadingParameter] 
	private HttpContext HttpContext { get; set; } = default!;

	[SupplyParameterFromForm] 
	private InputModel Input { get; set; } = new();

	[SupplyParameterFromQuery] 
	private string? RemoteError { get; set; }

	[SupplyParameterFromQuery] 
	private string? ReturnUrl { get; set; }

	[SupplyParameterFromQuery] 
	private string? Action { get; set; }

	private string? ProviderDisplayName => externalLoginInfo.ProviderDisplayName;

	protected override async Task OnInitializedAsync() {
		if (RemoteError is not null) {
			RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext);
		}

		var info = await SignInManager.GetExternalLoginInfoAsync();
		if (info is null) {
			RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
		}

		externalLoginInfo = info;

		if (HttpMethods.IsGet(HttpContext.Request.Method)) {
			if (Action == LoginCallbackAction) {
				await OnLoginCallbackAsync();
				return;
			}

			// We should only reach this page via the login callback, so redirect back to
			// the login page if we get here some other way.
			RedirectManager.RedirectTo("Account/Login");
		}
	}

	private async Task OnLoginCallbackAsync() {
		// Sign in the user with this external login provider if the user already has a login.
		var result = await SignInManager.ExternalLoginSignInAsync(
			externalLoginInfo.LoginProvider,
			externalLoginInfo.ProviderKey,
			isPersistent: false,
			bypassTwoFactor: true);

		if (result.Succeeded) {
			Logger.LogInformation(
				"{Name} logged in with {LoginProvider} provider.",
				externalLoginInfo.Principal.Identity?.Name,
				externalLoginInfo.LoginProvider);
			RedirectManager.RedirectTo(ReturnUrl);
		} else if (result.IsLockedOut) {
			RedirectManager.RedirectTo("Account/Lockout");
		}

		// If the user does not have an account, then ask the user to create an account.
		if (externalLoginInfo.Principal.Claims.FirstOrDefault(c => c.Type is ClaimTypes.Email or "email") is {} claim) {
			Input.Email = claim.Value;
		}
	}

	private async Task OnValidSubmitAsync() {
		var emailStore = GetEmailStore();
		var user = CreateUser();

		await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
		await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

		var info = await SignInManager.GetExternalLoginInfoAsync();
		if (info?.Principal.Claims.FirstOrDefault(c => c.Type is ClaimTypes.Name or ClaimTypes.GivenName or "name") is {} nameClaim) {
			user.FullName = nameClaim.Value;
		}

		var result = await UserManager.CreateAsync(user);
		if (result.Succeeded) {
			result = await UserManager.AddLoginAsync(user, externalLoginInfo);
			if (result.Succeeded) {
				Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider);

				string userId = await UserManager.GetUserIdAsync(user);
				string code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
				code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

				string callbackUrl = NavigationManager.GetUriWithQueryParameters(
					NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
					new Dictionary<string, object?> {["userId"] = userId, ["code"] = code});
				await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));

				if (info?.Principal.Claims.FirstOrDefault(c => c.Type is ClaimTypes.Role or "roles") is { } roleClaim) {
					var roles = roleClaim.Value.Split(",").Select(s => s.Trim());

					foreach (string role in roles) {
						try {
							string r = role.Titleize();
							if (r is "Author" or "Reviewer" or "Moderator" or "Admin" && 
							    !await RoleManager.RoleExistsAsync(r) && 
							    !(await RoleManager.CreateAsync(new IdentityRole(r))).Succeeded) {
								Logger.LogError("Failed to create role {role}. Failed to assign it to user {name}.", r, user.UserName);
								continue;
							}

							await UserManager.AddToRoleAsync(user, r);
						} catch (Exception ex) {
							Logger.LogWarning(ex, "Failed to add newly created user {name} to role {role}.", user.UserName, role);
						}
					}
				}

				// If account confirmation is required, we need to show the link if we don't have a real email sender
				if (UserManager.Options.SignIn.RequireConfirmedAccount) {
					RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email });
				}

				await SignInManager.SignInAsync(user, isPersistent: false, externalLoginInfo.LoginProvider);
				RedirectManager.RedirectTo(ReturnUrl);
			}
		}

		message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}";
	}

	private ApplicationUser CreateUser() {
		try {
			return Activator.CreateInstance<ApplicationUser>();
		} catch {
			throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
			                                    $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor");
		}
	}

	private IUserEmailStore<ApplicationUser> GetEmailStore() {
		if (!UserManager.SupportsUserEmail) {
			throw new NotSupportedException("The default UI requires a user store with email support.");
		}

		return (IUserEmailStore<ApplicationUser>) UserStore;
	}

	private sealed class InputModel {
		[Required] [EmailAddress] 
		public string Email { get; set; } = "";
	}

}