using DeepDrftModels.DTOs;

namespace DeepDrftPublic.Client.Services;

/// <summary>
/// Default <see cref="IQueueService"/>: a two-level deque orchestrator over an
/// <see cref="IStreamingPlayerService"/>. Holds the ordered list and current index as pure state,
/// drives playback through the player's existing <see cref="IStreamingPlayerService.SelectTrackStreaming"/>,
/// and auto-advances on the player's <see cref="IPlayerService.TrackEnded"/> signal. PLAY mutations enter
/// the front (prepend); add-to-queue mutations enter the back (append) — see <see cref="IQueueService"/>
/// for the full invariant.
///
/// <para>
/// The player instance is not DI-registered — <c>AudioPlayerProvider</c> constructs and cascades it.
/// So the queue is bound to the player via <see cref="Attach"/> (called once by the provider after it
/// creates the player) rather than constructor injection. This keeps the player single-slot, avoids a
/// construction cycle between provider/player/queue, and needs no <c>IServiceProvider</c>. The queue's
/// own constructor stays parameterless, so the queue logic is unit-testable against a fake player with
/// no container. The attached player is also the seam by which the queue learns the externally-playing
/// track when a dormant <see cref="Enqueue"/> needs to seed the head.
/// </para>
/// </summary>
public sealed class QueueService : IQueueService, IDisposable
{
    private readonly List<TrackDto> _items = new();
    private IStreamingPlayerService? _player;

    public IReadOnlyList<TrackDto> Items => _items;

    public int CurrentIndex { get; private set; } = -1;

    public bool IsArmed { get; private set; }

    public TrackDto? Current =>
        CurrentIndex >= 0 && CurrentIndex < _items.Count ? _items[CurrentIndex] : null;

    public bool HasNext => CurrentIndex >= 0 && CurrentIndex < _items.Count - 1;

    public bool HasPrevious => CurrentIndex > 0;

    public event Action? QueueChanged;

    /// <summary>
    /// Binds the queue to the player instance the provider owns, and subscribes to its track-ended
    /// signal so the queue auto-advances. Idempotent and re-bindable: re-attaching detaches the prior
    /// player first, so the queue never holds a stale subscription after a player swap. Owned by the
    /// provider's lifecycle; <see cref="Dispose"/> unsubscribes.
    /// </summary>
    public void Attach(IStreamingPlayerService player)
    {
        if (ReferenceEquals(_player, player)) return;

        if (_player != null)
            _player.TrackEnded -= OnTrackEnded;

        _player = player;
        _player.TrackEnded += OnTrackEnded;
    }

    public async Task PlayTrack(TrackDto track)
    {
        PrependForPlay(new[] { track }, prependIndex: 0);
        QueueChanged?.Invoke();
        await PlayCurrent();
    }

    public async Task PlayRelease(IEnumerable<TrackDto> tracks, int startIndex = 0)
    {
        var list = tracks.ToList();
        if (list.Count == 0) return;

        var start = Math.Clamp(startIndex, 0, list.Count - 1);
        PrependForPlay(list, start);
        QueueChanged?.Invoke();

        await PlayCurrent();
    }

    // The shared PLAY-prepend mutation (bug #5). Removes the previously-current track, inserts the
    // played track(s) at the front in order, and points CurrentIndex at the prepended item the caller
    // chose to start on. Whatever sat AFTER the old current stays intact behind the prepend; the old
    // back-history (items before the old current) is discarded because a fresh PLAY defines a new
    // front. Pure state — callers invoke QueueChanged + PlayCurrent. IsArmed clears: playback is real now.
    private void PrependForPlay(IReadOnlyList<TrackDto> played, int prependIndex)
    {
        // Drop the previously-current track only (its tail — the up-next after it — is preserved).
        // Anything before the old current is back-history that a new PLAY supersedes.
        if (CurrentIndex >= 0 && CurrentIndex < _items.Count)
            _items.RemoveRange(0, CurrentIndex + 1);

        _items.InsertRange(0, played);
        CurrentIndex = prependIndex;
        IsArmed = false;
    }

