@page "/settings/security/change-password"
@using AliasVault.Client.Utilities
@using AliasVault.Client.Main.Models.Validation
@using AliasVault.Shared.Models.WebApi.PasswordChange
@using AliasVault.Shared.Models.WebApi.Vault;
@using AliasVault.Cryptography.Client
@using AliasVault.Client.Services.JsInterop.RustCore
@using AliasVault.Client.Main.Components.Shared
@using AliasVault.Client.Main.Constants
@using AliasVault.Client.Auth.Components
@using Microsoft.Extensions.Localization
@using System.Timers
@inherits MainBase
@inject HttpClient Http
@inject RustCoreService RustCoreService

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

<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
    <div class="mb-4 col-span-full xl:mb-2">
        <Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
        <H1>@Localizer["PageTitle"]</H1>
        <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">@Localizer["PageDescription"]</p>
    </div>
</div>

@if (IsLoading)
{
    <LoadingIndicator />
}
else
{
    <div class="p-4 mb-4  mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
        <EditForm Model="@PasswordChangeFormModel" OnValidSubmit="@InitiatePasswordChange" class="space-y-4">
            <DataAnnotationsValidator />
            <ValidationSummary />

            <div>
                <label for="currentPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["CurrentPasswordLabel"]</label>
                <PasswordInputField
                    Id="currentPassword"
                    @bind-Value="PasswordChangeFormModel.CurrentPassword"
                    Placeholder="@Localizer["CurrentPasswordLabel"]" />
            </div>

            <div>
                <label for="newPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["NewPasswordLabel"]</label>
                <PasswordInputField
                    Id="newPassword"
                    @bind-Value="NewPassword"
                    Placeholder="@Localizer["NewPasswordLabel"]"
                    @onfocus="OnPasswordFocus" />
                <PasswordStrengthIndicator @ref="PasswordStrengthIndicatorRef" Password="@PasswordChangeFormModel.NewPassword" />
            </div>

            <div>
                <label for="newPasswordConfirm" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ConfirmNewPasswordLabel"]</label>
                <PasswordInputField
                    Id="newPasswordConfirm"
                    @bind-Value="NewPasswordConfirm"
                    Placeholder="@Localizer["ConfirmNewPasswordLabel"]"
                    @onfocus="OnPasswordFocus" />
            </div>

            @if (!string.IsNullOrEmpty(PasswordValidationError))
            {
                <div class="mt-2 text-sm text-red-600 dark:text-red-400">
                    @PasswordValidationError
                </div>
            }

            <button type="submit"
                    disabled="@(!IsSubmitEnabled)"
                    class="w-full bg-primary-500 text-white py-2 px-4 rounded-md hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition duration-150 ease-in-out @(IsSubmitEnabled ? "" : "opacity-50 cursor-not-allowed")">
                @Localizer["ChangePasswordButton"]
            </button>
        </EditForm>
    </div>
}

