@namespace Lumeo
@typeparam TItem
@implements IAsyncDisposable
@inject Lumeo.Services.ComponentInteropService Interop
@inject Lumeo.Services.Localization.ILumeoLocalizer L

<div id="@_containerId" class="@CssClass" @attributes="AdditionalAttributes">
    @foreach (var (item, index) in _items.Select((x, i) => (x, i)))
    {
        <div @key="@(Key is not null ? Key(item) : (object?)item)" class="@GetItemClass(index)"
             data-sortable-item
             data-sortable-index="@index"
             draggable="@(!Disabled ? "true" : "false")"
             @ondragstart="() => OnDragStart(index)"
             @ondragover:preventDefault="true"
             @ondragover="() => OnDragOver(index)"
             @ondragleave="OnDragLeave"
             @ondrop="() => OnDrop(index)"
             @ondragend="OnDragEnd">
            @if (Handle && !Disabled)
            {
                <div class="flex-shrink-0 cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground mr-2 flex items-center justify-center h-11 w-11 -my-2 sm:h-6 sm:w-6 sm:my-0 rounded focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring focus-visible:ring-offset-1"
                     role="button" aria-label="@L["Common.DragHandle"]" tabindex="0">
                    <Blazicon Svg="Lucide.GripVertical" class="h-4 w-4" />
                </div>
            }
            <div class="flex-1 min-w-0">
                @if (ItemTemplate is not null)
                {
                    @ItemTemplate(item)
                }
            </div>
        </div>
    }
</div>

@code {
    [Parameter] public List<TItem> Items { get; set; } = [];
    [Parameter] public EventCallback<List<TItem>> ItemsChanged { get; set; }
    [Parameter] public RenderFragment<TItem>? ItemTemplate { get; set; }
    [Parameter] public bool Handle { get; set; } = true;
    [Parameter] public bool Disabled { get; set; }
    [Parameter] public string? Group { get; set; }

    /// <summary>
    /// Optional selector that returns a stable, unique key for each item.
    /// Providing this ensures Blazor's diffing engine tracks items by identity
    /// rather than index — critical for drag-reorder to map DOM state correctly.
    /// If omitted, the item reference itself is used as the key.
    /// </summary>
    [Parameter] public Func<TItem, object>? Key { get; set; }

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

    private readonly string _containerId = $"sortable-{Guid.NewGuid():N}";
    private List<TItem> _items = [];
    private int _dragFromIndex = -1;
    private int _dragOverIndex = -1;
    private bool _touchRegistered;

    protected override void OnParametersSet()
    {
        _items = Items.ToList();
    }

    // rc.44: HTML5 Drag-and-Drop doesn't fire on touch devices, so we register
    // a parallel touch path that round-trips source/target indices via the
    // interop service. The handler converges with the desktop OnDrop logic.
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && !Disabled && !_touchRegistered)
        {
            await Interop.RegisterSortableTouch(_containerId, HandleTouchDrop);
            _touchRegistered = true;
        }
    }

    private async Task HandleTouchDrop(int source, int target)
    {
        if (Disabled) return;
        await InvokeAsync(async () =>
        {
            if (source < 0 || target < 0 || source >= _items.Count || target >= _items.Count || source == target)
                return;

            var item = _items[source];
            _items.RemoveAt(source);
            _items.Insert(target, item);

            _dragFromIndex = -1;
            _dragOverIndex = -1;

            StateHasChanged();
            await ItemsChanged.InvokeAsync(_items.ToList());
        });
    }

    public async ValueTask DisposeAsync()
    {
        if (_touchRegistered)
        {
            await Interop.UnregisterSortableTouch(_containerId);
            _touchRegistered = false;
        }
    }

    private string CssClass => $"flex flex-col gap-1 {Class}".Trim();

    private string GetItemClass(int index)
    {
        var classes = "flex items-center rounded-md border border-border/40 bg-card px-3 py-2 transition-all touch-none select-none";
        if (index == _dragFromIndex)
            classes += " opacity-50";
        if (index == _dragOverIndex && index != _dragFromIndex)
            classes += " border-primary border-2";
        if (!Handle && !Disabled)
            classes += " cursor-grab active:cursor-grabbing";
        return classes;
    }

    private void OnDragStart(int index)
    {
        _dragFromIndex = index;
    }

    private void OnDragOver(int index)
    {
        _dragOverIndex = index;
    }

    private void OnDragLeave()
    {
        _dragOverIndex = -1;
    }

    private async Task OnDrop(int toIndex)
    {
        if (_dragFromIndex < 0 || _dragFromIndex == toIndex)
        {
            _dragFromIndex = -1;
            _dragOverIndex = -1;
            return;
        }

        var item = _items[_dragFromIndex];
        _items.RemoveAt(_dragFromIndex);
        _items.Insert(toIndex, item);

        _dragFromIndex = -1;
        _dragOverIndex = -1;

        await ItemsChanged.InvokeAsync(_items.ToList());
    }

    private void OnDragEnd()
    {
        _dragFromIndex = -1;
        _dragOverIndex = -1;
    }
}