    public void Arm(IEnumerable<TrackDto> tracks)
    {
        var list = tracks as IReadOnlyList<TrackDto> ?? tracks.ToList();
        if (list.Count == 0) return;

        _items.Clear();
        _items.AddRange(list);
        CurrentIndex = 0;
        IsArmed = true;
        // No PlayCurrent: arming is interop-free state only. The first play gesture drives Start().
        QueueChanged?.Invoke();
    }

    public async Task Start()
    {
        if (!IsArmed) return;
        IsArmed = false;
        QueueChanged?.Invoke();
        await PlayCurrent();
    }

    public void Enqueue(TrackDto track)
    {
        SeedHeadFromPlayerIfDormant();
        _items.Add(track);
        EnsureCoherentDormantIndex();
        QueueChanged?.Invoke();
    }

    public void EnqueueRange(IEnumerable<TrackDto> tracks)
    {
        var toAdd = tracks as IReadOnlyList<TrackDto> ?? tracks.ToList();
        if (toAdd.Count == 0) return;

        SeedHeadFromPlayerIfDormant();
        _items.AddRange(toAdd);
        EnsureCoherentDormantIndex();
        QueueChanged?.Invoke();
    }

    // Bug #3: the first add into a dormant queue while a track is already playing externally (through
    // the attached player but not via the queue) must seed the head with that now-playing track, so the
    // append yields [now-playing, added] instead of a phantom single entry. We read the player's
    // CurrentTrack — the same seam OnTrackEnded uses — so no extra dependency is introduced. Only seeds
    // when truly dormant (empty list) AND a player track exists; a non-dormant queue is untouched.
    private void SeedHeadFromPlayerIfDormant()
    {
        if (_items.Count != 0) return;
        var playing = _player?.CurrentTrack;
        if (playing is null) return;

        _items.Add(playing);
        CurrentIndex = 0;
    }

    // After an append, a dormant queue (CurrentIndex == -1, e.g. nothing was playing to seed from)
    // needs a coherent head so a subsequent play/skip is correct — but add is not play, so we never
    // stream here. A queue that already has a current index is left untouched.
    private void EnsureCoherentDormantIndex()
    {
        if (CurrentIndex == -1 && _items.Count > 0)
            CurrentIndex = 0;
    }

    public void Move(int fromIndex, int toIndex)
    {
        if (fromIndex == toIndex) return;
        if (fromIndex < 0 || fromIndex >= _items.Count) return;
        if (toIndex < 0 || toIndex >= _items.Count) return;

        var moved = _items[fromIndex];
        _items.RemoveAt(fromIndex);
        _items.Insert(toIndex, moved);

        // Keep the same track current across the reorder. No playback is touched (C2): we only
        // recompute which index the current track now sits at.
        if (CurrentIndex == fromIndex)
        {
            CurrentIndex = toIndex;
        }
        else if (fromIndex < CurrentIndex && toIndex >= CurrentIndex)
        {
            // The current track shifted one slot toward the front to fill the vacated lower slot.
            CurrentIndex--;
        }
        else if (fromIndex > CurrentIndex && toIndex <= CurrentIndex)
        {
            // An item inserted at/above the current slot pushed the current track one slot back.
            CurrentIndex++;
        }

        QueueChanged?.Invoke();
    }

