//! TRACES: UR-009 | JA-001, JA-002, JA-003, JA-004, JA-007, JA-010, JA-011, JA-012, JA-017, JA-021 | IR-009, IR-010, IR-011

use log::{debug, error, info};
use reqwest::Client;
use serde::Deserialize;
use std::sync::Arc;

use super::types::*;

const APP_NAME: &str = "JellyTau";
const APP_VERSION: &str = "0.1.0";

/// Jellyfin API client for playback reporting
#[derive(Clone)]
pub struct JellyfinClient {
    config: Arc<JellyfinConfig>,
    http_client: Client,
}

impl JellyfinClient {
    /// Create a new Jellyfin API client
    pub fn new(config: JellyfinConfig) -> Result<Self, String> {
        let http_client = Client::builder()
            .timeout(std::time::Duration::from_secs(10))
            .https_only(true)
            .build()
            .map_err(|e| format!("Failed to create HTTP client: {}", e))?;

        Ok(Self {
            config: Arc::new(config),
            http_client,
        })
    }

    /// Get device name based on platform
    fn get_device_name() -> &'static str {
        #[cfg(target_os = "android")]
        return "Android";
        #[cfg(target_os = "linux")]
        return "Linux";
        #[cfg(target_os = "windows")]
        return "Windows";
        #[cfg(target_os = "macos")]
        return "macOS";
        #[cfg(target_os = "ios")]
        return "iOS";
        #[cfg(not(any(
            target_os = "android",
            target_os = "linux",
            target_os = "windows",
            target_os = "macos",
            target_os = "ios"
        )))]
        return "Unknown";
    }

    /// Build the X-Emby-Authorization header value
    fn get_auth_header(&self) -> String {
        format!(
            "MediaBrowser Client=\"{}\", Version=\"{}\", Device=\"{}\", DeviceId=\"{}\", Token=\"{}\"",
            APP_NAME,
            APP_VERSION,
            Self::get_device_name(),
            self.config.device_id,
            self.config.access_token
        )
    }

    /// Make a GET request to the Jellyfin API
    async fn get<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T, String> {
        let url = format!("{}{}", self.config.server_url, endpoint);

        log::debug!("[JellyfinClient] GET {}", endpoint);

        let response = self
            .http_client
            .get(&url)
            .header("X-Emby-Authorization", self.get_auth_header())
            .send()
            .await
            .map_err(|e| {
                log::error!("[JellyfinClient] Request failed for {}: {}", endpoint, e);
                format!("Network request failed: {}", e)
            })?;

        let status = response.status();
        log::debug!(
            "[JellyfinClient] Response status for {}: {}",
            endpoint,
            status
        );

        if !status.is_success() {
            let error_text = response
                .text()
                .await
                .unwrap_or_else(|_| "Unknown error".to_string());
            log::error!("[JellyfinClient] Request failed: {} {}", status, endpoint);
            log::error!("[JellyfinClient] Response: {}", error_text);
            return Err(format!(
                "Jellyfin API error {}: {}",
                status.as_u16(),
                error_text
            ));
        }

        // Get the response text first so we can log it
        let response_text = response.text().await.map_err(|e| {
            log::error!("[JellyfinClient] Failed to read response body: {}", e);
            format!("Failed to read response: {}", e)
        })?;

        // Log the raw response for sessions endpoint to help debug
        if endpoint.contains("/Sessions") {
            debug!(
                "[JellyfinClient] Raw response for {}: {}",
                endpoint,
                if response_text.len() > 500 {
                    format!(
                        "{}... (truncated, {} bytes total)",
                        &response_text[..500],
                        response_text.len()
                    )
                } else {
                    response_text.clone()
                }
            );
        }

        // Parse the response text as JSON
        let data = serde_json::from_str::<T>(&response_text).map_err(|e| {
            log::error!("[JellyfinClient] Failed to parse response: {}", e);
            log::error!(
                "[JellyfinClient] Response was: {}",
                if response_text.len() > 200 {
                    format!("{}...", &response_text[..200])
                } else {
                    response_text.clone()
                }
            );
            format!("Failed to parse response: {}", e)
        })?;

        log::debug!("[JellyfinClient] Request successful for {}", endpoint);
        Ok(data)
    }

    /// Make a POST request to the Jellyfin API
    async fn post<T: serde::Serialize>(&self, endpoint: &str, body: &T) -> Result<(), String> {
        let url = format!("{}{}", self.config.server_url, endpoint);

        log::debug!("[JellyfinClient] POST {} to {}", endpoint, url);

        let response: reqwest::Response = self
            .http_client
            .post(&url)
            .header("Content-Type", "application/json")
            .header("X-Emby-Authorization", self.get_auth_header())
            .json(body)
            .send()
            .await
            .map_err(|e| {
                log::error!("[JellyfinClient] Request failed for {}: {}", endpoint, e);
                format!("Network request failed: {}", e)
            })?;

        let status = response.status();
        log::debug!(
            "[JellyfinClient] Response status for {}: {}",
            endpoint,
            status
        );

        if !status.is_success() {
            let error_text: String = response
                .text()
                .await
                .unwrap_or_else(|_| "Unknown error".to_string());
            log::error!("[JellyfinClient] Request failed: {} {}", status, endpoint);
            log::error!("[JellyfinClient] Response: {}", error_text);
            return Err(format!(
                "Jellyfin API error {}: {}",
                status.as_u16(),
                error_text
            ));
        }

        log::debug!("[JellyfinClient] Request successful for {}", endpoint);
        Ok(())
    }

    /// Report playback start to Jellyfin
    pub async fn report_playback_start(
        &self,
        item_id: String,
        position_ticks: i64,
        play_session_id: Option<String>,
    ) -> Result<(), String> {
        let request = PlaybackStartRequest {
            item_id,
            position_ticks,
            play_session_id,
            play_command: "PlayNow".to_string(),
            is_paused: false,
        };

        self.post("/Sessions/Playing", &request).await
    }

    /// Report playback stopped to Jellyfin
    pub async fn report_playback_stopped(
        &self,
        item_id: String,
        position_ticks: i64,
        play_session_id: Option<String>,
    ) -> Result<(), String> {
        let request = PlaybackStoppedRequest {
            item_id,
            position_ticks,
            play_session_id,
        };

        self.post("/Sessions/Playing/Stopped", &request).await
    }

    /// Report playback progress to Jellyfin
    #[allow(dead_code)] // Will be used when playback_reporting is integrated
    pub async fn report_playback_progress(
        &self,
        item_id: String,
        position_ticks: i64,
        is_paused: bool,
        play_session_id: Option<String>,
    ) -> Result<(), String> {
        let request = PlaybackProgressRequest {
            item_id,
            position_ticks,
            is_paused,
            play_session_id,
        };

        self.post("/Sessions/Playing/Progress", &request).await
    }

    /// Play items on a remote session (casting)
    pub async fn play_on_session(
        &self,
        session_id: String,
        item_ids: Vec<String>,
        start_index: usize,
        start_position_ticks: Option<i64>,
    ) -> Result<(), String> {
        log::info!("[JellyfinClient] Playing on session: {}", session_id);
        log::info!(
            "[JellyfinClient] Item IDs: {:?}, Start index: {}",
            item_ids,
            start_index
        );
        debug!(
            "[JellyfinClient] play_on_session called: session={}, {} items, start_index={}",
            session_id,
            item_ids.len(),
            start_index
        );

        // Build URL with query parameters (Jellyfin expects PascalCase query params)
        let mut url = format!(
            "{}/Sessions/{}/Playing?PlayCommand=PlayNow&StartIndex={}",
            self.config.server_url, session_id, start_index
        );

        // Add item IDs as repeated query parameters
        for item_id in &item_ids {
            url.push_str(&format!("&ItemIds={}", item_id));
        }

        // Add start position if provided
        if let Some(ticks) = start_position_ticks {
            url.push_str(&format!("&StartPositionTicks={}", ticks));
            log::info!("[JellyfinClient] Starting at position: {} ticks", ticks);
        }

        log::info!("[JellyfinClient] POST {}", url);
        debug!("[JellyfinClient] Full URL length: {} chars", url.len());
        // Don't log full URL as it may contain sensitive tokens, just log the endpoint
        debug!(
            "[JellyfinClient] POST to Sessions/{}/Playing with {} itemIds",
            session_id,
            item_ids.len()
        );

        debug!("[JellyfinClient] Sending HTTP POST request...");
        let response = self
            .http_client
            .post(&url)
            .header("X-Emby-Authorization", self.get_auth_header())
            .send()
            .await
            .map_err(|e| {
                log::error!("[JellyfinClient] Request failed: {}", e);
                error!("[JellyfinClient] HTTP request failed: {}", e);
                format!("Network request failed: {}", e)
            })?;

        let status = response.status();
        log::debug!("[JellyfinClient] Response status: {}", status);
        debug!("[JellyfinClient] Response status: {}", status);

        if !status.is_success() {
            let error_text = response
                .text()
                .await
                .unwrap_or_else(|_| "Unknown error".to_string());
            log::error!("[JellyfinClient] Request failed: {}", error_text);
            error!(
                "[JellyfinClient] API error {}: {}",
                status.as_u16(),
                error_text
            );
            return Err(format!(
                "Jellyfin API error {}: {}",
                status.as_u16(),
                error_text
            ));
        }

        log::info!("[JellyfinClient] Successfully sent play command to remote session");
        info!("[JellyfinClient] Play command sent to remote session");
        Ok(())
    }

    /// Send a playback command to a remote session
    pub async fn send_session_command(
        &self,
        session_id: String,
        command: &str,
    ) -> Result<(), String> {
        self.post(
            &format!("/Sessions/{}/Playing/{}", session_id, command),
            &serde_json::json!({}),
        )
        .await
    }

    /// Seek on a remote session
    ///
    /// Jellyfin's `/Sessions/{id}/Playing/Seek` endpoint takes the target as the
    /// `SeekPositionTicks` *query parameter*, not a JSON body. Sending it in the
    /// body (as we used to) is silently ignored and the remote never seeks.
    pub async fn session_seek(
        &self,
        session_id: String,
        position_ticks: i64,
    ) -> Result<(), String> {
        let url = format!(
            "{}/Sessions/{}/Playing/Seek?SeekPositionTicks={}",
            self.config.server_url, session_id, position_ticks
        );

        let response = self
            .http_client
            .post(&url)
            .header("X-Emby-Authorization", self.get_auth_header())
            .send()
            .await
            .map_err(|e| format!("Network request failed: {}", e))?;

        let status = response.status();
        if !status.is_success() {
            let error_text = response
                .text()
                .await
                .unwrap_or_else(|_| "Unknown error".to_string());
            return Err(format!(
                "Jellyfin API error {}: {}",
                status.as_u16(),
                error_text
            ));
        }

        log::info!(
            "[JellyfinClient] Seek to {} ticks on session {}",
            position_ticks,
            session_id
        );
        Ok(())
    }

    /// Send a full GeneralCommand to a remote session.
    /// Uses POST /Sessions/{id}/Command with a body containing Name and Arguments.
    /// This is required for commands that need arguments (e.g. SetVolume).
    async fn send_general_command(
        &self,
        session_id: &str,
        command_name: &str,
        arguments: Option<serde_json::Value>,
    ) -> Result<(), String> {
        let mut payload = serde_json::json!({
            "Name": command_name,
        });

        if let Some(args) = arguments {
            payload["Arguments"] = args;
        }

        log::info!(
            "[JellyfinClient] Sending GeneralCommand '{}' to session {} with payload: {}",
            command_name,
            session_id,
            serde_json::to_string(&payload).unwrap_or_default()
        );

        self.post(&format!("/Sessions/{}/Command", session_id), &payload)
            .await
    }

    /// Set volume on a remote session
    pub async fn session_set_volume(&self, session_id: String, volume: i32) -> Result<(), String> {
        self.send_general_command(
            &session_id,
            "SetVolume",
            Some(serde_json::json!({ "Volume": volume.to_string() })),
        )
        .await
    }

    /// Toggle mute on a remote session
    pub async fn session_toggle_mute(&self, session_id: String) -> Result<(), String> {
        log::info!("[JellyfinClient] Toggling mute on session {}", session_id);

        self.send_general_command(&session_id, "ToggleMute", None)
            .await
    }

    /// Get all active sessions
    pub async fn get_sessions(&self) -> Result<Vec<SessionInfo>, String> {
        let sessions: Vec<SessionInfo> = self.get("/Sessions").await?;
        info!(
            "[JellyfinClient] Fetched {} sessions from API",
            sessions.len()
        );
        for session in &sessions {
            debug!("[JellyfinClient] Session: id={:?}, device={:?}, client={:?}, supportsRemoteControl={}",
                session.id, session.device_name, session.client, session.supports_remote_control);
        }
        Ok(sessions)
    }

    /// Get a specific session by ID
    pub async fn get_session(&self, session_id: &str) -> Result<Option<SessionInfo>, String> {
        let sessions = self.get_sessions().await?;
        Ok(sessions
            .into_iter()
            .find(|s| s.id.as_deref() == Some(session_id)))
    }

    // --- JellyLMS multi-room sync groups -----------------------------------
    //
    // The JellyLMS plugin exposes a REST API under `/JellyLms` for grouping LMS
    // players ("zones") into synchronized multi-room sync groups. Players are
    // addressed by MAC address; JellyTau maps a Jellyfin session to a MAC by
    // stripping the `lms-` prefix off the session's device id (see
    // LmsDeviceDiscoveryService in the jellyLMS repo, which registers each player
    // with deviceId = "lms-{MacAddress}").

    /// List current LMS sync groups.
    pub async fn lms_get_sync_groups(&self) -> Result<Vec<LmsSyncGroup>, String> {
        self.get("/JellyLms/SyncGroups").await
    }

    /// Fuse LMS zones: create a sync group with `master_mac` as the sync master
    /// and `slave_macs` joining it. The master keeps playing; slaves follow.
    pub async fn lms_create_sync_group(
        &self,
        master_mac: &str,
        slave_macs: Vec<String>,
    ) -> Result<(), String> {
        let payload = serde_json::json!({
            "MasterMac": master_mac,
            "SlaveMacs": slave_macs,
        });
        self.post("/JellyLms/SyncGroups", &payload).await
    }

    /// Remove a single LMS player from whatever sync group it's in.
    pub async fn lms_unsync_player(&self, mac: &str) -> Result<(), String> {
        self.delete(&format!("/JellyLms/SyncGroups/Players/{}", mac))
            .await
    }

    /// Dissolve an entire LMS sync group, identified by its master's MAC.
    pub async fn lms_dissolve_sync_group(&self, master_mac: &str) -> Result<(), String> {
        self.delete(&format!("/JellyLms/SyncGroups/{}", master_mac))
            .await
    }

    /// Make a DELETE request to the Jellyfin API (used by the JellyLMS endpoints).
    async fn delete(&self, endpoint: &str) -> Result<(), String> {
        let url = format!("{}{}", self.config.server_url, endpoint);

        log::debug!("[JellyfinClient] DELETE {}", endpoint);

        let response = self
            .http_client
            .delete(&url)
            .header("X-Emby-Authorization", self.get_auth_header())
            .send()
            .await
            .map_err(|e| format!("Network request failed: {}", e))?;

        let status = response.status();
        if !status.is_success() {
            let error_text = response
                .text()
                .await
                .unwrap_or_else(|_| "Unknown error".to_string());
            return Err(format!(
                "Jellyfin API error {}: {}",
                status.as_u16(),
                error_text
            ));
        }
        Ok(())
    }
}

