@page "/tracks/album/{AlbumName}/edit"
@using System.Security.Claims
@using DeepDrftManager.Services
@using DeepDrftModels.DTOs
@using DeepDrftModels.Enums
@using Microsoft.AspNetCore.Components.Forms
@attribute [Authorize]

@inject ICmsTrackService CmsTrackService
@inject CmsTrackBrowserViewModel VM
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject ILogger<BatchEdit> Logger

<PageTitle>Edit Release — DeepDrft CMS</PageTitle>

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
    <MudText Typo="Typo.h4" GutterBottom="true">Edit Release</MudText>

    @if (_loading)
    {
        <MudProgressCircular Indeterminate="true" Class="mt-4" />
    }
    else if (_loadError is { } loadError)
    {
        <MudAlert Severity="Severity.Warning" Class="mt-4">@loadError</MudAlert>
    }
    else
    {
        <AlbumHeaderFields @bind-AlbumName="_albumName"
                           @bind-Artist="_artist"
                           @bind-Genre="_genre"
                           @bind-ReleaseDate="_releaseDate"
                           @bind-ReleaseType="_releaseType"
                           Medium="_medium"
                           MediumChanged="OnMediumChanged"
                           @bind-SelectedImageFile="_selectedImageFile"
                           ExistingImagePath="_existingImagePath"
                           Disabled="_saving" />

        @if (_existingImagePath is not null && _selectedImageFile is null)
        {
            <MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-4">
                <MudButton Variant="Variant.Text"
                           Color="Color.Error"
                           StartIcon="@Icons.Material.Filled.Delete"
                           Disabled="_saving"
                           OnClick="RemoveCover">
                    Remove cover
                </MudButton>
            </MudStack>
        }

        @* Session/Mix are single-track releases (§9.3): suppress the add-track affordance and keep the
           list collapsed to one row — OnMediumChanged trims rows 2..n when the medium switches to a
           single-track medium, mirroring BatchUpload's same-named collapse. Cut keeps the full list. *@
        <MudGrid>
            <MudItem xs="12" md="5">
                <BatchTrackList Tracks="_tracks"
                                @bind-SelectedIndex="_selectedIndex"
                                Disabled="_saving"
                                AllowNewTracks="@(_medium == ReleaseMedium.Cut)"
                                OnWavFilesSelected="HandleWavFilesSelected"
                                OnMoveUp="MoveUp"
                                OnMoveDown="MoveDown"
                                OnRemove="RemoveRow" />
            </MudItem>

            <MudItem xs="12" md="7">
                <MudPaper Class="pa-4" Elevation="2">
                    <BatchTrackDetail SelectedTrack="@(_selectedIndex >= 0 && _tracks.Count > 0 ? _tracks[_selectedIndex] : null)"
                                      Disabled="_saving"
                                      ShowTrackName="@(!MediumRules.CardinalityOf(_medium).IsSingleTrack)"
                                      TrackNameChanged="@(name => { if (_selectedIndex >= 0) { _tracks[_selectedIndex].TrackName = name; } })" />
                </MudPaper>
            </MudItem>
        </MudGrid>

        @if (!string.IsNullOrEmpty(_errorMessage))
        {
            <MudAlert Severity="Severity.Error" Class="mt-4">@_errorMessage</MudAlert>
        }

        <MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2" Class="mt-4">
            <MudButton Variant="Variant.Text"
                       OnClick="@(() => Navigation.NavigateTo("/tracks/albums"))"
                       Disabled="_saving">
                Cancel
            </MudButton>
            <MudButton Variant="Variant.Filled"
                       Color="Color.Primary"
                       OnClick="SaveAsync"
                       Disabled="@(_saving || _tracks.Count == 0)">
                @if (_saving)
                {
                    <MudProgressCircular Indeterminate="true" Size="Size.Small" Class="mr-2" />
                    <text>Saving @_processedCount / @_tracks.Count…</text>
                }
                else
                {
                    <text>Save Changes</text>
                }
            </MudButton>
        </MudStack>
    }
</MudContainer>

