1use std::collections::BTreeMap;
2use std::io::{self, BufRead, Write};
3
4use owo_colors::OwoColorize;
5
6use crate::client::accounting::AccountingClient;
7use crate::config::{AdminEntry, Config};
8use crate::error::YukiError;
9use crate::output::is_tty;
10
11fn sym_ok() -> String {
12 if is_tty() {
13 "✔".green().to_string()
14 } else {
15 "✔".to_owned()
16 }
17}
18
19fn sym_fail() -> String {
20 if is_tty() {
21 "✖".red().to_string()
22 } else {
23 "✖".to_owned()
24 }
25}
26
27fn bold(s: &str) -> String {
28 if is_tty() {
29 s.bold().to_string()
30 } else {
31 s.to_owned()
32 }
33}
34
35fn dim(s: &str) -> String {
36 if is_tty() {
37 s.dimmed().to_string()
38 } else {
39 s.to_owned()
40 }
41}
42
43fn safe_name(name: &str) -> String {
47 name.chars()
48 .map(|c| {
49 if c.is_alphanumeric() {
50 c.to_ascii_lowercase()
51 } else {
52 '_'
53 }
54 })
55 .collect()
56}
57
58pub async fn run(api_key: Option<&str>, default_admin: Option<&str>) -> Result<(), YukiError> {
59 let stdin = io::stdin();
60 let path = Config::default_path();
61
62 if let (Some(key), None) = (api_key, default_admin)
64 && let Ok(mut config) = Config::load_from(&path)
65 {
66 let key = key.trim().to_string();
67 if key.is_empty() {
68 return Err(YukiError::Config("API key cannot be empty".to_string()));
69 }
70
71 eprintln!("Verifying new API key...");
72 let mut client = AccountingClient::new();
73 client.authenticate(&key).await?;
74
75 config.api_key = key;
76 config.save_to(&path)?;
77 eprintln!("{} API key updated in {}", sym_ok(), path.display());
78 return Ok(());
79 }
80
81 let api_key = match api_key {
82 Some(k) => k.trim().to_string(),
83 None => {
84 eprintln!(" {} Yuki Portal → Settings → API keys", dim("→"));
85 eprint!("Yuki API key: ");
86 io::stderr().flush().ok();
87 stdin
88 .lock()
89 .lines()
90 .next()
91 .and_then(|l| l.ok())
92 .map(|l| l.trim().to_string())
93 .unwrap_or_default()
94 }
95 };
96
97 if api_key.is_empty() {
98 return Err(YukiError::Config("API key cannot be empty".to_string()));
99 }
100
101 eprint!("Authenticating...");
102 io::stderr().flush().ok();
103 let mut client = AccountingClient::new();
104 match client.authenticate(&api_key).await {
105 Ok(_) => eprintln!(" {}", sym_ok()),
106 Err(e) => {
107 eprintln!(" {}", sym_fail());
108 return Err(e);
109 }
110 }
111
112 eprint!("Fetching administrations...");
113 io::stderr().flush().ok();
114 let admins = match client.administrations().await {
115 Ok(admins) => {
116 eprintln!(" {}", sym_ok());
117 admins
118 }
119 Err(e) => {
120 eprintln!(" {}", sym_fail());
121 return Err(e);
122 }
123 };
124
125 if admins.is_empty() {
126 return Err(YukiError::NotFound(
127 "no administrations found for this API key".to_string(),
128 ));
129 }
130
131 eprintln!("Found {} administration(s):", admins.len());
132 for (i, a) in admins.iter().enumerate() {
133 eprintln!(" [{}] {}", i + 1, a.name);
134 }
135
136 let default_name = if let Some(name) = default_admin {
137 let key = safe_name(name);
139 if !admins.iter().any(|a| safe_name(&a.name) == key) {
140 return Err(YukiError::Config(format!(
141 "administration not found: {name}"
142 )));
143 }
144 eprintln!("Using \"{name}\" as the default administration.");
145 key
146 } else if admins.len() == 1 {
147 eprintln!(
148 "Using \"{}\" as the default administration.",
149 admins[0].name
150 );
151 safe_name(&admins[0].name)
152 } else {
153 eprint!("Select default administration [1]: ");
154 io::stderr().flush().ok();
155
156 let choice = stdin
157 .lock()
158 .lines()
159 .next()
160 .and_then(|l| l.ok())
161 .map(|l| l.trim().to_string())
162 .unwrap_or_default();
163
164 let idx: usize = if choice.is_empty() {
165 1
166 } else {
167 choice
168 .parse::<usize>()
169 .map_err(|_| YukiError::Config(format!("invalid selection: {choice}")))?
170 };
171
172 if idx == 0 || idx > admins.len() {
173 return Err(YukiError::Config(format!("selection out of range: {idx}")));
174 }
175 safe_name(&admins[idx - 1].name)
176 };
177
178 let administrations: BTreeMap<String, AdminEntry> = admins
180 .iter()
181 .map(|a| {
182 (
183 safe_name(&a.name),
184 AdminEntry {
185 domain_id: a.domain_id.clone(),
186 admin_id: a.id.clone(),
187 },
188 )
189 })
190 .collect();
191
192 let unmatched_ignore = Config::load_from(&path)
194 .map(|c| c.unmatched_ignore)
195 .unwrap_or_default();
196
197 let config = Config {
198 api_key,
199 default_admin: default_name.clone(),
200 administrations,
201 unmatched_ignore,
202 };
203
204 config.save_to(&path)?;
205
206 eprintln!();
207 eprintln!("{} Configuration saved to {}", sym_ok(), path.display());
208 eprintln!();
209 eprintln!("{}:", bold("Next steps"));
210 eprintln!(
211 " yuki documents list {}",
212 dim("# list archived documents")
213 );
214 eprintln!(" yuki contacts list {}", dim("# list contacts"));
215 eprintln!(" yuki invoices list {}", dim("# list invoices"));
216 eprintln!(" yuki completions zsh {}", dim("# shell completions"));
217 eprintln!();
218
219 Ok(())
220}