using DeepDrftModels.DTOs;

namespace DeepDrftPublic.Client.Services;

/// <summary>
/// Default <see cref="IQueueService"/>: a single-slot 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.
///
/// <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.
/// </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 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 PlayRelease(IEnumerable<TrackDto> tracks, int startIndex = 0)
    {
        var list = tracks as IReadOnlyList<TrackDto> ?? tracks.ToList();
        if (list.Count == 0) return;

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

        _items.Clear();
        _items.AddRange(list);
        CurrentIndex = start;
        QueueChanged?.Invoke();

        await PlayCurrent();
    }

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

    public void EnqueueRange(IEnumerable<TrackDto> tracks)
    {
        var before = _items.Count;
        _items.AddRange(tracks);
        if (_items.Count != before)
            QueueChanged?.Invoke();
    }

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

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

    public void Clear()
    {
        if (_items.Count == 0 && CurrentIndex == -1) return;
        _items.Clear();
        CurrentIndex = -1;
        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. When the queue is past its last track, end-of-stream simply stops — nothing to advance.
    //
    // Guard: only advance 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 advance. Id-based equality is used rather than ReferenceEquals
    // because DTO copies through serialisation are not reference-equal.
    private void OnTrackEnded()
    {
        if (!HasNext) return;
        if (_player?.CurrentTrack?.Id != Current?.Id) return;
        // 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();
    }

    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;
        }
    }
}
