@page "/unlock"
@page "/unlock/{SkipWebAuthn:bool}"
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
@inject ILogger<Unlock> Logger
@inject IJSRuntime JSRuntime
@layout Auth.Layout.MainLayout
@using System.Text.Json
@using AliasVault.Client.Auth.Components
@using AliasVault.Client.Auth.Models
@using AliasVault.Client.Services
@using AliasVault.Client.Utilities
@using AliasVault.Shared.Models.WebApi.Auth
@using AliasVault.Cryptography.Client
@using Microsoft.Extensions.Localization

<FullScreenLoadingIndicator @ref="_loadingIndicator" />

@if (IsLoading) {
    <ServerValidationErrors @ref="_serverValidationErrors" />
    <BoldLoadingIndicator />
}
else if (IsWebAuthnLoading) {
    <ServerValidationErrors @ref="_serverValidationErrors" />
    <BoldLoadingIndicator />
    <p class="mt-6 text-center font-normal text-gray-500 dark:text-gray-400">
        @Localizer["LoggingInWithWebAuthn"]
    </p>
}
else
{
    <div class="flex space-x-4">
        <div class="w-8 h-8 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
            <span class="text-primary-600 dark:text-primary-400 text-base font-medium">
                @(Username?.FirstOrDefault().ToString().ToUpper() ?? "?")
            </span>
        </div>
        <h2 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">@Username</h2>
    </div>

    @if (ShowWebAuthnButton)
    {
        <div class="mb-6">
            <p class="text-base font-normal text-gray-500 dark:text-gray-400 mb-4">
                @Localizer["QuickUnlockDescription"]
            </p>

            <ServerValidationErrors @ref="_serverValidationErrors" />

            <div class="flex space-x-4">
                <button type="button" @onclick="UnlockWithWebAuthn" class="flex-grow inline-flex items-center justify-center px-5 py-2 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
                    <svg class="w-5 h-5 mr-2 -ml-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"></path></svg>
                    @Localizer["UnlockWithWebAuthn"]
                </button>
                <button type="button" @onclick="async () => await ShowPasswordLogin()" class="inline-flex items-center justify-center px-5 py-2 text-base font-medium text-center text-gray-900 rounded-lg border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 dark:text-white dark:border-gray-700 dark:hover:bg-gray-700 dark:focus:ring-gray-800">
                    @Localizer["UnlockWithPassword"]
                </button>
            </div>
        </div>
    }
    else
    {
        <p class="text-base font-normal text-gray-500 dark:text-gray-400 mb-4">
            @Localizer["EnterMasterPasswordDescription"]
        </p>

        <ServerValidationErrors @ref="_serverValidationErrors" />

        <EditForm Model="_unlockModel" OnValidSubmit="UnlockSubmit" class="mt-4 space-y-6" av-enable="true" av-suppress-save="true">
            <DataAnnotationsValidator/>
            <div>
                <label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["YourPasswordLabel"]</label>
                <PasswordInputField @ref="passwordField" id="password" @bind-Value="_unlockModel.Password" placeholder="••••••••"/>
                <ValidationMessage For="() => _unlockModel.Password"/>
            </div>

            <button type="submit" id="unlock-button" class="w-full px-5 py-2 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800 flex items-center justify-center gap-2">
                @Localizer["UnlockButton"]
            </button>
        </EditForm>

        <button type="button" id="mobile-unlock-button" @onclick="() => _showMobileUnlockModal = true" class="hidden md:flex w-full px-5 py-2 text-base font-medium text-center text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600 dark:focus:ring-gray-700 items-center justify-center gap-2 mt-4">
            <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
            </svg>
            @Localizer["UnlockWithMobileButton"]
        </button>
    }

    <div class="text-sm text-center font-medium text-gray-500 dark:text-gray-400 mt-6">
        @Localizer["SwitchAccountsText"] <a href="/user/logout" class="text-primary-700 hover:underline dark:text-primary-500">@Localizer["LogOutLink"]</a>
    </div>
}

<FooterLogin />

<MobileUnlockModal IsOpen="@_showMobileUnlockModal"
                   OnClose="() => _showMobileUnlockModal = false"
                   OnSuccess="HandleMobileUnlockSuccess"
                   Mode="unlock" />

