@namespace Lumeo
@inject Lumeo.Services.Localization.ILumeoLocalizer L

@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="@CssClass" role="radiogroup" aria-label="@L["Rating.Rate"]" @onkeydown="HandleKeyDown" @onpointerup="HandlePointerUp" @onpointerleave="HandlePointerLeaveContainer" @onpointercancel="HandlePointerUp" @attributes="AdditionalAttributes">
    @for (int i = 1; i <= Max; i++)
    {
        var starIndex = i;
        var isFull = Value >= starIndex;
        var isHalf = AllowHalf && !isFull && Value >= starIndex - 0.5;

        @if (IconContent is not null)
        {
            <button type="button"
                    class="@GetStarClasses(isFull || isHalf) @SizeClasses @StarInteractClasses"
                    tabindex="@GetStarTabIndex(starIndex)"
                    disabled="@ReadOnly"
                    aria-label="@(string.Format(L["Rating.RateOf"], starIndex, Max))"
                    @onclick="() => HandleClick(starIndex)"
                    @onpointerdown="(e) => HandlePointerDown(starIndex, e)"
                    @onpointerenter="() => HandlePointerEnter(starIndex)"
                    @onmouseover="() => HandleHover(starIndex)"
                    @onmouseout="HandleMouseLeave">
                @IconContent
            </button>
        }
        else
        {
            <button type="button"
                    class="@SizeClasses @StarInteractClasses relative"
                    tabindex="@GetStarTabIndex(starIndex)"
                    disabled="@ReadOnly"
                    aria-label="@(string.Format(L["Rating.RateOf"], starIndex, Max))"
                    @onclick="() => HandleClick(starIndex)"
                    @onpointerdown="(e) => HandlePointerDown(starIndex, e)"
                    @onpointerenter="() => HandlePointerEnter(starIndex)"
                    @onmouseover="() => HandleHover(starIndex)"
                    @onmouseout="HandleMouseLeave">
                @* Always render the stars inside the same inline-grid wrapper — using the wrapper for half-stars
                   only put them on a different baseline than the plain-Blazicon full/empty stars and made
                   half-stars sit slightly lower in the row. Now every star shares the same line-box geometry. *@
                <span class="inline-grid align-middle leading-none [&_svg]:block" style="grid-template-areas: 'stack';">
                    <span style="grid-area: stack;">
                        <Blazicon Svg="Lucide.Star" class="text-muted-foreground/30 fill-muted-foreground/10" />
                    </span>
                    @if (isFull || (_hoverValue > 0 && starIndex <= _hoverValue))
                    {
                        <span style="grid-area: stack;">
                            <Blazicon Svg="Lucide.Star" class="text-[var(--color-rating)] fill-[var(--color-rating)]" />
                        </span>
                    }
                    else if (isHalf)
                    {
                        <span style="grid-area: stack; clip-path: inset(0 50% 0 0);">
                            <Blazicon Svg="Lucide.Star" class="text-[var(--color-rating)] fill-[var(--color-rating)]" />
                        </span>
                    }
                </span>
            </button>
        }
    }
