//! Player events for frontend communication via Tauri events.
//!
//! These events are emitted from the player backend to notify the frontend
//! of playback state changes, position updates, etc.
//!
//! TRACES: UR-005, UR-019, UR-023, UR-026 | DR-001, DR-028, DR-047

#[cfg(test)]
use crate::utils::lock::MutexSafe;
use log::error;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tauri::AppHandle;
use tauri_specta::Event;

use super::{MediaSessionType, SleepTimerMode};

/// Events emitted by the player backend to the frontend via Tauri events.
///
/// These are distinct from `PlayerEvent` in state.rs, which handles internal
/// state machine transitions.
///
/// TRACES: UR-005, UR-019, UR-023, UR-026 | DR-001, DR-028, DR-047
#[derive(Debug, Clone, Serialize, Deserialize, specta::Type, tauri_specta::Event)]
// NOTE: fields are intentionally snake_case on the wire. specta generates the
// TypeScript bindings with snake_case field names (it does not apply serde's
// `rename_all_fields`), so adding `rename_all_fields = "camelCase"` here makes
// serde emit camelCase payloads that no longer match the generated schema —
// tauri-specta then silently drops those events (e.g. state_changed,
// queue_changed never reach the frontend, so the mini player never appears).
// Keep serde and specta agreeing: snake_case fields, snake_case variant tags.
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PlayerStatusEvent {
    /// Playback position updated (emitted periodically during playback)
    PositionUpdate {
        /// Current position in seconds
        position: f64,
        /// Total duration in seconds
        duration: f64,
    },
    /// Player state changed
    StateChanged {
        /// New state: "playing", "paused", "stopped", "loading", "idle"
        state: String,
        /// ID of the current media item, if any
        media_id: Option<String>,
    },
    /// Media has finished loading and is ready to play
    MediaLoaded {
        /// Total duration in seconds
        duration: f64,
    },
    /// Playback has ended naturally (reached end of media)
    PlaybackEnded,
    /// Buffering state changed
    Buffering {
        /// Buffering progress (0-100)
        percent: u8,
    },
    /// An error occurred during playback
    Error {
        /// Error message
        message: String,
        /// Whether the error is recoverable
        recoverable: bool,
    },
    /// Volume changed
    VolumeChanged {
        /// New volume level (0.0-1.0)
        volume: f32,
        /// Whether audio is muted
        muted: bool,
    },
    /// Sleep timer state changed
    SleepTimerChanged {
        /// Sleep timer mode
        mode: SleepTimerMode,
        /// Remaining seconds (for time-based timer)
        remaining_seconds: u32,
    },
    /// Time-based sleep timer expired: playback must stop. The backend stops
    /// its own (MPV/ExoPlayer) playback, but HTML5 video on Linux plays in the
    /// webview outside the backend's control — the frontend pauses it on this
    /// event.
    SleepTimerExpired,
    /// Show next episode popup with countdown
    ShowNextEpisodePopup {
        /// Current episode that just finished
        current_episode: crate::repository::types::MediaItem,
        /// Next episode to play
        next_episode: crate::repository::types::MediaItem,
        /// Countdown duration in seconds
        countdown_seconds: u32,
        /// Whether to auto-advance when countdown reaches 0
        auto_advance: bool,
    },
    /// Countdown tick (emitted every second during autoplay countdown)
    CountdownTick {
        /// Remaining seconds in countdown
        remaining_seconds: u32,
    },
    /// Queue changed (items added, removed, reordered, or playback mode changed)
    QueueChanged {
        /// All items in the queue
        items: Vec<crate::player::media::MediaItem>,
        /// Current item index
        current_index: Option<usize>,
        /// Whether shuffle is enabled
        shuffle: bool,
        /// Current repeat mode
        repeat: crate::player::queue::RepeatMode,
        /// Whether there's a next track available
        has_next: bool,
        /// Whether there's a previous track available
        has_previous: bool,
    },
    /// Media session changed (activity context changed: Audio/Movie/TvShow/Idle)
    SessionChanged {
        /// Current session state
        session: MediaSessionType,
    },
    /// Remote sessions updated (for cast/remote control UI)
    SessionsUpdated {
        /// All active controllable sessions from Jellyfin
        sessions: Vec<crate::jellyfin::client::SessionInfo>,
    },
    /// The authoritative playback mode changed in the Rust backend.
    ///
    /// The Rust `PlaybackModeManager` is the single source of truth for which
    /// device playback commands route to (local vs a remote session). The
    /// frontend keeps a mirror store for the UI; without this event that mirror
    /// drifts out of sync (e.g. a mode transition happens inside a transfer or a
    /// local stop that the frontend never learns about), and controls then route
    /// to the wrong device — the classic "it keeps playing on the remote" bug.
    /// The frontend reconciles its store to this payload whenever it fires.
    PlaybackModeChanged {
        /// New mode: "local", "remote", or "idle".
        mode: String,
        /// Session id when `mode == "remote"`, otherwise `None`.
        session_id: Option<String>,
    },
    /// The user asked to disconnect from the remote session and resume locally.
    ///
    /// Emitted when the lockscreen Stop button is pressed while casting. The
    /// frontend owns the two-step remote->local transfer (it must reload the
    /// media item locally), so the native side only signals intent here.
    RemoteDisconnectRequested,
    /// Backend-originated control command targeting the active frontend player
    /// adapter (the HTML5 <video> that lives in the webview, which Rust cannot
    /// drive directly). Emitted by control paths like the sleep timer, lockscreen,
    /// or remote so they can pause/play/seek/stop the webview element.
    /// `playerEvents.ts` routes this to the active PlayerAdapter via the facade.
    ControlCommand {
        /// One of: "play", "pause", "stop", "seek".
        action: String,
        /// Target position in seconds (only meaningful for "seek").
        position: Option<f64>,
    },
    /// Ask the frontend webview `<audio>` element to load and play a stream.
    ///
    /// Emitted by `WebviewAudioBackend` on platforms with no native audio
    /// backend (e.g. Windows): audio-only playback is rendered by an `<audio>`
    /// element in the webview, mirroring how all video already renders through
    /// the webview `<video>`. The element then reports its state/position back
    /// through the `player_report_*` commands, so the Rust controller stays the
    /// single source of truth. Subsequent play/pause/seek/stop reach the element
    /// via `ControlCommand`.
    WebviewAudioLoad {
        /// Stream URL for the `<audio>` element to play.
        url: String,
        /// Jellyfin item id, used as the media_id when reporting state back.
        media_id: Option<String>,
        /// Resume position in seconds (0 = start from the beginning).
        position: f64,
        /// Whether to begin playing immediately after loading.
        autoplay: bool,
    },
}

