use reqwest::{Client, Request, Response, StatusCode};
use serde::de::DeserializeOwned;
use std::time::Duration;

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

// Default timeout for requests (30 seconds - large library queries can be slow)
const DEFAULT_TIMEOUT_MS: u64 = 30000;

// Retry configuration - matches TypeScript exactly
const DEFAULT_MAX_RETRIES: u32 = 3;
const RETRY_DELAYS_MS: [u64; 3] = [1000, 2000, 4000]; // Exponential backoff

/// HTTP client configuration
#[derive(Clone, Debug)]
pub struct HttpConfig {
    pub timeout: Duration,
    pub max_retries: u32,
}

impl Default for HttpConfig {
    fn default() -> Self {
        Self {
            timeout: Duration::from_millis(DEFAULT_TIMEOUT_MS),
            max_retries: DEFAULT_MAX_RETRIES,
        }
    }
}

/// Error classification for retry logic
#[derive(Debug, Clone, PartialEq)]
pub enum ErrorKind {
    Network,
    Authentication,
    Server,
    Client,
}

/// Enhanced HTTP client with retry logic and error classification
#[derive(Clone)]
pub struct HttpClient {
    pub(crate) client: Client, // Make accessible within crate for custom requests
    config: HttpConfig,
}

impl HttpClient {
    /// Create a new HTTP client with default configuration
    pub fn new(config: HttpConfig) -> Result<Self, String> {
        let client = Client::builder()
            .timeout(config.timeout)
            .https_only(true)
            .build()
            .map_err(|e| format!("Failed to create HTTP client: {}", e))?;

        Ok(Self { client, config })
    }

    /// 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
    pub fn build_auth_header(access_token: Option<&str>, device_id: &str) -> String {
        let mut parts = vec![
            format!("MediaBrowser Client=\"{}\"", APP_NAME),
            format!("Version=\"{}\"", APP_VERSION),
            format!("Device=\"{}\"", Self::get_device_name()),
            format!("DeviceId=\"{}\"", device_id),
        ];

        if let Some(token) = access_token {
            parts.push(format!("Token=\"{}\"", token));
        }

        parts.join(", ")
    }

    /// Classify an error for retry logic
    pub fn classify_error(error: &reqwest::Error) -> ErrorKind {
        // Network errors (connection failures, timeouts, DNS failures)
        if error.is_timeout() || error.is_connect() {
            return ErrorKind::Network;
        }

        // Check status code if available
        if let Some(status) = error.status() {
            if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN {
                return ErrorKind::Authentication;
            } else if status.is_server_error() {
                return ErrorKind::Server;
            } else if status.is_client_error() {
                return ErrorKind::Client;
            }
        }

        // If no status code, check error message for network-related keywords
        let error_msg = error.to_string().to_lowercase();
        if error_msg.contains("network")
            || error_msg.contains("connection")
            || error_msg.contains("timeout")
            || error_msg.contains("dns")
            || error_msg.contains("refused")
            || error_msg.contains("reset")
        {
            return ErrorKind::Network;
        }

        // Default to client error
        ErrorKind::Client
    }

    /// Check if a request should be retried based on the error
    pub fn should_retry(error: &reqwest::Error) -> bool {
        match Self::classify_error(error) {
            ErrorKind::Network => true,         // Retry network errors
            ErrorKind::Server => true,          // Retry 5xx server errors
            ErrorKind::Authentication => false, // Don't retry 401/403
            ErrorKind::Client => false,         // Don't retry other 4xx errors
        }
    }

    /// Make a request with automatic retry on network errors
    pub async fn request_with_retry(&self, request: Request) -> Result<Response, reqwest::Error> {
        let max_retries = self.config.max_retries;
        let mut last_error: Option<reqwest::Error> = None;

        for attempt in 0..=max_retries {
            // Clone the request for retry attempts
            // If request cannot be cloned (e.g., streaming body), we cannot retry
            let Some(req) = request.try_clone() else {
                log::warn!("[HttpClient] Request body cannot be cloned, retries not possible");
                return self.client.execute(request).await;
            };

            match self.client.execute(req).await {
                Ok(response) => return Ok(response),
                Err(error) => {
                    last_error = Some(error);
                    let err = last_error.as_ref().unwrap();

                    // Don't retry if it's not a retryable error
                    if !Self::should_retry(err) {
                        return Err(last_error.unwrap());
                    }

                    // Don't retry on last attempt
                    if attempt == max_retries {
                        break;
                    }

                    // Wait before retrying (exponential backoff)
                    let delay_ms = RETRY_DELAYS_MS
                        .get(attempt as usize)
                        .copied()
                        .unwrap_or(*RETRY_DELAYS_MS.last().unwrap());

                    log::info!(
                        "[HttpClient] Retry {}/{} after {}ms (error: {})",
                        attempt + 1,
                        max_retries,
                        delay_ms,
                        err
                    );

                    tokio::time::sleep(Duration::from_millis(delay_ms)).await;
                }
            }
        }

        Err(last_error.unwrap())
    }

    /// Make a GET request and deserialize JSON with a short timeout and no retries.
    ///
    /// Intended for the initial "connect to server" probe on the login screen:
    /// a wrong/unreachable URL must fail fast instead of burning through the
    /// default 30s-per-attempt timeout and exponential backoff retries.
    pub async fn get_json_fast<T: DeserializeOwned>(&self, url: &str) -> Result<T, String> {
        // Short timeout so an unreachable host fails quickly.
        const FAST_TIMEOUT: Duration = Duration::from_secs(10);

        let request = self
            .client
            .get(url)
            .timeout(FAST_TIMEOUT)
            .build()
            .map_err(|e| format!("Failed to build request: {}", e))?;

        // No retry: connection failures on a wrong URL won't succeed on retry,
        // they'd only multiply the wait the user sees before an error.
        let response = self
            .client
            .execute(request)
            .await
            .map_err(|e| format!("Request failed: {}", e))?;

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

        response
            .json::<T>()
            .await
            .map_err(|e| format!("Failed to parse JSON: {}", e))
    }

    /// Quick ping to check if a server is reachable (no retry)
    pub async fn ping(&self, url: &str) -> bool {
        let request = self
            .client
            .get(url)
            .timeout(Duration::from_secs(5)) // Shorter timeout for ping
            .build();

        match request {
            Ok(req) => match self.client.execute(req).await {
                Ok(response) => response.status().is_success(),
                Err(_) => false,
            },
            Err(_) => false,
        }
    }
}

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

    #[test]
    fn test_auth_header_format() {
        let header = HttpClient::build_auth_header(Some("test_token"), "device456");
        assert!(header.contains("MediaBrowser Client=\"JellyTau\""));
        assert!(header.contains("Token=\"test_token\""));
        assert!(header.contains("DeviceId=\"device456\""));
    }

    #[test]
    fn test_auth_header_without_token() {
        let header = HttpClient::build_auth_header(None, "device456");
        assert!(header.contains("MediaBrowser Client=\"JellyTau\""));
        assert!(!header.contains("Token="));
        assert!(header.contains("DeviceId=\"device456\""));
    }

    #[test]
    fn test_retry_delays() {
        // Verify retry delays match TypeScript
        assert_eq!(RETRY_DELAYS_MS, [1000, 2000, 4000]);
    }
}
