use crate::utils::lock::{MutexSafe, RwLockSafe};
use log::{debug, error, info};
use serde::{Deserialize, Serialize};
use std::sync::{
    atomic::{AtomicBool, Ordering},
    Arc, Mutex, RwLock,
};
use tokio::sync::Mutex as TokioMutex;
use tokio::time::{sleep, Duration};

use crate::jellyfin::JellyfinClient;
use crate::player::{PlayerController, PlayerEventEmitter, PlayerStatusEvent, QueueContext};

/// Playback mode - local device, remote session, or idle
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum PlaybackMode {
    Local,
    Remote { session_id: String },
    Idle,
}

/// Number of Jellyfin ticks per second (100ns units).
const TICKS_PER_SECOND: f64 = 10_000_000.0;

/// Below this many seconds we treat the position as "at the start" and don't
/// send a resume position, so a fresh track casts from 0 rather than ~0.
const RESUME_THRESHOLD_SECONDS: f64 = 0.5;

/// Volume level (0-100) the remote volume slider starts at. The real level is
/// corrected by the session poller once the remote session reports its volume.
const DEFAULT_REMOTE_VOLUME: i32 = 50;

/// Convert a live playback position (seconds) into the `StartPositionTicks` to
/// hand to a remote session, or `None` if we're effectively at the start.
///
/// Pure helper so the resume-position math is unit-testable without a remote
/// session or HTTP. The *source* of `position_seconds` matters too: callers
/// must pass the live backend position (`PlayerController::position()`), not the
/// snapshot embedded in `PlayerState`, which is stale mid-track on Android.
fn start_position_ticks_from_seconds(position_seconds: f64) -> Option<i64> {
    if position_seconds > RESUME_THRESHOLD_SECONDS {
        Some((position_seconds * TICKS_PER_SECOND) as i64)
    } else {
        None
    }
}

/// Platform hook for attaching/detaching the OS remote-volume control.
///
/// On Android, entering remote mode hands the `MediaSession` a
/// `VolumeProviderCompat` so hardware volume buttons and the system slider drive
/// the *remote* session; leaving remote mode must hand it back to the local
/// media stream. Behind a trait so the routing rule (see
/// [`PlaybackModeManager::set_mode`]) is unit-testable off-device — the real
/// implementation is JNI and only exists on Android.
pub trait RemoteVolumeControl: Send + Sync {
    /// Attach remote-volume control (and, on Android, start the playback service).
    fn enable(&self, initial_volume: i32);
    /// Return volume control to the local device speaker.
    fn disable(&self);
}

/// Production hook: forwards to the Android JNI bridge; no-op elsewhere.
struct PlatformRemoteVolumeControl;

impl RemoteVolumeControl for PlatformRemoteVolumeControl {
    #[allow(unused_variables)]
    fn enable(&self, initial_volume: i32) {
        #[cfg(target_os = "android")]
        {
            if let Err(e) = crate::player::enable_remote_volume(initial_volume) {
                log::warn!(
                    "[PlaybackMode] Failed to enable remote volume/service: {}",
                    e
                );
                // Non-fatal - continue; the next poll tick will retry metadata.
            }
        }
    }

    fn disable(&self) {
        #[cfg(target_os = "android")]
        {
            if let Err(e) = crate::player::disable_remote_volume() {
                log::warn!("[PlaybackMode] Failed to disable remote volume: {}", e);
                // Non-fatal - the mode change itself has already happened.
            }
        }
    }
}

/// Manages playback mode transfers between local and remote sessions
pub struct PlaybackModeManager {
    jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
    player_controller: Arc<TokioMutex<PlayerController>>,
    current_mode: Arc<RwLock<PlaybackMode>>,
    is_transferring: Arc<AtomicBool>,
    /// Optional emitter used to notify the frontend when the mode changes, so its
    /// mirror store stays in sync with this authoritative one. `None` in tests.
    event_emitter: Arc<Mutex<Option<Arc<dyn PlayerEventEmitter>>>>,
    /// Platform hook for OS-level remote volume routing (swapped in tests).
    remote_volume: Arc<dyn RemoteVolumeControl>,
}

impl PlaybackModeManager {
    /// Create a new playback mode manager
    pub fn new(
        jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
        player_controller: Arc<TokioMutex<PlayerController>>,
    ) -> Self {
        Self {
            jellyfin_client,
            player_controller,
            current_mode: Arc::new(RwLock::new(PlaybackMode::Idle)),
            is_transferring: Arc::new(AtomicBool::new(false)),
            event_emitter: Arc::new(Mutex::new(None)),
            remote_volume: Arc::new(PlatformRemoteVolumeControl),
        }
    }

    /// Construct with a custom remote-volume hook (tests).
    #[cfg(test)]
    fn with_remote_volume(
        jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
        player_controller: Arc<TokioMutex<PlayerController>>,
        remote_volume: Arc<dyn RemoteVolumeControl>,
    ) -> Self {
        Self {
            jellyfin_client,
            player_controller,
            current_mode: Arc::new(RwLock::new(PlaybackMode::Idle)),
            is_transferring: Arc::new(AtomicBool::new(false)),
            event_emitter: Arc::new(Mutex::new(None)),
            remote_volume,
        }
    }

    /// Wire the event emitter so `set_mode` notifies the frontend. Called once
    /// during setup; safe to leave unset (tests do), in which case mode changes
    /// simply aren't broadcast.
    pub fn set_event_emitter(&self, emitter: Arc<dyn PlayerEventEmitter>) {
        *self.event_emitter.lock_safe() = Some(emitter);
    }

