@page "/releases"
@page "/tracks"
@page "/tracks/albums"
@page "/tracks/archive"
@using DeepDrftManager.Services
@using DeepDrftModels.DTOs
@using DeepDrftModels.Enums
@inject ICmsTrackService CmsTrackService
@inject ISnackbar Snackbar
@inject ILogger<Releases> Logger
@inject NavigationManager NavigationManager
@implements IDisposable
@attribute [Authorize]

<PageTitle>Releases — Deep DRFT Management</PageTitle>

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
    <MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
        <MudText Typo="Typo.h3">Releases</MudText>

        @* Catalogue-wide waveform backfill (migrated from the retired /tracks view). Both buttons act over
           every track's waveform status — independent of any single grid — so the page owns the status map
           directly: it computes the missing counts and re-fetches after a run. No grid reference involved. *@
        <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
            <MudButton Variant="Variant.Outlined"
                       Color="Color.Primary"
                       StartIcon="@Icons.Material.Filled.AutoFixHigh"
                       Disabled="@(_bulkRunning || _highResBulkRunning || MissingProfileCount == 0)"
                       OnClick="GenerateAllMissingAsync">
                @if (_bulkRunning)
                {
                    <MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
                    <span>Generating @_bulkDone / @_bulkTotal…</span>
                }
                else
                {
                    <span>Generate All Profiles (@MissingProfileCount)</span>
                }
            </MudButton>
            <MudButton Variant="Variant.Outlined"
                       Color="Color.Primary"
                       StartIcon="@Icons.Material.Filled.Waves"
                       Disabled="@(_bulkRunning || _highResBulkRunning || MissingHighResCount == 0)"
                       OnClick="GenerateAllMissingHighResAsync">
                @if (_highResBulkRunning)
                {
                    <MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
                    <span>Backfilling @_highResBulkDone / @_highResBulkTotal…</span>
                }
                else
                {
                    <span>Backfill High-res (@MissingHighResCount)</span>
                }
            </MudButton>
            @* Backfill-Opus (Phase 18.5 + 18.6 badge). The Opus derive runs on a server-side background
               worker: pressing the button enqueues every track lacking a complete Opus artifact and reports
               the (enqueued / skipped) outcome. The "missing N" badge (18.6) reads the same opus-status map
               the page polls, giving visual parity with the two waveform backfill buttons — but unlike them,
               the count is informational, not a per-track client loop (the work is scheduled, not driven from
               here). The button stays pressable at N=0 (a no-op re-run is harmless); it only disables while a
               press is in flight or another bulk run holds the page. *@
            <MudButton Variant="Variant.Outlined"
                       Color="Color.Primary"
                       StartIcon="@Icons.Material.Filled.GraphicEq"
                       Disabled="@(_bulkRunning || _highResBulkRunning || _opusBackfillRunning)"
                       OnClick="BackfillOpusAsync">
                @if (_opusBackfillRunning)
                {
                    <MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
                    <span>Scheduling…</span>
                }
                else
                {
                    <span>Backfill Opus (@MissingOpusCount)</span>
                }
            </MudButton>
        </MudStack>
    </MudStack>

    @* Medium tab strip: an ALL tab plus one explicit MudTabPanel per ReleaseMedium, ALL left-most. Each
       panel is hand-declared in markup (not enum-driven) so @ref captures of the per-tab grid components
       are possible. Adding a future medium requires a hand-added MudTabPanel; its position in markup must
       match ReleaseMedium enum order, since the ?medium= deep-link seed and ActiveMedium getter are
       position-based (panel 0 = ALL, panels 1.. = enum values in order). *@
    @* Medium-aware Add Track: the button reflects the active tab and pre-selects the upload form to that
       tab's medium via a single query-param (?medium=…); the ALL tab defaults to Cut. The medium is a seed
       only — the upload form's selector stays user-changeable after landing. *@
    <MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-2">
        <MudButton Variant="Variant.Filled"
                   Color="Color.Primary"
                   StartIcon="@Icons.Material.Filled.Add"
                   Href="@AddTrackHref(ActiveMedium)">
            Add Track
        </MudButton>
    </MudStack>

    <MudTabs Elevation="0" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pt-4"
             @bind-ActivePanelIndex="_activeTabIndex">
        <MudTabPanel Text="ALL">
            <CmsAllReleasesGrid @ref="_allGrid"
                                OnWaveformGenerated="RefreshWaveformStatusAsync" />
        </MudTabPanel>
        <MudTabPanel Text="@MediumTabLabels[ReleaseMedium.Cut]">
            <CmsCutBrowser @ref="_cutBrowser"
                           OnWaveformGenerated="RefreshWaveformStatusAsync" />
        </MudTabPanel>
        <MudTabPanel Text="@MediumTabLabels[ReleaseMedium.Session]">
            <CmsSessionBrowser @ref="_sessionBrowser"
                               Embedded="true"
                               OnWaveformGenerated="RefreshWaveformStatusAsync" />
        </MudTabPanel>
        <MudTabPanel Text="@MediumTabLabels[ReleaseMedium.Mix]">
            <CmsMixBrowser @ref="_mixBrowser"
                           Embedded="true"
                           OnWaveformGenerated="RefreshWaveformStatusAsync" />
        </MudTabPanel>
    </MudTabs>