/// An LMS multi-room sync group, as returned by JellyLMS `/JellyLms/SyncGroups`.
///
/// Mirrors the plugin's `SyncGroup` model. The master is the sync source; slaves
/// follow it in lockstep.
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LmsSyncGroup {
    #[serde(alias = "MasterMac")]
    pub master_mac: String,
    #[serde(default, alias = "MasterName")]
    pub master_name: String,
    #[serde(default, alias = "SlaveMacs")]
    pub slave_macs: Vec<String>,
    #[serde(default, alias = "SlaveNames")]
    pub slave_names: Vec<String>,
}

/// Default value for supports_remote_control when missing from API
/// We default to true to show all sessions. If a session explicitly doesn't
/// support remote control, the Jellyfin API will set this field to false.
fn default_true() -> bool {
    true
}

/// Session information from Jellyfin
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionInfo {
    #[serde(default)]
    #[serde(alias = "Id")]
    pub id: Option<String>,
    #[serde(default)]
    #[serde(alias = "UserId")]
    pub user_id: Option<String>,
    #[serde(default)]
    #[serde(alias = "UserName")]
    pub user_name: Option<String>,
    #[serde(default)]
    #[serde(alias = "Client")]
    pub client: Option<String>,
    #[serde(default)]
    #[serde(alias = "DeviceName")]
    pub device_name: Option<String>,
    #[serde(default)]
    #[serde(alias = "DeviceId")]
    pub device_id: Option<String>,
    #[serde(default)]
    #[serde(alias = "ApplicationVersion")]
    pub application_version: Option<String>,
    #[serde(default)]
    #[serde(alias = "IsActive")]
    pub is_active: Option<bool>,
    #[serde(default)]
    #[serde(alias = "SupportsMediaControl")]
    pub supports_media_control: Option<bool>,
    #[serde(default = "default_true")]
    #[serde(alias = "SupportsRemoteControl")]
    pub supports_remote_control: bool,
    #[serde(default)]
    #[serde(alias = "NowPlayingItem")]
    pub now_playing_item: Option<NowPlayingItem>,
    #[serde(default)]
    #[serde(alias = "PlayState")]
    pub play_state: Option<PlayState>,
    #[serde(default)]
    #[serde(alias = "PlayableMediaTypes")]
    pub playable_media_types: Option<Vec<String>>,
    #[serde(default)]
    #[serde(alias = "SupportedCommands")]
    pub supported_commands: Option<Vec<String>>,
}

