@page "/user/login"
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
@layout Auth.Layout.MainLayout
@attribute [AllowAnonymous]
@using System.Text.Json
@using AliasVault.Shared.Models.WebApi.Auth
@using AliasVault.Client.Auth.Components
@using AliasVault.Client.Utilities
@using AliasVault.Cryptography.Client
@using SecureRemotePassword

@if (_showTwoFactorAuthStep)
{
    <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
        Two-factor authentication
    </h2>

    <ServerValidationErrors @ref="_serverValidationErrors" />

    <p class="text-gray-700 dark:text-gray-300 mb-6">Your login is protected with an authenticator app. Enter your authenticator code below.</p>
    <div class="w-full">
        <EditForm Model="_loginModel2Fa" FormName="login-with-2fa" OnValidSubmit="Handle2Fa" method="post" class="space-y-6">
            <DataAnnotationsValidator/>
            <div>
                <label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authenticator code</label>
                <InputNumber @bind-Value="_loginModel2Fa.TwoFactorCode" id="two-factor-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
                <ValidationMessage For="() => _loginModel2Fa.TwoFactorCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
            </div>
            <div class="flex items-start">
                <div class="flex items-center h-5">
                    <InputCheckbox @bind-Value="_loginModel2Fa.RememberMachine" id="remember-machine" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"/>
                </div>
                <div class="ml-3 text-sm">
                    <label for="remember-machine" class="font-medium text-gray-900 dark:text-white">Remember this machine</label>
                </div>
            </div>
            <button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Log in</button>
        </EditForm>
    </div>
    <p class="mt-6 text-sm text-gray-700 dark:text-gray-300">
        Don't have access to your authenticator device? You can
        <button @onclick="LoginWithRecoveryCode" class="text-primary-600 hover:underline dark:text-primary-500">log in with a recovery code</button>.
    </p>
}
else if (_showLoginWithRecoveryCodeStep)
{
    <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
        Recovery code verification
    </h2>

    <ServerValidationErrors @ref="_serverValidationErrors" />

    <p class="text-gray-700 dark:text-gray-300 mb-6">
        You have requested to log in with a recovery code. A recovery code is a one-time code that can be used to log in to your account.
        Note that if you don't manually disable 2FA after login, you will be asked for an authenticator code again at the next login.
    </p>
    <div class="w-full">
        <EditForm Model="_loginModelRecoveryCode" FormName="login-with-recovery-code" OnValidSubmit="HandleRecoveryCode" method="post" class="space-y-6">
            <DataAnnotationsValidator/>
            <div>
                <label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Recovery Code</label>
                <InputText @bind-Value="_loginModelRecoveryCode.RecoveryCode" id="recovery-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
                <ValidationMessage For="() => _loginModelRecoveryCode.RecoveryCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
            </div>
            <button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Log in</button>
        </EditForm>
    </div>
    <p class="mt-6 text-sm text-gray-700 dark:text-gray-300">
        Regained access to your authenticator device? You can
        <button @onclick="LoginWithAuthenticator" class="text-primary-600 hover:underline dark:text-primary-500">log in with authenticator code</button> instead.
    </p>
}
else
{
    <h2 class="text-2xl font-bold text-gray-900 dark:text-white">
        Log in to AliasVault
    </h2>

    <FullScreenLoadingIndicator @ref="_loadingIndicator"/>
    <ServerValidationErrors @ref="_serverValidationErrors"/>

    <EditForm Model="_loginModel" OnValidSubmit="HandleLogin" class="mt-8 space-y-6">
        <DataAnnotationsValidator/>
        <div>
            <label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your username or email</label>
            <InputTextField id="email" @bind-Value="_loginModel.Username" placeholder="name / name@company.com"/>
            <ValidationMessage For="() => _loginModel.Username"/>
        </div>
        <div>
            <label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
            <InputTextField id="password" @bind-Value="_loginModel.Password" type="password" placeholder="••••••••"/>
            <ValidationMessage For="() => _loginModel.Password"/>
        </div>

        <div class="flex items-start">
            <div class="flex items-center h-5">
                <InputCheckbox @bind-Value="_loginModel.RememberMe" id="remember" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
            </div>
            <div class="ml-3 text-sm">
                <label for="remember" class="font-medium text-gray-900 dark:text-white">Remember me</label>
            </div>
            <a href="/user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">Lost Password?</a>
        </div>

        <button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg 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">Login to your account</button>
        <div class="text-sm font-medium text-gray-500 dark:text-gray-400">
            No account yet? <a href="/user/setup" class="text-primary-700 hover:underline dark:text-primary-500">Create new vault</a>
        </div>
    </EditForm>
}

