//! Session polling manager for remote playback control.
//!
//! Manages background polling of Jellyfin sessions with dynamic frequency adjustment
//! based on playback mode and UI state. Eliminates duplicate pollers across browser tabs.
//!
//! TRACES: UR-010 | JA-021

use crate::utils::lock::{MutexSafe, RwLockSafe};
use log::{debug, info, warn};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::Duration;

use crate::jellyfin::JellyfinClient;
use crate::playback_mode::{PlaybackMode, PlaybackModeManager};
use crate::player::PlayerEventEmitter;

/// Hint for adjusting poll frequency based on UI state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PollingHint {
    /// CastButton is active and needs frequent updates (500ms)
    CastActive,
    /// CastButton is in discovery mode (15s)
    CastDiscovery,
    /// No special hint, use mode-based frequency (default)
    Normal,
}

/// Manages background polling of Jellyfin sessions
pub struct SessionPollerManager {
    jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
    playback_mode_manager: Arc<PlaybackModeManager>,
    event_emitter: Arc<Mutex<Option<Arc<dyn PlayerEventEmitter>>>>,
    /// Optional connectivity reporter. The session poller is the one piece of
    /// server traffic that runs continuously even when the user is idle (not
    /// browsing the library), so feeding its poll outcomes into the reporter is
    /// what lets the app detect going offline — and, crucially, recover when the
    /// server returns — without any user interaction. Repository traffic alone
    /// can't do this because it only happens while browsing.
    connectivity_reporter: Arc<Mutex<Option<crate::connectivity::ConnectivityReporter>>>,

    // Polling state
    is_running: Arc<AtomicBool>,
    current_hint: Arc<RwLock<PollingHint>>,
    current_interval_ms: Arc<AtomicU64>,

    // Thread handle (for cleanup)
    thread_handle: Arc<Mutex<Option<thread::JoinHandle<()>>>>,
}

impl SessionPollerManager {
    /// Create a new SessionPollerManager
    pub fn new(
        jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
        playback_mode_manager: Arc<PlaybackModeManager>,
    ) -> Self {
        Self {
            jellyfin_client,
            playback_mode_manager,
            event_emitter: Arc::new(Mutex::new(None)),
            connectivity_reporter: Arc::new(Mutex::new(None)),
            is_running: Arc::new(AtomicBool::new(false)),
            current_hint: Arc::new(RwLock::new(PollingHint::Normal)),
            current_interval_ms: Arc::new(AtomicU64::new(10000)), // Default 10s
            thread_handle: Arc::new(Mutex::new(None)),
        }
    }

    /// Set event emitter for broadcasting session updates
    pub fn set_event_emitter(&self, emitter: Arc<dyn PlayerEventEmitter>) {
        *self.event_emitter.lock_safe() = Some(emitter);
    }

    /// Wire the connectivity reporter so each poll outcome updates reachability.
    /// A successful poll recovers the app to online instantly; sustained poll
    /// failures flip it offline (subject to the reporter's debounce window).
    pub fn set_connectivity_reporter(&self, reporter: crate::connectivity::ConnectivityReporter) {
        *self.connectivity_reporter.lock_safe() = Some(reporter);
    }