</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>
    }
}

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

    [Parameter] public double Value { get; set; }
    [Parameter] public EventCallback<double> ValueChanged { get; set; }
    [Parameter] public int Max { get; set; } = 5;
    [Parameter] public bool AllowHalf { get; set; }
    [Parameter] public bool ReadOnly { get; set; }

    /// <summary>Icon slot. Named <c>IconContent</c> to avoid shadowing the <c>&lt;Icon&gt;</c> component.</summary>
    [Parameter] public RenderFragment? IconContent { get; set; }

    [Parameter] public RatingSize Size { get; set; } = RatingSize.Default;
    [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 int _hoverValue;

    // Touch/pointer drag state. Lets users tap-and-drag across stars (iOS-style)
    // to scrub the value continuously. _suppressNextClick avoids double-handling
    // when the synthetic click follows a pointerdown that already set the value.
    private bool _isDragging;
    private bool _suppressNextClick;
    private bool _dragMovedAcrossStars;

    // Roving tabindex: only the selected star (or star 1 when nothing selected) is
    // in the tab sequence. Arrow-key navigation on the radiogroup shifts both focus
    // and value, so there is never more than one tabstop inside the widget.
    private string GetStarTabIndex(int starIndex)
    {
        var activeIndex = Value > 0 ? (int)Math.Ceiling(Value) : 1;
        return starIndex == activeIndex ? "0" : "-1";
    }

    private string CssClass => $"inline-flex items-center gap-0.5 {Class}".Trim();

    private string SizeClasses => Size switch
    {
        RatingSize.Small => "[&_svg]:h-4 [&_svg]:w-4",
        RatingSize.Large => "[&_svg]:h-8 [&_svg]:w-8",
        _ => "[&_svg]:h-5 [&_svg]:w-5"
    };

    private string GetStarClasses(bool isActive) => isActive
        ? "text-[var(--color-rating)]"
        : "text-muted-foreground/30";

    private string StarInteractClasses => ReadOnly
        ? "cursor-default"
        : "cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm";

    private async Task HandleClick(int index)
    {
        if (ReadOnly) return;

        // When a pointerdown already committed the value (touch/pen, or any drag),
        // skip the synthetic click that follows to avoid double-handling. For
        // AllowHalf this is critical: the click would otherwise toggle to N-0.5.
        if (_suppressNextClick)
        {
            _suppressNextClick = false;
            return;
        }

        var newValue = (double)index;
        if (AllowHalf && Value == index)
        {
            newValue = index - 0.5;
        }

        Value = newValue;
        await ValueChanged.InvokeAsync(Value);
    }

    private async Task HandlePointerDown(int index, PointerEventArgs e)
    {
        if (ReadOnly) return;

        // Only handle touch/pen here. For mouse we let @onclick continue to drive
        // value changes so the existing AllowHalf toggle behaviour (click again
        // for half-star) keeps working on desktop. Without this guard,
        // pointerdown commits Value=N then the synthetic click sees Value==N
        // and toggles to N-0.5 — double-fire with wrong final value.
        if (e.PointerType == "mouse") return;

        _isDragging = true;
        _dragMovedAcrossStars = false;

        // Commit initial value on touch so the first contact shows feedback
        // immediately, matching native iOS scrubber behaviour.
        if (Value != index)
        {
            Value = index;
            await ValueChanged.InvokeAsync(Value);
        }
    }

    private async Task HandlePointerEnter(int index)
    {
        if (ReadOnly || !_isDragging) return;

        // Pointer crossed into a different star while dragging — scrub value.
        _dragMovedAcrossStars = true;
        if (Value != index)
        {
            Value = index;
            await ValueChanged.InvokeAsync(Value);
        }
    }

    private void HandlePointerUp()
    {
        if (!_isDragging) return;

        // If the user actually dragged, suppress the trailing click so we don't
        // re-toggle the value. A pure tap (no movement) still falls through to
        // HandleClick which honours the AllowHalf half-star toggle.
        if (_dragMovedAcrossStars)
        {
            _suppressNextClick = true;
        }

        _isDragging = false;
        _dragMovedAcrossStars = false;
    }

    private void HandlePointerLeaveContainer()
    {
        // Finger slid out of the radiogroup entirely — end the drag so a later
        // pointerup outside the widget doesn't leave us stuck in dragging state.
        if (_isDragging)
        {
            _isDragging = false;
            _dragMovedAcrossStars = false;
        }
    }

    private void HandleHover(int index)
    {
        if (!ReadOnly)
        {
            _hoverValue = index;
        }
    }

    private void HandleMouseLeave()
    {
        _hoverValue = 0;
    }

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

        var step = AllowHalf ? 0.5 : 1.0;
        double newValue = Value;

        switch (e.Key)
        {
            case "ArrowRight":
            case "ArrowUp":
                newValue = Math.Min(Value + step, Max);
                break;
            case "ArrowLeft":
            case "ArrowDown":
                newValue = Math.Max(Value - step, 0);
                break;
            default:
                return;
        }

        if (newValue != Value)
        {
            Value = newValue;
            await ValueChanged.InvokeAsync(Value);
        }
    }

    public enum RatingSize { Small, Default, Large }
}
