use crate::error::Error::{self, *};
use crate::util::validate_model_type;
use serde::Deserialize;
use std::{env, fs};

#[derive(Debug, Deserialize, Clone)]
pub struct Model {
	pub h5_without_config_path: Option<String>,
	pub json_config_path: Option<String>,
	pub model_type: String,
	pub model_path: Option<String>,
	pub parameters: Vec<String>,
}

#[derive(Debug, Deserialize, Clone)]
pub struct Config {
	pub api_key: String,
}

#[derive(Debug, Deserialize, Clone)]
pub struct RezonConfig {
	pub config: Config,
	pub model: Model,
}

impl RezonConfig {
	/**
	 * Note: A side effect of this function is that it may find a rezon.toml in a parent directory when running
	 * from a directory that has not been initialized with rezon. Advisable to adhere to the principle of
	 * least astonishment and document this behavior.
	 */
	pub fn read_config() -> Result<RezonConfig, Error> {
		let mut path = match env::current_dir() {
			Ok(dir) => dir,
			Err(e) => {
				return Err(Io {
					path: std::path::PathBuf::from("current directory"),
					source: e,
				})
			}
		};
		loop {
			let candidate = path.join("rezon.toml");
			if candidate.exists() {
				let contents = match fs::read_to_string(&candidate) {
					Ok(contents) => contents,
					Err(e) => return Err(Io { path: candidate, source: e }),
				};
				let rezon_config: toml::Value = toml::from_str(&contents)?;

				return parse_rezon_config(rezon_config, contents);
			}
			if !path.pop() {
				break;
			}
		}

		return Err(CliError(format!("Could not find a valid config file in the current working directory. Please make sure you are in a project scaffolded with the Rezon CLI.")));
	}
}

fn parse_rezon_config(config: toml::Value, config_contents: String) -> Result<RezonConfig, Error> {
	// Before doing anything we want to validate that there are no fields inside of the config file that are invalid (aka not in our base RezonConfig struct)
	if let Some(table) = config.as_table() {
		validate_config_fields(table.clone())?;
	}

	// Now that we know the config file is valid, we can safely deserialize it into our RezonConfig struct
	let rezon_config: RezonConfig = toml::from_str(&config_contents)?;

	// Validate that the model type is either `tensorflow` or `pytorch`
	validate_model_type(&rezon_config.model.model_type)?;

	// If we got to this point, we can safely return the RezonConfig struct because we know it is valid now
	Ok(rezon_config)
}

/// Function for validating that the fields in the config file are all valid (makes sure that users dont specify fields that arent in our base schema)
fn validate_config_fields(table: toml::value::Table) -> Result<(), Error> {
	let parent_keys = vec!["config", "model"];

	let config_keys = vec!["api_key"];

	let model_keys = vec!["h5_without_config_path", "json_config_path", "model_type", "parameters", "model_path"];

	let keys = vec![config_keys, model_keys];

	// First, check to make sure that the parent keys and then parse their child keys...
	for (index, key) in table.iter().enumerate() {
		// Make sure there are no keys/fields in the toml file that we have not clearly defined in the config schema
		let field_keys = key.1.as_table().unwrap().keys().collect::<Vec<&String>>();

		if field_keys.contains(&&"json_config_path".to_owned()) && field_keys.contains(&&"model_path".to_owned()) {
			return Err(InvalidConfig("json_config_path".to_string(), "model_path".to_string()));
		}

		if field_keys.contains(&&"h5_without_config_path".to_owned()) && field_keys.contains(&&"model_path".to_owned()) {
			return Err(InvalidConfig("h5_without_config_path".to_string(), "model_path".to_string()));
		}

		for nested_key in field_keys {
			if !keys[index].contains(&nested_key.as_str()) {
				return Err(CliError(format!("Invalid field found in rezon config file `{}`", nested_key)));
			}
		}

		if !parent_keys.contains(&key.0.as_str()) {
			return Err(CliError(format!("Invalid field found in rezon config file `{}`", key.0)));
		}
	}

	Ok(())
}
