@namespace Lumeo
@typeparam TItem
@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="@ContainerClass" @onclick="FocusInput" @attributes="AdditionalAttributes">
    @if (Tags is { Count: > 0 })
    {
        @foreach (var tag in Tags)
        {
            if (TagTemplate is not null)
            {
                <div class="inline-flex items-center" @key="tag">
                    @TagTemplate(tag)
                    @if (!Disabled)
                    {
                        <button type="button" class="ml-1 rounded-full hover:bg-foreground/20 focus:outline-none" aria-label="@($"Remove {GetTagTextSafe(tag)}")" @onclick="() => RemoveTag(tag)" @onclick:stopPropagation="true">
                            <Blazicon Svg="Lucide.X" class="h-3 w-3" />
                        </button>
                    }
                </div>
            }
            else
            {
                <span class="inline-flex items-center gap-1 rounded-md border border-border/40 bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground" @key="tag">
                    @GetTagTextSafe(tag)
                    @if (!Disabled)
                    {
                        <button type="button" class="rounded-full hover:bg-foreground/20 focus:outline-none" aria-label="@($"Remove {GetTagTextSafe(tag)}")" @onclick="() => RemoveTag(tag)" @onclick:stopPropagation="true">
                            <Blazicon Svg="Lucide.X" class="h-3 w-3" />
                        </button>
                    }
                </span>
            }
        }
    }

    <div class="relative flex-1 min-w-[80px]">
        <input id="@_inputId"
               type="text"
               class="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
               placeholder="@(Tags is { Count: > 0 } ? "" : (Placeholder ?? L["TagInput.Placeholder"]))"
               value="@_inputValue"
               name="@(Name ?? FormField?.Name)"
               aria-required="@(Required ? "true" : "false")"
               aria-invalid="@(EffectiveInvalid ? "true" : "false")"
               @oninput="OnInput"
               @onkeydown="OnKeyDown"
               @onfocus="() => _showSuggestions = FilteredSuggestions.Count > 0"
               disabled="@(IsAtMax || Disabled)"
               autocomplete="off" />

        @if (_showSuggestions && FilteredSuggestions.Count > 0)
        {
            <div id="@_suggestionsId" class="fixed z-50 min-w-[150px] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg animate-fade-in">
                @foreach (var suggestion in FilteredSuggestions)
                {
                    <button type="button"
                            class="flex w-full items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none cursor-pointer transition-colors"
                            @onclick="() => SelectSuggestion(suggestion)">
                        @if (SuggestionTemplate is not null)
                        {
                            @SuggestionTemplate(suggestion)
                        }
                        else
                        {
                            @GetSuggestionTextSafe(suggestion)
                        }
                    </button>
                }
            </div>
        }
    </div>
</div>

@if (!InsideFormField)
{
    @if (_validationError is not null)
    {
        <p class="text-xs text-destructive mt-1.5">@_validationError</p>
    }
    else if (EffectiveInvalid && ErrorText is not null)
    {
        <p class="text-xs text-destructive mt-1.5">@ErrorText</p>
    }
    else if (IsAtMax && (MaxTagsHelperText is not null || MaxTags > 0))
    {
        <p class="text-xs text-muted-foreground mt-1.5">@(MaxTagsHelperText ?? L["TagInput.MaxTagsReached"])</p>
    }
    else if (HelperText is not null)
    {
        <p class="text-xs text-muted-foreground mt-1.5">@HelperText</p>
    }
}