    /// Start the background polling thread
    pub fn start(&self) {
        if self.is_running.swap(true, Ordering::Relaxed) {
            warn!("[SessionPoller] Already running, ignoring start request");
            return;
        }

        info!("[SessionPoller] Starting session polling");

        // Clone Arc references for the thread
        let client = self.jellyfin_client.clone();
        let mode_manager = self.playback_mode_manager.clone();
        let emitter = self.event_emitter.clone();
        let connectivity_reporter = self.connectivity_reporter.clone();
        let is_running = self.is_running.clone();
        let hint = self.current_hint.clone();
        let interval_ms = self.current_interval_ms.clone();

        let handle = thread::spawn(move || {
            // Create Tokio runtime for async operations in this thread
            let rt = tokio::runtime::Runtime::new().unwrap();

            while is_running.load(Ordering::Relaxed) {
                // Calculate poll interval based on mode and hint
                let new_interval =
                    Self::calculate_interval(&mode_manager.get_mode(), *hint.read_safe());

                interval_ms.store(new_interval, Ordering::Relaxed);

                debug!("[SessionPoller] Polling with interval: {}ms", new_interval);

                // Fetch sessions. `had_client` distinguishes "server didn't
                // answer" from "no client configured" so we only feed real
                // request outcomes into the connectivity reporter.
                let (sessions_result, had_client) = rt.block_on(async {
                    let client_opt = client.lock_safe().clone();
                    match client_opt {
                        Some(c) => (c.get_sessions().await, true),
                        None => {
                            debug!("[SessionPoller] Jellyfin client not configured, skipping poll");
                            (Ok(Vec::new()), false)
                        }
                    }
                });

                // Drive the connectivity reporter from this poll's outcome. This
                // is what recovers the app to online when the server returns
                // while the user is idle, and detects going offline when no
                // library browsing is happening. See connectivity/mod.rs.
                if had_client {
                    if let Some(reporter) = connectivity_reporter.lock_safe().clone() {
                        rt.block_on(async {
                            match &sessions_result {
                                Ok(_) => reporter.report_success().await,
                                Err(e) => reporter.report_network_failure(Some(e.clone())).await,
                            }
                        });
                    }
                }

                // Emit event if successful
                match sessions_result {
                    Ok(sessions) => {
                        debug!("[SessionPoller] Fetched {} sessions", sessions.len());

                        // In remote (cast) mode, mirror the remote session's
                        // now-playing onto the Android lockscreen. The local
                        // ExoPlayer is idle while casting, so without this the
                        // lockscreen shows stale local metadata and a frozen
                        // scrubber. Driving it here (native poll thread) rather
                        // than from the WebView keeps it live even when the
                        // screen is locked and JS timers are throttled.
                        if let PlaybackMode::Remote { session_id } = mode_manager.get_mode() {
                            Self::push_remote_lockscreen(&sessions, &session_id);
                        }

                        if let Some(em) = emitter.lock_safe().as_ref() {
                            em.emit(crate::player::PlayerStatusEvent::SessionsUpdated { sessions });
                        }
                    }
                    Err(e) => {
                        warn!("[SessionPoller] Failed to fetch sessions: {}", e);
                    }
                }

                // Sleep for the calculated interval
                thread::sleep(Duration::from_millis(new_interval));
            }

            info!("[SessionPoller] Polling thread stopped");
        });

        *self.thread_handle.lock_safe() = Some(handle);
    }

    /// Stop the polling thread
    pub fn stop(&self) {
        info!("[SessionPoller] Stopping session polling");
        self.is_running.store(false, Ordering::Relaxed);

        // Join the thread if possible (don't block indefinitely)
        if let Some(handle) = self.thread_handle.lock_safe().take() {
            let _ = handle.join();
        }
    }

    /// Set UI hint for polling frequency adjustment
    pub fn set_polling_hint(&self, hint: PollingHint) {
        debug!("[SessionPoller] Setting polling hint: {:?}", hint);
        *self.current_hint.write_safe() = hint;
    }

    /// Push the remote session's now-playing onto the Android lockscreen.
    ///
    /// Looks up the active remote session by id and forwards its title/artist/
    /// album, duration and position to the media notification. Silently does
    /// nothing if the session isn't found or has no now-playing item (e.g. the
    /// remote stopped) - the next state change will refresh it.
    fn push_remote_lockscreen(sessions: &[crate::jellyfin::client::SessionInfo], session_id: &str) {
        // 100ns Jellyfin ticks -> milliseconds.
        const TICKS_PER_MS: i64 = 10_000;

        let Some(session) = sessions
            .iter()
            .find(|s| s.id.as_deref() == Some(session_id))
        else {
            return;
        };

        let Some(now_playing) = session.now_playing_item.as_ref() else {
            return;
        };

        let title = now_playing.name.clone().unwrap_or_default();
        let artist = now_playing
            .artists
            .as_ref()
            .map(|a| a.join(", "))
            .filter(|s| !s.is_empty())
            .or_else(|| now_playing.album_artist.clone())
            .unwrap_or_default();
        let album = now_playing.album.clone();
        let duration_ms = now_playing.run_time_ticks.unwrap_or(0) / TICKS_PER_MS;

        let (position_ms, is_playing) = session
            .play_state
            .as_ref()
            .map(|ps| {
                (
                    ps.position_ticks.unwrap_or(0) / TICKS_PER_MS,
                    !ps.is_paused.unwrap_or(false),
                )
            })
            .unwrap_or((0, false));

        let meta = crate::player::LockscreenMetadata {
            title,
            artist,
            album,
            duration_ms,
            position_ms,
            is_playing,
        };

        if let Err(e) = crate::player::update_lockscreen_metadata(&meta) {
            warn!(
                "[SessionPoller] Failed to update lockscreen metadata: {}",
                e
            );
        }
    }