@code {
    /// <summary>
    /// Skip automatic WebAuthn unlock during page load if set to true.
    /// </summary>
    [Parameter]
    public bool SkipWebAuthn { get; set; }

    private string? Username { get; set; }
    private bool IsLoading { get; set; } = true;
    private bool IsWebAuthnLoading { get; set; }
    private bool ShowWebAuthnButton { get; set; }
    private bool IsPasswordFocused { get; set; }
    private bool _showMobileUnlockModal;
    private readonly UnlockModel _unlockModel = new();
    private FullScreenLoadingIndicator _loadingIndicator = new();
    private ServerValidationErrors _serverValidationErrors = new();
    private PasswordInputField? passwordField;
    private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Auth.Unlock", "AliasVault.Client");
    private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
    private IStringLocalizer SharedLocalizer => LocalizerFactory.Create("SharedResources", "AliasVault.Client");

    /// <inheritdoc />
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Trigger status API call to check if the user is still authenticated.
            // If user is not authenticated a redirect to the login page will be triggered automatically.
            await StatusCheck();

            // Always check if WebAuthn is enabled
            ShowWebAuthnButton = await AuthService.IsWebAuthnEnabledAsync();

            // Try to unlock with WebAuthn if enabled and not explicitly skipped
            if (ShowWebAuthnButton && !SkipWebAuthn)
            {
                // Add delay to ensure the AliasVault browser extension interceptor doesn't filter
                // this as an "automatic request". The extension blocks WebAuthn requests within
                // 1 second of page load to prevent interference with sites that auto-trigger passkeys.
                await Task.Delay(1100);
                await UnlockWithWebAuthn();
            }

            IsLoading = false;
            StateHasChanged();
        }

        if (!IsLoading && !IsWebAuthnLoading && !ShowWebAuthnButton && !IsPasswordFocused)
        {
            IsPasswordFocused = true;
            await FocusPasswordField();
        }
    }

    /// <summary>
    /// Execute the unlock form submission.
    /// </summary>
    private async Task UnlockSubmit()
    {
        _loadingIndicator.Show(Localizer["UnlockingVaultMessage"]);
        _serverValidationErrors.Clear();

        try
        {
            await StatusCheck();

            // Send request to server with email to get user salt.
            var result = await Http.PostAsJsonAsync("v1/Auth/login", new LoginInitiateRequest(Username!));
            var responseContent = await result.Content.ReadAsStringAsync();

            if (!result.IsSuccessStatusCode)
            {
                var errors = ApiResponseUtility.ParseErrorResponse(responseContent, ApiErrorLocalizer);
                foreach (var error in errors)
                {
                    _serverValidationErrors.AddError(error);
                }

                return;
            }

            var loginResponse = JsonSerializer.Deserialize<LoginInitiateResponse>(responseContent);
            if (loginResponse == null)
            {
                _serverValidationErrors.AddError(Localizer["UnlockRequestError"]);
                return;
            }

            // 3. Client derives shared session key.
            byte[] passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_unlockModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings);

            // Check if the password is correct locally by decrypting the test string.
            var validPassword = await AuthService.ValidateEncryptionKeyAsync(passwordHash);

            if (!validPassword)
            {
                _serverValidationErrors.AddError(Localizer["IncorrectPasswordError"]);
                return;
            }

            // Store the encryption key in memory.
            await AuthService.StoreEncryptionKeyAsync(passwordHash);

            // Redirect to the page the user was trying to access before if set.
            var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>(StorageKeys.ReturnUrl);
            if (!string.IsNullOrEmpty(localStorageReturnUrl))
            {
                await LocalStorage.RemoveItemAsync(StorageKeys.ReturnUrl);
                NavigationManager.NavigateTo(localStorageReturnUrl);
            }
            else
            {
                NavigationManager.NavigateTo("/");
            }
        }
#if DEBUG
        catch (Exception ex)
        {
            // If in debug mode show the actual exception.
            _serverValidationErrors.AddError(ex.ToString());
        }
#else
        catch
        {
            // If in release mode show a generic error.
            _serverValidationErrors.AddError(Localizer["GenericUnlockError"]);
        }
