@namespace Lumeo
@implements IAsyncDisposable

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

<div class="relative" @attributes="AdditionalAttributes">
    <textarea id="@_textareaId"
              class="@TextareaCssClass"
              value="@Value"
              rows="@Rows"
              placeholder="@Placeholder"
              disabled="@Disabled"
              name="@(Name ?? FormField?.Name)"
              aria-required="@(Required ? "true" : "false")"
              aria-invalid="@(EffectiveInvalid ? "true" : "false")"
              @ref="_textareaRef"
              @oninput="HandleInput"
              @onkeydown="HandleKeyDown"
              @onfocus="HandleFocus"
              @onblur="HandleBlur" />

    @if (_showDropdown && FilteredOptions.Any())
    {
        <div id="@_dropdownId" class="@DropdownCssClass"
             style="position: fixed; left: @(_caretLeft)px; top: @(_caretTop + 20)px;">
            @foreach (var (option, index) in FilteredOptions.Select((o, i) => (o, i)))
            {
                var isActive = index == _activeIndex;
                <button @key="option.Value" type="button"
                        class="@OptionCssClass(isActive)"
                        @onmousedown:preventDefault
                        @onclick="() => SelectOption(option)">
                    @if (!string.IsNullOrEmpty(option.Avatar))
                    {
                        <img src="@option.Avatar" alt="" class="h-6 w-6 rounded-full" />
                    }
                    <div class="flex flex-col">
                        <span class="text-sm font-medium text-foreground">@option.Label</span>
                        @if (!string.IsNullOrEmpty(option.Description))
                        {
                            <span class="text-xs text-muted-foreground">@option.Description</span>
                        }
                    </div>
                </button>
            }
        </div>
    }
</div>

@if (!InsideFormField)
{
    @if (EffectiveInvalid && ErrorText is not null)
    {
        <p class="text-xs text-destructive mt-1.5">@ErrorText</p>
    }
    else if (HelperText is not null)
    {
        <p class="text-xs text-muted-foreground mt-1.5">@HelperText</p>
    }
}

