using DeepDrftModels.DTOs;
using DeepDrftModels.Enums;
using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;

namespace DeepDrftPublic.Client.Pages;

/// <summary>
/// The public archive: a release-cardinal searchable browser over every release across all media
/// (Phase 9 §8.H, decision H2). Replaces the former three-card medium overview. Search (Title /
/// Artist), an enum-driven medium filter, and a genre filter narrow the release list; each card
/// routes to its per-medium detail. Mirrors the <see cref="MediumBrowseBase"/> seam: the unfiltered
/// first page is bridged across the prerender -> WASM boundary so hydration neither re-fetches nor
/// replays the card entrance animations.
///
/// Filter state is URL-bound (Phase 11 §5): <c>q</c> / <c>medium</c> / <c>genre</c> enter via
/// <see cref="SupplyParameterFromQueryAttribute"/> and leave via <see cref="NavigationManager.NavigateTo(string)"/>.
/// The URL is the single source of truth — filter handlers only navigate; the seed-and-fetch reaction
/// lives in <see cref="OnParametersSetAsync"/> so back/forward history "just works" (§5.3 Option A).
/// </summary>
public partial class ArchiveView : ComponentBase, IDisposable
{
    private const string PersistKey = "archive-releases";

    // A large page covers the full library in one fetch — the archive has no pager, matching the
    // medium galleries (AlbumsView / MediumBrowseBase) which also pull pageSize: 100.
    private const int PageSize = 100;

    [Inject] public required IReleaseDataService ReleaseData { get; set; }
    [Inject] public required ITrackDataService TrackData { get; set; }
    [Inject] public required PersistentComponentState PersistentState { get; set; }
    [Inject] public required NavigationManager Navigation { get; set; }

    // Query-string-bound filter inputs (§5.1). All optional; an absent param means "no filter on that
    // axis". `medium` is the lowercase enum token the data service already speaks, parsed back with
    // Enum.TryParse(ignoreCase) + Enum.IsDefined (the BatchUpload / API posture).
    [SupplyParameterFromQuery(Name = "q")] public string? QueryParam { get; set; }
    [SupplyParameterFromQuery(Name = "medium")] public string? MediumParam { get; set; }
    [SupplyParameterFromQuery(Name = "genre")] public string? GenreParam { get; set; }

    // Medium filter chips are enum-driven so a fourth medium surfaces a chip from one lookup entry,
    // with no markup fork (Phase 9 extension discipline).
    private static readonly ReleaseMedium[] _media = Enum.GetValues<ReleaseMedium>();

    private bool _loading = true;
    private List<ReleaseDto> _releases = [];
    private List<GenreSummaryDto> _genres = [];

    // null medium == All; null genre == no genre filter. SearchText null/empty == no search.
    private ReleaseMedium? _selectedMedium;
    private string? _selectedGenre;

    private string? SearchText { get; set; }

    private PersistingComponentStateSubscription _persistingSubscription;

    // Idempotency guard for the OnParametersSet reaction (§5.3): a same-route query change re-fires
    // OnParametersSet, and the search debounce can race a rapid medium-chip nav. We only re-seed and
    // re-fetch when the composed filter triple actually changed. Sentinel != any real key so the first
    // reaction always runs.
    private string? _loadedFilterKey;

    private bool HasActiveFilter =>
        _selectedMedium is not null
        || !string.IsNullOrWhiteSpace(_selectedGenre)
        || !string.IsNullOrWhiteSpace(SearchText);

    protected override Task OnInitializedAsync()
    {
        _persistingSubscription = PersistentState.RegisterOnPersisting(Persist);
        return Task.CompletedTask;
    }

    // The seed-and-fetch reaction lives here, not in OnInitialized: a same-route query change reuses
    // the component and fires only OnParametersSet (§5.3). Keyed off the composed filter triple so an
    // identical param set (debounce vs. chip-nav race) is a no-op.
    protected override async Task OnParametersSetAsync()
    {
        // Seed filter fields from the URL before the restore/fetch decision so HasActiveFilter reflects
        // the requested URL (a direct /archive?medium=mix load must fetch, not restore the bridge).
        SeedFromQuery();

        var filterKey = ComposeFilterKey();
        if (filterKey == _loadedFilterKey)
            return;

        var firstReaction = _loadedFilterKey is null;
        _loadedFilterKey = filterKey;

        // The genre chip source is the release-cardinal distinct-genre list (already sourced from the
        // release join — see GetDistinctGenresAsync). It only renders interactively, so it is fetched
        // lazily on the interactive pass rather than persisted.
        if (RendererInfo.IsInteractive && _genres.Count == 0)
            await LoadGenres();

        // The prerendered page is always the unfiltered first page. Restore it only on the first
        // reaction and only when no filter is active; a filtered load (or a later filter change) must
        // fetch its own narrowed result instead.
        if (firstReaction
            && !HasActiveFilter
            && PersistentState.TryTakeFromJson<List<ReleaseDto>>(PersistKey, out var restored)
            && restored is not null)
        {
            _releases = restored;
            _loading = false;
            return;
        }

        await LoadReleases();
    }