@code {
    // 1 GB ceiling matches DeepDrftAPI's per-request limit on api/track/upload.
    private const long MaxUploadBytes = 1_073_741_824L;

    [Parameter] public string AlbumName { get; set; } = string.Empty;

    private List<BatchRowModel> _tracks = new();
    private int _selectedIndex = -1;
    private bool _loading = true;
    private string? _loadError;
    private bool _saving;
    private int _processedCount;
    private string? _errorMessage;

    private IBrowserFile? _selectedImageFile;
    private string? _imagePath;
    private string? _existingImagePath;
    private bool _clearExistingImage;

    private string _albumName = string.Empty;
    private string _artist = string.Empty;
    private string _genre = string.Empty;
    private string _releaseDate = string.Empty;
    private ReleaseType _releaseType = ReleaseType.Single;
    private ReleaseMedium _medium = ReleaseMedium.Cut;

    // The medium selector drives ReleaseType visibility and is persisted on save: every UpdateAsync /
    // UploadTrackAsync call below passes _medium, and PUT api/track/meta resets ReleaseType to its
    // default server-side for a non-Cut medium.
    //
    // Switching to a single-track medium collapses any multi-track list to the first row so the
    // single-track invariant (§9.3) holds before save — the same collapse BatchUpload.OnMediumChanged
    // performs, reading the same MediumRules cardinality the upload service enforces. Dropping rows
    // 2..n is an in-memory trim only; existing tracks are not deleted server-side (RemoveRow owns
    // deletion), so the hidden rows simply fall out of this edit session.
    private void OnMediumChanged(ReleaseMedium medium)
    {
        _medium = medium;
        if (MediumRules.CardinalityOf(medium).IsSingleTrack && _tracks.Count > 1)
        {
            _tracks.RemoveRange(1, _tracks.Count - 1);
            _selectedIndex = _tracks.Count > 0 ? 0 : -1;
        }
    }

    protected override async Task OnInitializedAsync()
    {
        // A single page of 100 covers the full release (albums are small — same assumption as
        // CmsAlbumBrowser). Sorted by track number so list order matches the saved ordinals.
        var result = await CmsTrackService.GetPagedAsync(
            page: 1, pageSize: 100,
            sortColumn: "TrackNumber", sortDescending: false,
            album: AlbumName);

        if (!result.Success || result.Value is null)
        {
            _loadError = result.Messages.FirstOrDefault()?.Message ?? "Failed to load release.";
            _loading = false;
            return;
        }

        var tracks = result.Value.Items.ToList();
        if (tracks.Count == 0)
        {
            _loadError = $"No tracks found for release '{AlbumName}'.";
            _loading = false;
            return;
        }

        var release = tracks[0].Release;
        _albumName = AlbumName;
        _artist = release?.Artist ?? string.Empty;
        _genre = release?.Genre ?? string.Empty;
        _releaseDate = release?.ReleaseDate?.ToString("yyyy-MM-dd") ?? string.Empty;
        _releaseType = release?.ReleaseType ?? ReleaseType.Single;
        _medium = release?.Medium ?? ReleaseMedium.Cut;
        _existingImagePath = release?.ImagePath;

        _tracks = tracks.Select(t => new BatchRowModel
        {
            Id = t.Id,
            EntryKey = t.EntryKey,
            OriginalFileName = t.OriginalFileName,
            TrackName = t.TrackName,
            TrackNumber = t.TrackNumber,
            WavFile = null,
            Status = BatchRowStatus.Queued
        }).ToList();

        // Same single-track collapse on the load path, via the shared MediumRules declaration: a
        // release whose stored medium is single-track surfaces only its first row for editing.
        if (MediumRules.CardinalityOf(_medium).IsSingleTrack && _tracks.Count > 1)
        {
            _tracks.RemoveRange(1, _tracks.Count - 1);
        }

        _selectedIndex = _tracks.Count > 0 ? 0 : -1;
        _loading = false;
    }

    private void HandleWavFilesSelected(IReadOnlyList<IBrowserFile> files)
    {
        _errorMessage = null;
        foreach (var file in files)
        {
            if (!file.Name.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
            {
                Snackbar.Add($"Skipped '{file.Name}' — not a .wav file.", Severity.Warning);
                continue;
            }

            // New rows carry no Id — they take the upload path on save.
            _tracks.Add(new BatchRowModel
            {
                WavFile = file,
                TrackName = Path.GetFileNameWithoutExtension(file.Name)
            });
        }

        if (_selectedIndex < 0 && _tracks.Count > 0)
        {
            _selectedIndex = 0;
        }
    }

    private void MoveUp(int i)
    {
        if (i == 0) return;
        (_tracks[i], _tracks[i - 1]) = (_tracks[i - 1], _tracks[i]);
        if (_selectedIndex == i) _selectedIndex = i - 1;
        else if (_selectedIndex == i - 1) _selectedIndex = i;
    }

    private void MoveDown(int i)
    {
        if (i == _tracks.Count - 1) return;
        (_tracks[i], _tracks[i + 1]) = (_tracks[i + 1], _tracks[i]);
        if (_selectedIndex == i) _selectedIndex = i + 1;
        else if (_selectedIndex == i + 1) _selectedIndex = i;
    }

    private async Task RemoveRow(int index)
    {
        var row = _tracks[index];
        if (row.Id.HasValue)
        {
            // Existing track — confirm before deleting.
            var confirmed = await DialogService.ShowMessageBox(
                "Remove track",
                $"Remove '{row.TrackName}' from this release? This deletes the track permanently.",
                yesText: "Remove", cancelText: "Cancel");
            if (confirmed != true) return;

            var result = await CmsTrackService.DeleteTrackAsync(row.Id.Value);
            if (!result.Success)
            {
                var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
                Snackbar.Add($"Delete failed: {error}", Severity.Error);
                return;
            }
        }

        // New track (not yet uploaded) or confirmed existing delete — remove from list.
        _tracks.RemoveAt(index);
        if (index < _selectedIndex) _selectedIndex--;
        if (_selectedIndex >= _tracks.Count) _selectedIndex = _tracks.Count - 1;
    }

    private void RemoveCover()
    {
        // Defer the actual clear to save: pass "" to UpdateAsync's tri-state imagePath. Nulling
        // the existing path here drops the preview in AlbumHeaderFields.
        _clearExistingImage = true;
        _existingImagePath = null;
    }

    private async Task SaveAsync()
    {
        _errorMessage = null;

        if (string.IsNullOrWhiteSpace(_albumName))
        {
            _errorMessage = "Release Name is required.";
            return;
        }

        if (string.IsNullOrWhiteSpace(_artist))
        {
            _errorMessage = "Artist is required.";
            return;
        }

        if (_tracks.Count == 0)
        {
            _errorMessage = "A release must have at least one track.";
            return;
        }

        if (!string.IsNullOrWhiteSpace(_releaseDate)
            && !DateOnly.TryParseExact(_releaseDate, "yyyy-MM-dd", out _))
        {
            _errorMessage = "Release Date must be in YYYY-MM-DD format.";
            return;
        }

        // New rows (no Id) need a WAV; existing rows keep their vault audio.
        foreach (var t in _tracks)
        {
            if (!t.Id.HasValue && t.WavFile is null)
            {
                _errorMessage = $"'{t.TrackName}' has no WAV file selected.";
                return;
            }
        }

        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        var userIdValue = authState.User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (!long.TryParse(userIdValue, out var createdByUserId))
        {
            // [Authorize]/Admin-gated page — an unparseable id here is a configuration bug.
            Logger.LogError("Authenticated user has no parseable NameIdentifier claim: {Value}", userIdValue);
            _errorMessage = "Your session is missing a valid identifier. Please sign in again.";
            return;
        }

        DateOnly? releaseDate = string.IsNullOrWhiteSpace(_releaseDate)
            ? null
            : DateOnly.ParseExact(_releaseDate, "yyyy-MM-dd");
        var album = string.IsNullOrWhiteSpace(_albumName) ? null : _albumName;
        var genre = string.IsNullOrWhiteSpace(_genre) ? null : _genre;

        // For single-track media (Session/Mix) the track name is derived from the Release Name —
        // no separate Track Name editor is shown. Sync here so changes to the Release Name always
        // carry through to the stored track name.
        if (MediumRules.CardinalityOf(_medium).IsSingleTrack && _tracks.Count > 0)
        {
            _tracks[0].TrackName = _albumName;
        }

        _imagePath = null; // Clear any stale uploaded path from a prior partial attempt.
        _saving = true;
        _processedCount = 0;

        try
        {
            // Upload any newly picked cover art once; abort if it fails so we never point metadata
            // at an image that was never stored.
            if (_selectedImageFile is { } imgFile)
            {
                await using var imgStream = imgFile.OpenReadStream(maxAllowedSize: 50_000_000);
                var imgResult = await CmsTrackService.UploadImageAsync(imgStream, imgFile.Name, imgFile.ContentType);
                if (!imgResult.Success)
                {
                    var imgError = imgResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
                    _errorMessage = $"Image upload failed: {imgError}";
                    return;
                }
                _imagePath = imgResult.Value;
            }

            // Tri-state cover for UpdateAsync: a freshly uploaded path sets it; an explicit clear
            // sends ""; otherwise null leaves the existing cover untouched.
            string? imagePathForUpdate =
                _imagePath is { } newPath ? newPath
                : _clearExistingImage ? ""
                : null;

            int succeeded = 0, failed = 0;
            for (int i = 0; i < _tracks.Count; i++)
            {
                var row = _tracks[i];

                if (row.Status == BatchRowStatus.Done)
                {
                    _processedCount++;
                    continue;
                }

                var trackNumber = i + 1; // 1-based ordinal from list position

                row.Status = BatchRowStatus.Uploading;
                StateHasChanged();

                try
                {
                    if (row.Id.HasValue)
                    {
                        // Existing track — metadata-only update; audio stays in the vault.
                        var updateResult = await CmsTrackService.UpdateAsync(
                            row.Id.Value,
                            row.TrackName,
                            _artist,
                            album,
                            genre,
                            releaseDate,
                            imagePathForUpdate,
                            _releaseType,
                            _medium,
                            trackNumber);

                        if (!updateResult.Success)
                        {
                            var error = updateResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
                            row.Status = BatchRowStatus.Failed;
                            row.ErrorMessage = error;
                            failed++;
                            Logger.LogWarning("Batch edit: update for '{TrackName}' (id={Id}) failed: {Error}",
                                row.TrackName, row.Id.Value, error);
                        }
                        else
                        {
                            row.Status = BatchRowStatus.Done;
                            succeeded++;
                        }
                    }
                    else
                    {
                        // New track — upload, then link cover art with a follow-up update (same
                        // two-step pattern as BatchUpload; the upload endpoint takes no imagePath).
                        await using var wavStream = row.WavFile!.OpenReadStream(MaxUploadBytes);
                        var uploadResult = await CmsTrackService.UploadTrackAsync(
                            wavStream,
                            row.WavFile.Name,
                            row.WavFile.ContentType,
                            row.TrackName,
                            _artist,
                            album,
                            genre,
                            string.IsNullOrWhiteSpace(_releaseDate) ? null : _releaseDate,
                            row.WavFile.Name,
                            createdByUserId,
                            _releaseType,
                            trackNumber,
                            _medium);

                        if (!uploadResult.Success || uploadResult.Value is null)
                        {
                            var error = uploadResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
                            row.Status = BatchRowStatus.Failed;
                            row.ErrorMessage = error;
                            failed++;
                            Logger.LogWarning("Batch edit: upload for new track '{TrackName}' failed: {Error}",
                                row.TrackName, error);
                        }
                        else
                        {
                            // Link a cover only when one is actively set ("" clear doesn't apply to
                            // a brand-new track that has no cover yet).
                            if (imagePathForUpdate is { Length: > 0 } linkPath)
                            {
                                var linkResult = await CmsTrackService.UpdateAsync(
                                    uploadResult.Value.Id,
                                    row.TrackName,
                                    _artist,
                                    album,
                                    genre,
                                    releaseDate,
                                    linkPath,
                                    _releaseType,
                                    _medium,
                                    trackNumber);

                                if (!linkResult.Success)
                                {
                                    // Non-blocking: track persisted; cover can be linked via TrackEdit.
                                    Logger.LogWarning("Batch edit: cover link failed for new track '{TrackName}' (id={Id})",
                                        row.TrackName, uploadResult.Value.Id);
                                }
                            }

                            row.Status = BatchRowStatus.Done;
                            succeeded++;
                        }
                    }
                }
                catch (Exception ex)
                {
                    Logger.LogError(ex, "Batch edit: exception processing '{TrackName}'", row.TrackName);
                    row.Status = BatchRowStatus.Failed;
                    row.ErrorMessage = "Save failed — please try again.";
                    failed++;
                }

                _processedCount++;
                StateHasChanged();
            }

            // Either branch changed catalogue data, so the browse caches are stale regardless of
            // whether every track saved. Invalidate before navigating (or staying) so the /tracks
            // album and genre lists re-fetch.
            VM.Invalidate();

            if (failed == 0)
            {
                Snackbar.Add($"Saved {succeeded} track(s).", Severity.Success);
                Navigation.NavigateTo("/tracks/albums");
            }
            else
            {
                Snackbar.Add($"Saved {succeeded} track(s); {failed} failed — review errors below.", Severity.Warning);
                // Stay on page so the admin can see the failed rows.
            }
        }
        finally
        {
            _saving = false;
            StateHasChanged();
        }
    }
}
