use std::collections::HashMap;
use std::thread;
use std::time::Duration;

use reqwest::blocking::{Client, Response};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};

use crate::error::BioLibError;

pub struct HttpClient {
    client: Client,
}

impl Default for HttpClient {
    fn default() -> Self {
        Self::new()
    }
}

impl HttpClient {
    pub fn new() -> Self {
        let client = Client::builder()
            .timeout(Duration::from_secs(180))
            .build()
            .expect("Failed to create HTTP client");
        Self { client }
    }

    pub fn request(
        &self,
        method: &str,
        url: &str,
        data: Option<&[u8]>,
        headers: Option<&HashMap<String, String>>,
        retries: u32,
        timeout_secs: Option<u64>,
    ) -> crate::Result<HttpResponse> {
        let mut header_map = HeaderMap::new();
        if let Some(hdrs) = headers {
            for (key, value) in hdrs {
                if let (Ok(name), Ok(val)) = (
                    HeaderName::from_bytes(key.as_bytes()),
                    HeaderValue::from_str(value),
                ) {
                    header_map.insert(name, val);
                }
            }
        }

        if data.is_some() && !header_map.contains_key("content-type") {
            header_map.insert("content-type", HeaderValue::from_static("application/json"));
            header_map.insert("accept", HeaderValue::from_static("application/json"));
        }

        let timeout = Duration::from_secs(timeout_secs.unwrap_or(180));

        let mut last_error: Option<BioLibError> = None;

        crate::logging::debug(&format!("{method} {url}"));

        for retry_count in 0..=retries {
            if retry_count > 0 {
                crate::logging::debug(&format!("Retrying {method} {url} (attempt {retry_count})"));
                thread::sleep(Duration::from_secs(5 * retry_count as u64));
            }

            let mut builder = match method {
                "GET" => self.client.get(url),
                "POST" => self.client.post(url),
                "PATCH" => self.client.patch(url),
                "PUT" => self.client.put(url),
                "DELETE" => self.client.delete(url),
                _ => {
                    return Err(BioLibError::General(format!(
                        "Unsupported method: {method}"
                    )))
                }
            };

            builder = builder.headers(header_map.clone()).timeout(timeout);

            if let Some(body) = data {
                builder = builder.body(body.to_vec());
            }

            match builder.send() {
                Ok(response) => {
                    let status = response.status().as_u16();
                    if status == 429 || status == 502 || status == 503 || status == 504 {
                        last_error = Some(BioLibError::Http {
                            status,
                            message: format!("HTTP {method} request failed with status {status}"),
                        });
                        continue;
                    }
                    if status >= 400 {
                        let body_text = response.text().unwrap_or_default();
                        return Err(BioLibError::Http {
                            status,
                            message: body_text,
                        });
                    }
                    return HttpResponse::from_response(response);
                }
                Err(err) => {
                    if err.is_timeout() {
                        crate::logging::debug(&format!("Request timed out: {method} {url}"));
                        last_error = Some(BioLibError::Request(err));
                        continue;
                    }
                    return Err(BioLibError::Request(err));
                }
            }
        }

        Err(last_error.unwrap_or_else(|| {
            BioLibError::General(format!("Request failed after {retries} retries"))
        }))
    }

    pub fn get(
        &self,
        url: &str,
        headers: Option<&HashMap<String, String>>,
    ) -> crate::Result<HttpResponse> {
        self.request("GET", url, None, headers, 10, None)
    }

    pub fn get_with_range(&self, url: &str, start: u64, end: u64) -> crate::Result<HttpResponse> {
        let mut headers = HashMap::new();
        headers.insert("range".to_string(), format!("bytes={start}-{end}"));
        self.request("GET", url, None, Some(&headers), 5, None)
    }
}

pub struct HttpResponse {
    pub status_code: u16,
    pub body: Vec<u8>,
    pub headers: HashMap<String, String>,
}

impl HttpResponse {
    fn from_response(response: Response) -> crate::Result<Self> {
        let status_code = response.status().as_u16();
        let mut headers = HashMap::new();
        for (key, value) in response.headers() {
            if let Ok(v) = value.to_str() {
                headers.insert(key.to_string(), v.to_string());
            }
        }
        let body = response.bytes()?.to_vec();
        Ok(Self {
            status_code,
            body,
            headers,
        })
    }

    pub fn text(&self) -> String {
        String::from_utf8_lossy(&self.body).to_string()
    }

    pub fn json<T: serde::de::DeserializeOwned>(&self) -> crate::Result<T> {
        serde_json::from_slice(&self.body).map_err(BioLibError::Json)
    }
}
