@namespace Lumeo

@if (Label is not null && !InsideFormField)
{
    <label class="block text-sm font-medium mb-1.5">
        @Label
        @if (Required)
        {
            <span class="text-destructive ml-0.5">*</span>
        }
    </label>
}

<div class="@CssClass" @attributes="AdditionalAttributes">
    @{
        var groups = GetGroups();
        for (var g = 0; g < groups.Count; g++)
        {
            var group = groups[g];
            <div class="flex items-center gap-2">
                @foreach (var idx in group)
                {
                    var index = idx;
                    <input id="@($"{_baseId}-{index}")"
                           aria-required="@Required.ToString().ToLowerInvariant()"
                           aria-invalid="@EffectiveInvalid.ToString().ToLowerInvariant()"
                           type="@(Masked ? "password" : "text")"
                           inputmode="@InputModeAttr"
                           maxlength="1"
                           autocomplete="one-time-code"
                           class="@($"flex h-10 w-10 items-center justify-center rounded-md border {(EffectiveInvalid ? "border-destructive focus-visible:ring-[1.5px] focus-visible:ring-destructive/20" : "border-input")} bg-transparent text-center text-sm font-medium transition-colors focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50")"
                           disabled="@Disabled"
                           value="@GetChar(index)"
                           @oninput="@(e => HandleInput(index, e))"
                           @onkeydown="@(e => HandleKeyDown(index, e))"
                           @onkeydown:stopPropagation="true" />
                }
            </div>
            @if (g < groups.Count - 1 && Separator is not null)
            {
                <span class="text-muted-foreground text-lg font-medium px-1">@Separator</span>
            }
        }
    }
</div>

@if (!InsideFormField)
{
    @if (EffectiveInvalid && !string.IsNullOrEmpty(ErrorText))
    {
        <p class="mt-1 text-xs text-destructive">@ErrorText</p>
    }
    else if (!string.IsNullOrEmpty(HelperText))
    {
        <p class="mt-1 text-xs text-muted-foreground">@HelperText</p>
    }
}

@implements IAsyncDisposable
@inject ComponentInteropService Interop

@code {
    [CascadingParameter] private FormField.FormFieldContext? FormField { get; set; }

    [Parameter] public int Length { get; set; } = 6;
    [Parameter] public string Value { get; set; } = "";
    [Parameter] public EventCallback<string> ValueChanged { get; set; }
    [Parameter] public EventCallback<string> OnComplete { get; set; }
    [Parameter] public string InputMode { get; set; } = "Numeric";
    [Parameter] public bool Disabled { get; set; }
    [Parameter] public bool Masked { get; set; }
    [Parameter] public string? Separator { get; set; }
    [Parameter] public int[]? GroupSizes { get; set; }
    [Parameter] public bool Required { get; set; }
    [Parameter] public bool Invalid { get; set; }
    [Parameter] public string? ErrorText { get; set; }
    [Parameter] public string? HelperText { get; set; }
    [Parameter] public string? Label { get; set; }
    [Parameter] public string? Name { get; set; }
    [Parameter] public string? Class { get; set; }
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }

    private bool EffectiveInvalid => Invalid || FormField?.HasError == true;
    private bool InsideFormField => FormField is not null;

    private readonly string _baseId = $"otp-{Guid.NewGuid():N}";

    private string CssClass => $"flex items-center gap-2 {Class}".Trim();

    private string InputModeAttr => InputMode switch
    {
        "Alphanumeric" => "text",
        "Password" => "text",
        _ => "numeric"
    };

    private List<List<int>> GetGroups()
    {
        var groups = new List<List<int>>();
        if (GroupSizes is not null && GroupSizes.Length > 0)
        {
            var idx = 0;
            foreach (var size in GroupSizes)
            {
                var group = new List<int>();
                for (var i = 0; i < size && idx < Length; i++, idx++)
                {
                    group.Add(idx);
                }
                if (group.Count > 0) groups.Add(group);
            }
            // Handle any remaining inputs not covered by GroupSizes
            if (idx < Length)
            {
                var remaining = new List<int>();
                for (; idx < Length; idx++) remaining.Add(idx);
                groups.Add(remaining);
            }
        }
        else
        {
            var all = new List<int>();
            for (var i = 0; i < Length; i++) all.Add(i);
            groups.Add(all);
        }
        return groups;
    }

    private string GetChar(int index) => index < Value.Length ? Value[index].ToString() : "";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender) return;
        try
        {
            await Interop.RegisterOtpPaste(_baseId, Length, HandlePaste);
        }
        catch (JSDisconnectedException) { }
        catch (ObjectDisposedException) { }
    }

    private async Task HandlePaste(string text)
    {
        var filtered = FilterInput(text);
        var padded = filtered.PadRight(Length).Substring(0, Length);
        Value = padded.TrimEnd();
        await ValueChanged.InvokeAsync(Value);

        if (Value.Length == Length)
        {
            await OnComplete.InvokeAsync(Value);
        }
        else if (Value.Length > 0)
        {
            await Interop.FocusElement($"{_baseId}-{Value.Length}");
        }
    }

    private bool IsValidChar(char c) => InputMode switch
    {
        "Numeric" => char.IsDigit(c),
        "Alphanumeric" => char.IsLetterOrDigit(c),
        _ => true // Password mode allows any character
    };

    private string FilterInput(string input)
    {
        return new string(input.Where(IsValidChar).ToArray());
    }

    private async Task HandleInput(int index, ChangeEventArgs e)
    {
        var input = e.Value?.ToString() ?? "";
        if (input.Length > 0)
        {
            var lastChar = input[^1];
            if (!IsValidChar(lastChar)) return;

            var chars = Value.PadRight(Length).ToCharArray();
            chars[index] = lastChar;
            Value = new string(chars).TrimEnd();
            await ValueChanged.InvokeAsync(Value);

            if (index < Length - 1)
            {
                await Interop.FocusElement($"{_baseId}-{index + 1}");
            }

            if (Value.Length == Length)
            {
                await OnComplete.InvokeAsync(Value);
            }
        }
    }

    private async Task HandleKeyDown(int index, KeyboardEventArgs e)
    {
        if (e.Key == "Backspace" || e.Key == "Delete")
        {
            var chars = Value.PadRight(Length).ToCharArray();
            if (GetChar(index) != "")
            {
                chars[index] = ' ';
                Value = new string(chars).TrimEnd();
                await ValueChanged.InvokeAsync(Value);
                if (index > 0)
                    await Interop.FocusElement($"{_baseId}-{index - 1}");
            }
            else if (index > 0)
            {
                chars[index - 1] = ' ';
                Value = new string(chars).TrimEnd();
                await ValueChanged.InvokeAsync(Value);
                await Interop.FocusElement($"{_baseId}-{index - 1}");
            }
        }
    }

    public async ValueTask DisposeAsync()
    {
        try
        {
            await Interop.UnregisterOtpPaste(_baseId, Length);
        }
        catch (JSDisconnectedException) { }
    }
}
