@using System.Globalization
@implements IAsyncDisposable
@inject IJSRuntime JS

<div class="localized-date-input" @ref="rootElement" @onkeydown="HandleKeyDown">
    <div class="input-group">
        <input id="@InputId"
               data-testid="@GetInputTestId()"
               type="text"
               class="form-control"
               value="@displayValue"
               @onchange="OnTextChangedAsync"
               @onfocus="OpenPopup"
               placeholder="dd/MM/yyyy"
               inputmode="numeric"
               autocomplete="off"
               disabled="@Disabled" />
        <button type="button"
                class="btn btn-outline-secondary localized-date-input-toggle"
                aria-label="Open calendar"
                @onclick="TogglePopup"
                disabled="@Disabled">
            Cal
        </button>
    </div>

    @if (isOpen && !Disabled)
    {
        <div class="localized-date-input-popover" data-testid="@GetPopoverTestId()" @onclick:stopPropagation="true">
            <div class="localized-date-input-header">
                <button type="button" class="btn btn-sm btn-outline-secondary" @onclick="ShowPreviousMonth" aria-label="Previous month">&lsaquo;</button>
                <div class="localized-date-input-month">@visibleMonth.ToDateTime(TimeOnly.MinValue).ToString("MMMM yyyy", ItalianCulture)</div>
                <button type="button" class="btn btn-sm btn-outline-secondary" @onclick="ShowNextMonth" aria-label="Next month">&rsaquo;</button>
            </div>

            <div class="localized-date-input-weekdays">
                @foreach (var weekday in mondayFirstWeekdays)
                {
                    <div class="localized-date-input-weekday" data-testid="date-picker-weekday">@weekday</div>
                }
            </div>

            <div class="localized-date-input-grid">
                @foreach (var day in calendarDays)
                {
                    <button type="button"
                            class="localized-date-input-day @(day.IsCurrentMonth ? string.Empty : "localized-date-input-day-outside") @(day.Date == Value ? "localized-date-input-day-selected" : string.Empty)"
                            data-testid="@GetDayTestId(day.Date)"
                            @onclick="() => SelectDateAsync(day.Date)">
                        @day.Date.Day
                    </button>
                }
            </div>

            @if (AllowEmpty)
            {
                <div class="localized-date-input-actions">
                    <button type="button" class="btn btn-sm btn-outline-secondary ms-auto" @onclick="ClearAsync">Clear</button>
                </div>
            }
        </div>
    }
</div>

