@namespace Lumeo
@typeparam TItem

<td data-slot="datagrid-cell" class="@CssClass" style="@PinStyle"
    id="@CellId"
    role="gridcell"
    aria-colindex="@AriaColIndex"
    aria-readonly="@AriaReadonly"
    tabindex="@TabIndexValue"
    @onclick="HandleClick"
    @ondblclick="HandleDoubleClick"
    @onkeydown="HandleCellKeyDown"
    @attributes="AdditionalAttributes">
    @if (_isEditing && Column.EditTemplate is not null)
    {
        @Column.EditTemplate(_editContext!)
    }
    else if (_isEditing)
    {
        <input class="w-full bg-transparent border-none outline-none text-sm"
               value="@_editValue"
               @oninput="OnEditInput"
               @onblur="CommitEdit"
               @onkeydown="OnEditKeyDown" />
    }
    else if (Column.CellTemplate is not null)
    {
        @Column.CellTemplate(Item)
    }
    else if (IsBatchDirty)
    {
        @* Batch-edit: show the buffered value (overrides the row's current
           value) so the user sees what will be saved. *@
        @FormatBatchValue(Context!.GetBatchValue(Item, Column))
    }
    else
    {
        @Column.GetFormattedValue(Item, Context?.Culture ?? System.Globalization.CultureInfo.CurrentCulture)
    }
</td>