#endif
        finally
        {
            _loadingIndicator.Hide();
        }
    }

    /// <summary>
    /// Make a request to the server to check if access token is still valid.
    /// If not, then this call will automatically result in redirect to the login page.
    /// </summary>
    private async Task StatusCheck()
    {
        await AuthStateProvider.GetAuthenticationStateAsync();
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        if (authState.User.Identity?.IsAuthenticated == false) {
            // Not authenticated (anymore), redirect to login page.
            GlobalNotificationService.AddErrorMessage(Localizer["SessionTimedOutError"]);
            NavigationManager.NavigateTo("/user/login");
            return;
        }

        // Check if encryption key test string is available. If not
        // user should log in again.
        if (!await AuthService.HasEncryptionKeyTestStringAsync())
        {
            // Clear all tokens and redirect to login page.
            await AuthService.RemoveTokensAsync();
            GlobalNotificationService.ClearMessages();
            GlobalNotificationService.AddErrorMessage(Localizer["SessionTimedOutError"]);
            NavigationManager.NavigateTo("/user/login");
            return;
        }

        // Check if username is set.
        // If not, redirect to login page.
        Username = authState.User.Identity?.Name;
        if (Username is null)
        {
            // Clear all tokens and redirect to login page.
            await AuthService.RemoveTokensAsync();
            await AuthStateProvider.GetAuthenticationStateAsync();
            GlobalNotificationService.ClearMessages();
            GlobalNotificationService.AddErrorMessage(Localizer["SessionTimedOutError"]);
            NavigationManager.NavigateTo("/user/login");
            return;
        }

        // Make a request to the server to check if the user is still authenticated.
        // If user has no valid authentication an automatic redirect to login page will take place.
        try
        {
            await Http.GetAsync("v1/Auth/status");
        }
        catch (Exception ex)
        {
            _serverValidationErrors.AddError(Localizer["ConnectionFailedError"]);
            Logger.LogError(ex, "An error occurred while checking the user status.");
            StateHasChanged();
        }
    }

    /// <summary>
    /// Unlock the vault with WebAuthn if enabled.
    /// </summary>
    /// <returns>Task.</returns>
    private async Task UnlockWithWebAuthn()
    {
        await AuthStateProvider.GetAuthenticationStateAsync();
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();

        // Check if webauthn is enabled. If so, try to unlock the vault with it.
        if (await AuthService.IsWebAuthnEnabledAsync())
        {
            IsLoading = false;
            IsWebAuthnLoading = true;
            StateHasChanged();

            try
            {
                var decryptionKey = await AuthService.GetDecryptedWebAuthnEncryptionKeyAsync(authState.User.Identity!.Name!);
                if (decryptionKey.Length == 0)
                {
                    Logger.LogWarning("No decryption key found for user {Username}. Falling back to password unlock.", authState.User.Identity!.Name!);
                    return;
                }

                if (await AuthService.ValidateEncryptionKeyAsync(decryptionKey))
                {
                    await AuthService.StoreEncryptionKeyAsync(decryptionKey);
                    NavigationManager.NavigateTo("/");
                }
                else
                {
                    Logger.LogWarning("The decrypted encryption key does not match the persisted encryption key. Falling back to password unlock.");
                }
            }
            catch (NotSupportedException)
            {
                GlobalNotificationService.AddErrorMessage(Localizer["WebAuthnNotSupportedError"], true);
            }
            catch (Exception ex)
            {
                Logger.LogError(ex, "An error occurred while trying to unlock the vault with WebAuthn.");
            }
            finally
            {
                IsWebAuthnLoading = false;
                StateHasChanged();
            }
        }
    }

    /// <summary>
    /// Show the password login form.
    /// </summary>
    private async Task ShowPasswordLogin()
    {
        ShowWebAuthnButton = false;
        StateHasChanged();

        // Focus the password field after the form is rendered
        if (!IsPasswordFocused)
        {
            IsPasswordFocused = true;
            await FocusPasswordField();
        }
    }

    /// <summary>
    /// Focus the password input field using component reference.
    /// </summary>
    private async Task FocusPasswordField()
    {
        try
        {
            if (passwordField != null)
            {
                await passwordField.FocusAsync();
            }
        }
        catch
        {
            // Do nothing
        }
    }

    /// <summary>
    /// Handle successful mobile unlock.
    /// </summary>
    private async Task HandleMobileUnlockSuccess(MobileLoginResult result)
    {
        _loadingIndicator.Show(Localizer["UnlockingVaultMessage"]);
        _serverValidationErrors.Clear();

        try
        {
            await StatusCheck();

            // Revoke only the current specific token before setting new ones.
            await AuthService.RevokeCurrentTokenAsync();

            // Store the new tokens
            await AuthService.StoreAccessTokenAsync(result.Token);
            await AuthService.StoreRefreshTokenAsync(result.RefreshToken);

            // Convert decryption key from base64 string to byte array
            var decryptionKeyBytes = Convert.FromBase64String(result.DecryptionKey);

            // Store the encryption key in memory
            await AuthService.StoreEncryptionKeyAsync(decryptionKeyBytes);

            await AuthStateProvider.GetAuthenticationStateAsync();

            // Redirect to the page the user was trying to access before if set
            var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>(StorageKeys.ReturnUrl);
            if (!string.IsNullOrEmpty(localStorageReturnUrl))
            {
                await LocalStorage.RemoveItemAsync(StorageKeys.ReturnUrl);
                NavigationManager.NavigateTo(localStorageReturnUrl);
            }
            else
            {
                NavigationManager.NavigateTo("/");
            }
        }
#if DEBUG
        catch (Exception ex)
        {
            // If in debug mode show the actual exception.
            _serverValidationErrors.AddError(ex.ToString());
        }
#else
        catch
        {
            // If in release mode show a generic error.
            _serverValidationErrors.AddError(Localizer["GenericUnlockError"]);
        }
#endif
        finally
        {
            _loadingIndicator.Hide();
        }
    }
}
