1use crate::platform::api::client::PlatformApiClient;
7use crate::platform::api::types::{CloudProvider, ClusterSummary, Environment};
8use crate::wizard::cloud_provider_data::{get_default_region, get_regions_for_provider};
9use crate::wizard::provider_selection::get_provider_deployment_statuses;
10use crate::wizard::render::{display_step_header, wizard_render_config};
11use colored::Colorize;
12use inquire::{InquireError, MultiSelect, Select, Text};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
19enum EnvironmentType {
20 Cluster,
21 Cloud,
22}
23
24impl EnvironmentType {
25 fn as_str(&self) -> &'static str {
26 match self {
27 EnvironmentType::Cluster => "cluster",
28 EnvironmentType::Cloud => "cloud",
29 }
30 }
31
32 fn display_name(&self) -> &'static str {
33 match self {
34 EnvironmentType::Cluster => "Kubernetes",
35 EnvironmentType::Cloud => "Cloud Runner",
36 }
37 }
38}
39
40#[derive(Debug)]
42pub enum EnvironmentCreationResult {
43 Created(Environment),
45 Cancelled,
47 Error(String),
49}
50
51pub async fn create_environment_wizard(
58 client: &PlatformApiClient,
59 project_id: &str,
60) -> EnvironmentCreationResult {
61 display_step_header(
62 1,
63 "Create Environment",
64 "Set up a new deployment environment for your project.",
65 );
66
67 let name = match Text::new("Environment name:")
69 .with_placeholder("e.g., production, staging, development")
70 .with_help_message("Choose a descriptive name for this environment")
71 .prompt()
72 {
73 Ok(name) => {
74 if name.trim().is_empty() {
75 println!("\n{}", "Environment name cannot be empty.".red());
76 return EnvironmentCreationResult::Cancelled;
77 }
78 name.trim().to_string()
79 }
80 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
81 println!("\n{}", "Wizard cancelled.".dimmed());
82 return EnvironmentCreationResult::Cancelled;
83 }
84 Err(e) => {
85 return EnvironmentCreationResult::Error(format!("Input error: {}", e));
86 }
87 };
88
89 display_step_header(
91 2,
92 "Select Target Type",
93 "Choose how this environment will deploy services.",
94 );
95
96 let target_options = vec![
97 format!(
98 "{} {}",
99 "Cloud Runner".cyan(),
100 "Fully managed, auto-scaling containers".dimmed()
101 ),
102 format!(
103 "{} {}",
104 "Kubernetes".cyan(),
105 "Deploy to your own K8s cluster".dimmed()
106 ),
107 ];
108
109 let target_selection = Select::new("Select target type:", target_options)
110 .with_render_config(wizard_render_config())
111 .with_help_message("Cloud Runner: serverless, Kubernetes: full control")
112 .prompt();
113
114 let env_type = match target_selection {
115 Ok(answer) => {
116 if answer.contains("Cloud Runner") {
117 EnvironmentType::Cloud
118 } else {
119 EnvironmentType::Cluster
120 }
121 }
122 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
123 println!("\n{}", "Wizard cancelled.".dimmed());
124 return EnvironmentCreationResult::Cancelled;
125 }
126 Err(e) => {
127 return EnvironmentCreationResult::Error(format!("Selection error: {}", e));
128 }
129 };
130
131 println!(
132 "\n{} Target: {}",
133 "✓".green(),
134 env_type.display_name().bold()
135 );
136
137 let cluster_id = if env_type == EnvironmentType::Cluster {
139 match select_cluster_for_env(client, project_id).await {
140 ClusterSelectionResult::Selected(id) => Some(id),
141 ClusterSelectionResult::NoClusters => {
142 println!(
143 "\n{}",
144 "No Kubernetes clusters available. Please provision a cluster first.".red()
145 );
146 return EnvironmentCreationResult::Cancelled;
147 }
148 ClusterSelectionResult::Cancelled => {
149 return EnvironmentCreationResult::Cancelled;
150 }
151 ClusterSelectionResult::Error(e) => {
152 return EnvironmentCreationResult::Error(e);
153 }
154 }
155 } else {
156 None
157 };
158
159 let provider_regions = if env_type == EnvironmentType::Cloud {
161 select_provider_regions()
162 } else {
163 None
164 };
165
166 println!("\n{}", "Creating environment...".dimmed());
168
169 match client
170 .create_environment(
171 project_id,
172 &name,
173 env_type.as_str(),
174 cluster_id.as_deref(),
175 provider_regions.as_ref(),
176 )
177 .await
178 {
179 Ok(env) => {
180 println!(
181 "\n{} Environment {} created successfully!",
182 "✓".green().bold(),
183 env.name.bold()
184 );
185 println!(" ID: {}", env.id.dimmed());
186 println!(" Type: {}", env.environment_type);
187 if let Some(cid) = &env.cluster_id {
188 println!(" Cluster: {}", cid);
189 }
190 EnvironmentCreationResult::Created(env)
191 }
192 Err(e) => EnvironmentCreationResult::Error(format!("Failed to create environment: {}", e)),
193 }
194}
195
196enum ClusterSelectionResult {
198 Selected(String),
199 NoClusters,
200 Cancelled,
201 Error(String),
202}
203
204async fn select_cluster_for_env(
206 client: &PlatformApiClient,
207 project_id: &str,
208) -> ClusterSelectionResult {
209 display_step_header(
210 3,
211 "Select Cluster",
212 "Choose a Kubernetes cluster for this environment.",
213 );
214
215 let clusters: Vec<ClusterSummary> =
217 match get_available_clusters_for_project(client, project_id).await {
218 Ok(c) => c,
219 Err(e) => return ClusterSelectionResult::Error(e),
220 };
221
222 if clusters.is_empty() {
223 return ClusterSelectionResult::NoClusters;
224 }
225
226 let options: Vec<String> = clusters
228 .iter()
229 .map(|c| {
230 let health = if c.is_healthy {
231 "healthy".green()
232 } else {
233 "unhealthy".red()
234 };
235 format!("{} ({}) - {}", c.name.bold(), c.region.dimmed(), health)
236 })
237 .collect();
238
239 let selection = Select::new("Select cluster:", options.clone())
240 .with_render_config(wizard_render_config())
241 .with_help_message("Choose the cluster to deploy to")
242 .prompt();
243
244 match selection {
245 Ok(answer) => {
246 let selected_name = answer.split(" (").next().unwrap_or("");
248 if let Some(cluster) = clusters.iter().find(|c| c.name == selected_name) {
249 println!("\n{} Selected: {}", "✓".green(), cluster.name.bold());
250 ClusterSelectionResult::Selected(cluster.id.clone())
251 } else {
252 ClusterSelectionResult::Error("Failed to match selected cluster".to_string())
253 }
254 }
255 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
256 println!("\n{}", "Wizard cancelled.".dimmed());
257 ClusterSelectionResult::Cancelled
258 }
259 Err(e) => ClusterSelectionResult::Error(format!("Selection error: {}", e)),
260 }
261}
262
263async fn get_available_clusters_for_project(
265 client: &PlatformApiClient,
266 project_id: &str,
267) -> Result<Vec<ClusterSummary>, String> {
268 let statuses = get_provider_deployment_statuses(client, project_id)
270 .await
271 .map_err(|e| format!("Failed to get provider statuses: {}", e))?;
272
273 let mut all_clusters = Vec::new();
275 for status in statuses {
276 if status.is_connected {
277 all_clusters.extend(status.clusters);
278 }
279 }
280
281 Ok(all_clusters)
282}
283
284fn select_provider_regions() -> Option<HashMap<String, String>> {
289 display_step_header(
290 4,
291 "Provider Regions (Optional)",
292 "Set default regions for each cloud provider. Press Esc to skip.",
293 );
294
295 let available_providers = [
296 ("GCP", CloudProvider::Gcp),
297 ("Hetzner", CloudProvider::Hetzner),
298 ("Azure", CloudProvider::Azure),
299 ];
300
301 let provider_labels: Vec<String> = available_providers
302 .iter()
303 .map(|(label, _)| label.to_string())
304 .collect();
305
306 let selected = match MultiSelect::new(
307 "Select providers to set default regions for:",
308 provider_labels,
309 )
310 .with_render_config(wizard_render_config())
311 .with_help_message("Space to select, Enter to confirm, Esc to skip")
312 .prompt()
313 {
314 Ok(s) if !s.is_empty() => s,
315 _ => return None,
316 };
317
318 let mut regions = HashMap::new();
319
320 for provider_label in &selected {
321 let (_, provider) = available_providers
322 .iter()
323 .find(|(label, _)| label == provider_label)
324 .unwrap();
325
326 let provider_regions = get_regions_for_provider(provider);
327 let default_region = get_default_region(provider);
328
329 if provider_regions.is_empty() {
330 let region = match Text::new(&format!("{} region:", provider_label))
332 .with_default(default_region)
333 .with_render_config(wizard_render_config())
334 .prompt()
335 {
336 Ok(r) => r,
337 Err(_) => continue,
338 };
339 regions.insert(provider.as_str().to_string(), region);
340 } else {
341 let region_labels: Vec<String> = provider_regions
342 .iter()
343 .map(|r| format!("{} - {} ({})", r.id, r.name, r.location))
344 .collect();
345
346 let default_idx = provider_regions
347 .iter()
348 .position(|r| r.id == default_region)
349 .unwrap_or(0);
350
351 let region = match Select::new(&format!("{} region:", provider_label), region_labels)
352 .with_render_config(wizard_render_config())
353 .with_starting_cursor(default_idx)
354 .prompt()
355 {
356 Ok(r) => {
357 r.split(" - ").next().unwrap_or(default_region).to_string()
359 }
360 Err(_) => continue,
361 };
362 regions.insert(provider.as_str().to_string(), region);
363 }
364 }
365
366 if regions.is_empty() {
367 None
368 } else {
369 println!("\n{} Provider regions configured:", "✓".green());
370 for (provider, region) in ®ions {
371 println!(" {}: {}", provider, region.bold());
372 }
373 Some(regions)
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn test_environment_creation_result_variants() {
383 let created = EnvironmentCreationResult::Created(Environment {
384 id: "env-1".to_string(),
385 name: "test".to_string(),
386 project_id: "proj-1".to_string(),
387 environment_type: "cloud".to_string(),
388 cluster_id: None,
389 namespace: None,
390 description: None,
391 is_active: true,
392 created_at: None,
393 updated_at: None,
394 provider_regions: None,
395 });
396 assert!(matches!(created, EnvironmentCreationResult::Created(_)));
397
398 let cancelled = EnvironmentCreationResult::Cancelled;
399 assert!(matches!(cancelled, EnvironmentCreationResult::Cancelled));
400
401 let error = EnvironmentCreationResult::Error("test error".to_string());
402 assert!(matches!(error, EnvironmentCreationResult::Error(_)));
403 }
404
405 #[test]
406 fn test_environment_type_as_str() {
407 assert_eq!(EnvironmentType::Cluster.as_str(), "cluster");
408 assert_eq!(EnvironmentType::Cloud.as_str(), "cloud");
409 }
410
411 #[test]
412 fn test_environment_type_display_name() {
413 assert_eq!(EnvironmentType::Cluster.display_name(), "Kubernetes");
414 assert_eq!(EnvironmentType::Cloud.display_name(), "Cloud Runner");
415 }
416}