using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;

namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;

public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
{
    [CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
    [CascadingParameter] public IQueueService? QueueService { get; set; }
    [Parameter] public bool Fixed { get; set; } = false;

    [Parameter] public EventCallback<bool> OnMinimized { get; set; }

    [Inject] private IJSRuntime JsRuntime { get; set; } = default!;

    private bool _isMinimized = true;
    private bool _isSeeking = false;
    private double _seekPosition = 0;
    private IStreamingPlayerService? _subscribedService;
    private IQueueService? _subscribedQueue;

    // Spacer-height bridge: the expanded dock is position:fixed, so MainLayout's
    // spacer reserves its space. We mirror this element's live height into a CSS
    // var via a ResizeObserver (see Interop/layout/spacer.ts) rather than a static
    // value, because the player reflows across breakpoints and grows with the
    // error banner.
    //
    // _miniDock is the minimized FAB container. We observe it in minimized state so
    // --player-height stays non-zero (the FAB's actual height) and the MixWaveformVisualizer
    // clips to the top of the FAB rather than extending to the viewport bottom (fix §1).
    // The player-spacer's .minimized class uses a hardcoded 60px and ignores the var,
    // so publishing the FAB height here does not regress the spacer.
    private ElementReference _playerRoot;
    private ElementReference _miniDock;
    private ElementReference _lastObservedElement;
    private IJSObjectReference? _spacerModule;
    private bool _spacerObserved;

    private bool IsLoaded => PlayerService?.IsLoaded ?? false;
    private bool IsLoading => PlayerService?.IsLoading ?? false;

    /// <summary>
    /// A track is staged when it has been selected as the current track but not yet loaded into
    /// the audio context (the embed's pre-gesture state). The first play click loads + plays it.
    /// </summary>
    private bool IsStaged => PlayerService is { IsLoaded: false, IsLoading: false, CurrentTrack: not null };

    /// <summary>Play is available once a track is loaded, or staged and waiting for the first gesture.</summary>
    private bool CanPlay => IsLoaded || IsStaged;
    private bool IsStreaming => PlayerService?.CanStartStreaming ?? false;
    private bool IsStreamingMode => PlayerService?.IsStreamingMode ?? false;
    private double? Duration => PlayerService?.Duration;
    private TrackDto? CurrentTrack => PlayerService?.CurrentTrack;
    private double Volume => PlayerService?.Volume ?? 0;
    private double LoadProgress => PlayerService?.LoadProgress ?? 0;
    private string? ErrorMessage => PlayerService?.ErrorMessage;

    // Skip affordances reflect live queue state. With no queue (null) or an empty queue both are
    // false, so the buttons sit disabled and the bar behaves exactly as it did before the queue.
    private bool HasNext => QueueService?.HasNext ?? false;
    private bool HasPrevious => QueueService?.HasPrevious ?? false;

    /// <summary>
    /// Display time - shows seek position while dragging, otherwise current playback time.
    /// </summary>
    private double DisplayTime => _isSeeking ? _seekPosition : (PlayerService?.CurrentTime ?? 0);
    private string PlayerModeClass => Fixed ? "player-fixed" : "player-dock";

    protected override void OnParametersSet()
    {
        if (Fixed)
        {
            _isMinimized = false;
        }
        
        // PlayerService is cascaded by AudioPlayerProvider; once it arrives,
        // wire our track-selection handler. The provider owns OnStateChanged —
        // we intentionally do NOT wrap or replace it. Because the cascade is
        // IsFixed, the provider's re-render does NOT reliably re-render this bar
        // (it has no incoming parameters that change), so we subscribe to the
        // multicast StateChanged side-channel to re-render ourselves.
        if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService))
        {
            if (_subscribedService != null)
                _subscribedService.StateChanged -= OnPlayerStateChanged;

            PlayerService.OnTrackSelected = new EventCallback(this, Expand);
            PlayerService.StateChanged += OnPlayerStateChanged;
            _subscribedService = PlayerService;
        }

        // The queue cascade is also IsFixed, so re-render the skip affordances off its own
        // change signal — same posture as the player StateChanged subscription above.
        if (QueueService != null && !ReferenceEquals(QueueService, _subscribedQueue))
        {
            if (_subscribedQueue != null)
                _subscribedQueue.QueueChanged -= OnQueueChanged;

            QueueService.QueueChanged += OnQueueChanged;
            _subscribedQueue = QueueService;
        }
    }

    private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged);

    private void OnQueueChanged() => InvokeAsync(StateHasChanged);

    private async Task SkipNext()
    {
        if (QueueService == null) return;
        await QueueService.Next();
    }

    private async Task SkipPrevious()
    {
        if (QueueService == null) return;
        await QueueService.Previous();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // The Fixed embed is already in normal flow — no spacer/clip needed.
        // For the docked player: we observe in BOTH expanded and minimized states
        // so --player-height always reflects the live height of whichever element
        // is visible. This keeps the MixWaveformVisualizer clipped to the top of
        // the footer in both states (fix §1).
        //   expanded  → observe _playerRoot (full player bar, reflows across breakpoints)
        //   minimized → observe _miniDock   (floating FAB container, ~56–60px)
        // The player-spacer's .minimized class uses a hardcoded height and ignores
        // the var, so publishing the FAB height here does not regress the spacer.
        if (Fixed) return;

        var elementToObserve = _isMinimized ? _miniDock : _playerRoot;
        var alreadyOnThisElement = _spacerObserved && elementToObserve.Id == _lastObservedElement.Id;
        if (alreadyOnThisElement) return;

        var module = await GetSpacerModuleAsync();
        if (module is null) return;

        await module.InvokeVoidAsync("observe", elementToObserve);
        _spacerObserved = true;
        _lastObservedElement = elementToObserve;
    }

    private async Task<IJSObjectReference?> GetSpacerModuleAsync()
    {
        try
        {
            return _spacerModule ??= await JsRuntime.InvokeAsync<IJSObjectReference>(
                "import", "./js/layout/spacer.js");
        }
        catch (JSException)
        {
            // Module failed to load — the spacer falls back to 0px (no overlap
            // guard, but the player still works). Nothing actionable here.
            return null;
        }
    }

    private async Task Expand() => await SetMinimized(false);

    /// <summary>
    /// The single assignment site for <see cref="_isMinimized"/>. Guards no-op transitions,
    /// fires <see cref="OnMinimized"/> so MainLayout's spacer class stays in sync, and renders
    /// so OnAfterRenderAsync re-evaluates the ResizeObserver on every transition path.
    /// The <c>Fixed</c> branch in OnParametersSet intentionally bypasses this — it is a
    /// prerender/parameter pass, not a user-driven transition, and the embed host has no spacer.
    /// </summary>
    private async Task SetMinimized(bool value)
    {
        if (_isMinimized == value) return;

        _isMinimized = value;
        if (OnMinimized.HasDelegate) await OnMinimized.InvokeAsync(value);
        StateHasChanged();
    }

    private async Task TogglePlayPause()
    {
        if (PlayerService == null) return;

        // Gesture-gated start: a staged-but-unloaded track (the embed autoplay path) is loaded on
        // the first play click — the user gesture the browser requires before audio can start.
        if (IsStaged)
        {
            await PlayerService.SelectTrackStreaming(PlayerService.CurrentTrack!);
            return;
        }

        await PlayerService.TogglePlayPause();
    }

    private async Task Stop()
    {
        if (PlayerService == null) return;
        await PlayerService.Stop();
    }

    private void OnSeekStart()
    {
        if (PlayerService == null) return;
        _isSeeking = true;
        _seekPosition = PlayerService.CurrentTime;
    }

    private void OnSeekChange(double position)
    {
        _seekPosition = position;
        StateHasChanged();
    }

    private async Task OnSeekEnd(double position)
    {
        if (PlayerService == null) return;
        _isSeeking = false;
        await PlayerService.Seek(position);
    }

    private async Task OnVolumeChange(double volume)
    {
        if (PlayerService == null) return;
        await PlayerService.SetVolume(volume);
    }

    private void ClearError()
    {
        PlayerService?.ClearError();
    }

    private async Task ToggleMinimized() => await SetMinimized(!_isMinimized);

    private async Task Close()
    {
        if (PlayerService != null && PlayerService.IsLoaded)
        {
            await PlayerService.Unload();
        }

        await SetMinimized(true);
    }
    
    public async ValueTask DisposeAsync()
    {
        if (_subscribedService != null)
        {
            _subscribedService.StateChanged -= OnPlayerStateChanged;
            _subscribedService = null;
        }

        if (_spacerModule is not null)
        {
            try
            {
                // Clear the var so a torn-down player can't strand a spacer height.
                await _spacerModule.InvokeVoidAsync("unobserve");
                await _spacerModule.DisposeAsync();
            }
            catch (JSException)
            {
                // Runtime already gone (navigation/teardown) — nothing to clean up.
            }
            _spacerModule = null;
        }
    }
}