/// Trait for emitting player events to the frontend.
///
/// This abstraction allows backends to emit events without depending
/// directly on Tauri, making them easier to test.
pub trait PlayerEventEmitter: Send + Sync {
    /// Emit a player status event to the frontend
    fn emit(&self, event: PlayerStatusEvent);
}

/// Tauri-based implementation of PlayerEventEmitter.
///
/// Uses Tauri's `AppHandle::emit()` to broadcast events to all windows.
pub struct TauriEventEmitter {
    app_handle: AppHandle,
}

impl TauriEventEmitter {
    /// Create a new TauriEventEmitter with the given app handle.
    pub fn new(app_handle: AppHandle) -> Self {
        Self { app_handle }
    }
}

impl PlayerEventEmitter for TauriEventEmitter {
    fn emit(&self, event: PlayerStatusEvent) {
        // Emitted via the tauri-specta Event trait so the payload shape and event
        // name match the generated TypeScript bindings (events.playerStatusEvent).
        if let Err(e) = Event::emit(&event, &self.app_handle) {
            error!("Failed to emit player event: {}", e);
        }
    }
}

/// Thread-safe wrapper for event emitters.
#[allow(dead_code)]
pub type SharedEventEmitter = Arc<dyn PlayerEventEmitter>;

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Mutex;
    use std::thread;

    /// Test event emitter that captures events for verification
    pub struct TestEventEmitter {
        events: Mutex<Vec<PlayerStatusEvent>>,
    }

    impl TestEventEmitter {
        pub fn new() -> Self {
            Self {
                events: Mutex::new(Vec::new()),
            }
        }

        pub fn events(&self) -> Vec<PlayerStatusEvent> {
            self.events.lock_safe().clone()
        }
    }

    impl PlayerEventEmitter for TestEventEmitter {
        fn emit(&self, event: PlayerStatusEvent) {
            self.events.lock_safe().push(event);
        }
    }

    #[test]
    fn test_position_update_serialization() {
        let event = PlayerStatusEvent::PositionUpdate {
            position: 30.5,
            duration: 180.0,
        };
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("position_update"));
        assert!(json.contains("30.5"));
        assert!(json.contains("180"));
    }

    #[test]
    fn test_state_changed_serialization() {
        let event = PlayerStatusEvent::StateChanged {
            state: "playing".to_string(),
            media_id: Some("test-id-123".to_string()),
        };
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("state_changed"));
        assert!(json.contains("playing"));
        assert!(json.contains("test-id-123"));
    }

    #[test]
    fn test_state_changed_no_media_id() {
        let event = PlayerStatusEvent::StateChanged {
            state: "idle".to_string(),
            media_id: None,
        };
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("state_changed"));
        assert!(json.contains("idle"));
        assert!(json.contains("null"));
    }

    #[test]
    fn test_media_loaded_serialization() {
        let event = PlayerStatusEvent::MediaLoaded { duration: 245.5 };
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("media_loaded"));
        assert!(json.contains("245.5"));
    }

    #[test]
    fn test_playback_ended_serialization() {
        let event = PlayerStatusEvent::PlaybackEnded;
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("playback_ended"));
    }

    #[test]
    fn test_buffering_serialization() {
        let event = PlayerStatusEvent::Buffering { percent: 75 };
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("buffering"));
        assert!(json.contains("75"));
    }

    #[test]
    fn test_error_serialization() {
        let event = PlayerStatusEvent::Error {
            message: "Failed to load media".to_string(),
            recoverable: true,
        };
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("error"));
        assert!(json.contains("Failed to load media"));
        assert!(json.contains("true"));
    }

    #[test]
    fn test_volume_changed_serialization() {
        let event = PlayerStatusEvent::VolumeChanged {
            volume: 0.75,
            muted: false,
        };
        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains("volume_changed"));
        assert!(json.contains("0.75"));
        assert!(json.contains("false"));
    }

    #[test]
    fn test_event_emitter_captures_events() {
        let emitter = TestEventEmitter::new();
        emitter.emit(PlayerStatusEvent::PlaybackEnded);
        assert_eq!(emitter.events().len(), 1);
    }

    #[test]
    fn test_event_emitter_multiple_events() {
        let emitter = TestEventEmitter::new();
        emitter.emit(PlayerStatusEvent::PlaybackEnded);
        emitter.emit(PlayerStatusEvent::PositionUpdate {
            position: 10.0,
            duration: 100.0,
        });
        emitter.emit(PlayerStatusEvent::StateChanged {
            state: "paused".to_string(),
            media_id: None,
        });
        assert_eq!(emitter.events().len(), 3);
    }

    #[test]
    fn test_event_emitter_thread_safety() {
        let emitter = Arc::new(TestEventEmitter::new());
        let mut handles = vec![];

        for i in 0..10 {
            let emitter_clone = Arc::clone(&emitter);
            let handle = thread::spawn(move || {
                emitter_clone.emit(PlayerStatusEvent::PositionUpdate {
                    position: i as f64,
                    duration: 100.0,
                });
            });
            handles.push(handle);
        }

        for handle in handles {
            handle.join().unwrap();
        }

        assert_eq!(emitter.events().len(), 10);
    }

    #[test]
    fn test_shared_event_emitter() {
        let emitter: SharedEventEmitter = Arc::new(TestEventEmitter::new());
        emitter.emit(PlayerStatusEvent::PlaybackEnded);
        // Verify it compiles and works as a trait object
    }
}
