use std::fs::{self, File};
use std::io::{BufReader, Write, copy};
use std::path::PathBuf;

pub use anyhow::{Context, Result, anyhow};
use indicatif::{ProgressBar, ProgressStyle};
use semver::Version;
use serde_json::Value;
use ureq;

use crate::cache::parse_version;
use crate::{get_user_confirmation, validate_version_format};

pub fn get_stable_version(url: &str) -> Result<Version> {
    let version_str: String = ureq::get(url)
        .call()
        .with_context(|| format!("Korrect failed to fetch version from '{}'", url))?
        .body_mut()
        .read_to_string()?
        .trim()
        .to_string();
    log::debug!("Url: [{url}], Version: [{version_str}]");
    validate_version_format(&version_str)?;
    let version = parse_version(&version_str)?;
    Ok(version)
}

pub fn get_helm_release_versions(url: &str) -> Result<Vec<Version>> {
    let response: String = ureq::get(url)
        .call()
        .with_context(|| format!("Korrect failed to fetch helm versions from '{}'", url))?
        .body_mut()
        .read_to_string()?
        .trim()
        .to_string();
    let mut tags: Vec<String> = Vec::new();
    let mut versions = Vec::new();

    log::debug!("{}", response);
    let json: Vec<Value> = serde_json::from_str(&response).unwrap();
    json.into_iter()
        .for_each(|x| tags.push(x["name"].to_string()));
    for tag in tags {
        if let Some(version) = parse_version(&tag).ok() {
            versions.push(version)
        }
    }

    Ok(versions)
}

pub fn get_latest_minor_helm_version(minor: u64, versions: Vec<Version>) -> Option<Version> {
    let max_patch = versions
        .iter()
        .filter_map(|version| {
            if version.minor == minor {
                Some(version)
            } else {
                None
            }
        })
        .max_by(|a, b| a.patch.cmp(&b.patch));
    match max_patch {
        Some(version) => Some(version.clone()),
        None => None,
    }
}

