@namespace Lumeo
@implements IAsyncDisposable
@inject Lumeo.Services.IComponentInteropService Interop
@inject Lumeo.Services.Localization.ILumeoLocalizer L

@if (_initError is not null)
{
    <div class="@($"flex items-center justify-center text-sm text-destructive border border-destructive/30 rounded-md bg-destructive/5 {Class}".Trim())" style="height: @Height;" @attributes="AdditionalAttributes">
        <span>@_initError</span>
    </div>
}
else
{
<div class="@($"flex flex-col gap-3 rounded-md border border-border/60 bg-card p-3 {Class}".Trim())" @attributes="AdditionalAttributes">
    @* Lumeo-styled toolbar above the FullCalendar canvas.
       FullCalendar's built-in header is hidden in JS (headerToolbar: false). *@
    <div class="flex flex-wrap items-center justify-between gap-2">
        <div class="flex items-center gap-1">
            <Button Variant="Button.ButtonVariant.Outline" Size="Button.ButtonSize.Sm" OnClick="PrevAsync" aria-label="@L["Scheduler.Previous"]">
                <Blazicon Svg="Lucide.ChevronLeft" class="h-4 w-4" />
            </Button>
            <Button Variant="Button.ButtonVariant.Outline" Size="Button.ButtonSize.Sm" OnClick="TodayAsync">@L["Scheduler.Today"]</Button>
            <Button Variant="Button.ButtonVariant.Outline" Size="Button.ButtonSize.Sm" OnClick="NextAsync" aria-label="@L["Scheduler.Next"]">
                <Blazicon Svg="Lucide.ChevronRight" class="h-4 w-4" />
            </Button>
        </div>
        <div class="flex-1 text-center text-sm font-medium text-foreground truncate px-2">
            @_title
        </div>
        <ToggleGroup Type="ToggleGroup.ToggleGroupType.Single"
                     Variant="ToggleGroup.ToggleGroupVariant.Outline"
                     Size="ToggleGroup.ToggleGroupSize.Sm"
                     Value="@_currentViewValue"
                     ValueChanged="OnViewChangedAsync">
            <ToggleGroupItem Value="Month">@L["Scheduler.Month"]</ToggleGroupItem>
            <ToggleGroupItem Value="Week">@L["Scheduler.Week"]</ToggleGroupItem>
            <ToggleGroupItem Value="Day">@L["Scheduler.Day"]</ToggleGroupItem>
            <ToggleGroupItem Value="List">@L["Scheduler.List"]</ToggleGroupItem>
        </ToggleGroup>
    </div>

    @* Resource legend — rendered when Resources are provided.
       Uses simple color-coded dots; no FullCalendar Premium plugin required. *@
    @if (_resolvedResources is { Count: > 0 })
    {
        <div class="flex flex-wrap items-center gap-x-4 gap-y-1 px-1 pb-1" aria-label="@L["Scheduler.ResourceLegend"]">
            @foreach (var resource in _resolvedResources)
            {
                <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
                    <span class="inline-block h-2.5 w-2.5 rounded-full flex-shrink-0"
                          style="background: @(resource.Color ?? "var(--color-primary)")"></span>
                    <span>@resource.Title</span>
                </div>
            }
        </div>
    }

    <div @ref="_hostRef" style="height: @Height;"></div>
</div>
}