    /// Calculate polling interval based on mode and hint
    fn calculate_interval(mode: &PlaybackMode, hint: PollingHint) -> u64 {
        match hint {
            PollingHint::CastActive => 500,      // Very fast for active control
            PollingHint::CastDiscovery => 15000, // Slow discovery
            PollingHint::Normal => {
                match mode {
                    PlaybackMode::Remote { .. } => 2000, // Fast in remote mode
                    PlaybackMode::Local | PlaybackMode::Idle => 10000, // Default
                }
            }
        }
    }

    /// Manually trigger a poll (for frontend refresh button)
    pub async fn poll_now(&self) -> Result<Vec<crate::jellyfin::client::SessionInfo>, String> {
        let client = self
            .jellyfin_client
            .lock_safe()
            .clone()
            .ok_or("Jellyfin client not configured")?;

        client.get_sessions().await
    }
}

impl Drop for SessionPollerManager {
    fn drop(&mut self) {
        self.stop();
    }
}

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

    /// Test polling interval calculation for different modes and hints
    #[test]
    fn test_calculate_interval() {
        // CastActive hint should always be 500ms regardless of mode
        assert_eq!(
            SessionPollerManager::calculate_interval(&PlaybackMode::Idle, PollingHint::CastActive),
            500
        );
        assert_eq!(
            SessionPollerManager::calculate_interval(&PlaybackMode::Local, PollingHint::CastActive),
            500
        );
        assert_eq!(
            SessionPollerManager::calculate_interval(
                &PlaybackMode::Remote {
                    session_id: "test".to_string()
                },
                PollingHint::CastActive
            ),
            500
        );

        // CastDiscovery hint should always be 15s regardless of mode
        assert_eq!(
            SessionPollerManager::calculate_interval(
                &PlaybackMode::Idle,
                PollingHint::CastDiscovery
            ),
            15000
        );
        assert_eq!(
            SessionPollerManager::calculate_interval(
                &PlaybackMode::Local,
                PollingHint::CastDiscovery
            ),
            15000
        );
        assert_eq!(
            SessionPollerManager::calculate_interval(
                &PlaybackMode::Remote {
                    session_id: "test".to_string()
                },
                PollingHint::CastDiscovery
            ),
            15000
        );

        // Normal hint should depend on mode
        // Idle and Local modes -> 10s
        assert_eq!(
            SessionPollerManager::calculate_interval(&PlaybackMode::Idle, PollingHint::Normal),
            10000
        );
        assert_eq!(
            SessionPollerManager::calculate_interval(&PlaybackMode::Local, PollingHint::Normal),
            10000
        );
        // Remote mode -> 2s
        assert_eq!(
            SessionPollerManager::calculate_interval(
                &PlaybackMode::Remote {
                    session_id: "test".to_string()
                },
                PollingHint::Normal
            ),
            2000
        );
    }

    /// Test PollingHint enum equality
    #[test]
    fn test_polling_hint_equality() {
        assert_eq!(PollingHint::Normal, PollingHint::Normal);
        assert_eq!(PollingHint::CastActive, PollingHint::CastActive);
        assert_eq!(PollingHint::CastDiscovery, PollingHint::CastDiscovery);

        assert_ne!(PollingHint::Normal, PollingHint::CastActive);
        assert_ne!(PollingHint::CastActive, PollingHint::CastDiscovery);
    }
}
