ztnet 0.1.5

ZTNet CLI — manage ZeroTier networks via ZTNet
use serde_json::{json, Value};

use crate::cli::{ConfigCommand, GlobalOpts, OutputFormat};
use crate::config::{self, Config};
use crate::context::resolve_effective_config;
use crate::error::CliError;
use crate::output;

use super::common::{
	load_config_store, opt_string, print_human_or_machine, redact_token, render_scalar,
};

pub(super) async fn run(global: &GlobalOpts, command: ConfigCommand) -> Result<(), CliError> {
	let (config_path, mut cfg) = load_config_store()?;
	let effective = resolve_effective_config(global, &cfg)?;

	match command {
		ConfigCommand::Path => {
			println!("{}", config_path.display());
			Ok(())
		}
		ConfigCommand::Get(args) => {
			let value = get_config_key(&cfg, &args.key)?;
			if matches!(effective.output, OutputFormat::Table) {
				println!("{}", render_scalar(&value));
				return Ok(());
			}
			output::print_value(&value, effective.output, global.no_color)?;
			Ok(())
		}
		ConfigCommand::Set(args) => {
			set_config_key(&mut cfg, &args.key, &args.value)?;
			config::save_config(&config_path, &cfg)?;
			if !global.quiet {
				eprintln!("Set {}.", args.key);
			}
			Ok(())
		}
		ConfigCommand::Unset(args) => {
			unset_config_key(&mut cfg, &args.key)?;
			config::save_config(&config_path, &cfg)?;
			if !global.quiet {
				eprintln!("Unset {}.", args.key);
			}
			Ok(())
		}
		ConfigCommand::List => {
			let value = json!({
				"config_path": config_path.to_string_lossy(),
				"profile": effective.profile,
				"host": effective.host,
				"token": effective.token.as_deref().map(redact_token),
				"org": effective.org,
				"network": effective.network,
				"output": effective.output.to_string(),
				"timeout": humantime::format_duration(effective.timeout).to_string(),
				"retries": effective.retries,
			});
			print_human_or_machine(&value, effective.output, global.no_color)?;
			Ok(())
		}
		ConfigCommand::Context { command } => match command {
			crate::cli::ConfigContextCommand::Show => {
				let profile_cfg = cfg.profile(&effective.profile);
				let value = json!({
					"profile": effective.profile,
					"org": profile_cfg.default_org,
					"network": profile_cfg.default_network,
				});
				print_human_or_machine(&value, effective.output, global.no_color)?;
				Ok(())
			}
			crate::cli::ConfigContextCommand::Set(args) => {
				if args.org.is_none() && args.network.is_none() {
					return Err(CliError::InvalidArgument(
						"context set requires at least one of --org or --network".to_string(),
					));
				}
				let profile_cfg = cfg.profile_mut(&effective.profile);
				if let Some(org) = args.org {
					profile_cfg.default_org = Some(org);
				}
				if let Some(network) = args.network {
					profile_cfg.default_network = Some(network);
				}
				config::save_config(&config_path, &cfg)?;
				if !global.quiet {
					eprintln!("Context updated for profile '{}'.", effective.profile);
				}
				Ok(())
			}
			crate::cli::ConfigContextCommand::Clear => {
				let profile_cfg = cfg.profile_mut(&effective.profile);
				profile_cfg.default_org = None;
				profile_cfg.default_network = None;
				config::save_config(&config_path, &cfg)?;
				if !global.quiet {
					eprintln!("Context cleared for profile '{}'.", effective.profile);
				}
				Ok(())
			}
		},
	}
}

fn get_config_key(cfg: &Config, key: &str) -> Result<Value, CliError> {
	let parts: Vec<&str> = key.split('.').collect();
	match parts.as_slice() {
		["active_profile"] => Ok(cfg
			.active_profile
			.clone()
			.map(Value::String)
			.unwrap_or(Value::Null)),
		["profiles"] => Ok(serde_json::to_value(&cfg.profiles)?),
		["profiles", profile] => Ok(serde_json::to_value(cfg.profile(profile))?),
		["profiles", profile, field] => {
			let p = cfg.profile(profile);
			let v = match *field {
				"host" => opt_string(p.host),
				"token" => opt_string(p.token),
				"default_org" => opt_string(p.default_org),
				"default_network" => opt_string(p.default_network),
				"output" => p
					.output
					.map(|f| Value::String(f.to_string()))
					.unwrap_or(Value::Null),
				"timeout" => opt_string(p.timeout),
				"retries" => p
					.retries
					.map(|n| Value::Number(n.into()))
					.unwrap_or(Value::Null),
				_ => {
					return Err(CliError::InvalidArgument(format!(
						"unsupported key: {key}"
					)))
				}
			};
			Ok(v)
		}
		_ => Err(CliError::InvalidArgument(format!("unsupported key: {key}"))),
	}
}

fn set_config_key(cfg: &mut Config, key: &str, value: &str) -> Result<(), CliError> {
	let parts: Vec<&str> = key.split('.').collect();
	match parts.as_slice() {
		["active_profile"] => {
			cfg.active_profile = Some(value.to_string());
			Ok(())
		}
		["profiles", profile, field] => {
			let p = cfg.profile_mut(profile);
			match *field {
				"host" => p.host = Some(value.to_string()),
				"token" => p.token = Some(value.to_string()),
				"default_org" => p.default_org = Some(value.to_string()),
				"default_network" => p.default_network = Some(value.to_string()),
				"output" => {
					p.output = Some(parse_output_format(value)?);
				}
				"timeout" => {
					humantime::parse_duration(value).map_err(|_| {
						CliError::InvalidArgument(format!("invalid timeout value: {value}"))
					})?;
					p.timeout = Some(value.to_string());
				}
				"retries" => {
					let n = value.parse::<u32>().map_err(|_| {
						CliError::InvalidArgument(format!("invalid retries value: {value}"))
					})?;
					p.retries = Some(n);
				}
				_ => {
					return Err(CliError::InvalidArgument(format!(
						"unsupported key: {key}"
					)))
				}
			}
			Ok(())
		}
		_ => Err(CliError::InvalidArgument(format!("unsupported key: {key}"))),
	}
}

fn unset_config_key(cfg: &mut Config, key: &str) -> Result<(), CliError> {
	let parts: Vec<&str> = key.split('.').collect();
	match parts.as_slice() {
		["active_profile"] => {
			cfg.active_profile = None;
			Ok(())
		}
		["profiles", profile, field] => {
			let p = cfg.profile_mut(profile);
			match *field {
				"host" => p.host = None,
				"token" => p.token = None,
				"default_org" => p.default_org = None,
				"default_network" => p.default_network = None,
				"output" => p.output = None,
				"timeout" => p.timeout = None,
				"retries" => p.retries = None,
				_ => {
					return Err(CliError::InvalidArgument(format!(
						"unsupported key: {key}"
					)))
				}
			}
			Ok(())
		}
		_ => Err(CliError::InvalidArgument(format!("unsupported key: {key}"))),
	}
}

fn parse_output_format(value: &str) -> Result<crate::cli::OutputFormat, CliError> {
	let normalized = value.trim().to_ascii_lowercase();
	match normalized.as_str() {
		"table" => Ok(crate::cli::OutputFormat::Table),
		"json" => Ok(crate::cli::OutputFormat::Json),
		"yaml" | "yml" => Ok(crate::cli::OutputFormat::Yaml),
		"raw" => Ok(crate::cli::OutputFormat::Raw),
		_ => Err(CliError::InvalidArgument(format!(
			"invalid output format: {value}"
		))),
	}
}