pub fn download_file_with_progress(url: &str, output_path: &PathBuf) -> Result<()> {
    //TODO if configuration
    if false {
        // if metered::is_connection_metered() {
        if !get_user_confirmation("Connection seems metered. Do you wish to download anyway?") {
            return Ok(());
        }
    }

    let response = ureq::get(url)
        .call()
        .with_context(|| format!("Korrect failed to download from '{}'", url))?;
    let total_size = response.body().content_length().unwrap_or(0);

    // Create a progress bar
    let pb = ProgressBar::new(total_size);
    // pb.set_style(ProgressStyle::default_spinner());
    pb.set_style(ProgressStyle::default_bar().template("{msg} {spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")?
    .progress_chars("#>-"));
    pb.set_message(format!("Downloading {}", &url));

    let mut dest = File::create(output_path)?;
    let progress_reader = pb.wrap_read(BufReader::new(response.into_body().into_reader()));
    copy(&mut BufReader::new(progress_reader), &mut dest)?;
    pb.finish_with_message("Download complete");

    dest.flush()?;

    //FIXME We must set permissions this AFTER any post-processing is performed (i.e. in the case of helm)
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = dest.metadata()?.permissions();
        perms.set_mode(0o755);
        fs::set_permissions(output_path, perms)?;
    }
    Ok(())
}

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

    use tempfile::TempDir;

    use crate::{detect_cpu_arch, detect_os, test_utils::init_logger};

    #[test]
    fn test_200_get_stable_version() {
        let body = "v1.20.4";
        let version = parse_version(body).unwrap();
        let mut server = mockito::Server::new();
        let url = server.url();
        server
            .mock("GET", "/")
            .with_status(200)
            .with_header("content-type", "text/plain")
            .with_body(body)
            .create();

        let result = get_stable_version(&url);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), version);
    }

    #[test]
    fn test_400_get_stable_version() {
        let body = "v1.20.4";
        let mut server = mockito::Server::new();
        let url = server.url();
        server
            .mock("GET", "/")
            .with_status(400)
            .with_header("content-type", "text/plain")
            .with_body(body)
            .create();

        // Call the function and assert that it returns the correct version
        let result = get_stable_version(&url);
        assert!(result.is_err());
    }

    #[test]
    fn test_200_get_invalid_stable_version() {
        // for body in ["an invalid version string", "hello"] {
        for body in ["an invalid version string", "v1.2.X", "1.2.3"] {
            log::debug!("Body is: {}", body);
            let mut server = mockito::Server::new();
            let url = server.url();
            server
                .mock("GET", "/")
                .with_status(200)
                .with_header("content-type", "text/plain")
                .with_body(body)
                .create();

            // Call the function and assert that it returns an error
            let version = get_stable_version(&url).unwrap_err();
            assert!(
                matches!(version, err if err.to_string() == format!("Invalid version format from upstream: {}, expected vX.Y.Z pattern", body))
            );
        }
    }

    #[test]
    fn test_200_download_file_with_progress() {
        init_logger();
        let mut server = mockito::Server::new();
        let base_url = server.url();
        let body = b"v1.3.0";
        let version = "v1.3.0";
        let os = detect_os();
        let cpu = detect_cpu_arch();

        let path = format!("/release/{}/bin/{}/{}/kubectl", version, os, cpu);
        server
            .mock("GET", path.as_str())
            .with_status(200)
            .with_header("content-type", "text/plain")
            .with_header("content-length", body.len().to_string().as_str())
            .with_body(body)
            .create();

        let temp_dir = TempDir::new().unwrap();
        let output_path = temp_dir.path().join("test-file");
        let url = format!("{base_url}{path}");
        log::debug!("FULL URL: {url}");
        let result = download_file_with_progress(&url, &output_path);
        log::debug!("RESULT {:?}", result);
        assert!(result.is_ok());
        assert!(output_path.exists());
        assert_eq!(std::fs::read(&output_path).unwrap(), body);
    }

    #[test]
    fn test_200_download_file_with_incorrect_length_progress() {
        init_logger();
        let mut server = mockito::Server::new();
        let base_url = server.url();
        let body = b"v1.3.0";
        let version = "v1.3.0";
        let os = detect_os();
        let cpu = detect_cpu_arch();

        let path = format!("/release/{}/bin/{}/{}/kubectl", version, os, cpu);
        server
            .mock("GET", path.as_str())
            .with_status(200)
            .with_header("content-type", "text/plain")
            .with_body(body)
            .create();

        let temp_dir = TempDir::new().unwrap();
        let output_path = temp_dir.path().join("test-file");
        let url = format!("{base_url}{path}");
        log::debug!("FULL URL: {url}");
        let result = download_file_with_progress(&url, &output_path);
        log::debug!("RESULT {:?}", result);
        assert!(result.is_ok());
        assert!(output_path.exists());
        assert_eq!(std::fs::read(&output_path).unwrap(), body);
    }

    #[test]
    fn test_400_download_file_with_progress() {
        init_logger();
        let mut server = mockito::Server::new();
        let url = server.url();
        let test_file_content = b"v1.3.0";

        server
            .mock("GET", "/test-file")
            .with_status(400)
            .with_header("content-type", "text/plain")
            .with_body(test_file_content)
            .create();

        let temp_dir = TempDir::new().unwrap();
        let output_path = temp_dir.path().join("test-file");

        let url = format!("{url}/test-file");
        let result = download_file_with_progress(&url, &output_path);

        assert!(result.is_err());
        assert!(!output_path.exists());
    }

    #[test]
    fn test_200_helm_release_tags() {
        init_logger();
        let mut server = mockito::Server::new();
        let base_url = server.url();
        let path = "/repos/helm/helm/tags";
        let url = format!("{}{}", base_url, path);
        let test_file_content =
            r#"[{"name": "v3.17.0"}, {"name": "v3.18.1"}, {"name": "v3.17.1"}]"#;

        server
            .mock("GET", path)
            .with_status(200)
            .with_header("content-type", "application/json; charset=utf-8")
            .with_body(test_file_content)
            .create();

        let versions = get_helm_release_versions(&url).expect("get_helm_release_versions failed");

        assert!(versions.len() == 3)
    }
}