@code {
    private readonly LoginModel _loginModel = new();
    private readonly LoginModel2Fa _loginModel2Fa = new();
    private readonly LoginModelRecoveryCode _loginModelRecoveryCode = new();
    private FullScreenLoadingIndicator _loadingIndicator = new();
    private ServerValidationErrors _serverValidationErrors = new();
    private bool _showTwoFactorAuthStep;
    private bool _showLoginWithRecoveryCodeStep;

    private SrpEphemeral _clientEphemeral = new();
    private SrpSession _clientSession = new();
    private byte[] _passwordHash = [];

    /// <inheritdoc />
    protected override async Task OnInitializedAsync()
    {
        await AuthStateProvider.GetAuthenticationStateAsync();
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        if (authState.User.Identity?.IsAuthenticated == true) {
            // Already authenticated, redirect to home page.a
            NavigationManager.NavigateTo("/");
        }
    }

    /// <inheritdoc />
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await Task.Delay(300); // Give time for the DOM to update
            await JsInteropService.FocusElementById("email");
        }
    }

    private async Task LoginWithAuthenticator()
    {
        _showLoginWithRecoveryCodeStep = false;
        _showTwoFactorAuthStep = true;
        StateHasChanged();

        await Task.Delay(100); // Give time for the DOM to update
        await JsInteropService.FocusElementById("two-factor-code");
    }

    private void LoginWithRecoveryCode()
    {
        _showLoginWithRecoveryCodeStep = true;
        _showTwoFactorAuthStep = false;
        StateHasChanged();
    }

    /// <summary>
    /// Handle the basic login request with username and password.
    /// </summary>
    private async Task HandleLogin()
    {
        _loadingIndicator.Show();
        _serverValidationErrors.Clear();

        try
        {
            var errors = await ProcessLoginAsync();
            foreach (var error in errors)
            {
                _serverValidationErrors.AddError(error);
            }
        }
#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("An error occurred while processing the login request. Try again (later).");
        }
