use std::collections::BTreeMap;
use std::io::{ErrorKind, Read, Write};
use std::time::Duration;

use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Number, Value};

use crate::fn_common::FnResult;

#[derive(Clone, Debug, Serialize, Deserialize)]
struct FetchParams {
    url: String,
    options: Option<FetchOptions>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct FetchOptions {
    method: Option<String>,
    headers: Option<BTreeMap<String, String>>,
    body: Option<String>,
}

pub fn do_fetch(params: &str) -> FnResult {
    let fetch_params: FetchParams = match serde_json::from_str(params) {
        Err(e) => return FnResult::fail(format!("Fetch param parse error: {}, raw params: {}", e, params)),
        Ok(params) => params,
    };
    if fetch_params.url.is_empty() {
        return FnResult::fail("Param url cannot be empty");
    }
    let client = match reqwest::blocking::Client::builder()
        .timeout(Duration::from_secs(8))
        .connect_timeout(Duration::from_secs(3))
        // .proxy(reqwest::Proxy::all("http://127.0.0.1:1086").expect("to proxy failed"))
        .build() {
        Err(e) => return FnResult::fail(format!("Create client failed: {}", e)),
        Ok(client) => client,
    };
    let method = fetch_params.options.as_ref().map(|options| options.method.as_ref().map(|m| m.to_uppercase())).flatten();
    let method_str = method.as_ref().map(|m| m.as_str()).unwrap_or_else(|| "GET");
    let request_method = match method_str {
        "GET" => Method::GET,
        "POST" => Method::POST,
        m => return FnResult::fail(format!("Unsupported method: {}", m)),
    };
    let mut request_builder = client.request(request_method.clone(), &fetch_params.url);
    let mut has_user_agent = false;
    if let Some(options) = &fetch_params.options {
        if let Some(headers) = &options.headers {
            for (k, v) in headers {
                let k = k.to_lowercase();
                if k == "user-agent" {
                    has_user_agent = true;
                }
                request_builder = request_builder.header(k, v.to_string());
            }
        }
    }
    if !has_user_agent {
        request_builder = request_builder.header("User-Agent", "JavaScriptSandboxContainer/0.1");
    }
    if let Some(options) = &fetch_params.options {
        if let Some(body) = &options.body {
            if Method::POST == request_method {
                let body = reqwest::blocking::Body::from(body.to_string());
                request_builder = request_builder.body(body);
            }
        }
    }

    let mut fetch_response = match request_builder.send() {
        Err(e) => return FnResult::fail(format!("Send request failed: {}", e)),
        Ok(fetch_response) => fetch_response,
    };

    let status = fetch_response.status().as_u16();
    let headers = fetch_response.headers().clone();

    let mut body_buff = Vec::new();
    let mut buff = [0_u8; 1024];
    loop {
        match fetch_response.read(&mut buff) {
            Err(e) if e.kind() == ErrorKind::Interrupted => continue,
            Err(e) => return FnResult::fail(format!("Fetch response failed: {}", e)),
            Ok(len) => if len == 0 {
                break;
            } else {
                if let Err(e) = body_buff.write_all(&buff[0..len]) {
                    return FnResult::fail(format!("Fetch response failed: {}", e));
                }
                if body_buff.len() > 1024 * 1024 {
                    return FnResult::fail(format!("Fetch response too large, {} more than 1MB", body_buff.len()));
                }
            }
        }
    }

    let mut result_map = Map::new();
    result_map.insert("status".to_string(), Value::Number(Number::from(status)));
    let mut headers_map = Map::new();
    for (k, v) in headers {
        let header_key = k.map(|n| n.as_str().to_string()).unwrap_or_else(|| "_".to_string());
        let header_value = match v.to_str() {
            Err(_) => continue,
            Ok(v) => v.to_string(),
        };
        headers_map.insert(header_key, Value::String(header_value));
    }
    result_map.insert("headers".to_string(), Value::Object(headers_map));
    result_map.insert("body".to_string(), Value::String(String::from_utf8_lossy(&body_buff).to_string()));

    FnResult::success(format!("{}", Value::Object(result_map)))
}
