1use std::collections::BTreeMap;
2use std::io::{self, BufRead, Write};
3
4use crate::client::accounting::AccountingClient;
5use crate::config::{AdminEntry, Config};
6use crate::error::YukiError;
7
8fn safe_name(name: &str) -> String {
12 name.chars()
13 .map(|c| {
14 if c.is_alphanumeric() {
15 c.to_ascii_lowercase()
16 } else {
17 '_'
18 }
19 })
20 .collect()
21}
22
23pub async fn run(api_key: Option<&str>, default_admin: Option<&str>) -> Result<(), YukiError> {
24 let stdin = io::stdin();
25 let path = Config::default_path();
26
27 if let (Some(key), None) = (api_key, default_admin)
29 && let Ok(mut config) = Config::load_from(&path)
30 {
31 let key = key.trim().to_string();
32 if key.is_empty() {
33 return Err(YukiError::Config("API key cannot be empty".to_string()));
34 }
35
36 eprintln!("Verifying new API key...");
37 let mut client = AccountingClient::new();
38 client.authenticate(&key).await?;
39
40 config.api_key = key;
41 config.save_to(&path)?;
42 eprintln!("API key updated in {}", path.display());
43 return Ok(());
44 }
45
46 let api_key = match api_key {
47 Some(k) => k.trim().to_string(),
48 None => {
49 eprint!("Yuki API key: ");
50 io::stderr().flush().ok();
51 stdin
52 .lock()
53 .lines()
54 .next()
55 .and_then(|l| l.ok())
56 .map(|l| l.trim().to_string())
57 .unwrap_or_default()
58 }
59 };
60
61 if api_key.is_empty() {
62 return Err(YukiError::Config("API key cannot be empty".to_string()));
63 }
64
65 eprintln!("Authenticating...");
66 let mut client = AccountingClient::new();
67 client.authenticate(&api_key).await?;
68
69 eprintln!("Fetching administrations...");
70 let admins = client.administrations().await?;
71
72 if admins.is_empty() {
73 return Err(YukiError::NotFound(
74 "no administrations found for this API key".to_string(),
75 ));
76 }
77
78 eprintln!("Found {} administration(s):", admins.len());
79 for (i, a) in admins.iter().enumerate() {
80 eprintln!(" [{}] {}", i + 1, a.name);
81 }
82
83 let default_name = if let Some(name) = default_admin {
84 let key = safe_name(name);
86 if !admins.iter().any(|a| safe_name(&a.name) == key) {
87 return Err(YukiError::Config(format!(
88 "administration not found: {name}"
89 )));
90 }
91 eprintln!("Using \"{name}\" as the default administration.");
92 key
93 } else if admins.len() == 1 {
94 eprintln!(
95 "Using \"{}\" as the default administration.",
96 admins[0].name
97 );
98 safe_name(&admins[0].name)
99 } else {
100 eprint!("Select default administration [1]: ");
101 io::stderr().flush().ok();
102
103 let choice = stdin
104 .lock()
105 .lines()
106 .next()
107 .and_then(|l| l.ok())
108 .map(|l| l.trim().to_string())
109 .unwrap_or_default();
110
111 let idx: usize = if choice.is_empty() {
112 1
113 } else {
114 choice
115 .parse::<usize>()
116 .map_err(|_| YukiError::Config(format!("invalid selection: {choice}")))?
117 };
118
119 if idx == 0 || idx > admins.len() {
120 return Err(YukiError::Config(format!("selection out of range: {idx}")));
121 }
122 safe_name(&admins[idx - 1].name)
123 };
124
125 let administrations: BTreeMap<String, AdminEntry> = admins
127 .iter()
128 .map(|a| {
129 (
130 safe_name(&a.name),
131 AdminEntry {
132 domain_id: a.domain_id.clone(),
133 admin_id: a.id.clone(),
134 },
135 )
136 })
137 .collect();
138
139 let unmatched_ignore = Config::load_from(&path)
141 .map(|c| c.unmatched_ignore)
142 .unwrap_or_default();
143
144 let config = Config {
145 api_key,
146 default_admin: default_name.clone(),
147 administrations,
148 unmatched_ignore,
149 };
150
151 config.save_to(&path)?;
152
153 eprintln!("Configuration saved to {}", path.display());
154 eprintln!("Default administration: {default_name}");
155
156 Ok(())
157}