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

<th data-slot="datagrid-header-cell" class="@CssClass" style="@StyleString"
    id="@CellId"
    role="columnheader"
    aria-colindex="@AriaColIndex"
    aria-sort="@AriaSort"
    tabindex="@TabIndexValue"
    @onkeydown="HandleKeyDown"
    draggable="@(IsDraggable ? "true" : "false")"
    @ondragstart="HandleDragStart"
    @ondragover="HandleDragOver"
    @ondragover:preventDefault="true"
    @ondrop="HandleDrop"
    @ondragenter="HandleDragEnter"
    @ondragleave="HandleDragLeave"
    @attributes="AdditionalAttributes">
    <div class="flex items-center gap-1">
        @if (Column.HeaderTemplate is not null)
        {
            @Column.HeaderTemplate
        }
        else
        {
            <button type="button"
                    class="flex items-center gap-1 select-none rounded focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring focus-visible:ring-offset-2 @(Column.Sortable ? "cursor-pointer hover:text-foreground" : "cursor-default")"
                    @onclick="HandleSort"
                    disabled="@(!Column.Sortable)">
                <span>@(Column.Title ?? Column.Field ?? "")</span>
                @if (Column.Sortable && SortDirection != SortDirection.None)
                {
                    @if (SortDirection == SortDirection.Ascending)
                    {
                        <Blazicon Svg="Lucide.ArrowUp" class="h-3.5 w-3.5" />
                    }
                    else
                    {
                        <Blazicon Svg="Lucide.ArrowDown" class="h-3.5 w-3.5" />
                    }
                    @if (TotalSortCount > 1 && SortPriority > 0)
                    {
                        <span class="flex items-center justify-center h-3.5 w-3.5 rounded-full bg-primary text-primary-foreground text-[9px] font-bold leading-none">@SortPriority</span>
                    }
                }
                else if (Column.Sortable)
                {
                    <Blazicon Svg="Lucide.ArrowUpDown" class="h-3.5 w-3.5 opacity-30" />
                }
            </button>
        }

        @* 2.2.2: visible drag-to-group affordance on columns that can be
           grouped. Without this, users had to try each column header to
           discover which ones supported drag-into-panel — draggable="true"
           on its own gave no visual cue. Mirrors the filter button's
           opacity-on-hover pattern; hidden when ShowGroupPanel is off (no
           point advertising drag-to-group when there's nowhere to drop). *@
        @if (Context?.ShowGroupPanel == true && Column.Groupable)
        {
            <span class="inline-flex items-center justify-center h-5 w-5 rounded text-muted-foreground opacity-0 group-hover/th:opacity-100 transition-opacity cursor-grab"
                  title="@L["DataGrid.DragToGroup"]">
                <Blazicon Svg="Lucide.Group" class="h-3 w-3" />
            </span>
        }
        @if (Column.Filterable)
        {
            <button type="button"
                    id="@_filterTriggerId"
                    class="inline-flex items-center justify-center h-5 w-5 rounded hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring focus-visible:ring-offset-1 @(HasActiveFilter ? "text-primary" : "text-muted-foreground opacity-0 group-hover/th:opacity-100")"
                    @onclick="ToggleFilter"
                    @onclick:stopPropagation="true">
                <Blazicon Svg="Lucide.ListFilter" class="h-3 w-3" />
            </button>
        }
    </div>

    @if (IsFilterOpen && Column.Filterable)
    {
        @* Rendered with position:fixed and pinned to the trigger via
           Interop.PositionFixed (JS reads the trigger's getBoundingClientRect
           on open + on scroll/resize). Was previously position:absolute relative
           to the <th> which extended the grid's overflow:auto scrollWidth on the
           rightmost columns and made a phantom horizontal scrollbar appear when
           opening the filter. Floating-UI-style positioning sidesteps that
           entirely: the popover lives in the same DOM subtree but its layout
           box doesn't touch the scroll container. *@
        <div id="@_filterContentId" style="position: fixed; z-index: 50;">
            <DataGridColumnFilter TItem="TItem"
                                  Column="Column"
                                  FilterOptions="Column.FilterOptions"
                                  CurrentFilter="@CurrentFilter"
                                  OnApply="HandleFilterApply" />
        </div>
    }

    @if (Column.Resizable)
    {
        <div id="@_resizeHandleId"
             class="absolute right-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-primary/30 active:bg-primary/50"
             style="touch-action: none;"
             @onpointerdown="OnResizeStart"
             @onpointerdown:stopPropagation="true"
             @onpointerdown:preventDefault="true"></div>
    }

    @if (_isDragOver && EffectiveReorderable && _dropSide is not null)
    {
        <div class="lumeo-datagrid-drop-indicator absolute top-0 bottom-0 w-0.5 bg-primary pointer-events-none"
             style="@(_dropSide == "left" ? "left: 0;" : "right: 0;")"></div>
    }
</th>

@code {
    [CascadingParameter] private DataGridContext<TItem>? Context { get; set; }
    [CascadingParameter] private DataGrid<TItem>? ParentGrid { get; set; }
    [CascadingParameter] private DataGridDragState? DragState { get; set; }
    [Inject] private ComponentInteropService Interop { get; set; } = default!;

    [Parameter] public DataGridColumn<TItem> Column { get; set; } = default!;
    [Parameter] public int ColumnIndex { get; set; }
    /// <summary>1-based ARIA column index (includes any leading non-data columns).</summary>
    [Parameter] public int AriaColIndex { get; set; }
    [Parameter] public SortDirection SortDirection { get; set; } = SortDirection.None;
    [Parameter] public bool HasActiveFilter { get; set; }
    [Parameter] public bool IsFilterOpen { get; set; }
    [Parameter] public bool Reorderable { get; set; }
    [Parameter] public EventCallback OnFilterToggle { get; set; }
    [Parameter] public EventCallback<FilterApplyEventArgs> OnFilterApply { get; set; }
    [Parameter] public EventCallback<ColumnReorderEventArgs> OnColumnReorder { get; set; }
    [Parameter] public int SortPriority { get; set; }
    [Parameter] public int TotalSortCount { get; set; }
    [Parameter] public string? Class { get; set; }
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }

    // --- Keyboard navigation / ARIA (rc.32) ---
    private string? CellId => Context is null ? null : $"{Context.GridId}-cell-h-{ColumnIndex}";

    private string? AriaSort => Column.Sortable
        ? SortDirection switch
        {
            SortDirection.Ascending => "ascending",
            SortDirection.Descending => "descending",
            _ => "none"
        }
        : null;

    // Roving tabindex: exactly one header/body cell carries tabindex="0".
    private int TabIndexValue =>
        Context is not null && Context.FocusedRow == -1 && Context.FocusedCol == ColumnIndex ? 0 : -1;

    private async Task HandleKeyDown(KeyboardEventArgs e)
    {
        // Note: we intentionally do NOT preventDefault here. Doing it conditionally
        // per-key in Blazor requires the directive value to be known at render time,
        // which would also block Tab from leaving the grid. Arrow keys may scroll the
        // container a little, but FocusElement() scrolls the target cell back into
        // view, so the net effect is correct.
        if (Context is null) return;

        if ((e.Key == "Enter" || e.Key == " " || e.Key == "Spacebar") && Column.Sortable)
        {
            await HandleSort();
            return;
        }

        await Context.OnHeaderKeyDown(ColumnIndex, e);
    }

    private FilterDescriptor? CurrentFilter =>
        Column?.Field is null || Context is null
            ? null
            : Context.Filters.FirstOrDefault(f => f.Field == Column.Field);

    private string _resizeHandleId = $"dg-resize-{Guid.NewGuid():N}";
    private bool _resizeRegistered;
    private bool _isDragOver;
    private string? _dropSide; // "left" or "right" — where the drop indicator is rendered

    // Filter popover positioning (2.1.2): the filter content is rendered with
    // position:fixed and pinned to the trigger via JS-computed coords from
    // getBoundingClientRect, matching the pattern in PopoverContent. Stable IDs
    // are needed so the JS positioner can find both elements and so click-outside
    // can exclude the trigger from its dismiss target list.
    private readonly string _filterTriggerId = $"dg-filter-trigger-{Guid.NewGuid():N}";
    private readonly string _filterContentId = $"dg-filter-content-{Guid.NewGuid():N}";
    private bool _filterPositionRegistered;

    /// <summary>
    /// Drag-reorder is only active when both the grid-level <c>Reorderable</c>
    /// flag and the per-column <c>Reorderable</c> flag are true.
    /// </summary>
    private bool EffectiveReorderable => Reorderable && Column.Reorderable;

    /// <summary>True when this header is HTML5-draggable. Two reasons to drag a
    /// header: reorder columns (Reorderable, drop on another header) or add the
    /// column as a new grouping level (Groupable, drop on the group panel —
    /// rc.41). The two pathways share <see cref="DataGridDragState"/> for the
    /// source-column record; drop targets decide which action to take.</summary>
    private bool IsDraggable =>
        EffectiveReorderable
        || (Context?.ShowGroupPanel == true && Column.Groupable);

    private string CssClass
    {
        get
        {
            var pinClass = Column.Pin switch
            {
                PinDirection.Left => "sticky left-0 z-20 bg-muted/30",
                PinDirection.Right => "sticky right-0 z-20 bg-muted/30",
                _ => "bg-muted/30"
            };
            return string.Join(" ", new[]
            {
                "group/th relative px-3 py-2 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider",
                "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
                pinClass,
                // cursor-grab signals drag-affordance both for reorder AND for drag-to-group
                // (Groupable + ShowGroupPanel) — previously only the reorder case got it,
                // leaving Groupable-only columns with the default arrow cursor that hid
                // the drag-into-panel UX from users (2.2.2).
                IsDraggable ? "cursor-grab" : null,
                Column.HeaderCssClass,
                Class
            }.Where(s => !string.IsNullOrWhiteSpace(s)));
        }
    }

    private string? StyleString
    {
        get
        {
            var parts = new List<string>();
            if (Column.Width.HasValue)
            {
                parts.Add($"width: {Column.Width.Value}px");
                parts.Add($"min-width: {Column.Width.Value}px");
            }
            if (Column.Pin == PinDirection.Left && Context?.PinnedLeftOffsets.TryGetValue(Column.Id, out var leftOff) == true)
                parts.Add($"left: {leftOff}px");
            else if (Column.Pin == PinDirection.Right && Context?.PinnedRightOffsets.TryGetValue(Column.Id, out var rightOff) == true)
                parts.Add($"right: {rightOff}px");
            return parts.Count > 0 ? string.Join("; ", parts) + ";" : null;
        }
    }

    private async Task HandleSort()
    {
        if (!Column.Sortable || Context is null || Column.Field is null) return;

        var newDirection = SortDirection switch
        {
            SortDirection.None => SortDirection.Ascending,
            SortDirection.Ascending => SortDirection.Descending,
            SortDirection.Descending => SortDirection.None,
            _ => SortDirection.None
        };

        await Context.OnSort.InvokeAsync(new SortDescriptor(Column.Field, newDirection));
    }

    private async Task ToggleFilter()
    {
        await OnFilterToggle.InvokeAsync();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!Column.Filterable) return;

        if (IsFilterOpen && !_filterPositionRegistered)
        {
            // Pin the filter content (position:fixed) to the trigger button's
            // viewport rect. The "start" align + "bottom" side mirror the
            // previous absolute top-full left-0 layout. JS handles flip if the
            // popover would overflow the viewport (e.g. last row on a tall
            // grid) and follows the trigger on scroll/resize.
            await Interop.PositionFixed(_filterContentId, _filterTriggerId, align: "start", matchWidth: false, side: "bottom");
            // Click-outside dismiss — explicitly exclude the trigger so the
            // toggle button doesn't immediately close-then-reopen on click.
            // Closing routes through OnFilterToggle (the same callback the
            // button uses) so parent state stays consistent.
            await Interop.RegisterClickOutside(_filterContentId, _filterTriggerId, async () =>
            {
                await OnFilterToggle.InvokeAsync();
                await InvokeAsync(StateHasChanged);
            });
            _filterPositionRegistered = true;
        }
        else if (!IsFilterOpen && _filterPositionRegistered)
        {
            await CleanupFilterPosition();
        }
    }

    private async Task CleanupFilterPosition()
    {
        try { await Interop.UnpositionFixed(_filterContentId); } catch (JSDisconnectedException) { }
        try { await Interop.UnregisterClickOutside(_filterContentId); } catch (JSDisconnectedException) { }
        _filterPositionRegistered = false;
    }

    private async Task HandleFilterApply(FilterDescriptor? descriptor)
    {
        var args = new FilterApplyEventArgs(Column.Field, descriptor);
        await OnFilterApply.InvokeAsync(args);
    }

    private async Task OnResizeStart(PointerEventArgs e)
    {
        if (!Column.Resizable) return;

        if (!_resizeRegistered)
        {
            // Register once, pass the column's min/max constraints up-front. JS drags the
            // header + body cells in the DOM directly (smooth, no Blazor round-trips per
            // pixel) and invokes the commit callback with the final width on mouseup.
            await Interop.RegisterColumnResize(
                _resizeHandleId,
                Column.MinWidth ?? 50,
                Column.MaxWidth,
                async finalWidth =>
                {
                    ParentGrid?.UpdateColumnWidth(Column.Id, finalWidth);
                    await InvokeAsync(StateHasChanged);
                });
            _resizeRegistered = true;
        }
    }

    // --- Column Drag & Drop Reordering ---
    //
    // Drag state is held by an instance-bound DataGridDragState cascaded from
    // the owning DataGrid<TItem>. Replaces the previous static-class holder
    // which was process-global and cross-talked between grid instances on the
    // same page (drag started in grid A, dropped on grid B → grid B applied
    // grid A's source index). Drop handler verifies SourceGridId matches via
    // DragState.IsOwnDragColumn, otherwise the drop is rejected.

    private void HandleDragStart(DragEventArgs e)
    {
        // rc.41: also start the drag when the column is Groupable + ShowGroupPanel
        // (drag target = the group panel, not another header). The single
        // StartColumnDrag call covers both code paths — reorder vs group is
        // decided at the drop site.
        if (!IsDraggable || DragState is null) return;
        DragState.StartColumnDrag(ColumnIndex, Column.Id);
    }

    private void HandleDragOver(DragEventArgs e)
    {
        if (!EffectiveReorderable || DragState is null) return;
        if (!DragState.IsOwnDragColumn(ColumnIndex)) return;
        if (DragState.SourceColumnId == Column.Id) return;
        // Choose the drop side based on drag direction:
        // dragging from a lower index → drop AFTER this column (right edge).
        // dragging from a higher index → drop BEFORE this column (left edge).
        var src = DragState.SourceColumnIndex;
        _dropSide = src < ColumnIndex ? "right" : "left";
    }

    private void HandleDragEnter(DragEventArgs e)
    {
        if (!EffectiveReorderable || DragState is null) return;
        if (!DragState.IsOwnDragColumn(ColumnIndex)) return;
        if (DragState.SourceColumnId == Column.Id) return;
        _isDragOver = true;
        var src = DragState.SourceColumnIndex;
        _dropSide = src >= 0 && src < ColumnIndex ? "right" : "left";
    }

    private void HandleDragLeave(DragEventArgs e)
    {
        _isDragOver = false;
        _dropSide = null;
    }

    private async Task HandleDrop(DragEventArgs e)
    {
        _isDragOver = false;
        _dropSide = null;
        if (!EffectiveReorderable || DragState is null) return;

        // Reject drops from drags that originated in a different grid instance.
        if (!DragState.IsOwnDragColumn(ColumnIndex))
        {
            DragState.ResetColumn();
            return;
        }

        var sourceIndex = DragState.SourceColumnIndex;
        var sourceId = DragState.SourceColumnId ?? Column.Id;

        // Reset BEFORE invoking the consumer callback so re-entrant drops don't
        // see a stale source index, and so a faulting consumer can't leave the
        // grid in a "drag in progress" state.
        DragState.ResetColumn();

        if (sourceIndex < 0 || sourceIndex == ColumnIndex) return;

        var args = new ColumnReorderEventArgs(sourceId, sourceIndex, ColumnIndex);
        await OnColumnReorder.InvokeAsync(args);
    }

    public async ValueTask DisposeAsync()
    {
        if (_resizeRegistered)
        {
            try
            {
                await Interop.UnregisterColumnResize(_resizeHandleId);
            }
            catch (JSDisconnectedException) { }
        }
        if (_filterPositionRegistered)
        {
            await CleanupFilterPosition();
        }
    }
}