@code {
    /// <summary>
    /// Gets or sets a value indicating whether the component is loading.
    /// </summary>
    private bool IsLoading { get; set; } = true;

    /// <summary>
    /// Gets or sets the password change form model.
    /// </summary>
    private PasswordChangeFormModel PasswordChangeFormModel { get; set; } = new();

    private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Pages.Settings.Security.ChangePassword", "AliasVault.Client");
    private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
    private IStringLocalizer ValidationLocalizer => LocalizerFactory.Create("ValidationMessages", "AliasVault.Client");

    /// <summary>
    /// Gets or sets the current user's password salt.
    /// </summary>
    private string CurrentSalt { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the current user's password server ephemeral.
    /// </summary>
    private string CurrentServerEphemeral { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the current user's password encryption type.
    /// </summary>
    private string CurrentEncryptionType { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the current user's password encryption settings.
    /// </summary>
    private string CurrentEncryptionSettings { get; set; } = string.Empty;

    /// <summary>
    /// Gets or sets the SRP identity for authentication. This is a fixed value that doesn't change
    /// even if the display username is updated.
    /// </summary>
    private string? CurrentSrpIdentity { get; set; }

    private SrpEphemeral ClientEphemeral = new();
    private SrpSession ClientSession = new();
    private string PrivateKey = string.Empty;

    /// <summary>
    /// Reference to the password strength indicator component.
    /// </summary>
    private PasswordStrengthIndicator? PasswordStrengthIndicatorRef { get; set; }

    /// <summary>
    /// Error message for password validation.
    /// </summary>
    private string PasswordValidationError { get; set; } = string.Empty;

    /// <summary>
    /// Timer for debouncing password validation.
    /// </summary>
    private Timer? _passwordDebounceTimer;

    /// <summary>
    /// Wrapper property for new password that triggers validation.
    /// </summary>
    private string NewPassword
    {
        get => PasswordChangeFormModel.NewPassword;
        set
        {
            if (PasswordChangeFormModel.NewPassword != value)
            {
                PasswordChangeFormModel.NewPassword = value;
                ValidatePasswordWithDebounce();
            }
        }
    }

    /// <summary>
    /// Wrapper property for password confirmation that triggers validation.
    /// </summary>
    private string NewPasswordConfirm
    {
        get => PasswordChangeFormModel.NewPasswordConfirm;
        set
        {
            if (PasswordChangeFormModel.NewPasswordConfirm != value)
            {
                PasswordChangeFormModel.NewPasswordConfirm = value;
                ValidatePasswordImmediate();
            }
        }
    }

    /// <summary>
    /// Gets a value indicating whether the submit button should be enabled.
    /// </summary>
    private bool IsSubmitEnabled =>
        !string.IsNullOrWhiteSpace(PasswordChangeFormModel.CurrentPassword) &&
        !string.IsNullOrWhiteSpace(PasswordChangeFormModel.NewPassword) &&
        !string.IsNullOrWhiteSpace(PasswordChangeFormModel.NewPasswordConfirm) &&
        PasswordChangeFormModel.NewPassword == PasswordChangeFormModel.NewPasswordConfirm &&
        string.IsNullOrEmpty(PasswordValidationError) &&
        PasswordStrengthIndicatorRef != null &&
        PasswordStrengthIndicatorRef.MeetsMinimumRequirement();

    /// <summary>
    /// Handles password field focus to clear error messages.
    /// </summary>
    private void OnPasswordFocus(FocusEventArgs e)
    {
        PasswordValidationError = string.Empty;
        StateHasChanged();
    }

    /// <summary>
    /// Validates the password with debounce.
    /// </summary>
    private void ValidatePasswordWithDebounce()
    {
        PasswordValidationError = string.Empty;
        StateHasChanged();

        _passwordDebounceTimer?.Stop();
        _passwordDebounceTimer?.Start();
    }

    /// <summary>
    /// Validates the password immediately (for confirm password field).
    /// </summary>
    private async void ValidatePasswordImmediate()
    {
        await ValidatePasswordDebounced();
    }

    /// <summary>
    /// Validates the password after input has stopped.
    /// </summary>
    private async Task ValidatePasswordDebounced()
    {
        await InvokeAsync(() =>
        {
            var minLength = PasswordStrengthConstants.MinimumGoodPasswordLength;
            if (!string.IsNullOrWhiteSpace(PasswordChangeFormModel.NewPassword) &&
                PasswordChangeFormModel.NewPassword.Length < minLength)
            {
                PasswordValidationError = ValidationLocalizer["PasswordMinLengthGeneric", minLength];
                StateHasChanged();
                return;
            }

            if (!string.IsNullOrWhiteSpace(PasswordChangeFormModel.NewPasswordConfirm) &&
                PasswordChangeFormModel.NewPassword != PasswordChangeFormModel.NewPasswordConfirm)
            {
                PasswordValidationError = ValidationLocalizer["PasswordsDoNotMatchGeneric"];
                StateHasChanged();
                return;
            }

            PasswordValidationError = string.Empty;
            StateHasChanged();
        });
    }

    /// <inheritdoc />
    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();

        BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbSecuritySettings"], Url = "/settings/security" });
        BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbChangePassword"] });

        // Initialize debounce timer
        _passwordDebounceTimer = new Timer(800);
        _passwordDebounceTimer.Elapsed += async (sender, e) => await ValidatePasswordDebounced();
        _passwordDebounceTimer.AutoReset = false;
    }

    /// <inheritdoc />
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _passwordDebounceTimer?.Dispose();
        }

        base.Dispose(disposing);
    }

    /// <inheritdoc />
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);

        // Get the current password ephemeral and salt from the server
        // which is required to confirm the current password.
        if (firstRender)
        {
            await GetCurrentPasswordEphemeralAndSalt();

            IsLoading = false;
            StateHasChanged();
        }
    }

    /// <summary>
    /// Gets the current password ephemeral and salt from the server which
    /// is required to confirm the current password.
    /// </summary>
    private async Task GetCurrentPasswordEphemeralAndSalt()
    {
        var response = await Http.GetFromJsonAsync<PasswordChangeInitiateResponse>("v1/Auth/change-password/initiate");

        if (response == null)
        {
            GlobalNotificationService.AddErrorMessage(Localizer["FailedToInitiatePasswordChange"], true);
            IsLoading = false;
            StateHasChanged();
            return;
        }

        CurrentServerEphemeral = response.ServerEphemeral;
        CurrentSalt = response.Salt;
        CurrentEncryptionType = response.EncryptionType;
        CurrentEncryptionSettings = response.EncryptionSettings;
        CurrentSrpIdentity = response.SrpIdentity;
    }

    /// <summary>
    /// Initiates the password change process.
    /// </summary>
    private async Task InitiatePasswordChange()
    {
        // Clear any previous errors
        PasswordValidationError = string.Empty;

        // Validate password before proceeding
        var minLength = PasswordStrengthConstants.MinimumGoodPasswordLength;
        if (PasswordChangeFormModel.NewPassword.Length < minLength)
        {
            PasswordValidationError = ValidationLocalizer["PasswordMinLengthGeneric", minLength];
            StateHasChanged();
            return;
        }

        // Validate password strength before proceeding
        if (PasswordStrengthIndicatorRef == null || !PasswordStrengthIndicatorRef.MeetsMinimumRequirement())
        {
            PasswordValidationError = ValidationLocalizer["PasswordMinLengthGeneric", PasswordStrengthConstants.MinimumGoodPasswordLength];
            StateHasChanged();
            return;
        }

        GlobalLoadingSpinner.Show(Localizer["ChangingPasswordMessage"]);
        GlobalNotificationService.ClearMessages();
        StateHasChanged();

        // Generate ephemeral for current password to verify it.
        var currentPasswordHash = await Encryption.DeriveKeyFromPasswordAsync(PasswordChangeFormModel.CurrentPassword, CurrentSalt, CurrentEncryptionType, CurrentEncryptionSettings);
        var currentPasswordHashString = BitConverter.ToString(currentPasswordHash).Replace("-", string.Empty);

        ClientEphemeral = await RustCoreService.SrpGenerateEphemeralAsync();
        var username = await GetUsernameAsync();

        // Use srpIdentity from server response if available, otherwise fall back to username.
        // Note: the fallback can be removed in the future after 0.26.0+ is deployed.
        var srpIdentity = CurrentSrpIdentity ?? username.ToLowerInvariant();

        PrivateKey = await RustCoreService.SrpDerivePrivateKeyAsync(CurrentSalt, srpIdentity, currentPasswordHashString);
        ClientSession = await RustCoreService.SrpDeriveSessionClientAsync(
            PrivateKey,
            ClientEphemeral.Secret,
            CurrentServerEphemeral,
            CurrentSalt,
            srpIdentity);

        // Generate salt for new password using Rust WASM.
        // IMPORTANT: We must use the same salt for both key derivation AND SRP verifier generation.
        // Otherwise login will fail because the server returns this salt, but the password hash
        // was derived with a different salt.
        var newSalt = await RustCoreService.SrpGenerateSaltAsync();

        byte[] newPasswordHash = await Encryption.DeriveKeyFromPasswordAsync(PasswordChangeFormModel.NewPassword, newSalt);
        var newPasswordHashString = BitConverter.ToString(newPasswordHash).Replace("-", string.Empty);

        // Backup current password hash in case of failure.
        var backupPasswordHash = AuthService.GetEncryptionKey();

        // Set new currentPasswordHash locally as it is required for the new database encryption call below so
        // it is encrypted with the new password hash.
        await AuthService.StoreEncryptionKeyAsync(newPasswordHash);

        // Derive the SRP verifier using the same salt that was used for key derivation.
        var newPrivateKey = await RustCoreService.SrpDerivePrivateKeyAsync(newSalt, srpIdentity, newPasswordHashString);
        var newVerifier = await RustCoreService.SrpDeriveVerifierAsync(newPrivateKey);

        // Prepare new vault model to update to.
        var encryptedBase64String = await DbService.GetEncryptedDatabaseBase64String();
        var vault = await DbService.PrepareVaultForUploadAsync(encryptedBase64String);

        var vaultPasswordChangeObject = new VaultPasswordChangeRequest
        {
            Username = username,
            Blob = vault.Blob,
            Version = vault.Version,
            CurrentRevisionNumber = vault.CurrentRevisionNumber,
            EncryptionPublicKey = vault.EncryptionPublicKey,
            CredentialsCount = vault.CredentialsCount,
            EmailAddressList = vault.EmailAddressList,
            PrivateEmailDomainList = [],
            HiddenPrivateEmailDomainList = [],
            PublicEmailDomainList = [],
            CreatedAt = vault.CreatedAt,
            UpdatedAt = vault.UpdatedAt,
            CurrentClientPublicEphemeral = ClientEphemeral.Public,
            CurrentClientSessionProof = ClientSession.Proof,
            NewPasswordSalt = newSalt,
            NewPasswordVerifier = newVerifier
        };

        // Clear form.
        PasswordChangeFormModel = new PasswordChangeFormModel();

        // 4. Client sends proof of session key to server.
        try {
            var response = await Http.PostAsJsonAsync("v1/Vault/change-password", vaultPasswordChangeObject);
            var responseContent = await response.Content.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent, ApiErrorLocalizer))
                {
                    GlobalNotificationService.AddErrorMessage(error, true);
                }

                // Set currentPasswordHash back to original, so we're back to the original state.
                await AuthService.StoreEncryptionKeyAsync(backupPasswordHash);

                GlobalLoadingSpinner.Hide();
                StateHasChanged();
                return;
            }

            // Deserialize the response content as a VaultUpdateResponse in case of success.
            var vaultUpdateResponse = await response.Content.ReadFromJsonAsync<VaultUpdateResponse>();

            if (vaultUpdateResponse != null)
            {
                DbService.StoreVaultRevisionNumber(vaultUpdateResponse.NewRevisionNumber);
            }
        }
        catch
        {
            GlobalNotificationService.AddErrorMessage(Localizer["FailedToChangePassword"], true);

            // Set currentPasswordHash back to original, so we're back to the original state.
            await AuthService.StoreEncryptionKeyAsync(backupPasswordHash);

            GlobalLoadingSpinner.Hide();
            StateHasChanged();
            return;
        }

        // Set success message.
        GlobalNotificationService.AddSuccessMessage(Localizer["PasswordChangedSuccessfully"], true);

        // Get the new password ephemeral and salt from the server, which is required if the usre
        // wants to change the password again.
        await GetCurrentPasswordEphemeralAndSalt();

        GlobalLoadingSpinner.Hide();
        StateHasChanged();
    }
}
