@using AliasVault.Client.Services.JsInterop.RustCore
@using System.Globalization
@inject DbService DbService
@inject GlobalLoadingService GlobalLoadingService
@inject GlobalNotificationService GlobalNotificationService
@inject RustCoreService RustCoreService
@inject IStringLocalizerFactory LocalizerFactory
@using Microsoft.Extensions.Localization

<ClickOutsideHandler OnClose="OnClose" ContentId="passwordSettingsModal">
    <ModalWrapper OnEnter="HandleEnterKey">
        <div id="passwordSettingsModal" class="relative top-20 mx-auto p-5 pt-0 shadow-lg rounded-md bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-400 w-96 max-w-[90vw]">
            <div class="m-2">
                @* Type switcher (Basic | Diceware). *@
                <div class="flex border-b border-gray-200 dark:border-gray-700">
                    <button type="button" @onclick='() => SetGeneratorType("basic")'
                        class="@GetTabClasses("basic")">
                        @Localizer["PasswordTypeBasic"]
                    </button>
                    <button type="button" @onclick='() => SetGeneratorType("diceware")'
                        class="@GetTabClasses("diceware")">
                        @Localizer["PasswordTypeDiceware"]
                    </button>
                </div>

                <div class="mt-4 space-y-4">
                    @* Preview. *@
                    <div>
                        <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">@Localizer["PreviewLabel"]</label>
                        <div class="mt-1 flex">
                            <CopyPasteFormRow Id="preview-password" Value="@_previewPassword" />
                            <button type="button" class="ml-2 px-3 py-2 text-sm text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-md dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="@(() => RefreshPreview(true))">
                                <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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
                                </svg>
                            </button>
                        </div>
                    </div>

                    @if (!IsDiceware)
                    {
                        @* Basic generator options. *@
                        <div>
                            <label for="password-length" class="block text-sm font-medium text-gray-700 dark:text-gray-300">@string.Format(CultureInfo.CurrentCulture, Localizer["PasswordLengthLabel"], _workingSettings.Length)</label>
                            <input type="range" id="password-length" min="@PasswordLengthSlider.SliderMin.ToString(CultureInfo.InvariantCulture)" max="@PasswordLengthSlider.SliderMax.ToString(CultureInfo.InvariantCulture)" step="0.1"
                                class="mt-1 w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 av-range-slider"
                                value="@_sliderValue.ToString(CultureInfo.InvariantCulture)" @oninput="HandleLengthInput">
                        </div>

                        <div class="grid grid-cols-2 gap-2">
                            <button type="button" class="@GetToggleClasses(_workingSettings.UseLowercase)" title="@Localizer["IncludeLowercaseLabel"]" @onclick="@(() => ToggleCharClass(nameof(_workingSettings.UseLowercase)))">
                                <span class="font-mono text-base">a-z</span>
                            </button>
                            <button type="button" class="@GetToggleClasses(_workingSettings.UseUppercase)" title="@Localizer["IncludeUppercaseLabel"]" @onclick="@(() => ToggleCharClass(nameof(_workingSettings.UseUppercase)))">
                                <span class="font-mono text-base">A-Z</span>
                            </button>
                            <button type="button" class="@GetToggleClasses(_workingSettings.UseNumbers)" title="@Localizer["IncludeNumbersLabel"]" @onclick="@(() => ToggleCharClass(nameof(_workingSettings.UseNumbers)))">
                                <span class="font-mono text-base">0-9</span>
                            </button>
                            <button type="button" class="@GetToggleClasses(_workingSettings.UseSpecialChars)" title="@Localizer["IncludeSpecialCharsLabel"]" @onclick="@(() => ToggleCharClass(nameof(_workingSettings.UseSpecialChars)))">
                                <span class="font-mono text-base">!@@#</span>
                            </button>
                        </div>

                        <div class="flex items-center">
                            <input id="use-non-ambiguous" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
                                @bind="_workingSettings.UseNonAmbiguousChars" @bind:after="OnSettingChanged">
                            <label for="use-non-ambiguous" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">@Localizer["AvoidAmbiguousCharsLabel"]</label>
                        </div>
                    }
                    else
                    {
                        @* Diceware generator options. *@
                        <div>
                            <div class="flex items-center justify-between mb-2">
                                <label for="diceware-word-count" class="text-sm font-medium text-gray-700 dark:text-gray-300">@Localizer["WordCountLabel"]</label>
                                <span class="text-sm text-gray-600 dark:text-gray-400 font-mono">@_workingSettings.WordCount</span>
                            </div>
                            <input type="range" id="diceware-word-count" min="@MinWordCount" max="@MaxWordCount" step="1"
                                class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 av-range-slider"
                                value="@_workingSettings.WordCount" @oninput="HandleWordCountInput">
                        </div>

                        <div class="grid grid-cols-3 gap-2">
                            <button type="button" class="@OptionButtonClass(_workingSettings.Capitalization != DefaultCapitalization)" title="@CapitalizationLabel(_workingSettings.Capitalization)" @onclick="CycleCapitalization">
                                <span class="font-mono text-base">@CapitalizationGlyph(_workingSettings.Capitalization)</span>
                            </button>
                            <button type="button" class="@OptionButtonClass(_workingSettings.Separator != DefaultSeparator)" title="@($"{Localizer["SeparatorLabel"]}: {SeparatorLabel(_workingSettings.Separator)}")" @onclick="CycleSeparator">
                                <span class="font-mono text-base">@SeparatorGlyph(_workingSettings.Separator)</span>
                            </button>
                            <button type="button" class="@OptionButtonClass(_workingSettings.Salt != DefaultSalt)" title="@($"{Localizer["SaltLabel"]}: {SaltLabel(_workingSettings.Salt)}")" @onclick="CycleSalt">
                                <span class="font-mono text-base">@SaltGlyph(_workingSettings.Salt)</span>
                            </button>
                        </div>

                        @* Passphrase wordlist language. *@
                        <div>
                            <label for="diceware-language" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">@Localizer["LanguageLabel"]</label>
                            <select id="diceware-language" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-white focus:ring-primary-500 focus:border-primary-500"
                                @bind="SelectedLanguage" @bind:after="OnSettingChanged">
                                @foreach (var language in _dicewareLanguages)
                                {
                                    <option value="@language">@Languages.GetDisplayLabel(language)</option>
                                }
                            </select>
                        </div>
                    }

                    <div class="flex justify-end pt-4 gap-2">
                        @if (IsTemporary)
                        {
                            <button type="button" class="px-4 py-2 bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-white rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500" @onclick="OnSaveTemporary">
                                @Localizer["UseJustOnceButton"]
                            </button>
                        }
                        <button type="button" id="save-button" class="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-primary-700 dark:hover:bg-primary-600" @onclick="OnSaveGlobal">
                            @Localizer["SaveGloballyButton"]
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </ModalWrapper>
</ClickOutsideHandler>

