@namespace Lumeo.Docs.Shared
@inject IJSRuntime JS
@implements IAsyncDisposable

@if (_headings.Count > 0)
{
    <aside class="hidden lg:block w-52 shrink-0">
        <div class="sticky py-4 pr-2 text-sm flex flex-col"
             style="top: 88px; max-height: calc(100vh - 88px - 16px); overflow: hidden;">
            <div class="font-semibold text-foreground mb-3 text-xs uppercase tracking-wide shrink-0">On this page</div>
            <nav class="border-l border-border/40 overflow-y-auto lumeo-otp-scroll"
                 style="scrollbar-width: thin;">
                @foreach (var h in _headings)
                {
                    <a href="@($"#{h.Id}")"
                       @onclick:preventDefault
                       @onclick="() => ScrollTo(h.Id)"
                       class="@LinkClass(h)"
                       style="@($"padding-left: {(h.Level == 3 ? 24 : 12)}px")">
                        @h.Text
                    </a>
                }
            </nav>
        </div>
    </aside>
}

@code {
    [Parameter] public string Container { get; set; } = "main";

    private readonly string _id = $"otp-{Guid.NewGuid():N}";
    private List<Heading> _headings = new();
    private string? _activeId;
    private IJSObjectReference? _module;
    private DotNetObjectReference<OnThisPage>? _selfRef;
    private string? _lastPath;
    private CancellationTokenSource? _scanCts;

    [Inject] NavigationManager Nav { get; set; } = default!;

    protected override void OnInitialized()
    {
        Nav.LocationChanged += OnLocationChanged;
        _lastPath = Nav.Uri;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _scanCts?.Cancel();
            _scanCts?.Dispose();
            _scanCts = new CancellationTokenSource();
            var token = _scanCts.Token;
            try { await RescanWithRetry(token); }
            catch (OperationCanceledException) { /* superseded */ }
        }
    }

    private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
    {
        if (e.Location == _lastPath) return;
        _lastPath = e.Location;

        // Cancel any in-flight rescan from a previous route
        _scanCts?.Cancel();
        _scanCts?.Dispose();
        _scanCts = new CancellationTokenSource();
        var token = _scanCts.Token;

        _ = InvokeAsync(async () =>
        {
            try { await RescanWithRetry(token); }
            catch (OperationCanceledException) { /* superseded */ }
        });
    }

    private async Task RescanWithRetry(CancellationToken ct)
    {
        // Child page may not have rendered its headings yet when we first scan.
        // Try a few times with increasing delays.
        int[] delays = { 50, 150, 300, 600 };
        foreach (var d in delays)
        {
            await Task.Delay(d, ct);
            await Rescan();
            if (_headings.Count > 0) return;
        }
    }

    private async Task Rescan()
    {
        try
        {
            _module ??= await JS.InvokeAsync<IJSObjectReference>("import", "./_content/Lumeo/js/components.js");
            _selfRef ??= DotNetObjectReference.Create(this);

            // Stop previous observer (new route / new content)
            try { await _module.InvokeVoidAsync("onThisPageUnobserve", _id); } catch { }

            var result = await _module.InvokeAsync<Heading[]>("onThisPageScan", Container);
            _headings = result.ToList();
            _activeId = _headings.FirstOrDefault()?.Id;

            if (_headings.Count > 0)
            {
                await _module.InvokeVoidAsync("onThisPageObserve", _id, Container, _selfRef);
            }

            await InvokeAsync(StateHasChanged);
        }
        catch (JSDisconnectedException) { }
        catch (Exception) { /* circuit lifecycle — ignore */ }
    }

    [JSInvokable]
    public Task SetActive(string? id)
    {
        if (_activeId != id)
        {
            _activeId = id;
            InvokeAsync(StateHasChanged);
        }
        return Task.CompletedTask;
    }

    private async Task ScrollTo(string id)
    {
        try
        {
            // docs.js is loaded as a regular global script — call via window.lumeo namespace
            await JS.InvokeVoidAsync("lumeo.navScrollActiveIntoView", id);
            _activeId = id;
            StateHasChanged();
        }
        catch (JSDisconnectedException) { }
    }

    private string LinkClass(Heading h)
    {
        var isActive = h.Id == _activeId;
        return string.Join(" ", new[]
        {
            "block -ml-px border-l-2 py-1 pr-2 no-underline transition-colors",
            isActive
                ? "border-primary text-foreground font-medium"
                : "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
        });
    }

    public async ValueTask DisposeAsync()
    {
        _scanCts?.Cancel();
        _scanCts?.Dispose();
        Nav.LocationChanged -= OnLocationChanged;
        if (_module is not null)
        {
            try { await _module.InvokeVoidAsync("onThisPageUnobserve", _id); } catch { }
            try { await _module.DisposeAsync(); } catch { }
        }
        _selfRef?.Dispose();
    }

    public record Heading(string Id, string Text, int Level);
}