@inject ComponentInteropService Interop
@inject Lumeo.Services.Localization.ILumeoLocalizer L

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

    [Parameter] public List<TItem>? Tags { get; set; }
    [Parameter] public EventCallback<List<TItem>> TagsChanged { get; set; }
    [Parameter] public int MaxTags { get; set; }
    [Parameter] public bool AllowDuplicates { get; set; }
    [Parameter] public string? Placeholder { get; set; }
    [Parameter] public bool Disabled { get; set; }
    [Parameter] public List<TItem>? Suggestions { 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] public string? MaxTagsHelperText { get; set; }

    [Parameter] public Func<TItem, string>? GetTagText { get; set; }
    [Parameter] public Func<TItem, string>? GetSuggestionText { get; set; }
    [Parameter] public RenderFragment<TItem>? TagTemplate { get; set; }
    [Parameter] public RenderFragment<TItem>? SuggestionTemplate { get; set; }
    [Parameter] public string[] Separators { get; set; } = new[] { "," };
    [Parameter] public Func<string, string?>? ValidateInput { get; set; }
    [Parameter] public Func<string, Task<string?>>? ValidateInputAsync { get; set; }
    [Parameter] public Func<string, TItem?>? CreateTag { get; set; }

    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }

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

    private string _inputValue = "";
    private bool _showSuggestions;
    private string? _validationError;
    private readonly string _inputId = $"taginput-{Guid.NewGuid():N}";
    private readonly string _suggestionsId = $"taginput-suggestions-{Guid.NewGuid():N}";
    private bool _suggestionsRegistered;

    private bool IsAtMax => MaxTags > 0 && (Tags?.Count ?? 0) >= MaxTags;

    private string ContainerClass => $"flex flex-wrap items-center gap-1.5 min-h-9 w-full rounded-md border {(EffectiveInvalid ? "border-destructive focus-within:ring-destructive/20" : "border-input focus-within:ring-ring")} bg-transparent px-3 py-1.5 text-sm transition-colors focus-within:ring-[1.5px] {(Disabled ? "cursor-not-allowed opacity-50" : "cursor-text")} {Class}".Trim();

    private string GetTagTextSafe(TItem item)
        => GetTagText is not null ? GetTagText(item) : (item?.ToString() ?? "");

    private string GetSuggestionTextSafe(TItem item)
        => GetSuggestionText is not null ? GetSuggestionText(item)
            : GetTagText is not null ? GetTagText(item)
            : (item?.ToString() ?? "");

    private List<TItem> FilteredSuggestions
    {
        get
        {
            if (Suggestions is null || string.IsNullOrWhiteSpace(_inputValue)) return [];
            var tags = Tags ?? [];
            return Suggestions
                .Where(s => GetSuggestionTextSafe(s).Contains(_inputValue, StringComparison.OrdinalIgnoreCase)
                            && (AllowDuplicates || !tags.Any(t => EqualityComparer<TItem>.Default.Equals(t, s))))
                .Take(8)
                .ToList();
        }
    }

    private async Task OnInput(ChangeEventArgs e)
    {
        _inputValue = e.Value?.ToString() ?? "";
        _validationError = null;

        // Multi-separator splitting on every input event so paste handling works.
        if (Separators is { Length: > 0 } && Separators.Any(sep => !string.IsNullOrEmpty(sep) && _inputValue.Contains(sep)))
        {
            var parts = _inputValue.Split(Separators, StringSplitOptions.RemoveEmptyEntries);
            foreach (var raw in parts)
            {
                var part = raw.Trim();
                if (string.IsNullOrEmpty(part)) continue;
                await TryAddFreeText(part);
            }
            _inputValue = "";
        }

        _showSuggestions = FilteredSuggestions.Count > 0;
    }

    private async Task OnKeyDown(KeyboardEventArgs e)
    {
        switch (e.Key)
        {
            case "Enter":
                if (!string.IsNullOrWhiteSpace(_inputValue))
                {
                    await TryAddFreeText(_inputValue.Trim());
                    if (_validationError is null)
                    {
                        _inputValue = "";
                        _showSuggestions = false;
                    }
                }
                break;
            case "Backspace":
                if (string.IsNullOrEmpty(_inputValue) && Tags is { Count: > 0 })
                {
                    await RemoveTag(Tags[^1]);
                }
                break;
            case "Escape":
                _showSuggestions = false;
                break;
        }
    }

    private async Task TryAddFreeText(string text)
    {
        if (string.IsNullOrWhiteSpace(text)) return;

        // Sync validation
        if (ValidateInput is not null)
        {
            var err = ValidateInput(text);
            if (err is not null)
            {
                _validationError = err;
                return;
            }
        }

        // Async validation
        if (ValidateInputAsync is not null)
        {
            var err = await ValidateInputAsync(text);
            if (err is not null)
            {
                _validationError = err;
                return;
            }
        }

        // Match existing suggestion (use suggestion text for comparison)
        if (Suggestions is not null)
        {
            foreach (var s in Suggestions)
            {
                if (string.Equals(GetSuggestionTextSafe(s), text, StringComparison.OrdinalIgnoreCase))
                {
                    await AddTag(s);
                    return;
                }
            }
        }

        // Convert free text → TItem
        TItem created;
        if (CreateTag is not null)
        {
            var maybe = CreateTag(text);
            if (maybe is null) return;
            created = maybe;
        }
        else if (typeof(TItem) == typeof(string))
        {
            created = (TItem)(object)text;
        }
        else
        {
            throw new InvalidOperationException(
                "Free-text input requires CreateTag callback for non-string TItem");
        }

        await AddTag(created);
    }

    private async Task AddTag(TItem tag)
    {
        if (IsAtMax) return;

        Tags ??= [];
        if (!AllowDuplicates && Tags.Any(t => EqualityComparer<TItem>.Default.Equals(t, tag))) return;

        Tags = [..Tags, tag];
        _validationError = null;
        await TagsChanged.InvokeAsync(Tags);
    }

    private async Task RemoveTag(TItem tag)
    {
        if (Tags is null) return;
        Tags = Tags.Where(t => !EqualityComparer<TItem>.Default.Equals(t, tag)).ToList();
        await TagsChanged.InvokeAsync(Tags);
    }

    private async Task SelectSuggestion(TItem suggestion)
    {
        await AddTag(suggestion);
        _inputValue = "";
        _showSuggestions = false;
        await Interop.FocusElement(_inputId);
    }

    private async Task FocusInput()
    {
        if (!Disabled) await Interop.FocusElement(_inputId);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (_showSuggestions && !_suggestionsRegistered)
        {
            try
            {
                await Interop.PositionFixed(_suggestionsId, _inputId, "start", false, "bottom");
                await Interop.RegisterClickOutside(_suggestionsId, _inputId, async () =>
                {
                    _showSuggestions = false;
                    await InvokeAsync(StateHasChanged);
                });
                _suggestionsRegistered = true;
            }
            catch (JSException) { /* element may not exist yet */ }
        }
        else if (!_showSuggestions && _suggestionsRegistered)
        {
            await CleanupSuggestions();
        }
    }

    private async Task CleanupSuggestions()
    {
        try { await Interop.UnpositionFixed(_suggestionsId); } catch (JSDisconnectedException) { }
        try { await Interop.UnregisterClickOutside(_suggestionsId); } catch (JSDisconnectedException) { }
        _suggestionsRegistered = false;
    }

    public async ValueTask DisposeAsync()
    {
        if (_suggestionsRegistered) await CleanupSuggestions();
    }
}