    /// Get current playback mode
    pub fn get_mode(&self) -> PlaybackMode {
        self.current_mode.read_safe().clone()
    }

    /// Set playback mode (internal use).
    ///
    /// Broadcasts a `PlaybackModeChanged` event when the mode actually changes so
    /// the frontend's mirror store reconciles to this authoritative value. The
    /// write lock is released before emitting to avoid holding it across the
    /// emitter call.
    ///
    /// Also owns **OS volume routing**, which is derived from the transition
    /// rather than from each call site: entering remote mode attaches the remote
    /// volume control, and *any* exit from remote mode hands it back to the local
    /// speaker. Doing this per-call-site is what caused the bug where stopping a
    /// remote session (`player_stop` → Idle) left Android stuck on the remote
    /// volume slider — only the transfer-to-local path tore it down.
    ///
    /// TRACES: UR-010 | DR-059, IR-021
    pub fn set_mode(&self, mode: PlaybackMode) {
        log::info!("[PlaybackMode] Setting mode to: {:?}", mode);
        let (changed, was_remote) = {
            let mut current = self.current_mode.write_safe();
            let changed = *current != mode;
            let was_remote = matches!(*current, PlaybackMode::Remote { .. });
            *current = mode.clone();
            (changed, was_remote)
        };

        if !changed {
            return;
        }

        // Volume routing follows the transition. Note remote->remote (switching
        // target session) re-arms rather than releasing control.
        let is_remote = matches!(mode, PlaybackMode::Remote { .. });
        if is_remote {
            self.remote_volume.enable(DEFAULT_REMOTE_VOLUME);
        } else if was_remote {
            log::info!("[PlaybackMode] Leaving remote mode - restoring local volume control");
            self.remote_volume.disable();
        }

        let (mode_str, session_id) = match &mode {
            PlaybackMode::Local => ("local".to_string(), None),
            PlaybackMode::Idle => ("idle".to_string(), None),
            PlaybackMode::Remote { session_id } => ("remote".to_string(), Some(session_id.clone())),
        };

        if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
            emitter.emit(PlayerStatusEvent::PlaybackModeChanged {
                mode: mode_str,
                session_id,
            });
        }
    }

    /// Start the Android playback service and hand it remote-volume control.
    ///
    /// Must run on EVERY transition into remote mode, because it is what starts
    /// the foreground service. Without a running service there is no media
    /// notification (the lockscreen card is missing) AND system volume buttons
    /// aren't intercepted for the remote session (remote volume control dead).
    /// Both symptoms share this one cause, so this must not be skipped on any
    /// remote-entry path (notably the empty-queue early return in
    /// `transfer_to_remote_inner`). No-op / non-Android builds do nothing.
    ///
    /// [`set_mode`](Self::set_mode) already arms this on entry into remote mode;
    /// calling it again is harmless (the service start is idempotent) and keeps
    /// the guarantee when the mode was already remote, which `set_mode` skips as
    /// a no-op transition.
    fn enable_remote_control(&self) {
        self.remote_volume.enable(DEFAULT_REMOTE_VOLUME);
    }

    /// Check if currently transferring
    pub fn is_transferring(&self) -> bool {
        self.is_transferring.load(Ordering::Relaxed)
    }

    /// Set the transferring flag directly.
    ///
    /// The remote->local transfer is driven from the frontend in two steps
    /// (`player_play_tracks` to start local playback, then
    /// `playback_mode_transfer_to_local` to stop the remote). The first step's
    /// routing depends on this flag: while it's set, `player_play_tracks` plays
    /// locally instead of casting back to the remote session. The frontend must
    /// raise the flag *before* that first call and lower it when the sequence is
    /// done (or aborts), so it can't be left stuck on.
    pub fn set_transferring(&self, transferring: bool) {
        self.is_transferring.store(transferring, Ordering::Relaxed);
    }

    /// Send volume command to remote session
    /// Commands: "SetVolume", "VolumeUp", "VolumeDown"
    #[allow(dead_code)] // Called from Android JNI callback
    pub async fn send_remote_volume_command(
        &self,
        command: &str,
        volume: i32,
    ) -> Result<(), String> {
        log::info!(
            "[PlaybackMode] send_remote_volume_command ENTERED: command={}, volume={}",
            command,
            volume
        );

        // Get the current session ID
        let session_id = match self.get_mode() {
            PlaybackMode::Remote { session_id } => session_id,
            _ => {
                log::warn!("[PlaybackMode] Ignoring remote volume command - not in remote mode");
                return Ok(());
            }
        };

        log::info!(
            "[PlaybackMode] Current mode is Remote, session_id={}",
            session_id
        );

        // Get Jellyfin client
        let client = {
            log::info!("[PlaybackMode] Attempting to lock Jellyfin client...");
            let client_opt = self.jellyfin_client.lock().map_err(|e| {
                log::error!("[PlaybackMode] Failed to lock Jellyfin client: {}", e);
                format!("Failed to lock Jellyfin client: {}", e)
            })?;

            log::info!("[PlaybackMode] Jellyfin client lock acquired");

            match client_opt.as_ref() {
                Some(c) => {
                    log::info!("[PlaybackMode] Jellyfin client is configured, cloning...");
                    c.clone()
                }
                None => {
                    log::error!("[PlaybackMode] Jellyfin client is NOT configured!");
                    return Err("Jellyfin client not configured".to_string());
                }
            }
        };

        log::info!("[PlaybackMode] About to call client.session_set_volume...");

        // Send the volume command
        log::info!(
            "[PlaybackMode] Sending {} command to session {} (volume: {})",
            command,
            session_id,
            volume
        );
        let result = client.session_set_volume(session_id, volume).await;

        match &result {
            Ok(_) => log::info!("[PlaybackMode] session_set_volume returned Ok"),
            Err(e) => log::error!("[PlaybackMode] session_set_volume returned Err: {}", e),
        }

        result
    }

    /// Extract Jellyfin item IDs from queue items
    /// Returns (item_ids, adjusted_current_index)
    fn extract_jellyfin_ids(
        &self,
        items: &[crate::player::MediaItem],
        original_index: usize,
    ) -> Result<(Vec<String>, usize), String> {
        let mut jellyfin_ids: Vec<String> = Vec::new();
        let mut adjusted_index: Option<usize> = None;
        let mut jellyfin_item_count = 0;

        for (i, item) in items.iter().enumerate() {
            if let Some(id) = item.jellyfin_id() {
                jellyfin_ids.push(id.to_string());

                // If this is the currently playing item, record its new index
                if i == original_index {
                    adjusted_index = Some(jellyfin_item_count);
                }

                jellyfin_item_count += 1;
            }
        }

        // Ensure the currently playing item has a Jellyfin ID
        let final_index = match adjusted_index {
            Some(idx) => idx,
            None => {
                log::warn!(
                    "[PlaybackMode] Currently playing item (index {}) does not have a Jellyfin ID",
                    original_index
                );
                return Err(
                    "Cannot transfer: currently playing item is not from Jellyfin".to_string(),
                );
            }
        };

        log::info!(
            "[PlaybackMode] Extracted {} Jellyfin IDs from queue (original index: {} -> adjusted: {})",
            jellyfin_ids.len(),
            original_index,
            final_index
        );

        Ok((jellyfin_ids, final_index))
    }

    /// Transfer playback from local device to remote Jellyfin session
    pub async fn transfer_to_remote(
        &self,
        session_id: String,
        position_override: Option<f64>,
    ) -> Result<(), String> {
        debug!("[PlaybackMode] transfer_to_remote ENTERED");
        debug!("[PlaybackMode] session_id: {}", session_id);
        log::info!(
            "[PlaybackMode] Transferring to remote session: {}",
            session_id
        );

        // Set transferring flag
        debug!("[PlaybackMode] Setting is_transferring flag");
        self.is_transferring.store(true, Ordering::Relaxed);
        debug!("[PlaybackMode] Flag set, calling transfer_to_remote_inner");

        // Perform the transfer
        let result = self
            .transfer_to_remote_inner(&session_id, position_override)
            .await;

        // Clear transferring flag
        self.is_transferring.store(false, Ordering::Relaxed);

        result
    }

    async fn transfer_to_remote_inner(
        &self,
        session_id: &str,
        position_override: Option<f64>,
    ) -> Result<(), String> {
        log::info!("[PlaybackMode] transfer_to_remote_inner ENTERED");
        debug!(
            "[PlaybackMode] transfer_to_remote_inner: session_id={}",
            session_id
        );

        // If we're already controlling a remote session, that *old* session — not
        // the idle local player — is the source of truth for the current track and
        // position. Capture it so we can resume there and stop it afterwards.
        let previous_remote_session = match self.get_mode() {
            PlaybackMode::Remote { session_id: prev } if prev != session_id => Some(prev),
            _ => None,
        };

        // Get current player state and queue context
        let (queue_ids, mut current_index, mut position_seconds, queue_context) = {
            log::info!("[PlaybackMode] Acquiring player controller lock...");
            debug!("[PlaybackMode] Acquiring player controller lock...");
            let player = self.player_controller.lock().await;
            log::info!("[PlaybackMode] Player controller lock acquired");
            debug!("[PlaybackMode] Player controller lock acquired");

            let queue_arc = player.queue();
            let queue = queue_arc.lock_safe();

            let original_index = queue.current_index().unwrap_or(0);
            let items = queue.items();

            log::info!(
                "[PlaybackMode] Queue has {} items, original_index={}",
                items.len(),
                original_index
            );
            debug!(
                "[PlaybackMode] Queue has {} items, original_index={}",
                items.len(),
                original_index
            );

            // Log each item's jellyfin_id for debugging
            for (i, item) in items.iter().enumerate() {
                let jf_id = item.jellyfin_id().unwrap_or("NONE");
                log::debug!(
                    "[PlaybackMode] Item {}: id={}, jellyfin_id={}",
                    i,
                    item.id,
                    jf_id
                );
            }

            let (ids, adjusted_index) = self.extract_jellyfin_ids(items, original_index)?;
            // Prefer the frontend-supplied position when available. The backend
            // position is unreliable as a transfer source: on Linux, *video* plays
            // in the HTML5 <video> element and the MPV backend is never loaded, so
            // PlayerController::position() is always 0; only the frontend knows the
            // true position. We fall back to the live backend position (correct for
            // Linux audio via MPV) when the frontend doesn't pass one.
            let position = match position_override {
                Some(p) => {
                    log::info!("[PlaybackMode] Using frontend position override: {:.2}s", p);
                    p
                }
                None => player.position(),
            };
            let context = queue.context().clone();

            log::info!(
                "[PlaybackMode] Queue context: {:?}, {} items, current index: {}",
                context,
                ids.len(),
                adjusted_index
            );
            debug!(
                "[PlaybackMode] Extracted {} jellyfin IDs, adjusted_index={}, position={:.2}s",
                ids.len(),
                adjusted_index,
                position
            );

            (ids, adjusted_index, position, context)
        };

        // If queue is empty, just switch mode
        if queue_ids.is_empty() {
            log::info!("[PlaybackMode] Queue is empty, just switching mode");
            self.set_mode(PlaybackMode::Remote {
                session_id: session_id.to_string(),
            });
            // Start the service + remote-volume control here too — otherwise this
            // early return leaves remote mode with no media notification and no
            // volume interception (lockscreen card missing + remote volume dead).
            self.enable_remote_control();
            return Ok(());
        }

        log::info!(
            "[PlaybackMode] Queue has {} items, current index: {}, position: {:.2}s",
            queue_ids.len(),
            current_index,
            position_seconds
        );

        // Get Jellyfin client for remote transfer
        log::info!("[PlaybackMode] Getting Jellyfin client for transfer...");
        debug!("[PlaybackMode] Getting Jellyfin client for transfer...");
        let client = {
            let client_opt = self.jellyfin_client.lock().map_err(|e| {
                log::error!("[PlaybackMode] Failed to lock Jellyfin client: {}", e);
                error!("[PlaybackMode] Failed to lock Jellyfin client: {}", e);
                format!("Failed to lock Jellyfin client: {}", e)
            })?;

            match client_opt.as_ref() {
                Some(c) => {
                    log::info!("[PlaybackMode] Jellyfin client is configured");
                    debug!("[PlaybackMode] Jellyfin client is configured");
                    c.clone()
                }
                None => {
                    log::error!("[PlaybackMode] Jellyfin client NOT configured!");
                    error!("[PlaybackMode] Jellyfin client NOT configured!");
                    return Err("Jellyfin client not configured".to_string());
                }
            }
        };

        // Remote -> remote switch: take the current track and position from the
        // session we're leaving, since the local player is idle and reports 0.
        if let Some(ref prev_session_id) = previous_remote_session {
            log::info!(
                "[PlaybackMode] Remote->remote switch; reading state from previous session {}",
                prev_session_id
            );
            match client.get_session(prev_session_id).await {
                Ok(Some(session)) => {
                    // Resume at the previous session's position.
                    if let Some(ticks) =
                        session.play_state.as_ref().and_then(|ps| ps.position_ticks)
                    {
                        position_seconds = ticks as f64 / TICKS_PER_SECOND;
                        log::info!(
                            "[PlaybackMode] Using previous remote position: {:.2}s",
                            position_seconds
                        );
                    }
                    // Resume on whichever track the previous session reached.
                    if let Some(now_id) = session
                        .now_playing_item
                        .as_ref()
                        .and_then(|i| i.id.as_deref())
                    {
                        if let Some(idx) = queue_ids.iter().position(|id| id == now_id) {
                            log::info!(
                                "[PlaybackMode] Previous session is on track {} (queue index {})",
                                now_id,
                                idx
                            );
                            current_index = idx;
                        } else {
                            log::warn!(
                                "[PlaybackMode] Previous session's track {} not found in queue; keeping index {}",
                                now_id,
                                current_index
                            );
                        }
                    }
                }
                Ok(None) => log::warn!(
                    "[PlaybackMode] Previous remote session not found while reading state"
                ),
                Err(e) => log::warn!(
                    "[PlaybackMode] Failed to read previous remote session: {}",
                    e
                ),
            }
        }

        // Calculate position in ticks (from the live position read above)
        let start_position_ticks = start_position_ticks_from_seconds(position_seconds);

        // Log queue context for debugging (context is tracked but we always send track IDs)
        match &queue_context {
            QueueContext::Album {
                album_id,
                album_name,
            } => {
                log::info!(
                    "[PlaybackMode] Transferring album '{}' (ID: {}) with {} tracks to remote",
                    album_name,
                    album_id,
                    queue_ids.len()
                );
            }
            QueueContext::Playlist {
                playlist_id,
                playlist_name,
            } => {
                log::info!(
                    "[PlaybackMode] Transferring playlist '{}' (ID: {}) with {} tracks to remote",
                    playlist_name,
                    playlist_id,
                    queue_ids.len()
                );
            }
            QueueContext::Custom => {
                log::info!(
                    "[PlaybackMode] Transferring custom queue with {} tracks to remote",
                    queue_ids.len()
                );
            }
        }

        // Always send individual track IDs - Jellyfin's play_on_session expects track IDs,
        // not album/playlist container IDs
        let expected_item_id = queue_ids
            .get(current_index)
            .cloned()
            .ok_or("Invalid start index")?;

        // Send play command to remote session with all track IDs
        log::info!(
            "[PlaybackMode] Sending play command to remote session: {} ({} tracks, starting at index {}, position: {:.2}s)",
            session_id,
            queue_ids.len(),
            current_index,
            position_seconds
        );
        debug!(
            "[PlaybackMode] Calling play_on_session: session={}, tracks={}, index={}, position_ticks={:?}",
            session_id,
            queue_ids.len(),
            current_index,
            start_position_ticks
        );

        // Log first few track IDs for debugging
        if queue_ids.len() > 0 {
            let preview: Vec<&str> = queue_ids.iter().take(3).map(|s| s.as_str()).collect();
            debug!("[PlaybackMode] First track IDs: {:?}...", preview);
        }

        client
            .play_on_session(
                session_id.to_string(),
                queue_ids.clone(),
                current_index,
                start_position_ticks,
            )
            .await
            .map_err(|e| {
                log::error!("[PlaybackMode] Failed to send play command: {}", e);
                error!("[PlaybackMode] Failed to send play command: {}", e);
                format!("Failed to start playback on remote session: {}", e)
            })?;

        log::info!("[PlaybackMode] Play command sent successfully");
        info!("[PlaybackMode] Play command sent successfully to remote session");

        // Wait for remote session to load the track (poll with timeout)
        log::info!("[PlaybackMode] Waiting for remote session to load track...");

        let mut attempts = 0;
        let max_attempts = 50; // 5 seconds max (50 * 100ms)
        let mut track_loaded = false;

        while attempts < max_attempts {
            sleep(Duration::from_millis(100)).await;
            attempts += 1;

            match client.get_session(session_id).await {
                Ok(Some(session)) => {
                    if let Some(now_playing) = &session.now_playing_item {
                        if now_playing.id.as_deref() == Some(&expected_item_id) {
                            log::info!(
                                "[PlaybackMode] Remote session loaded track '{}' after {}ms",
                                now_playing.name.as_deref().unwrap_or("Unknown"),
                                attempts * 100
                            );
                            track_loaded = true;
                            break;
                        }
                    }
                }
                Ok(None) => {
                    log::warn!("[PlaybackMode] Remote session not found while polling");
                    return Err("Remote session not found".to_string());
                }
                Err(e) => {
                    log::warn!(
                        "[PlaybackMode] Error polling session (attempt {}): {}",
                        attempts,
                        e
                    );
                    // Continue polling - transient errors are OK
                }
            }
        }

        if !track_loaded {
            log::error!("[PlaybackMode] Timeout waiting for remote session to load track");
            return Err("Remote session did not load track in time".to_string());
        }

        // Resume at the right position. We send StartPositionTicks in the play
        // command above, but some Jellyfin client/server combinations ignore it
        // and start from 0. Now that the track is confirmed loaded, issue an
        // explicit seek as well (mirrors how the local resume path works). This
        // is the reliable mechanism; StartPositionTicks is best-effort.
        if let Some(ticks) = start_position_ticks {
            log::info!(
                "[PlaybackMode] Seeking remote session to resume position: {} ticks",
                ticks
            );
            if let Err(e) = client.session_seek(session_id.to_string(), ticks).await {
                // Non-fatal: the track is already playing, just not at the
                // resume point. Log and continue rather than failing the transfer.
                log::warn!("[PlaybackMode] Resume seek on remote failed: {}", e);
            }
        }

        // Remote -> remote switch: stop the session we just left so we don't end
        // up with two devices playing at once. Do this only after the new session
        // is confirmed playing, so a failure here doesn't leave us with silence.
        if let Some(prev_session_id) = previous_remote_session {
            log::info!(
                "[PlaybackMode] Stopping previous remote session {}",
                prev_session_id
            );
            if let Err(e) = client.send_session_command(prev_session_id, "Stop").await {
                log::warn!(
                    "[PlaybackMode] Failed to stop previous remote session: {}",
                    e
                );
            }
        }

        // Stop local playback (queue should remain intact for remote session)
        log::info!("[PlaybackMode] Stopping local playback - queue should NOT be cleared");
        {
            let player = self.player_controller.lock().await;

            // Log queue state BEFORE stop
            {
                let queue_arc = player.queue();
                let queue = queue_arc.lock_safe();
                info!(
                    "[PlaybackMode] BEFORE STOP: Queue has {} items, current_index={:?}",
                    queue.items().len(),
                    queue.current_index()
                );
            }

            player
                .stop()
                .map_err(|e| format!("Failed to stop playback: {}", e))?;

            // Log queue state AFTER stop (should be unchanged)
            {
                let queue_arc = player.queue();
                let queue = queue_arc.lock_safe();
                info!(
                    "[PlaybackMode] AFTER STOP: Queue has {} items, current_index={:?}",
                    queue.items().len(),
                    queue.current_index()
                );
            }
        }

        // Update mode to remote
        self.set_mode(PlaybackMode::Remote {
            session_id: session_id.to_string(),
        });

        // Start the service + remote-volume control (intercepts volume buttons,
        // and starts the foreground service that renders the lockscreen card).
        self.enable_remote_control();

        log::info!("[PlaybackMode] Successfully transferred to remote");
        Ok(())
    }

    /// Transfer playback from remote session back to local device
    pub async fn transfer_to_local(
        &self,
        current_item_id: String,
        position_ticks: i64,
    ) -> Result<(), String> {
        log::info!("[PlaybackMode] Transferring to local playback");

        // Set transferring flag
        self.is_transferring.store(true, Ordering::Relaxed);

        // Perform the transfer
        let result = self
            .transfer_to_local_inner(&current_item_id, position_ticks)
            .await;

        // Clear transferring flag
        self.is_transferring.store(false, Ordering::Relaxed);

        result
    }

    async fn transfer_to_local_inner(
        &self,
        current_item_id: &str,
        position_ticks: i64,
    ) -> Result<(), String> {
        // Get current remote session info
        let session_id = match self.get_mode() {
            PlaybackMode::Remote { session_id } => session_id,
            _ => return Err("Not in remote playback mode".to_string()),
        };

        let position_seconds = position_ticks as f64 / 10_000_000.0;

        log::info!(
            "[PlaybackMode] Transfer to local: session={}, item_id={}, position={:.2}s",
            session_id,
            current_item_id,
            position_seconds
        );

        // Get Jellyfin client for stopping remote playback
        let client = {
            let client_opt = self
                .jellyfin_client
                .lock()
                .map_err(|e| format!("Failed to lock Jellyfin client: {}", e))?;

            client_opt
                .as_ref()
                .ok_or("Jellyfin client not configured")?
                .clone()
        };

        // Stop remote playback
        log::info!(
            "[PlaybackMode] Stopping remote playback on session: {}",
            session_id
        );
        match client
            .send_session_command(session_id.clone(), "Stop")
            .await
        {
            Ok(_) => log::info!("[PlaybackMode] Stop command sent successfully"),
            Err(e) => {
                log::warn!(
                    "[PlaybackMode] Failed to stop remote session (non-fatal): {}",
                    e
                );
                // Don't fail the transfer if we can't stop the remote session
                // The user is already playing locally, so this is not critical
            }
        }

        // For now, we'll return an error indicating that the TypeScript side needs to handle
        // loading the media item, since we don't have access to the repository here yet.
        // This will be improved in Phase 3 when repository is migrated to Rust.
        log::debug!("[PlaybackMode] Cannot load media item in Rust yet - frontend handled it");

        // Update mode to local. This also returns volume control to the local
        // device speaker — set_mode owns that for every exit from remote mode.
        self.set_mode(PlaybackMode::Local);

        log::info!("[PlaybackMode] Successfully transferred to local");
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Test PlaybackMode enum serialization to JSON
    ///
    /// @req-test: DR-003 - Playback mode manager (Local/Remote/Idle states)
    /// @req-test: UR-010 - Control playback of Jellyfin remote sessions
    #[test]
    fn test_playback_mode_serialization() {
        let mode_idle = PlaybackMode::Idle;
        let json = serde_json::to_string(&mode_idle).unwrap();
        assert_eq!(json, r#"{"type":"idle"}"#);

        let mode_local = PlaybackMode::Local;
        let json = serde_json::to_string(&mode_local).unwrap();
        assert_eq!(json, r#"{"type":"local"}"#);

        let mode_remote = PlaybackMode::Remote {
            session_id: "abc123".to_string(),
        };
        let json = serde_json::to_string(&mode_remote).unwrap();
        assert!(json.contains(r#""type":"remote""#));
        assert!(json.contains(r#""session_id":"abc123""#));
    }

    /// Test PlaybackMode enum deserialization from JSON
    ///
    /// @req-test: DR-003 - Playback mode manager (Local/Remote/Idle states)
    #[test]
    fn test_playback_mode_deserialization() {
        let json = r#"{"type":"idle"}"#;
        let mode: PlaybackMode = serde_json::from_str(json).unwrap();
        assert_eq!(mode, PlaybackMode::Idle);

        let json = r#"{"type":"local"}"#;
        let mode: PlaybackMode = serde_json::from_str(json).unwrap();
        assert_eq!(mode, PlaybackMode::Local);

        let json = r#"{"type":"remote","session_id":"test_session"}"#;
        let mode: PlaybackMode = serde_json::from_str(json).unwrap();
        assert_eq!(
            mode,
            PlaybackMode::Remote {
                session_id: "test_session".to_string()
            }
        );
    }

    /// Capturing emitter so we can assert what `set_mode` broadcasts.
    struct CapturingEmitter {
        events: Mutex<Vec<PlayerStatusEvent>>,
    }

    impl PlayerEventEmitter for CapturingEmitter {
        fn emit(&self, event: PlayerStatusEvent) {
            self.events.lock().unwrap().push(event);
        }
    }

    fn manager_with_emitter() -> (PlaybackModeManager, Arc<CapturingEmitter>) {
        let emitter = Arc::new(CapturingEmitter {
            events: Mutex::new(Vec::new()),
        });
        let manager = PlaybackModeManager::new(
            Arc::new(Mutex::new(None)),
            Arc::new(TokioMutex::new(crate::player::PlayerController::default())),
        );
        manager.set_event_emitter(emitter.clone());
        (manager, emitter)
    }

    /// set_mode broadcasts a PlaybackModeChanged event with the right payload so
    /// the frontend can reconcile its mirror store to this authoritative one.
    #[test]
    fn test_set_mode_emits_change_event() {
        let (manager, emitter) = manager_with_emitter();

        manager.set_mode(PlaybackMode::Remote {
            session_id: "sess-1".to_string(),
        });
        manager.set_mode(PlaybackMode::Local);
        manager.set_mode(PlaybackMode::Idle);

        let events = emitter.events.lock().unwrap();
        assert_eq!(events.len(), 3, "one event per real mode change");

        match &events[0] {
            PlayerStatusEvent::PlaybackModeChanged { mode, session_id } => {
                assert_eq!(mode, "remote");
                assert_eq!(session_id.as_deref(), Some("sess-1"));
            }
            other => panic!("expected PlaybackModeChanged, got {:?}", other),
        }
        match &events[1] {
            PlayerStatusEvent::PlaybackModeChanged { mode, session_id } => {
                assert_eq!(mode, "local");
                assert_eq!(session_id.as_deref(), None);
            }
            other => panic!("expected PlaybackModeChanged, got {:?}", other),
        }
        match &events[2] {
            PlayerStatusEvent::PlaybackModeChanged { mode, session_id } => {
                assert_eq!(mode, "idle");
                assert_eq!(session_id.as_deref(), None);
            }
            other => panic!("expected PlaybackModeChanged, got {:?}", other),
        }
    }

    /// Records enable/disable calls so tests can assert volume routing.
    struct RecordingVolumeControl {
        calls: Mutex<Vec<&'static str>>,
    }

    impl RemoteVolumeControl for RecordingVolumeControl {
        fn enable(&self, _initial_volume: i32) {
            self.calls.lock().unwrap().push("enable");
        }
        fn disable(&self) {
            self.calls.lock().unwrap().push("disable");
        }
    }

    fn manager_with_volume_control() -> (PlaybackModeManager, Arc<RecordingVolumeControl>) {
        let volume = Arc::new(RecordingVolumeControl {
            calls: Mutex::new(Vec::new()),
        });
        let manager = PlaybackModeManager::with_remote_volume(
            Arc::new(Mutex::new(None)),
            Arc::new(TokioMutex::new(crate::player::PlayerController::default())),
            volume.clone(),
        );
        (manager, volume)
    }

    /// Leaving remote mode must hand volume control back to the local device.
    ///
    /// Stopping a remote session (`player_stop`) drives the manager
    /// Remote -> Idle without going through `transfer_to_local`. Before this was
    /// centralised in `set_mode`, only the transfer path tore the Android
    /// `VolumeProviderCompat` down, so a plain stop left the system stuck on the
    /// remote volume slider with no way back to the phone speaker.
    ///
    /// @req-test: UR-010 - Control playback of Jellyfin remote sessions
    #[test]
    fn test_leaving_remote_mode_restores_local_volume() {
        let (manager, volume) = manager_with_volume_control();

        manager.set_mode(PlaybackMode::Remote {
            session_id: "sess-1".to_string(),
        });
        // The stop path: remote -> idle, no transfer involved.
        manager.set_mode(PlaybackMode::Idle);

        assert_eq!(
            *volume.calls.lock().unwrap(),
            vec!["enable", "disable"],
            "remote->idle must return volume control to the local speaker"
        );
    }

    /// The same must hold for remote -> local (transfer back to this device).
    ///
    /// @req-test: UR-010 - Control playback of Jellyfin remote sessions
    #[test]
    fn test_remote_to_local_restores_local_volume() {
        let (manager, volume) = manager_with_volume_control();

        manager.set_mode(PlaybackMode::Remote {
            session_id: "sess-1".to_string(),
        });
        manager.set_mode(PlaybackMode::Local);

        assert_eq!(
            *volume.calls.lock().unwrap(),
            vec!["enable", "disable"],
            "remote->local must return volume control to the local speaker"
        );
    }

    /// Volume routing must not be touched by transitions that never involve
    /// remote mode — an idle->local start would otherwise issue a pointless
    /// `setPlaybackToLocal` on every playback start.
    ///
    /// @req-test: UR-010 - Control playback of Jellyfin remote sessions
    #[test]
    fn test_non_remote_transitions_leave_volume_routing_alone() {
        let (manager, volume) = manager_with_volume_control();

        manager.set_mode(PlaybackMode::Local);
        manager.set_mode(PlaybackMode::Idle);
        manager.set_mode(PlaybackMode::Local);

        assert!(
            volume.calls.lock().unwrap().is_empty(),
            "local/idle transitions must not touch remote volume routing"
        );
    }

    /// Switching directly between two remote sessions stays remote: control must
    /// remain attached (re-armed for the new session), never handed back local.
    ///
    /// @req-test: UR-010 - Control playback of Jellyfin remote sessions
    #[test]
    fn test_remote_to_remote_keeps_remote_volume() {
        let (manager, volume) = manager_with_volume_control();

        manager.set_mode(PlaybackMode::Remote {
            session_id: "sess-1".to_string(),
        });
        manager.set_mode(PlaybackMode::Remote {
            session_id: "sess-2".to_string(),
        });

        assert_eq!(
            *volume.calls.lock().unwrap(),
            vec!["enable", "enable"],
            "remote->remote re-arms control without releasing it to local"
        );
    }

    /// Setting the same mode twice must not re-emit — the frontend reconciler
    /// (and the event channel) shouldn't be spammed on no-op transitions.
    #[test]
    fn test_set_mode_deduplicates_no_op() {
        let (manager, emitter) = manager_with_emitter();

        manager.set_mode(PlaybackMode::Local);
        manager.set_mode(PlaybackMode::Local);
        manager.set_mode(PlaybackMode::Local);

        assert_eq!(
            emitter.events.lock().unwrap().len(),
            1,
            "repeated identical mode set emits only once"
        );
    }

    /// The resume position handed to a remote session is derived from a live
    /// playback position. Guards the seconds->ticks conversion and the
    /// at-the-start threshold (Bug: casting restarted the track from 0).
    #[test]
    fn test_start_position_ticks_from_seconds() {
        // Mid-track positions convert to ticks (10M ticks per second).
        assert_eq!(start_position_ticks_from_seconds(5.0), Some(50_000_000));
        assert_eq!(
            start_position_ticks_from_seconds(123.45),
            Some(1_234_500_000)
        );

        // At/near the start, send no resume position so the track casts from 0.
        assert_eq!(start_position_ticks_from_seconds(0.0), None);
        assert_eq!(start_position_ticks_from_seconds(0.5), None);

        // Just past the threshold resumes rather than restarting.
        assert!(start_position_ticks_from_seconds(0.6).is_some());
    }

    // Tests for extract_jellyfin_ids - verify all track IDs are sent to remote, not just album/playlist ID
    mod extract_jellyfin_ids_tests {
        use crate::player::{MediaItem, MediaSource, MediaType};
        use std::sync::{Arc, Mutex};
        use tokio::sync::Mutex as TokioMutex;

        fn create_test_item_with_jellyfin_id(id: &str, jellyfin_id: &str) -> MediaItem {
            MediaItem {
                id: id.to_string(),
                title: format!("Track {}", id),
                name: Some(format!("Track {}", id)),
                artist: Some("Test Artist".to_string()),
                album: Some("Test Album".to_string()),
                album_name: Some("Test Album".to_string()),
                album_id: Some("album_123".to_string()),
                artist_items: None,
                artists: Some(vec!["Test Artist".to_string()]),
                primary_image_tag: None,
                image_id: None,
                item_type: Some("Audio".to_string()),
                playlist_id: None,
                duration: Some(180.0),
                artwork_url: None,
                media_type: MediaType::Audio,
                source: MediaSource::Remote {
                    stream_url: format!("http://example.com/{}.mp3", id),
                    jellyfin_item_id: jellyfin_id.to_string(),
                },
                video_codec: None,
                needs_transcoding: false,
                video_width: None,
                video_height: None,
                subtitles: vec![],
                series_id: None,
                server_id: None,
            }
        }

        fn create_test_item_local(id: &str) -> MediaItem {
            MediaItem {
                id: id.to_string(),
                title: format!("Local Track {}", id),
                name: Some(format!("Local Track {}", id)),
                artist: Some("Test Artist".to_string()),
                album: None,
                album_name: None,
                album_id: None,
                artist_items: None,
                artists: Some(vec!["Test Artist".to_string()]),
                primary_image_tag: None,
                image_id: None,
                item_type: Some("Audio".to_string()),
                playlist_id: None,
                duration: Some(180.0),
                artwork_url: None,
                media_type: MediaType::Audio,
                source: MediaSource::DirectUrl {
                    url: format!("http://example.com/{}.mp3", id),
                },
                video_codec: None,
                needs_transcoding: false,
                video_width: None,
                video_height: None,
                subtitles: vec![],
                series_id: None,
                server_id: None,
            }
        }

        /// Test extracting all Jellyfin track IDs from album
        ///
        /// Verifies that all individual track IDs are extracted, not just the album ID.
        ///
        /// @req-test: UR-010 - Control playback of Jellyfin remote sessions
        /// @req-test: DR-003 - Playback mode manager (Jellyfin ID extraction)
        /// @req-test: IR-012 - Jellyfin Sessions API for remote playback control
        #[test]
        fn test_extract_all_jellyfin_ids_from_album() {
            // Simulate an album with 5 tracks - all should be extracted
            let items: Vec<MediaItem> = (1..=5)
                .map(|i| {
                    create_test_item_with_jellyfin_id(
                        &format!("track_{}", i),
                        &format!("jf_track_{}", i),
                    )
                })
                .collect();

            let manager = super::PlaybackModeManager::new(
                Arc::new(Mutex::new(None)),
                Arc::new(TokioMutex::new(crate::player::PlayerController::default())),
            );

            let result = manager.extract_jellyfin_ids(&items, 2);
            assert!(result.is_ok());

            let (ids, index) = result.unwrap();

            // All 5 track IDs should be extracted (not just the album ID)
            assert_eq!(ids.len(), 5, "All 5 track IDs should be extracted");
            assert_eq!(ids[0], "jf_track_1");
            assert_eq!(ids[1], "jf_track_2");
            assert_eq!(ids[2], "jf_track_3");
            assert_eq!(ids[3], "jf_track_4");
            assert_eq!(ids[4], "jf_track_5");

            // Index should point to track 3 (original index 2)
            assert_eq!(index, 2, "Current index should be preserved");
        }

        /// Test extracting Jellyfin IDs filters out local items
        ///
        /// @req-test: UR-010 - Control playback of remote sessions (local filtering)
        /// @req-test: DR-003 - Playback mode manager (local item filtering)
        #[test]
        fn test_extract_filters_local_items() {
            // Mix of Jellyfin and local items - only Jellyfin items should be extracted
            let items = vec![
                create_test_item_with_jellyfin_id("1", "jf_1"),
                create_test_item_local("2"), // Local, no Jellyfin ID
                create_test_item_with_jellyfin_id("3", "jf_3"),
                create_test_item_with_jellyfin_id("4", "jf_4"),
            ];

            let manager = super::PlaybackModeManager::new(
                Arc::new(Mutex::new(None)),
                Arc::new(TokioMutex::new(crate::player::PlayerController::default())),
            );

            // Playing track 3 (index 2 in original, should become index 1 after filtering)
            let result = manager.extract_jellyfin_ids(&items, 2);
            assert!(result.is_ok());

            let (ids, index) = result.unwrap();

            // Only 3 Jellyfin tracks should be extracted
            assert_eq!(ids.len(), 3);
            assert_eq!(ids[0], "jf_1");
            assert_eq!(ids[1], "jf_3");
            assert_eq!(ids[2], "jf_4");

            // Index should be adjusted (track 3 is now at position 1)
            assert_eq!(index, 1);
        }

        /// Test extraction fails when current item is local
        ///
        /// @req-test: DR-003 - Playback mode manager (error handling)
        /// @req-test: UR-010 - Control playback of remote sessions (validation)
        #[test]
        fn test_extract_fails_when_current_item_is_local() {
            // Current item has no Jellyfin ID - should fail
            let items = vec![
                create_test_item_with_jellyfin_id("1", "jf_1"),
                create_test_item_local("2"), // Local, no Jellyfin ID
                create_test_item_with_jellyfin_id("3", "jf_3"),
            ];

            let manager = super::PlaybackModeManager::new(
                Arc::new(Mutex::new(None)),
                Arc::new(TokioMutex::new(crate::player::PlayerController::default())),
            );

            // Playing the local track (index 1) should fail
            let result = manager.extract_jellyfin_ids(&items, 1);
            assert!(result.is_err());
            assert!(result.unwrap_err().contains("not from Jellyfin"));
        }

        /// Test extraction fails on empty queue
        ///
        /// @req-test: DR-003 - Playback mode manager (edge case: empty queue)
        #[test]
        fn test_extract_empty_queue() {
            let items: Vec<MediaItem> = vec![];

            let manager = super::PlaybackModeManager::new(
                Arc::new(Mutex::new(None)),
                Arc::new(TokioMutex::new(crate::player::PlayerController::default())),
            );

            let result = manager.extract_jellyfin_ids(&items, 0);
            assert!(result.is_err());
        }
    }
}