#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NowPlayingItem {
    #[serde(alias = "Id")]
    pub id: Option<String>,
    #[serde(alias = "Name")]
    pub name: Option<String>,
    #[serde(alias = "RunTimeTicks")]
    pub run_time_ticks: Option<i64>,
    #[serde(alias = "Album")]
    pub album: Option<String>,
    #[serde(alias = "AlbumId")]
    pub album_id: Option<String>,
    #[serde(alias = "AlbumArtist")]
    pub album_artist: Option<String>,
    #[serde(alias = "Artists")]
    pub artists: Option<Vec<String>>,
    #[serde(alias = "ImageTags")]
    pub image_tags: Option<std::collections::HashMap<String, String>>,
    #[serde(alias = "PrimaryImageTag")]
    pub primary_image_tag: Option<String>,
    #[serde(alias = "AlbumPrimaryImageTag")]
    pub album_primary_image_tag: Option<String>,
    #[serde(rename = "Type")]
    pub item_type: Option<String>,
}

#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayState {
    #[serde(default)]
    #[serde(alias = "PositionTicks")]
    pub position_ticks: Option<i64>,
    #[serde(default)]
    #[serde(alias = "CanSeek")]
    pub can_seek: Option<bool>,
    #[serde(default)]
    #[serde(alias = "IsPaused")]
    pub is_paused: Option<bool>,
    #[serde(default)]
    #[serde(alias = "IsMuted")]
    pub is_muted: Option<bool>,
    #[serde(default)]
    #[serde(alias = "VolumeLevel")]
    pub volume_level: Option<i32>,
    #[serde(default)]
    #[serde(alias = "RepeatMode")]
    pub repeat_mode: Option<String>,
    #[serde(default)]
    #[serde(alias = "ShuffleMode")]
    pub shuffle_mode: Option<String>,
}

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

    #[test]
    fn test_auth_header_format() {
        let config = JellyfinConfig {
            server_url: "http://localhost:8096".to_string(),
            access_token: "test_token".to_string(),
            device_id: "device456".to_string(),
        };

        let client = JellyfinClient::new(config).unwrap();
        let header = client.get_auth_header();

        assert!(header.contains("MediaBrowser Client=\"JellyTau\""));
        assert!(header.contains("Token=\"test_token\""));
        assert!(header.contains("DeviceId=\"device456\""));
    }
}