    // Maps the query-string params onto the filter fields. `medium` parsed leniently (ignoreCase) and
    // validated with Enum.IsDefined so a stray token degrades to "All" rather than throwing.
    private void SeedFromQuery()
    {
        SearchText = string.IsNullOrWhiteSpace(QueryParam) ? null : QueryParam;
        _selectedGenre = string.IsNullOrWhiteSpace(GenreParam) ? null : GenreParam;
        _selectedMedium =
            !string.IsNullOrWhiteSpace(MediumParam)
            && Enum.TryParse<ReleaseMedium>(MediumParam, ignoreCase: true, out var medium)
            && Enum.IsDefined(medium)
                ? medium
                : null;
    }

    private string ComposeFilterKey() => $"{SearchText}|{_selectedMedium}|{_selectedGenre}";

    private async Task LoadGenres()
    {
        var result = await TrackData.GetGenres();
        if (result is { Success: true, Value: { } genres })
            _genres = genres;
    }

    private async Task LoadReleases()
    {
        _loading = true;

        var result = await ReleaseData.GetPaged(
            medium: _selectedMedium?.ToString().ToLowerInvariant(),
            page: 1,
            pageSize: PageSize,
            search: SearchText,
            genre: _selectedGenre);

        _releases = result is { Success: true, Value.Items: { } items }
            ? items.ToList()
            : [];

        _loading = false;
    }

    // Filter handlers only navigate (§5.3 Option A): the query-param change re-runs the seed-and-fetch
    // reaction in OnParametersSet, keeping the URL the single source of truth and making back/forward
    // history correct for free.

    // Fired by MudTextField after its 400ms DebounceInterval, so only the trailing keystroke in a
    // burst reaches here.
    private void OnSearchInput(string? value) => NavigateToFilter(
        search: string.IsNullOrWhiteSpace(value) ? null : value,
        medium: _selectedMedium,
        genre: _selectedGenre);

    private void OnMediumSelected(ReleaseMedium? medium) => NavigateToFilter(
        search: SearchText,
        medium: medium,
        genre: _selectedGenre);

    private void OnGenreSelected(string? genre) => NavigateToFilter(
        search: SearchText,
        medium: _selectedMedium,
        genre: string.IsNullOrWhiteSpace(genre) ? null : genre);

    // Composes the /archive?... URL from the requested filter triple. Each axis is omitted when null
    // (matches the null-means-all scheme); values are escaped. Plain /archive when all three are clear.
    private void NavigateToFilter(string? search, ReleaseMedium? medium, string? genre)
    {
        var query = new List<string>(3);
        if (!string.IsNullOrWhiteSpace(search))
            query.Add($"q={Uri.EscapeDataString(search)}");
        if (medium is not null)
            query.Add($"medium={medium.ToString()!.ToLowerInvariant()}");
        if (!string.IsNullOrWhiteSpace(genre))
            query.Add($"genre={Uri.EscapeDataString(genre)}");

        var url = query.Count > 0 ? $"/archive?{string.Join('&', query)}" : "/archive";
        Navigation.NavigateTo(url);
    }

    // Display label for a medium filter chip. Centralised so a new medium's label is one entry, not a
    // markup change. "DJ Mix" matches the CMS Type-chip wording (§8.D).
    private static string MediumLabel(ReleaseMedium medium) => medium switch
    {
        ReleaseMedium.Cut => "Cuts",
        ReleaseMedium.Session => "Sessions",
        ReleaseMedium.Mix => "Mixes",
        _ => medium.ToString(),
    };

    private Task Persist()
    {
        // Only the unfiltered first page is safe to restore onto a later plain /archive visit. A
        // filtered render leaves the cache untouched so the bridge never serves narrowed results to an
        // unfiltered load.
        if (_releases.Count > 0 && !HasActiveFilter)
            PersistentState.PersistAsJson(PersistKey, _releases);
        return Task.CompletedTask;
    }

    public void Dispose() => _persistingSubscription.Dispose();
}