#endif
        finally
        {
            _loadingIndicator.Hide();
        }
    }

    /// <summary>
    /// Process the login request using username and password.
    /// </summary>
    /// <returns>List of errors if something went wrong.</returns>
    private async Task<List<string>> ProcessLoginAsync()
    {
        GlobalNotificationService.ClearMessages();

        // Sanitize username
        var username = _loginModel.Username.ToLowerInvariant().Trim();

        // Send request to server with username to get server ephemeral public key.
        var result = await Http.PostAsJsonAsync("v1/Auth/login", new LoginInitiateRequest(username));
        var responseContent = await result.Content.ReadAsStringAsync();

        if (!result.IsSuccessStatusCode)
        {
            return ApiResponseUtility.ParseErrorResponse(responseContent);
        }

        var loginResponse = JsonSerializer.Deserialize<LoginInitiateResponse>(responseContent);
        if (loginResponse == null)
        {
            return
            [
                "An error occurred while processing the login request.",
        ];
        }

        // 3. Client derives shared session key.
        _passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_loginModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings);
        var passwordHashString = BitConverter.ToString(_passwordHash).Replace("-", string.Empty);

        _clientEphemeral = Srp.GenerateEphemeralClient();
        var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, username, passwordHashString);
        _clientSession = Srp.DeriveSessionClient(
            privateKey,
            _clientEphemeral.Secret,
            loginResponse.ServerEphemeral,
            loginResponse.Salt,
            username);

        // 4. Client sends proof of session key to server.
        result = await Http.PostAsJsonAsync("v1/Auth/validate", new ValidateLoginRequest(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof));
        responseContent = await result.Content.ReadAsStringAsync();

        if (!result.IsSuccessStatusCode)
        {
            return ApiResponseUtility.ParseErrorResponse(responseContent);
        }

        var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
        if (validateLoginResponse == null)
        {
            return
            [
                "An error occurred while processing the login request.",
        ];
        }

        // Check if 2FA is required, if yes, show 2FA step.
        if (validateLoginResponse.RequiresTwoFactor)
        {
            await LoginWithAuthenticator();
            return [];
        }

        // If no 2FA is required, verify the login.
        return await ProcessLoginVerify(validateLoginResponse);
    }

    /// <summary>
    /// Confirm 2-factor authentication protected login with recovery code.
    /// </summary>
    private async Task HandleRecoveryCode()
    {
        _loadingIndicator.Show();
        _serverValidationErrors.Clear();

        try
        {
            // Sanitize username
            var username = _loginModel.Username.ToLowerInvariant().Trim();

            // Validate 2-factor auth code auth and login
            var result = await Http.PostAsJsonAsync("v1/Auth/validate-recovery-code", new ValidateLoginRequestRecoveryCode(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof, _loginModelRecoveryCode.RecoveryCode));
            var responseContent = await result.Content.ReadAsStringAsync();

            if (!result.IsSuccessStatusCode)
            {
                foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent))
                {
                    _serverValidationErrors.AddError(error);
                }
                return;
            }

            var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
            if (validateLoginResponse == null)
            {
                _serverValidationErrors.AddError("An error occurred while processing the login request.");
                return;
            }

            var errors = await ProcessLoginVerify(validateLoginResponse);
            foreach (var error in errors)
            {
                _serverValidationErrors.AddError(error);
            }
        }
#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("An error occurred while processing the login request. Try again (later).");
        }
#endif
        finally
        {
            _loadingIndicator.Hide();
        }
    }

    /// <summary>
    /// Confirm 2-factor authentication protected login with authenticator code.
    /// </summary>
    private async Task Handle2Fa()
    {
        _loadingIndicator.Show();
        _serverValidationErrors.Clear();

        try
        {
            // Sanitize username
            var username = _loginModel.Username.ToLowerInvariant().Trim();

            // Validate 2-factor auth code auth and login
            var result = await Http.PostAsJsonAsync("v1/Auth/validate-2fa", new ValidateLoginRequest2Fa(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof, _loginModel2Fa.TwoFactorCode ?? 0));
            var responseContent = await result.Content.ReadAsStringAsync();

            if (!result.IsSuccessStatusCode)
            {
                foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent))
                {
                    _serverValidationErrors.AddError(error);
                }
                return;
            }

            var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
            if (validateLoginResponse == null)
            {
                _serverValidationErrors.AddError("An error occurred while processing the login request.");
                return;
            }

            var errors = await ProcessLoginVerify(validateLoginResponse);
            foreach (var error in errors)
            {
                _serverValidationErrors.AddError(error);
            }
        }
#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("An error occurred while processing the login request. Try again (later).");
        }
#endif
        finally
        {
            _loadingIndicator.Hide();
        }
    }

    /// <summary>
    /// Verify the login response and store the tokens.
    /// </summary>
    private async Task<List<string>> ProcessLoginVerify(ValidateLoginResponse validateLoginResponse)
    {
        // 5. Client verifies proof.
        Srp.VerifySession(_clientEphemeral.Public, _clientSession, validateLoginResponse.ServerSessionProof);

        // Store the tokens in local storage.
        await AuthService.StoreAccessTokenAsync(validateLoginResponse.Token!.Token);
        await AuthService.StoreRefreshTokenAsync(validateLoginResponse.Token!.RefreshToken);

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

        await AuthStateProvider.GetAuthenticationStateAsync();
        GlobalNotificationService.ClearMessages();

        // Redirect to the page the user was trying to access before if set.
        var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>("returnUrl");
        if (!string.IsNullOrEmpty(localStorageReturnUrl))
        {
            NavigationManager.NavigateTo(localStorageReturnUrl);
        }
        else
        {
            NavigationManager.NavigateTo("/");
        }

        return [];
    }
}