@code {
    private static readonly CultureInfo ItalianCulture = CultureInfo.GetCultureInfo("it-IT");
    private static readonly string[] SupportedFormats = ["dd/MM/yyyy", "d/M/yyyy", "dd/M/yyyy", "d/MM/yyyy"];
    private static readonly string[] mondayFirstWeekdays = BuildMondayFirstWeekdays();

    [Parameter] public DateOnly? Value { get; set; }
    [Parameter] public EventCallback<DateOnly?> ValueChanged { get; set; }
    [Parameter] public bool Disabled { get; set; }
    [Parameter] public bool AllowEmpty { get; set; }
    [Parameter] public string? InputId { get; set; }
    [Parameter] public string? TestId { get; set; }

    private DateOnly? lastValue;
    private string displayValue = string.Empty;
    private bool isOpen;
    private bool outsideClickListenerActive;
    private DateOnly visibleMonth;
    private IReadOnlyList<CalendarDayCell> calendarDays = [];
    private ElementReference rootElement;
    private DotNetObjectReference<LocalizedDateInput>? dotNetReference;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (isOpen && !outsideClickListenerActive)
        {
            dotNetReference ??= DotNetObjectReference.Create(this);
            await JS.InvokeVoidAsync("workTrackerDateInput.registerOutsideClick", rootElement, dotNetReference);
            outsideClickListenerActive = true;
        }
        else if (!isOpen && outsideClickListenerActive)
        {
            await JS.InvokeVoidAsync("workTrackerDateInput.unregisterOutsideClick", rootElement);
            outsideClickListenerActive = false;
        }

        await base.OnAfterRenderAsync(firstRender);
    }

    protected override void OnParametersSet()
    {
        if (visibleMonth == default)
        {
            visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today));
            calendarDays = BuildCalendarDays(visibleMonth);
        }

        if (Value != lastValue)
        {
            lastValue = Value;
            displayValue = FormatValue(Value);
            visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today));
            calendarDays = BuildCalendarDays(visibleMonth);
        }
    }

    private void OpenPopup(FocusEventArgs _)
    {
        if (Disabled)
        {
            return;
        }

        isOpen = true;
        visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today));
        calendarDays = BuildCalendarDays(visibleMonth);
    }

    private void TogglePopup()
    {
        if (Disabled)
        {
            return;
        }

        isOpen = !isOpen;
        if (isOpen)
        {
            visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today));
            calendarDays = BuildCalendarDays(visibleMonth);
        }
    }

    private async Task OnTextChangedAsync(ChangeEventArgs e)
    {
        displayValue = e.Value?.ToString() ?? string.Empty;

        if (TryParseInput(displayValue, out var parsedDate))
        {
            await SetValueAsync(parsedDate, closePopup: false);
            return;
        }

        if (AllowEmpty && string.IsNullOrWhiteSpace(displayValue))
        {
            await SetValueAsync(null, closePopup: false);
            return;
        }

        displayValue = FormatValue(Value);
    }

    private async Task SelectDateAsync(DateOnly date)
    {
        await SetValueAsync(date, closePopup: true);
    }

    private async Task ClearAsync()
    {
        await SetValueAsync(null, closePopup: true);
    }

    private async Task SetValueAsync(DateOnly? date, bool closePopup)
    {
        lastValue = date;
        Value = date;
        displayValue = FormatValue(date);
        if (date.HasValue)
        {
            visibleMonth = ToFirstOfMonth(date.Value);
            calendarDays = BuildCalendarDays(visibleMonth);
        }

        if (closePopup)
        {
            isOpen = false;
        }

        await ValueChanged.InvokeAsync(date);
    }

    private void ShowPreviousMonth()
    {
        visibleMonth = visibleMonth.AddMonths(-1);
        calendarDays = BuildCalendarDays(visibleMonth);
    }

    private void ShowNextMonth()
    {
        visibleMonth = visibleMonth.AddMonths(1);
        calendarDays = BuildCalendarDays(visibleMonth);
    }

    private void HandleKeyDown(KeyboardEventArgs e)
    {
        if (e.Key == "Escape")
        {
            isOpen = false;
        }
    }

    [JSInvokable]
    public Task ClosePopupFromOutsideClickAsync()
    {
        if (!isOpen)
        {
            return Task.CompletedTask;
        }

        isOpen = false;
        return InvokeAsync(StateHasChanged);
    }

    public async ValueTask DisposeAsync()
    {
        if (outsideClickListenerActive)
        {
            try
            {
                await JS.InvokeVoidAsync("workTrackerDateInput.unregisterOutsideClick", rootElement);
            }
            catch (JSDisconnectedException)
            {
            }
        }

        dotNetReference?.Dispose();
    }

    private string GetInputTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-input" : $"{TestId}-input";

    private string GetPopoverTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-popover" : $"{TestId}-popover";

    private string GetDayTestId(DateOnly date) => string.IsNullOrWhiteSpace(TestId)
        ? $"localized-date-day-{date:yyyy-MM-dd}"
        : $"{TestId}-day-{date:yyyy-MM-dd}";

    private static string FormatValue(DateOnly? date)
    {
        return date?.ToString("dd/MM/yyyy", ItalianCulture) ?? string.Empty;
    }

    private static bool TryParseInput(string? value, out DateOnly date)
    {
        foreach (var format in SupportedFormats)
        {
            if (DateOnly.TryParseExact(value, format, ItalianCulture, DateTimeStyles.None, out date))
            {
                return true;
            }
        }

        return DateOnly.TryParse(value, ItalianCulture, DateTimeStyles.None, out date);
    }

    private static DateOnly ToFirstOfMonth(DateOnly date) => new(date.Year, date.Month, 1);

    private static IReadOnlyList<CalendarDayCell> BuildCalendarDays(DateOnly month)
    {
        var firstDayOfMonth = ToFirstOfMonth(month);
        var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1);
        var gridStart = firstDayOfMonth.AddDays(-(((int)firstDayOfMonth.DayOfWeek + 6) % 7));
        var gridEnd = lastDayOfMonth.AddDays((7 - (((int)lastDayOfMonth.DayOfWeek + 6) % 7) - 1 + 7) % 7);
        var days = new List<CalendarDayCell>();

        for (var date = gridStart; date <= gridEnd; date = date.AddDays(1))
        {
            days.Add(new CalendarDayCell(date, date.Month == month.Month && date.Year == month.Year));
        }

        return days;
    }

    private static string[] BuildMondayFirstWeekdays()
    {
        return Enumerable.Range(0, 7)
            .Select(index => ItalianCulture.DateTimeFormat.AbbreviatedDayNames[(index + 1) % 7])
            .Select(dayName => ItalianCulture.TextInfo.ToTitleCase(dayName))
            .ToArray();
    }

    private sealed record CalendarDayCell(DateOnly Date, bool IsCurrentMonth);
}