use std::env;
use std::error::Error;
use std::path::Path;
use std::time::Duration;
use tokio::time::sleep;

use cached::proc_macro::cached;
use reqwest;
use reqwest::header::{
    HeaderMap, HeaderValue, USER_AGENT, AUTHORIZATION, ACCEPT, ACCEPT_LANGUAGE, CONNECTION, RETRY_AFTER,
};
use reqwest::StatusCode;

/// HTTP Client for making requests to external services
pub struct HttpClient {
    client: reqwest::Client,
    github_token: Option<String>,
}

/// Cache HTTP GET requests to avoid hitting rate limits
#[cached(
    time = 3600, // Cache for 1 hour
    size = 100, // Maximum number of cached responses
    key = "String",
    convert = r#"{ url.clone() }"#,
    result = true
)]
async fn cached_http_get(url: String, client: reqwest::Client, token: Option<String>) -> Result<String, Box<dyn Error + Send + Sync>> {
    const MAX_RETRIES: u32 = 3;
    const BASE_RETRY_DELAY_MS: u64 = 1000;

    let mut headers = HeaderMap::new();

    // Set common headers for all requests
    headers.insert(USER_AGENT, HeaderValue::from_static("PlugSnatcherApp/0.1.0"));
    headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
    headers.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("en-US,en;q=0.5"));
    headers.insert(CONNECTION, HeaderValue::from_static("keep-alive"));

    // Add authorization header if token is provided
    if let Some(token_value) = token {
        if url.contains("github.com") {
            headers.insert(
                AUTHORIZATION,
                HeaderValue::from_str(&format!("token {}", token_value)).unwrap_or_else(|_| HeaderValue::from_static("")),
            );
        }
    }

    let mut retry_count = 0;
    let mut retry_delay = BASE_RETRY_DELAY_MS;

    loop {
        let response = client
            .get(&url)
            .headers(headers.clone())
            .send()
            .await;

        match response {
            Ok(resp) => {
                // Handle rate limiting
                if resp.status() == StatusCode::TOO_MANY_REQUESTS {
                    if retry_count >= MAX_RETRIES {
                        return Err(format!("Rate limit exceeded for {}", url).into());
                    }

                    // Check for Retry-After header or use exponential backoff
                    let retry_after = resp.headers()
                        .get(RETRY_AFTER)
                        .and_then(|val| val.to_str().ok())
                        .and_then(|val| val.parse::<u64>().ok())
                        .map(|secs| secs * 1000) // Convert header seconds to ms
                        .unwrap_or_else(|| {
                            // If no Retry-After header, just use the current exponential delay
                            retry_delay
                        });

                    // Exponential backoff calculation for the *next* potential retry
                    retry_delay *= 2;
                    println!("Rate limited for {}. Retrying after {} ms...", url, retry_after);
                    sleep(Duration::from_millis(retry_after)).await;
                    retry_count += 1;
                    continue;
                }

                // Handle other responses
                if resp.status().is_success() {
                    return Ok(resp.text().await?);
                } else {
                    return Err(format!(
                        "Request to {} failed with status code: {}",
                        url,
                        resp.status()
                    ).into());
                }
            },
            Err(err) => {
                if retry_count >= MAX_RETRIES {
                    return Err(Box::new(err));
                }
                sleep(Duration::from_millis(retry_delay)).await;
                retry_delay *= 2;
                retry_count += 1;
            }
        }
    }
}

/// Helper function to parse Modrinth's rate limit format
fn parse_modrinth_ratelimit(error_body: &str) -> Option<u64> {
    if let Ok(json) = serde_json::from_str::<serde_json::Value>(error_body) {
        if let Some(retry_after) = json.get("retry_after") {
            if let Some(seconds) = retry_after.as_u64() {
                return Some(seconds * 1000);
            }
        }
    }
    None
}

impl HttpClient {
    /// Create a new HTTP client
    pub fn new() -> Self {
        let client = reqwest::ClientBuilder::new()
            .timeout(Duration::from_secs(30))
            .connect_timeout(Duration::from_secs(5))
            .pool_idle_timeout(Duration::from_secs(90))
            .build()
            .unwrap_or_else(|_| reqwest::Client::new());

        // Try to get GitHub token from environment variable
        let github_token = match env::var("GITHUB_API_TOKEN") {
            Ok(token) if !token.is_empty() => Some(token),
            _ => None,
        };

        HttpClient {
            client,
            github_token,
        }
    }

    /// Perform an HTTP GET request
    pub async fn get(&self, url: &str) -> Result<String, Box<dyn Error + Send + Sync>> {
        cached_http_get(url.to_string(), self.client.clone(), self.github_token.clone()).await
    }

    /// Download a file from a URL to the specified destination
    pub async fn download(&self, url: &str, destination: &Path) -> Result<(), Box<dyn Error + Send + Sync>> {
        // Create a client with a larger timeout for downloads
        let client = reqwest::ClientBuilder::new()
            .timeout(Duration::from_secs(180)) // Longer timeout for downloads
            .build()?;

        let mut headers = HeaderMap::new();
        headers.insert(USER_AGENT, HeaderValue::from_static("PlugSnatcherApp/0.1.0"));

        // Add GitHub token if URL is GitHub and we have a token
        if url.contains("github.com") && self.github_token.is_some() {
            headers.insert(
                AUTHORIZATION,
                HeaderValue::from_str(&format!("token {}", self.github_token.as_ref().unwrap()))
                    .unwrap_or_else(|_| HeaderValue::from_static("")),
            );
        }

        // Get response
        let response = client.get(url).headers(headers).send().await?;

        // Check if request was successful
        if !response.status().is_success() {
            return Err(format!("Failed to download: Status {}", response.status()).into());
        }

        // Get response bytes
        let bytes = response.bytes().await?;

        // Create parent directories if needed
        if let Some(parent) = destination.parent() {
            if !parent.exists() {
                std::fs::create_dir_all(parent)?;
            }
        }

        // Write to file
        std::fs::write(destination, bytes)?;

        Ok(())
    }
}