@code {
    // ── Core parameters ──────────────────────────────────────────────────
    [Parameter] public IEnumerable<SchedulerEvent>? Events { get; set; }
    [Parameter] public EventCallback<IEnumerable<SchedulerEvent>> EventsChanged { get; set; }
    [Parameter] public SchedulerView InitialView { get; set; } = SchedulerView.Month;
    [Parameter] public DateTime? InitialDate { get; set; }
    [Parameter] public bool Editable { get; set; } = true;
    [Parameter] public bool Selectable { get; set; } = true;
    [Parameter] public bool BusinessHours { get; set; }
    [Parameter] public string Height { get; set; } = "640px";
    [Parameter] public int FirstDayOfWeek { get; set; } = 1;
    [Parameter] public EventCallback<SchedulerEvent> OnEventClick { get; set; }
    [Parameter] public EventCallback<SchedulerDateRange> OnDateSelect { get; set; }
    [Parameter] public EventCallback<SchedulerEvent> OnEventChange { get; set; }

    // ── Time-grid display parameters ─────────────────────────────────────
    /// <summary>
    /// Show the current-time indicator line in time-grid views. Default: true.
    /// Maps to FullCalendar's <c>nowIndicator</c> option.
    /// </summary>
    [Parameter] public bool NowIndicator { get; set; } = true;

    /// <summary>
    /// Earliest visible time slot in time-grid views (e.g. 07:00).
    /// Maps to FullCalendar's <c>slotMinTime</c>. Default: 00:00 (FullCalendar shows the full day).
    /// </summary>
    [Parameter] public TimeOnly? SlotMinTime { get; set; }

    /// <summary>
    /// Latest visible time slot in time-grid views (e.g. 22:00).
    /// Maps to FullCalendar's <c>slotMaxTime</c>. Default: 24:00 (FullCalendar shows the full day).
    /// </summary>
    [Parameter] public TimeOnly? SlotMaxTime { get; set; }

    /// <summary>
    /// Duration of each time slot row in time-grid views.
    /// Maps to FullCalendar's <c>slotDuration</c> (formatted as HH:mm:ss). Default: 00:30:00.
    /// </summary>
    [Parameter] public TimeSpan? SlotDuration { get; set; }

    // ── Resource / color-coding parameters ──────────────────────────────
    /// <summary>
    /// Optional list of resources (people, rooms, equipment). When provided, events
    /// are color-coded by their <see cref="SchedulerEvent.ResourceId"/> and a legend
    /// is rendered above the calendar.
    /// <br />
    /// Note: this uses Lumeo's color-coding fallback, NOT FullCalendar Premium resource
    /// lanes (which require a paid FullCalendar Premium license not bundled here).
    /// </summary>
    [Parameter] public IReadOnlyList<SchedulerResource>? Resources { get; set; }

    // ── Event styling callbacks ──────────────────────────────────────────
    /// <summary>
    /// Optional callback that returns extra CSS class names for an event chip.
    /// The classes are appended to the FullCalendar event's <c>classNames</c>.
    /// Use for conditional styling without touching <see cref="SchedulerEvent.Color"/>.
    /// </summary>
    [Parameter] public Func<SchedulerEvent, string>? EventClassNames { get; set; }

    /// <summary>
    /// Optional callback that returns a CSS color string for an event.
    /// Applied as <c>backgroundColor</c> / <c>borderColor</c> in FullCalendar.
    /// Supersedes <see cref="SchedulerEvent.Color"/> when both are set; resource
    /// color applies as a fallback when neither is set.
    /// </summary>
    [Parameter] public Func<SchedulerEvent, string>? EventColor { get; set; }

    // ── Error handling ───────────────────────────────────────────────────
    /// <summary>
    /// Raised when JS initialization fails (e.g. CDN down, FullCalendar threw).
    /// The argument is the error message. When raised the component renders an
    /// error card instead of the calendar.
    /// </summary>
    [Parameter] public EventCallback<string> OnInitError { get; set; }

    // ── Standard pass-through ────────────────────────────────────────────
    [Parameter] public string? Class { get; set; }
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }

    private ElementReference _hostRef;
    private DotNetObjectReference<Scheduler>? _selfRef;
    private string? _instanceId;
    private bool _initialized;
    private string? _initError;
    private int _lastEventsHash;
    private SchedulerView _currentView;
    private string _currentViewValue => _currentView.ToString();
    private string _title = "";

    // Snapshot of the most recent events set — used by eventChange to build the
    // updated collection without reaching back into `Events` (which may have
    // been replaced by the parent since init).
    private List<SchedulerEvent> _events = new();

    // Resolved resource lookup — built once and used for both legend rendering
    // and event color-coding in ToJsEvent.
    private List<SchedulerResource> _resolvedResources = new();
    private Dictionary<string, SchedulerResource> _resourceById = new(StringComparer.Ordinal);

    protected override void OnInitialized()
    {
        _currentView = InitialView;
        _events = Events?.ToList() ?? new List<SchedulerEvent>();
        _lastEventsHash = ComputeEventsHash(_events);
        _title = ComputeTitle(InitialDate ?? DateTime.Today, _currentView);
        RebuildResourceLookup();
    }

    protected override async Task OnParametersSetAsync()
    {
        // Rebuild the resource lookup whenever the Resources param changes.
        RebuildResourceLookup();

        if (_initialized && Events is not null)
        {
            var incoming = Events.ToList();
            var hash = ComputeEventsHash(incoming);
            if (hash != _lastEventsHash)
            {
                _lastEventsHash = hash;
                _events = incoming;
                await Interop.SchedulerSetEventsAsync(_instanceId!, incoming.Select(ToJsEvent).Cast<object>());
            }
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender || _initialized) return;

        _selfRef = DotNetObjectReference.Create(this);

        var options = new
        {
            view = InitialView.ToString(),
            initialDate = (InitialDate ?? DateTime.Today).ToString("yyyy-MM-dd"),
            events = _events.Select(ToJsEvent).ToArray(),
            editable = Editable,
            selectable = Selectable,
            businessHours = BusinessHours,
            height = Height,
            firstDay = FirstDayOfWeek,
            nowIndicator = NowIndicator,
            slotMinTime = SlotMinTime.HasValue ? SlotMinTime.Value.ToString("HH:mm:ss") : (string?)null,
            slotMaxTime = SlotMaxTime.HasValue ? SlotMaxTime.Value.ToString("HH:mm:ss") : (string?)null,
            slotDuration = SlotDuration.HasValue ? $"{(int)SlotDuration.Value.TotalHours:D2}:{SlotDuration.Value.Minutes:D2}:{SlotDuration.Value.Seconds:D2}" : (string?)null
        };

        try
        {
            _instanceId = await Interop.SchedulerInitAsync(_hostRef, _selfRef, options);
            _initialized = true;
            // Read the real title from FullCalendar now that the view is rendered.
            _title = await Interop.SchedulerGetTitleAsync(_instanceId);
            StateHasChanged();
        }
        catch (Microsoft.JSInterop.JSException ex)
        {
            _initError = ex.Message;
            Console.Error.WriteLine($"[Lumeo Scheduler] Init failed: {ex.Message}");
            if (OnInitError.HasDelegate)
                await OnInitError.InvokeAsync(ex.Message);
            StateHasChanged();
        }
    }

    private object ToJsEvent(SchedulerEvent ev)
    {
        // Determine the effective color: EventColor callback > event.Color > resource color.
        var effectiveColor = EventColor?.Invoke(ev)
            ?? ev.Color
            ?? (ev.ResourceId is not null && _resourceById.TryGetValue(ev.ResourceId, out var res) ? res.Color : null);

        // Effective class names: EventClassNames callback merged with event.ClassNames.
        var cbClasses = EventClassNames?.Invoke(ev);
        var effectiveClassNames = string.IsNullOrEmpty(cbClasses)
            ? ev.ClassNames
            : string.IsNullOrEmpty(ev.ClassNames) ? cbClasses : $"{ev.ClassNames} {cbClasses}";

        // For recurring events with DaysOfWeek we use FullCalendar's free simple
        // recurrence model: the start/end fields become time-of-day values only.
        if (ev.DaysOfWeek is { Count: > 0 })
        {
            return new
            {
                id = ev.Id,
                title = ev.Title,
                // Time-of-day string for recurring events (HH:mm:ss).
                startTime = ev.Start.ToString("HH:mm:ss"),
                endTime = ev.End.ToString("HH:mm:ss"),
                daysOfWeek = ev.DaysOfWeek.Select(d => (int)d).ToArray(),
                startRecur = ev.Start.ToString("yyyy-MM-dd"),
                endRecur = ev.RecurrenceEnd.HasValue
                    ? ev.RecurrenceEnd.Value.ToString("yyyy-MM-dd")
                    : (string?)null,
                exdate = ev.ExceptionDates?.Select(d => d.ToString("yyyy-MM-dd")).ToArray(),
                allDay = ev.AllDay,
                color = effectiveColor,
                url = ev.Url,
                classNames = effectiveClassNames,
                extendedProps = ev.ExtendedProps
            };
        }

        // Standard (non-recurring) event.
        return new
        {
            id = ev.Id,
            title = ev.Title,
            start = ev.Start.ToString("o"),
            end = ev.End.ToString("o"),
            allDay = ev.AllDay,
            color = effectiveColor,
            url = ev.Url,
            classNames = effectiveClassNames,
            extendedProps = ev.ExtendedProps
        };
    }

    private void RebuildResourceLookup()
    {
        _resolvedResources = Resources?.ToList() ?? new List<SchedulerResource>();
        _resourceById = _resolvedResources.ToDictionary(r => r.Id, StringComparer.Ordinal);
    }

    private static int ComputeEventsHash(IEnumerable<SchedulerEvent> events)
    {
        // Cheap identity hash good enough to detect "did the caller hand us a
        // new event list?" without triggering a JS round-trip on every render.
        var hash = new HashCode();
        foreach (var ev in events)
        {
            hash.Add(ev.Id);
            hash.Add(ev.Title);
            hash.Add(ev.Start);
            hash.Add(ev.End);
            hash.Add(ev.AllDay);
            hash.Add(ev.Color);
            hash.Add(ev.ResourceId);
            hash.Add(ev.DaysOfWeek?.Count ?? 0);
            hash.Add(ev.RecurrenceEnd);
        }
        return hash.ToHashCode();
    }

    private string ComputeTitle(DateTime date, SchedulerView view) => view switch
    {
        SchedulerView.Month => date.ToString("MMMM yyyy"),
        SchedulerView.Week => string.Format(L["Scheduler.WeekOf"], StartOfWeek(date).ToString("MMM d, yyyy")),
        SchedulerView.Day => date.ToString("dddd, MMM d, yyyy"),
        SchedulerView.List => date.ToString("MMMM yyyy"),
        _ => date.ToString("MMMM yyyy")
    };

    private DateTime StartOfWeek(DateTime date)
    {
        // FirstDayOfWeek is 0-6 (Sun-Sat).
        var first = (DayOfWeek)FirstDayOfWeek;
        var diff = (7 + (date.DayOfWeek - first)) % 7;
        return date.AddDays(-diff).Date;
    }

    private async Task PrevAsync()
    {
        if (_instanceId is null) return;
        await Interop.SchedulerPrevAsync(_instanceId);
        await RefreshTitleAsync();
    }

    private async Task NextAsync()
    {
        if (_instanceId is null) return;
        await Interop.SchedulerNextAsync(_instanceId);
        await RefreshTitleAsync();
    }

    private async Task TodayAsync()
    {
        if (_instanceId is null) return;
        await Interop.SchedulerTodayAsync(_instanceId);
        await RefreshTitleAsync();
    }

    private async Task OnViewChangedAsync(string? v)
    {
        if (_instanceId is null || string.IsNullOrEmpty(v)) return;
        if (!Enum.TryParse<SchedulerView>(v, out var parsed)) return;
        _currentView = parsed;
        await Interop.SchedulerChangeViewAsync(_instanceId, v);
        await RefreshTitleAsync();
    }

    private async Task RefreshTitleAsync()
    {
        if (_instanceId is null) return;
        _title = await Interop.SchedulerGetTitleAsync(_instanceId);
        StateHasChanged();
    }

    [JSInvokable]
    public async Task JsOnEventClick(SchedulerEvent ev)
    {
        if (OnEventClick.HasDelegate) await OnEventClick.InvokeAsync(ev);
    }

    [JSInvokable]
    public async Task JsOnDateSelect(SchedulerDateRange range)
    {
        if (OnDateSelect.HasDelegate) await OnDateSelect.InvokeAsync(range);
    }

    [JSInvokable]
    public async Task JsOnEventChange(SchedulerEvent ev)
    {
        // Replace the matching event in our local snapshot, then bubble both
        // the per-event change and the new collection up to the parent.
        var idx = _events.FindIndex(e => e.Id == ev.Id);
        if (idx >= 0) _events[idx] = ev;

        _lastEventsHash = ComputeEventsHash(_events);

        if (OnEventChange.HasDelegate) await OnEventChange.InvokeAsync(ev);
        if (EventsChanged.HasDelegate) await EventsChanged.InvokeAsync(_events.ToList());
    }

    public async ValueTask DisposeAsync()
    {
        if (_instanceId is not null)
        {
            try { await Interop.SchedulerDestroyAsync(_instanceId); }
            catch (Microsoft.JSInterop.JSDisconnectedException) { }
        }
        _selfRef?.Dispose();
    }
}