    public void RemoveAt(int index)
    {
        if (index < 0 || index >= _items.Count) return;

        _items.RemoveAt(index);

        if (_items.Count == 0)
        {
            // Last remaining track removed → empty + dormant. Does not stop the player (C2).
            CurrentIndex = -1;
        }
        else if (index < CurrentIndex)
        {
            // A track before the current was removed: the same track stays current at a lower index.
            CurrentIndex--;
        }
        else if (index == CurrentIndex && CurrentIndex >= _items.Count)
        {
            // The current track was removed and it was the last slot: there is no "next" occupant to
            // resolve to, so the queue goes dormant (CurrentIndex == -1). Playback is NOT stopped
            // (C2) — the just-removed track keeps playing to its natural end; auto-advance simply has
            // nothing further. Removing current when it is NOT the last leaves CurrentIndex pointing
            // at the new occupant of that slot (the next track), so no adjustment is needed there.
            CurrentIndex = -1;
        }

        QueueChanged?.Invoke();
    }

    public async Task Next()
    {
        if (!HasNext) return;
        CurrentIndex++;
        IsArmed = false;
        QueueChanged?.Invoke();
        await PlayCurrent();
    }

    public async Task Previous()
    {
        if (!HasPrevious) return;
        CurrentIndex--;
        IsArmed = false;
        QueueChanged?.Invoke();
        await PlayCurrent();
    }

    public async Task JumpTo(int index)
    {
        if (index < 0 || index >= _items.Count) return;
        if (index == CurrentIndex) return;
        CurrentIndex = index;
        IsArmed = false;
        QueueChanged?.Invoke();
        await PlayCurrent();
    }

    public void Clear()
    {
        if (_items.Count == 0 && CurrentIndex == -1) return;
        _items.Clear();
        CurrentIndex = -1;
        IsArmed = false;
        QueueChanged?.Invoke();
    }

    public void ClearUpcoming()
    {
        // Keep the currently-playing track, drop everything else. No current track (dormant/empty) or a
        // queue that already holds only the current → nothing to clear.
        var current = Current;
        if (current is null || _items.Count <= 1) return;

        _items.Clear();
        _items.Add(current);
        CurrentIndex = 0;
        // Playback is untouched (C2): the current track keeps streaming; we only discarded the up-next.
        QueueChanged?.Invoke();
    }

    // Advance on organic end-of-stream only. TrackEnded is not raised by stop/unload/track-switch,
    // so a manual stop or a fresh single-track selection elsewhere never spuriously advances the
    // queue.
    //
    // Guard: only act when the track that just ended is the queue's own current item. Call sites that
    // stream a single track directly (SessionDetail, StreamNowButton, resume from AudioPlayerBar)
    // overwrite the player's CurrentTrack without touching the queue. If their track reaches natural
    // end, the player fires TrackEnded — but the queue's Current no longer matches the player's
    // CurrentTrack, so we must not touch the queue. Id-based equality is used rather than ReferenceEquals
    // because DTO copies through serialisation are not reference-equal.
    //
    // When the ended track IS the queue's current: advance if there is a next track, otherwise the queue
    // has reached its end — empty it (bug #2), so the finished last track is not stranded as current and
    // the queue goes dormant (panel/button gone per HasQueue gating).
    private void OnTrackEnded()
    {
        if (_player?.CurrentTrack?.Id != Current?.Id) return;
        if (Current is null) return;

        if (HasNext)
        {
            // Fire-and-forget is deliberate: TrackEnded is a synchronous event invoked from the player's
            // end-of-playback callback continuation; we must not block it. Advancing kicks off the next
            // stream, whose own failures surface through the player's ErrorMessage/state — the queue does
            // not own playback error handling.
            _ = Next();
        }
        else
        {
            // Last track ended naturally → empty the deque. The player is left alone (its stream has
            // already ended on its own); we only reset queue state.
            _items.Clear();
            CurrentIndex = -1;
            IsArmed = false;
            QueueChanged?.Invoke();
        }
    }

    private async Task PlayCurrent()
    {
        var track = Current;
        if (track is null || _player is null) return;
        await _player.SelectTrackStreaming(track);
    }

    public void Dispose()
    {
        if (_player != null)
        {
            _player.TrackEnded -= OnTrackEnded;
            _player = null;
        }
    }
}
