using DeepDrftPublic.Client.Services;
using DeepDrftPublic.Client.Clients;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace DeepDrftPublic.Client.Controls;

public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
{
    [Inject] public required AudioInteropService AudioInterop { get; set; }
    [Inject] public required TrackMediaClient TrackMediaClient { get; set; }
    [Inject] public required ILogger<StreamingAudioPlayerService> Logger { get; set; }
    [Inject] public required BeaconInterop Beacon { get; set; }
    [Inject] public required IPlayEventSink PlayEventSink { get; set; }
    [Inject] public required IAnonIdProvider AnonId { get; set; }

    private IStreamingPlayerService? _audioPlayerService;
    private QueueService? _queueService;

    [Parameter] public RenderFragment? ChildContent { get; set; }

    protected override void OnInitialized()
    {
        // Create the service immediately (but don't initialize yet).
        // The base class lazily initializes on first track selection via
        // EnsureInitializedAsync — that path is correct because audio contexts
        // require a user gesture anyway. Initializing eagerly here causes 4+
        // SignalR round-trips before any content is stable.
        var player = new StreamingAudioPlayerService(AudioInterop, TrackMediaClient, Logger);

        // Phase 16: bind the play-session tracker to the player after construction, the same way the
        // queue binds — the player is built with `new`, not DI, so threading telemetry through its
        // constructor would force the provider to over-resolve. The tracker owns the floor/bucket logic
        // and emits via the injected sink (the beacon in production); the beacon also drives the
        // page-unload close so a mid-play tab-close still records the listen. Attached on the concrete
        // type before it is exposed through the IStreamingPlayerService field.
        player.AttachTracker(new PlayTracker(PlayEventSink), Beacon);
        _audioPlayerService = player;

        // Provider is the SOLE owner of OnStateChanged. When the service fires,
        // the provider re-renders, which cascades to its children automatically.
        // Children must not wrap or replace this callback.
        _audioPlayerService.OnStateChanged = new EventCallback(this, () => InvokeAsync(StateHasChanged));
        // OnTrackSelected will be set by individual child components that need it

        // The queue orchestrates above the single-slot player. The player is not DI-registered
        // (constructed here), so the queue binds to it via Attach rather than constructor injection —
        // no construction cycle, no IServiceProvider. Cascaded alongside the player so the bar and a
        // future up-next panel both read it.
        _queueService = new QueueService();
        _queueService.Attach(_audioPlayerService);
    }

    /// <summary>
    /// Warm the anon-id cache once the provider is interactive (Phase 16 wave 16.3). Done here, after the
    /// first render, because the localStorage read is JS interop — not available during prerender. By the
    /// time any play session closes and the sink reads <c>AnonId.Current</c>, the cache is populated; a
    /// play that somehow closes before this completes simply sends no anonId (acceptable over-count). The
    /// provider is the natural warm point: it is mounted in MainLayout, so it goes interactive on every
    /// page the player can play from.
    /// </summary>
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            await AnonId.EnsureLoadedAsync();
    }

    /// <summary>
    /// Dispose the player on unmount so the JS setInterval driving progress
    /// callbacks no longer holds a DotNetObjectReference into a destroyed
    /// component (otherwise it throws every 100ms after navigation away).
    /// </summary>
    public async ValueTask DisposeAsync()
    {
        // Dispose the queue first so it unsubscribes from the player's TrackEnded before the
        // player tears down.
        _queueService?.Dispose();
        _queueService = null;

        if (_audioPlayerService is IAsyncDisposable disposable)
        {
            await disposable.DisposeAsync();
        }
        _audioPlayerService = null;
    }
}