@code {
    [CascadingParameter] private DataGridContext<TItem>? Context { get; set; }
    [Parameter] public TItem Item { get; set; } = default!;
    [Parameter] public DataGridColumn<TItem> Column { get; set; } = default!;
    /// <summary>0-based index of the owning row within DisplayedItems (non-grouped).</summary>
    [Parameter] public int RowIndex { get; set; }
    /// <summary>0-based index of this column within the visible columns.</summary>
    [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; }
    /// <summary>Whether this cell participates in roving-tabindex keyboard navigation.
    /// False in grouped mode where row indices are not globally unique.</summary>
    [Parameter] public bool Navigable { get; set; } = true;
    [Parameter] public string? Class { get; set; }
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }

    private bool _isEditing;
    private string? _editValue;
    private object? _originalValue;
    private CellEditContext<TItem>? _editContext;

    // --- Keyboard navigation / ARIA (rc.32) ---
    // Cell-level inline editing applies in both Cell and Batch modes — the
    // difference is where the commit lands (immediate vs buffered).
    private bool CellEditable =>
        (Context?.EditMode == DataGridEditMode.Cell || Context?.EditMode == DataGridEditMode.Batch)
        && Column.Field is not null;

    // True when this (row, column) currently carries a buffered batch edit.
    // Drives the dirty marker (left border accent) and the display override.
    private bool IsBatchDirty =>
        Context?.IsBatchEdit == true
        && Column.Field is not null
        && Context.IsCellDirty(Item, Column.Field!);

    private string? AriaReadonly => CellEditable ? null : "true";

    private string FormatBatchValue(object? value)
    {
        if (value is null) return "";
        var culture = Context?.Culture ?? System.Globalization.CultureInfo.CurrentCulture;
        if (value is IFormattable f)
            return f.ToString(Column.Format, culture);
        return value.ToString() ?? "";
    }

    private string? CellId =>
        Navigable && Context is not null ? $"{Context.GridId}-cell-{RowIndex}-{ColumnIndex}" : null;

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

    private async Task HandleCellKeyDown(KeyboardEventArgs e)
    {
        if (!Navigable || Context is null) return;

        // Enter / F2 → enter edit mode for this cell (when editable). Edit-mode key
        // handling (Escape/Enter/Tab) lives in the <input>'s own OnEditKeyDown.
        if ((e.Key == "Enter" || e.Key == "F2") && CellEditable && !_isEditing)
        {
            StartEdit();
            return;
        }

        // Navigation / selection keys → delegate to the grid.
        await Context.OnCellKeyDown(RowIndex, ColumnIndex, e);
    }

    // Memoization mirror of DataGridRow.CssClass: 10k cells × N renders worth
    // of string.Join allocations. The key carries every input that influences
    // the result; we recompute only when something actually changes.
    private (object Key, string Css)? _cssCache;

    private string CssClass
    {
        get
        {
            var cellClass = Column.CellClass?.Invoke(Item);
            var pin = Column.Pin;
            var dirty = IsBatchDirty;
            var key = (pin, dirty, Column.CssClass, cellClass, Class);
            if (_cssCache is { } c && c.Key.Equals(key)) return c.Css;

            var pinClass = pin switch
            {
                PinDirection.Left => "sticky z-10 bg-inherit",
                PinDirection.Right => "sticky z-10 bg-inherit",
                _ => (string?)null
            };
            var css = string.Join(" ", new[]
            {
                "px-3 py-2 text-sm",
                "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
                pinClass,
                // Batch-edit dirty marker: subtle left border accent + warning
                // background tint. Uses the lumeo.css --color-warning token so
                // it stays theme-aware in dark mode.
                dirty ? "border-l-2 border-warning bg-warning/10" : null,
                Column.CssClass,
                cellClass,
                Class
            }.Where(s => !string.IsNullOrWhiteSpace(s)));
            _cssCache = (key, css);
            return css;
        }
    }

    private string PinStyle
    {
        get
        {
            if (Column.Pin == PinDirection.Left && Context?.PinnedLeftOffsets.TryGetValue(Column.Id, out var leftOff) == true)
                return $"left: {leftOff}px;";
            if (Column.Pin == PinDirection.Right && Context?.PinnedRightOffsets.TryGetValue(Column.Id, out var rightOff) == true)
                return $"right: {rightOff}px;";
            return "";
        }
    }

    private void HandleClick()
    {
        if (CellEditable && !_isEditing)
        {
            StartEdit();
        }
    }

    private void HandleDoubleClick()
    {
        if (CellEditable && !_isEditing)
        {
            StartEdit();
        }
    }

    private void StartEdit()
    {
        // In batch mode, seed from the pending buffer if there's one — otherwise
        // the user would have to retype an edit they just committed.
        _originalValue = Column.GetValue(Item);
        var seedValue = Context?.IsBatchEdit == true
            ? Context.GetBatchValue(Item, Column)
            : _originalValue;
        _editValue = seedValue?.ToString() ?? "";
        _editContext = new CellEditContext<TItem>
        {
            Item = Item,
            Column = Column,
            Value = seedValue,
            ValueChanged = () =>
            {
                _editValue = _editContext?.Value?.ToString();
                StateHasChanged();
            }
        };
        _isEditing = true;
    }

    private void OnEditInput(ChangeEventArgs e)
    {
        _editValue = e.Value?.ToString();
        if (_editContext is not null)
        {
            _editContext.Value = _editValue;
        }
    }

    private async Task OnEditKeyDown(KeyboardEventArgs e)
    {
        if (e.Key == "Enter")
        {
            await CommitEdit();
            // Excel-like: after committing, move focus down one row.
            if (Navigable && Context is not null)
            {
                await Context.OnCellKeyDown(RowIndex, ColumnIndex, new KeyboardEventArgs { Key = "ArrowDown" });
            }
        }
        else if (e.Key == "Escape")
        {
            CancelEdit();
            if (Navigable && Context is not null)
            {
                // Re-focus the cell <td> so arrow nav resumes immediately.
                await Context.OnCellKeyDown(RowIndex, ColumnIndex, new KeyboardEventArgs { Key = "Home" });
                await Context.OnCellKeyDown(RowIndex, ColumnIndex, new KeyboardEventArgs { Key = "End" });
            }
        }
        else if (e.Key == "Tab")
        {
            await CommitEdit();
            // Move to the next editable cell to the right (browser handles plain
            // Tab too, but committing first keeps state consistent).
            if (Navigable && Context is not null)
            {
                await Context.OnCellKeyDown(RowIndex, ColumnIndex, new KeyboardEventArgs { Key = "ArrowRight" });
            }
        }
    }

    private async Task CommitEdit()
    {
        if (!_isEditing) return;
        _isEditing = false;

        if (Context is null || Column.Field is null) return;

        // Batch mode → write into the pending-changes buffer; don't mutate the
        // row and don't fire OnCellEdit (that fires later on Save all via the
        // consumer's OnBatchSave handler).
        if (Context.IsBatchEdit)
        {
            Context.SetBatchValue(Item, Column, _editValue);
            return;
        }

        if (_editValue != _originalValue?.ToString())
        {
            var args = new CellEditEventArgs<TItem>(Item, Column.Field, _originalValue, _editValue);
            await Context.OnCellEdit.InvokeAsync(args);
        }
    }

    private void CancelEdit()
    {
        _isEditing = false;
        _editValue = null;
    }
}
