use reqwest::Client;
use serde::Serialize;
use serde::de::DeserializeOwned;

pub struct ZeniiClient {
    base_url: String,
    token: Option<String>,
    http: Client,
}

impl ZeniiClient {
    pub fn new(host: &str, port: u16, token: Option<String>) -> Self {
        Self {
            base_url: format!("http://{host}:{port}"),
            token,
            http: Client::new(),
        }
    }

    pub fn ws_url(&self, path: &str) -> String {
        let base = self
            .base_url
            .replacen("http://", "ws://", 1)
            .replacen("https://", "wss://", 1);
        format!("{base}{path}")
    }

    pub fn auth_header_value(&self) -> Option<String> {
        self.token.as_ref().map(|t| format!("Bearer {t}"))
    }

    fn format_error(status: reqwest::StatusCode, body: &str) -> String {
        if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(body) {
            let msg = err_json
                .get("message")
                .and_then(|v| v.as_str())
                .unwrap_or(body);
            let code = err_json
                .get("error_code")
                .and_then(|v| v.as_str())
                .unwrap_or("UNKNOWN");
            let hint = err_json.get("hint").and_then(|v| v.as_str());
            let mut err_msg = format!("[{code}] {msg}");
            if let Some(h) = hint {
                err_msg.push_str(&format!("\n  Hint: {h}"));
            }
            return err_msg;
        }
        format!("HTTP {status}: {body}")
    }

    pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, String> {
        let mut req = self.http.get(format!("{}{path}", self.base_url));
        if let Some(ref val) = self.auth_header_value() {
            req = req.header("authorization", val);
        }
        let resp = req.send().await.map_err(|e| e.to_string())?;
        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(Self::format_error(status, &body));
        }
        resp.json().await.map_err(|e| e.to_string())
    }

    pub async fn post<B: Serialize, T: DeserializeOwned>(
        &self,
        path: &str,
        body: &B,
    ) -> Result<T, String> {
        let mut req = self
            .http
            .post(format!("{}{path}", self.base_url))
            .json(body);
        if let Some(ref val) = self.auth_header_value() {
            req = req.header("authorization", val);
        }
        let resp = req.send().await.map_err(|e| e.to_string())?;
        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(Self::format_error(status, &body));
        }
        resp.json().await.map_err(|e| e.to_string())
    }

    pub async fn post_no_response<B: Serialize>(&self, path: &str, body: &B) -> Result<(), String> {
        let mut req = self
            .http
            .post(format!("{}{path}", self.base_url))
            .json(body);
        if let Some(ref val) = self.auth_header_value() {
            req = req.header("authorization", val);
        }
        let resp = req.send().await.map_err(|e| e.to_string())?;
        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(Self::format_error(status, &body));
        }
        Ok(())
    }

    pub async fn put<B: Serialize, T: DeserializeOwned>(
        &self,
        path: &str,
        body: &B,
    ) -> Result<T, String> {
        let mut req = self.http.put(format!("{}{path}", self.base_url)).json(body);
        if let Some(ref val) = self.auth_header_value() {
            req = req.header("authorization", val);
        }
        let resp = req.send().await.map_err(|e| e.to_string())?;
        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(Self::format_error(status, &body));
        }
        resp.json().await.map_err(|e| e.to_string())
    }

    pub async fn get_text(&self, path: &str) -> Result<String, String> {
        let mut req = self.http.get(format!("{}{path}", self.base_url));
        if let Some(ref val) = self.auth_header_value() {
            req = req.header("authorization", val);
        }
        let resp = req.send().await.map_err(|e| e.to_string())?;
        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(Self::format_error(status, &body));
        }
        resp.text().await.map_err(|e| e.to_string())
    }

    pub async fn delete_json<T: serde::de::DeserializeOwned>(
        &self,
        path: &str,
    ) -> Result<T, String> {
        let mut req = self.http.delete(format!("{}{path}", self.base_url));
        if let Some(ref val) = self.auth_header_value() {
            req = req.header("authorization", val);
        }
        let resp = req.send().await.map_err(|e| e.to_string())?;
        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(Self::format_error(status, &body));
        }
        resp.json::<T>().await.map_err(|e| e.to_string())
    }

    pub async fn delete(&self, path: &str) -> Result<(), String> {
        let mut req = self.http.delete(format!("{}{path}", self.base_url));
        if let Some(ref val) = self.auth_header_value() {
            req = req.header("authorization", val);
        }
        let resp = req.send().await.map_err(|e| e.to_string())?;
        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(Self::format_error(status, &body));
        }
        Ok(())
    }

    /// Upload a binary or text file to `POST /wiki/upload` using multipart/form-data.
    ///
    /// The daemon handles format detection and conversion (binary → markitdown subprocess,
    /// text → UTF-8 read). No binary/text detection needed on the client side.
    pub async fn upload_file<T: serde::de::DeserializeOwned>(
        &self,
        path: &str,
        bytes: Vec<u8>,
        filename: String,
        model: Option<&str>,
    ) -> Result<T, String> {
        let mut form = reqwest::multipart::Form::new().part(
            "file",
            reqwest::multipart::Part::bytes(bytes).file_name(filename),
        );
        if let Some(m) = model {
            form = form.text("model", m.to_string());
        }
        let url = format!("{}{path}", self.base_url);
        let mut req = self.http.post(&url).multipart(form);
        if let Some(ref val) = self.auth_header_value() {
            req = req.header("authorization", val);
        }
        let resp = req.send().await.map_err(|e| e.to_string())?;
        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            return Err(Self::format_error(status, &body));
        }
        resp.json().await.map_err(|e| e.to_string())
    }

    pub async fn wiki_prompt_get(&self) -> Result<String, String> {
        #[derive(serde::Deserialize)]
        struct Resp {
            content: String,
        }
        let resp: Resp = self.get("/wiki/prompt").await?;
        Ok(resp.content)
    }

    pub async fn wiki_prompt_set(&self, content: &str) -> Result<(), String> {
        #[derive(serde::Serialize)]
        struct Body<'a> {
            content: &'a str,
        }
        self.put::<_, serde_json::Value>("/wiki/prompt", &Body { content })
            .await?;
        Ok(())
    }
}

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

    #[test]
    fn client_constructs_base_url() {
        let client = ZeniiClient::new("127.0.0.1", 18981, None);
        assert_eq!(client.base_url, "http://127.0.0.1:18981");

        let client = ZeniiClient::new("localhost", 9090, Some("tok".into()));
        assert_eq!(client.base_url, "http://localhost:9090");
        assert_eq!(client.auth_header_value(), Some("Bearer tok".to_string()));
    }

    #[test]
    fn client_ws_url() {
        let client = ZeniiClient::new("127.0.0.1", 18981, None);
        assert_eq!(client.ws_url("/ws/chat"), "ws://127.0.0.1:18981/ws/chat");
    }
}