@inject ComponentInteropService Interop

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

    [Parameter] public string? Value { get; set; }
    [Parameter] public EventCallback<string> ValueChanged { get; set; }
    [Parameter] public char Trigger { get; set; } = '@';
    [Parameter] public List<MentionOption> Options { get; set; } = [];
    [Parameter] public EventCallback<string> OnSearch { get; set; }
    [Parameter] public bool Disabled { get; set; }
    [Parameter] public string? Placeholder { get; set; }
    [Parameter] public int Rows { get; set; } = 3;
    [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 _textareaId = $"mention-{Guid.NewGuid():N}";
    private readonly string _dropdownId = $"mention-dd-{Guid.NewGuid():N}";
    private ElementReference _textareaRef;
    private bool _showDropdown;
    private string _searchTerm = "";
    private int _triggerStartIndex = -1;
    private int _activeIndex;
    private double _caretTop;
    private double _caretLeft;
    private bool _hasFocus;
    private CancellationTokenSource? _blurDelayCts;

    private string TextareaCssClass => $"flex min-h-[60px] w-full rounded-md border {(EffectiveInvalid ? "border-destructive focus-visible:ring-destructive/20" : "border-border/60 focus-visible:ring-ring")} bg-transparent px-3 py-2 text-sm text-foreground shadow-xs placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 {Class}".Trim();
    private string DropdownCssClass => "z-50 min-w-[200px] max-h-[200px] overflow-auto rounded-md border border-border/60 bg-popover p-1 shadow-lg animate-fade-in";

    private static string OptionCssClass(bool isActive) =>
        $"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left outline-none transition-colors {(isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground")}";

    private List<MentionOption> FilteredOptions =>
        string.IsNullOrEmpty(_searchTerm)
            ? Options
            : Options.Where(o => o.Label.Contains(_searchTerm, StringComparison.OrdinalIgnoreCase) ||
                                 o.Value.Contains(_searchTerm, StringComparison.OrdinalIgnoreCase)).ToList();

    private async Task HandleInput(ChangeEventArgs e)
    {
        var newValue = e.Value?.ToString() ?? "";
        Value = newValue;
        await ValueChanged.InvokeAsync(newValue);

        // Get caret position from JS
        var caretInfo = await Interop.GetTextareaCaretPosition(_textareaId);
        if (caretInfo is null) return;
        var selectionStart = caretInfo.SelectionStart;

        // Check if we are in a mention context
        var textBeforeCaret = selectionStart <= newValue.Length ? newValue[..selectionStart] : newValue;
        var triggerIndex = textBeforeCaret.LastIndexOf(Trigger);

        if (triggerIndex >= 0)
        {
            // Ensure trigger is at start or preceded by whitespace
            var isValidTrigger = triggerIndex == 0 || char.IsWhiteSpace(textBeforeCaret[triggerIndex - 1]);
            var textAfterTrigger = textBeforeCaret[(triggerIndex + 1)..];
            var hasSpace = textAfterTrigger.Contains(' ');

            if (isValidTrigger && !hasSpace)
            {
                _searchTerm = textAfterTrigger;
                _triggerStartIndex = triggerIndex;
                _showDropdown = true;
                _activeIndex = 0;
                _caretTop = caretInfo.Top;
                _caretLeft = caretInfo.Left;

                if (OnSearch.HasDelegate)
                {
                    await OnSearch.InvokeAsync(_searchTerm);
                }
                return;
            }
        }

        _showDropdown = false;
        _searchTerm = "";
        _triggerStartIndex = -1;
    }

    private async Task HandleKeyDown(KeyboardEventArgs e)
    {
        if (!_showDropdown) return;

        var filtered = FilteredOptions;
        switch (e.Key)
        {
            case "ArrowDown":
                _activeIndex = (_activeIndex + 1) % filtered.Count;
                break;
            case "ArrowUp":
                _activeIndex = (_activeIndex - 1 + filtered.Count) % filtered.Count;
                break;
            case "Enter" or "Tab":
                if (filtered.Count > 0 && _activeIndex < filtered.Count)
                {
                    await SelectOption(filtered[_activeIndex]);
                }
                break;
            case "Escape":
                _showDropdown = false;
                // Return focus to the textarea so screen readers announce the correct context.
                try { await _textareaRef.FocusAsync(); } catch { }
                break;
        }
    }

    private async Task SelectOption(MentionOption option)
    {
        if (Value is not null && _triggerStartIndex >= 0)
        {
            var caretInfo = await Interop.GetTextareaCaretPosition(_textareaId);
            if (caretInfo is null) return;
            var selectionStart = caretInfo.SelectionStart;
            var before = Value[.._triggerStartIndex];
            var after = selectionStart <= Value.Length ? Value[selectionStart..] : "";
            Value = $"{before}{Trigger}{option.Value} {after}";
            await ValueChanged.InvokeAsync(Value);
        }

        _showDropdown = false;
        _searchTerm = "";
        _triggerStartIndex = -1;
        _activeIndex = 0;
    }

    private void HandleFocus()
    {
        _hasFocus = true;
        // Cancel any pending blur-hide so re-focusing within the 150 ms window
        // does not close the dropdown.
        _blurDelayCts?.Cancel();
    }

    private async Task HandleBlur()
    {
        _hasFocus = false;
        // Cancel any previous blur delay and start a fresh one.
        _blurDelayCts?.Cancel();
        _blurDelayCts?.Dispose();
        _blurDelayCts = new CancellationTokenSource();
        var token = _blurDelayCts.Token;
        // Small delay to allow click on dropdown item
        try { await Task.Delay(150, token); }
        catch (TaskCanceledException) { return; }
        if (_hasFocus) return; // re-focused during the wait
        _showDropdown = false;
        StateHasChanged();
    }

    public async ValueTask DisposeAsync()
    {
        _blurDelayCts?.Cancel();
        _blurDelayCts?.Dispose();
        _blurDelayCts = null;
        await Task.CompletedTask;
    }

    public record MentionOption(string Label, string Value, string? Avatar = null, string? Description = null);
}