@code {
    private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Settings.PasswordSettingsPopup", "AliasVault.Client");

    /// <summary>
    /// Inclusive bounds for the Diceware word-count slider.
    /// </summary>
    private const int MinWordCount = AliasClientDb.Models.PasswordGeneratorDefaults.MinWordCount;
    private const int MaxWordCount = AliasClientDb.Models.PasswordGeneratorDefaults.MaxWordCount;

    /// <summary>
    /// Default Diceware option values; an option button is highlighted when its value differs from the default.
    /// </summary>
    private const string DefaultCapitalization = "Lowercase";
    private const string DefaultSeparator = "Dash";
    private const string DefaultSalt = "None";

    private static readonly string[] CapitalizationOptions = ["Lowercase", "TitleCase", "Uppercase"];
    private static readonly string[] SeparatorOptions = ["Dash", "Space", "Underscore", "Dot", "None"];
    private static readonly string[] SaltOptions = ["None", "Prefix", "Sprinkle", "Suffix"];

    /// <summary>
    /// The PasswordSettings to mutate.
    /// </summary>
    [Parameter]
    public PasswordSettings PasswordSettings { get; set; } = new();

    /// <summary>
    /// Whether temporary change is allowed. If true, component will show both global and temporary options.
    /// If false, only global settings are available.
    /// </summary>
    [Parameter]
    public bool IsTemporary { get; set; }

    /// <summary>
    /// Callback invoked when settings have been changed, with a boolean indicating if it's a global save.
    /// </summary>
    [Parameter]
    public EventCallback<(PasswordSettings Settings, string GeneratedPassword)> OnSaveSettings { get; set; }

    /// <summary>
    /// Callback invoked when popup is closed.
    /// </summary>
    [Parameter]
    public EventCallback OnClose { get; set; }

    /// <summary>
    /// Local copy of password settings that are currently being edited.
    /// </summary>
    private PasswordSettings _workingSettings = new();

    /// <summary>
    /// The preview password.
    /// </summary>
    private string _previewPassword = string.Empty;

    /// <summary>
    /// The slider value (0-100) for non-linear password length selection.
    /// </summary>
    private double _sliderValue;

    /// <summary>
    /// Static RNG seed used to generate the same base password/passphrase for easy comparison when changing
    /// options; refreshed on open and when the regenerate button is pressed.
    /// </summary>
    private string _seed = string.Empty;

    /// <summary>
    /// The Diceware wordlist languages available from the Rust core (first is the default, English).
    /// </summary>
    private List<string> _dicewareLanguages = [Languages.DefaultLanguageCode];

    /// <summary>
    /// The passphrase language shown when none is explicitly chosen ("auto"): the most appropriate
    /// available wordlist for the current app language.
    /// </summary>
    private string _effectiveLanguage = Languages.DefaultLanguageCode;

    /// <summary>
    /// Gets a value indicating whether the Diceware generator is currently selected.
    /// </summary>
    private bool IsDiceware => _workingSettings.Type == "diceware";

    /// <summary>
    /// Gets or sets the selected passphrase language for the dropdown. When the user has not made an
    /// explicit choice the resolved "auto" language is shown.
    /// </summary>
    private string SelectedLanguage
    {
        get => string.IsNullOrEmpty(_workingSettings.Language) ? _effectiveLanguage : _workingSettings.Language;
        set => _workingSettings.Language = value;
    }

    /// <inheritdoc />
    protected override async Task OnInitializedAsync()
    {
        // Clone the settings to avoid reference issues.
        _workingSettings = new PasswordSettings
        {
            Length = PasswordSettings.Length,
            UseLowercase = PasswordSettings.UseLowercase,
            UseUppercase = PasswordSettings.UseUppercase,
            UseNumbers = PasswordSettings.UseNumbers,
            UseSpecialChars = PasswordSettings.UseSpecialChars,
            UseNonAmbiguousChars = PasswordSettings.UseNonAmbiguousChars,
            Type = PasswordSettings.Type,
            WordCount = PasswordSettings.WordCount,
            Language = PasswordSettings.Language,
            Capitalization = PasswordSettings.Capitalization,
            Separator = PasswordSettings.Separator,
            Salt = PasswordSettings.Salt,
        };

        // Initialize slider value from password length.
        _sliderValue = PasswordLengthSlider.LengthToSlider(_workingSettings.Length);

        // Use a fixed seed so toggling options regenerates the same base password for easy comparison.
        _seed = RustCoreService.GenerateSeed();

        // Load the Diceware wordlist languages available from the Rust core for the language picker.
        _dicewareLanguages = (await RustCoreService.GetDicewareLanguagesAsync()).ToList();

        // Resolve the "auto" language shown when the user has not picked one explicitly. Uses the same
        // app language + shared resolver as the Rust core, so the dropdown matches the generated preview.
        _effectiveLanguage = Languages.ResolveDefaultLanguage(CultureInfo.CurrentCulture.Name, _dicewareLanguages);

        await RefreshPreview(false);
    }

    /// <summary>
    /// Regenerate the preview. When <paramref name="newSeed"/> is true a genuinely new password is produced,
    /// otherwise the existing seed is reused so only the changed option affects the output.
    /// </summary>
    /// <param name="newSeed">Whether to draw a fresh RNG seed.</param>
    private async Task RefreshPreview(bool newSeed = true)
    {
        if (newSeed)
        {
            _seed = RustCoreService.GenerateSeed();
        }

        try
        {
            _previewPassword = await RustCoreService.GenerateRandomPasswordAsync(_workingSettings, _seed);
        }
        catch
        {
            // If password generation fails, ignore it. This can happen if the settings are invalid.
        }
    }

    /// <summary>
    /// Switch the active generator type and refresh the preview.
    /// </summary>
    /// <param name="type">The generator type ("basic" or "diceware").</param>
    private async Task SetGeneratorType(string type)
    {
        _workingSettings.Type = type;
        await RefreshPreview(false);
    }

    /// <summary>
    /// Handle changes to a setting by regenerating the preview using the existing seed.
    /// </summary>
    private async Task OnSettingChanged()
    {
        await RefreshPreview(false);
    }

    /// <summary>
    /// Toggle one of the basic character-class settings.
    /// </summary>
    /// <param name="propertyName">The name of the boolean property to toggle.</param>
    private async Task ToggleCharClass(string propertyName)
    {
        switch (propertyName)
        {
            case nameof(_workingSettings.UseLowercase):
                _workingSettings.UseLowercase = !_workingSettings.UseLowercase;
                break;
            case nameof(_workingSettings.UseUppercase):
                _workingSettings.UseUppercase = !_workingSettings.UseUppercase;
                break;
            case nameof(_workingSettings.UseNumbers):
                _workingSettings.UseNumbers = !_workingSettings.UseNumbers;
                break;
            case nameof(_workingSettings.UseSpecialChars):
                _workingSettings.UseSpecialChars = !_workingSettings.UseSpecialChars;
                break;
        }

        await RefreshPreview(false);
    }

    /// <summary>
    /// Handle input from the password length slider.
    /// </summary>
    private async Task HandleLengthInput(ChangeEventArgs e)
    {
        if (double.TryParse(e.Value?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var sliderValue))
        {
            _sliderValue = sliderValue;
            _workingSettings.Length = PasswordLengthSlider.SliderToLength(sliderValue);
            await RefreshPreview(false);
        }
    }

    /// <summary>
    /// Handle input from the Diceware word-count slider.
    /// </summary>
    private async Task HandleWordCountInput(ChangeEventArgs e)
    {
        if (int.TryParse(e.Value?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var wordCount))
        {
            _workingSettings.WordCount = wordCount;
            await RefreshPreview(false);
        }
    }

    /// <summary>
    /// Advance the capitalization option to the next value.
    /// </summary>
    private async Task CycleCapitalization()
    {
        _workingSettings.Capitalization = Cycle(CapitalizationOptions, _workingSettings.Capitalization);
        await RefreshPreview(false);
    }

    /// <summary>
    /// Advance the separator option to the next value.
    /// </summary>
    private async Task CycleSeparator()
    {
        _workingSettings.Separator = Cycle(SeparatorOptions, _workingSettings.Separator);
        await RefreshPreview(false);
    }

    /// <summary>
    /// Advance the salt option to the next value.
    /// </summary>
    private async Task CycleSalt()
    {
        _workingSettings.Salt = Cycle(SaltOptions, _workingSettings.Salt);
        await RefreshPreview(false);
    }

    /// <summary>
    /// Persist changed password settings globally in vault.
    /// </summary>
    private async Task OnSaveGlobal()
    {
        // Save globally to DB.
        GlobalLoadingService.Show();
        var settingsJson = System.Text.Json.JsonSerializer.Serialize(_workingSettings);
        await DbService.Settings.SetSettingAsync("PasswordGenerationSettings", settingsJson);
        GlobalLoadingService.Hide();
        GlobalNotificationService.AddSuccessMessage(Localizer["SettingsUpdatedMessage"], true);

        // Notify parent with both settings and the generated password.
        await OnSaveSettings.InvokeAsync((_workingSettings, _previewPassword));
        await OnClose.InvokeAsync();
    }

    /// <summary>
    /// Do not persist changes in vault but just return the new settings to the parent component.
    /// </summary>
    private async Task OnSaveTemporary()
    {
        await OnSaveSettings.InvokeAsync((_workingSettings, _previewPassword));
        await OnClose.InvokeAsync();
    }

    /// <summary>
    /// Handle Enter key press, submits the form based on context.
    /// </summary>
    private async Task HandleEnterKey()
    {
        if (IsTemporary)
        {
            await OnSaveTemporary();
        }
        else
        {
            await OnSaveGlobal();
        }
    }

    /// <summary>
    /// Get the CSS classes for a generator type tab.
    /// </summary>
    /// <param name="type">The generator type the tab represents.</param>
    private string GetTabClasses(string type)
    {
        var isActive = (string.IsNullOrEmpty(_workingSettings.Type) ? "basic" : _workingSettings.Type) == type;
        var baseClasses = "-mb-px flex-1 border-b-2 px-3 py-3 text-sm font-medium transition-colors";
        return isActive
            ? $"{baseClasses} border-primary-600 text-primary-600 dark:border-primary-500 dark:text-primary-500"
            : $"{baseClasses} border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200";
    }

    /// <summary>
    /// Get the CSS classes for a basic character-class toggle button.
    /// </summary>
    /// <param name="enabled">Whether the toggle is currently enabled.</param>
    private static string GetToggleClasses(bool enabled)
    {
        var baseClasses = "flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors";
        return enabled
            ? $"{baseClasses} bg-primary-600 text-white hover:bg-primary-700"
            : $"{baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600";
    }

    /// <summary>
    /// Get the CSS classes for a Diceware cycle (option) button.
    /// </summary>
    /// <param name="customized">Whether the option differs from its default value.</param>
    private static string OptionButtonClass(bool customized)
    {
        var baseClasses = "flex items-center justify-center px-2 py-2 rounded-md transition-colors";
        return customized
            ? $"{baseClasses} bg-primary-600 text-white hover:bg-primary-700"
            : $"{baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600";
    }

    /// <summary>
    /// Return the next value in a list of options, wrapping around.
    /// </summary>
    private static string Cycle(string[] options, string current)
    {
        var index = Array.IndexOf(options, current);
        return options[(index + 1) % options.Length];
    }

    /// <summary>
    /// Get the glyph for the capitalization option.
    /// </summary>
    /// <param name="value">The capitalization option value.</param>
    private static string CapitalizationGlyph(string value) => value switch
    {
        "Uppercase" => "ABC",
        "TitleCase" => "Abc",
        _ => "abc",
    };

    /// <summary>
    /// Get the glyph for the separator option.
    /// </summary>
    /// <param name="value">The separator option value.</param>
    private static string SeparatorGlyph(string value) => value switch
    {
        "Dash" => "-",
        "Space" => "␣",
        "Underscore" => "_",
        "Dot" => ".",
        _ => "∅",
    };

    /// <summary>
    /// Get the glyph for the salt option.
    /// </summary>
    /// <param name="value">The salt option value.</param>
    private static string SaltGlyph(string value) => value switch
    {
        "Prefix" => "#ab",
        "Sprinkle" => "a#b",
        "Suffix" => "ab#",
        _ => "ab",
    };

    /// <summary>
    /// Get the label for the capitalization option.
    /// </summary>
    /// <param name="value">The capitalization option value.</param>
    private string CapitalizationLabel(string value) => Localizer[$"Capitalization{value}"];

    /// <summary>
    /// Get the label for the separator option.
    /// </summary>
    /// <param name="value">The separator option value.</param>
    private string SeparatorLabel(string value) => Localizer[$"Separator{value}"];

    /// <summary>
    /// Get the label for the salt option.
    /// </summary>
    /// <param name="value">The salt option value.</param>
    private string SaltLabel(string value) => Localizer[$"Salt{value}"];
}
