Documentation
// use reqwest::blocking::multipart;
use cf::json::FastJson;
use futures_util::StreamExt;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use std::cmp::min;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use tokio;

async fn download_file(client: &Client, url: &str, path: &str) -> Result<(), String> {
    // Download file from <url> to <path>
    let res = client
        .get(url)
        .send()
        .await
        .or(Err(format!("Failed to GET from '{}'", &url)))?;
    let total_size = res
        .content_length()
        .ok_or(format!("Failed to get content length from '{}'", &url))?;

    // Indicatif setup
    let pb = ProgressBar::new(total_size);
    pb.set_style(ProgressStyle::default_bar()
        .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
        .progress_chars("#>-"));
    pb.set_message(&format!("Downloading {} to {}", url, path));

    // download chunks
    let mut file = File::create(path).or(Err(format!("Failed to create file '{}'", path)))?;
    let mut downloaded: u64 = 0;
    let mut stream = res.bytes_stream();

    while let Some(item) = stream.next().await {
        let chunk = item.or(Err(format!("Error while downloading file")))?;
        file.write(&chunk)
            .or(Err(format!("Error while writing to file")))?;
        let new = min(downloaded + (chunk.len() as u64), total_size);
        downloaded = new;
        pb.set_position(new);
    }

    pb.finish_with_message(&format!("Downloaded {} to {}", url, path));
    Ok(())
}

pub fn download(url: &str, path: &str) {
    let client = reqwest::Client::new();
    let rt = tokio::runtime::Runtime::new().unwrap();
    let future = download_file(&client, url, path);
    let resp = rt.block_on(future);
    match resp {
        Err(e) => println!("{:?}", e),
        _ => (),
    }
}

fn trim_newline(s: &str) -> String {
    s.trim_end_matches('\n').to_string()
}

fn decode_account(account: &str) -> String {
    // Decode account from base64 encoded string.
    let s = String::from_utf8(base64::decode(account).unwrap()).expect("base64 decode failed");
    trim_newline(&s)
}

pub struct RedisSync {
    client: redis::Client,
    key: String,
}

impl RedisSync {
    pub fn new() -> Self {
        let (host, password) = (
            decode_account("YWxpLmRkb3QuY2MK"),
            decode_account("cTROUldxQVUrdDZnSlN4NVVPSkJLMUpMSHJmYUR6dmlFYTYyQkgrY21CND0K"),
        );
        let client = redis::Client::open(format!("redis://:{}@{}:13679", password, host))
            .expect("Failed to connect to redis");
        let key = "redis_sync".to_string();
        RedisSync { client, key }
    }

    pub fn from_server(&self) -> std::io::Result<()> {
        if let Ok(mut con) = self.client.get_connection() {
            if let Ok(resp) = redis::cmd("GET")
                .arg("redis_sync")
                .query::<String>(&mut con)
            {
                let hm = FastJson::new().from_str(&resp).unwrap();
                for (k, v) in hm.iter() {
                    println!("sync from server {}", k);
                    if let Ok(decoded_content) = base64::decode(v.replace(" ", "")) {
                        std::fs::File::create(format!("{}", k))?.write_all(&decoded_content)?
                    }
                }
            }
        }

        Ok(())
    }

    pub fn to_server(&self, files: Vec<&str>) {
        let mut json_string = String::new();
        json_string.push_str("{");
        for path in files.iter() {
            let content = std::fs::read(path).unwrap();
            let encoded_content = base64::encode(&content);
            let mut hm = HashMap::new();
            hm.insert(path.to_string(), encoded_content.to_string());
            json_string.push_str(&format!("\"{}\":\"{}\",", path, encoded_content));
        }
        json_string.pop();
        json_string.push_str("}");
        match self.client.get_connection() {
            Ok(mut con) => {
                let _ = redis::cmd("SET")
                    .arg("redis_sync")
                    .arg(json_string)
                    .query::<()>(&mut con);
                println!("sync to server {:?} SUCCESS", files);
            }
            Err(e) => println!("{:?}", e),
        }
    }
}