use crate::error::Error::{self, *};
use crate::tui::indent;
use crate::{
	commands::{deploy::DeployStatusResponse, run::Model},
	config::rezon_config::RezonConfig,
	tui::clean_console,
};
use clap::{crate_name, crate_version};
use colorful::{Color, Colorful};
use crates_io_api::AsyncClient;
use log::{trace, warn};
use requestty::{question::Choice as AliasChoice, Answer, Choice};
use reqwest::{Client, StatusCode};
use spinach::Spinach;
use std::env;
use std::{thread, time::Duration};
use uuid::Uuid;

pub const BASE_BFF_URL: &str = if cfg!(debug_assertions) {
	"https://rezon-api-staging-ptpvpa4e5a-uc.a.run.app"
} else {
	"https://api.rezon.ai"
};

pub async fn is_build_completed(http_client: Client, build_id: Uuid) -> (bool, String) {
	let build_status_req = http_client.get(format!("{BASE_BFF_URL}/models/deploy_status/{}", build_id)).send().await;
	let completed_build_status = vec![
		"SUCCESS",
		"FAILURE",
		"CANCELLED",
		"TIMEOUT",
		"INTERNAL_ERROR",
		"STATUS_UNKNOWN",
		"EXPIRED",
	];

	match build_status_req {
		Ok(res) => {
			// Check to make sure that our request returned a 200
			if !res.status().is_success() {
				return (false, "Error".to_owned());
			}

			match res.json::<DeployStatusResponse>().await {
				Ok(res) => {
					if completed_build_status.contains(&&res.status.as_str()) {
						return (true, res.status);
					}

					(false, res.status)
				}
				Err(_) => (false, "Error".to_owned()),
			}
		}
		Err(_) => (false, "Error".to_owned()),
	}
}

pub async fn wait_for_deployment(http_client: Client, build_id: Uuid) -> Result<(), Error> {
	// Load until the status is equal to "SUCCESS"
	while !is_build_completed(http_client.clone(), build_id).await.0 {
		println!(
			"{} Current deployment status: {}\n",
			"➜".color(Color::Purple1a).bold(),
			is_build_completed(http_client.clone(), build_id).await.1.color(Color::Purple1a).bold()
		);
		clean_console();
		// Wait to request the build status every 45 seconds to avoid a rate limit by GCP
		thread::sleep(Duration::from_secs(45));
	}

	// If the build was finished but it was not successful, return an error
	if is_build_completed(http_client, build_id).await.1 != "SUCCESS" {
		return Err(ModelDeployFailure);
	}

	Ok(())
}

pub async fn get_and_display_model_names(rezon_config: &RezonConfig) -> Result<String, Error> {
	let client = Client::new();

	let response = client
		.get(format!("{}/models/list", BASE_BFF_URL))
		.header("Authorization", rezon_config.config.api_key.clone())
		.send()
		.await?;

	if response.status() != StatusCode::OK {
		return Err(NotOk {
			response: response.text().await?,
		});
	}
	let model_list: Vec<Model> = response.json().await?;

	if model_list.is_empty() {
		return Err(ModelsNotFound);
	}

	let model_name_list: Vec<String> = model_list.iter().map(|model| model.name.clone()).collect();

	let question = requestty::Question::select("modelSelect")
		.message("Select a model:")
		.choices(
			model_name_list
				.iter()
				.map(|model_name| Choice(model_name.clone()))
				.collect::<Vec<AliasChoice<String>>>(),
		)
		.page_size(6)
		.build();

	let model_selection_request = requestty::prompt(vec![question])?;

	return match model_selection_request.get("modelSelect") {
		Some(Answer::ListItem(choice)) => Ok(choice.text.clone()),
		_ => Err(CliError(format!("Could not read your model selection!"))),
	};
}

pub fn terminal_cleanup(loader: Spinach) {
	loader.stop();
	clean_console();
}

/// Validates the model type by restricting it to only `tensorflow` and `pytorch`
pub fn validate_model_type(model_type: &str) -> Result<(), Error> {
	if model_type != "tensorflow" && model_type != "pytorch" {
		return Err(CliError(format!(
			"Error: Invalid `model_type`. Please use either `tensorflow` or `pytorch`!"
		)));
	}

	Ok(())
}

/// Gets the model file names based on the `model_type`
/// TODO: talk to Javier about this and see if we want to also support the `SavedModel` format for tensorflow or not...
pub fn get_model_file_names(is_json_and_h5: bool, model_type: &str) -> Vec<&'static str> {
	if is_json_and_h5 {
		return vec!["model.h5", "model.json"];
	}

	match model_type {
		"tensorflow" => vec!["model.h5"],
		"pytorch" => vec!["model.pt"],
		_ => unreachable!(),
	}
}

// Gets the source type based on the `model_type`
pub fn get_source_type(is_json_and_h5: bool, model_type: &str) -> &'static str {
	if is_json_and_h5 {
		return "JsonHdf5";
	}

	match model_type {
		"tensorflow" => "Hdf5",
		"pytorch" => "Pt",
		_ => unreachable!(),
	}
}

/// Check the version of the cli and notify of updates
pub async fn check_updates() {
	trace!("enter check_updates()");
	let client = AsyncClient::new(crate_name!(), std::time::Duration::from_millis(1000)).unwrap();
	let response = client.get_crate(crate_name!()).await.ok();

	// see https://doc.rust-lang.org/std/env/consts/constant.OS.html for possible values
	let install_script = match env::consts::OS {
		"macos" => "sudo curl -o /usr/local/bin/rezon -L \"https://storage.googleapis.com/cli.rezon.ai/rezon-cli/macos/rezon\"",
		"windows" => "curl -o %USERPROFILE%\\rezon.exe -L \"https://storage.googleapis.com/cli.rezon.ai/rezon-cli/windows/rezon.exe\"",
		_ => {
			warn!("Detected OS: {} is not supported.", env::consts::OS.color(Color::Purple1a));
			return;
		}
	};
	match response {
		Some(crate_info) => {
			let Some(version_info) = crate_info.versions.first() else {
				warn!("Cannot determine latest rezon cli version. If you experience an issue, check for updates.");
				return;
			};
			let (remote_version, local_version) = (&version_info.num, crate_version!());
			if remote_version == local_version {
				trace!("cli up to date");
				return;
			}
			let version_str = format!("{}", remote_version).color(Color::Purple1a).bold();
			let install_str = format!("'{}'", install_script).color(Color::Purple1a).bold();
			warn!("New Rezon CLI version is available: {version_str}. Run {install_str} to update!");
			println!("{}", indent(1));
		}
		_ => {
			warn!("Cannot determine latest rezon cli version. If you experience an issue, check for updates.")
		}
	}
	trace!("exit check_updates()")
}