</MudContainer>

@code {
    // Active tab. Panel 0 is ALL; panels 1.. map to Enum.GetValues<ReleaseMedium>() in order. Seeded
    // from the ?medium= query param so the catalogue cards can deep-link straight to a medium's tab.
    private int _activeTabIndex;

    // Optional deep-link target from the catalogue cards (?medium=session selects the Sessions tab) and the
    // seed for the Add Track button on the ALL tab. Read once on init; the user can switch tabs freely after.
    [SupplyParameterFromQuery(Name = "medium")] public string? MediumParam { get; set; }

    // The medium the Add Track button pre-selects for the active tab. ALL (panel 0) defaults to Cut; each
    // medium tab maps to its enum value by position, so a fourth medium tab gets a correct Add Track for
    // free — no markup fork.
    private ReleaseMedium ActiveMedium =>
        _activeTabIndex <= 0 ? ReleaseMedium.Cut : Enum.GetValues<ReleaseMedium>()[_activeTabIndex - 1];

    // Single query-param convention: the upload page reads ?medium=… and seeds its selector (which stays
    // user-changeable). Always explicit, including ALL→cut, so the link is unambiguous.
    private static string AddTrackHref(ReleaseMedium medium) =>
        $"/tracks/upload?medium={medium.ToString().ToLowerInvariant()}";

    // Medium → tab label. The one place medium display text lives for the tab strip. The ALL tab is
    // rendered separately (it is not a medium). Tabs are explicit markup so @ref captures work.
    private static readonly IReadOnlyDictionary<ReleaseMedium, string> MediumTabLabels =
        new Dictionary<ReleaseMedium, string>
        {
            [ReleaseMedium.Cut] = "CUTS",
            [ReleaseMedium.Session] = "SESSIONS",
            [ReleaseMedium.Mix] = "MIXES",
        };

    // @ref handles for the per-tab grids. Used to (a) invalidate their cached per-track waveform status
    // after a page-level bulk run, and (b) to wire OnWaveformGenerated so per-row generates bubble up
    // and refresh the page-level missing-count badges. Tabs are now explicit markup rather than the
    // former enum-driven MediumGrid() switch so @ref captures are possible.
    private CmsAllReleasesGrid? _allGrid;
    private CmsCutBrowser? _cutBrowser;
    private CmsSessionBrowser? _sessionBrowser;
    private CmsMixBrowser? _mixBrowser;

    // EntryKey → HasProfile / HasHighRes, loaded once on init so the bulk buttons can show accurate missing
    // counts without depending on any rendered grid. Re-fetched after each bulk run so the counts settle.
    private IReadOnlyList<WaveformStatusDto> _waveformStatus = Array.Empty<WaveformStatusDto>();

    private int MissingProfileCount => _waveformStatus.Count(s => !s.HasProfile);
    private int MissingHighResCount => _waveformStatus.Count(s => !s.HasHighRes);

    // EntryKey → HasOpus (a complete audio+sidecar derive). Loaded alongside the waveform status on init and
    // re-read after a Backfill-Opus run so the "missing N" badge settles. Also the source the Post-Processing
    // poll watches: a freshly uploaded track lands here with HasOpus=false and flips to true once the
    // server-side background transcode finishes — the durable surface for the upload meter's Post-Processing
    // phase after the form returns the admin to this view (§3.1a).
    private IReadOnlyList<OpusStatusDto> _opusStatus = Array.Empty<OpusStatusDto>();

    private int MissingOpusCount => _opusStatus.Count(s => !s.HasOpus);

    // Post-Processing poll: while any track is still missing Opus (a transcode in flight or not yet
    // backfilled), re-read the opus-status map on an interval so the "missing N" badge and any per-track
    // Post-Processing indicator settle without a manual refresh. Stops itself once nothing is missing, and is
    // torn down on dispose. Non-blocking — it never gates an upload or a button; it only refreshes a count.
    private const int OpusPollIntervalMs = 4000;
    private CancellationTokenSource? _opusPollCts;

    // Local state for the parent-owned "Generate All Profiles" bulk run.
    private bool _bulkRunning;
    private int _bulkTotal;
    private int _bulkDone;

    // Local state for the "Backfill High-res" bulk run. Independent of the profile bulk above.
    private bool _highResBulkRunning;
    private int _highResBulkTotal;
    private int _highResBulkDone;

    // Local state for the "Backfill Opus" action. The Opus derive is server-side and background-queued, so
    // there is no client-side per-track loop or progress total — this flag only guards the button while the
    // single scheduling call is in flight.
    private bool _opusBackfillRunning;

    protected override async Task OnInitializedAsync()
    {
        // Seed the active tab from ?medium= so a catalogue card deep-links straight to its medium. Panel 0
        // is ALL; a recognised medium maps to its 1-based position. Unrecognised/absent falls through to ALL.
        if (!string.IsNullOrWhiteSpace(MediumParam)
            && Enum.TryParse<ReleaseMedium>(MediumParam, ignoreCase: true, out var medium)
            && Enum.IsDefined(medium))
        {
            _activeTabIndex = Array.IndexOf(Enum.GetValues<ReleaseMedium>(), medium) + 1;
        }

        await RefreshWaveformStatusAsync();
    }

    private async Task RefreshWaveformStatusAsync()
    {
        var result = await CmsTrackService.GetWaveformStatusAsync();
        _waveformStatus = result.Success && result.Value is not null
            ? result.Value
            : Array.Empty<WaveformStatusDto>();

        await RefreshOpusStatusAsync();
        StateHasChanged();
    }

    /// <summary>
    /// Re-read the per-track Opus derive status and (re)arm the Post-Processing poll when work is still
    /// pending. Called on init, after a Backfill-Opus run, and from the poll itself. Best-effort: a failed
    /// fetch leaves the previous map in place rather than zeroing the badge on a transient API blip. Does not
    /// call StateHasChanged itself — callers batch it with their own render (the poll path renders explicitly).
    /// </summary>
    private async Task RefreshOpusStatusAsync()
    {
        var opusResult = await CmsTrackService.GetOpusStatusAsync();
        if (opusResult.Success && opusResult.Value is not null)
        {
            _opusStatus = opusResult.Value;
        }

        if (MissingOpusCount > 0)
        {
            EnsureOpusPollRunning();
        }
    }

    // Start the Post-Processing poll if it is not already running. The loop re-reads opus-status every
    // OpusPollIntervalMs and renders; it exits as soon as nothing is missing (or on cancel/dispose). Guarded
    // by a non-null CTS so overlapping callers (init + backfill) cannot start two loops.
    private void EnsureOpusPollRunning()
    {
        if (_opusPollCts is not null)
        {
            return;
        }

        _opusPollCts = new CancellationTokenSource();
        _ = PollOpusStatusAsync(_opusPollCts.Token);
    }

    private async Task PollOpusStatusAsync(CancellationToken ct)
    {
        try
        {
            while (!ct.IsCancellationRequested && MissingOpusCount > 0)
            {
                await Task.Delay(OpusPollIntervalMs, ct);

                var result = await CmsTrackService.GetOpusStatusAsync();
                if (result.Success && result.Value is not null)
                {
                    _opusStatus = result.Value;
                    await InvokeAsync(StateHasChanged);
                }
            }
        }
        catch (OperationCanceledException)
        {
            // Expected on dispose / navigation away — nothing to do.
        }
        catch (Exception ex)
        {
            Logger.LogWarning(ex, "Opus Post-Processing poll stopped on an unexpected error.");
        }
        finally
        {
            _opusPollCts?.Dispose();
            _opusPollCts = null;
        }
    }

    public void Dispose()
    {
        _opusPollCts?.Cancel();
    }

    // Invalidates the cached per-track waveform status on all embedded grids so the next row expand
    // re-fetches fresh data. Called after each catalogue-wide bulk run so already-expanded rows
    // reflect the new waveform state on the next expand interaction.
    private async Task InvalidateAllGridsAsync()
    {
        var tasks = new[]
        {
            _allGrid?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
            _cutBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
            _sessionBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
            _mixBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask,
        };
        await Task.WhenAll(tasks);
    }

    /// <summary>
    /// Backfill every track missing a waveform profile, one request at a time so a large backfill does not
    /// flood the API with concurrent WAV decodes. On completion, re-reads the status map so the missing
    /// count settles.
    /// </summary>
    private async Task GenerateAllMissingAsync()
    {
        var missing = _waveformStatus.Where(s => !s.HasProfile).ToList();
        if (missing.Count == 0)
        {
            return;
        }

        _bulkRunning = true;
        _bulkTotal = missing.Count;
        _bulkDone = 0;
        var failures = 0;

        foreach (var status in missing)
        {
            try
            {
                var result = await CmsTrackService.GenerateWaveformProfileAsync(status.EntryKey);
                if (!result.Success)
                {
                    failures++;
                }
            }
            catch (Exception ex)
            {
                Logger.LogError(ex, "Waveform generation failed for {EntryKey}", status.EntryKey);
                failures++;
            }
            _bulkDone++;
            StateHasChanged();
        }

        _bulkRunning = false;
        await RefreshWaveformStatusAsync();
        await InvalidateAllGridsAsync();

        var succeeded = missing.Count - failures;
        if (failures == 0)
        {
            Snackbar.Add($"Generated {succeeded} profile(s).", Severity.Success);
        }
        else
        {
            Snackbar.Add($"Generated {succeeded} profile(s); {failures} failed.", Severity.Warning);
        }
    }

    /// <summary>
    /// Backfill the per-track high-res visualizer datum for every track missing one, one request at a time
    /// so a large backfill does not flood the API with concurrent WAV decodes. Re-runnable (a second run
    /// re-reads status and only retries what is still missing). On completion, re-reads the status map.
    /// </summary>
    private async Task GenerateAllMissingHighResAsync()
    {
        var missing = _waveformStatus.Where(s => !s.HasHighRes).ToList();
        if (missing.Count == 0)
        {
            return;
        }

        _highResBulkRunning = true;
        _highResBulkTotal = missing.Count;
        _highResBulkDone = 0;
        var failures = 0;

        foreach (var status in missing)
        {
            try
            {
                var result = await CmsTrackService.GenerateHighResWaveformAsync(status.EntryKey);
                if (!result.Success)
                {
                    failures++;
                }
            }
            catch (Exception ex)
            {
                Logger.LogError(ex, "High-res waveform generation failed for {EntryKey}", status.EntryKey);
                failures++;
            }
            _highResBulkDone++;
            StateHasChanged();
        }

        _highResBulkRunning = false;
        await RefreshWaveformStatusAsync();
        await InvalidateAllGridsAsync();

        var succeeded = missing.Count - failures;
        if (failures == 0)
        {
            Snackbar.Add($"Backfilled {succeeded} high-res datum(s).", Severity.Success);
        }
        else
        {
            Snackbar.Add($"Backfilled {succeeded} high-res datum(s); {failures} failed.", Severity.Warning);
        }
    }

    /// <summary>
    /// Kick off the catalogue-wide Backfill-Opus pass. The API enumerates the tracks lacking a complete Opus
    /// artifact, enqueues a background derive for each, and returns the (enqueued, skipped) counts. This is a
    /// single scheduling call — the transcodes run server-side afterward — so there is no per-track progress
    /// to render here, just a busy flag and a result snackbar. Re-runnable: a second press only schedules
    /// tracks still missing Opus.
    /// </summary>
    private async Task BackfillOpusAsync()
    {
        _opusBackfillRunning = true;
        StateHasChanged();
        try
        {
            var result = await CmsTrackService.BackfillOpusAsync();
            if (!result.Success)
            {
                var error = result.Messages.FirstOrDefault()?.Message ?? "Failed to start the Opus backfill.";
                Snackbar.Add(error, Severity.Error);
                return;
            }

            // Re-read the status map so the "missing N" badge reflects the just-enqueued work and the
            // Post-Processing poll arms to watch the transcodes settle from N→0 as each finishes.
            await RefreshOpusStatusAsync();

            var (enqueued, skipped) = (result.Value.Enqueued, result.Value.Skipped);
            if (enqueued == 0)
            {
                Snackbar.Add($"All {skipped} track(s) already have Opus — nothing to backfill.", Severity.Info);
            }
            else
            {
                Snackbar.Add(
                    $"Scheduled {enqueued} Opus transcode(s) in the background ({skipped} already had Opus). " +
                    "They will appear as each finishes.",
                    Severity.Success);
            }
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, "Opus backfill failed to start");
            Snackbar.Add("Failed to start the Opus backfill.", Severity.Error);
        }
        finally
        {
            _opusBackfillRunning = false;
            StateHasChanged();
        }
    }
}
