partiri-cli 0.2.0

Partiri CLI — Deploy and manage services on Partiri Cloud
use inquire::{Select, Text};

use crate::client::ApiClient;
use crate::error::{CliError, Result};
use crate::output::{ctx, print_success};

pub struct CreateArgs {
    pub workspace: Option<String>,
    pub name: Option<String>,
    pub environment: Option<String>,
}

pub fn run_create(client: &ApiClient, args: CreateArgs) -> Result<()> {
    let workspace_id = match args.workspace {
        Some(id) => id,
        None => resolve_workspace(client)?,
    };

    let name = match args.name {
        Some(n) => n,
        None => {
            if ctx().no_input {
                return Err(Box::new(
                    CliError::new(
                        "validation",
                        "--name is required when running non-interactively.",
                    )
                    .enriched(),
                ));
            }
            Text::new("Project name:").prompt().map_err(|_| {
                Box::new(CliError::new("cancelled", "Operation cancelled by user."))
                    as crate::error::Error
            })?
        }
    };

    let environment = match args.environment {
        Some(e) => normalise_environment(&e)?,
        None => {
            if ctx().no_input {
                return Err(Box::new(
                    CliError::new(
                        "validation",
                        "--environment is required when running non-interactively. Allowed values: dev, staging, prod.",
                    )
                    .enriched(),
                ));
            }
            let env_options = vec!["Development", "Staging", "Production"];
            let env_choice = Select::new("Environment:", env_options)
                .prompt()
                .map_err(|_| {
                    Box::new(CliError::new("cancelled", "Operation cancelled by user."))
                        as crate::error::Error
                })?;
            match env_choice {
                "Development" => "dev",
                "Staging" => "staging",
                "Production" => "prod",
                _ => unreachable!(),
            }
            .to_string()
        }
    };

    client.create_project(&name, &environment, &workspace_id)?;
    print_success(&format!("Project '{}' created.", name));
    Ok(())
}

fn resolve_workspace(client: &ApiClient) -> Result<String> {
    let workspaces = client.list_workspaces()?;
    if workspaces.is_empty() {
        return Err("No workspaces found. Create one at https://partiri.cloud".into());
    }
    if workspaces.len() == 1 {
        return Ok(workspaces[0].id.clone());
    }
    if ctx().no_input {
        return Err(
            "Multiple workspaces — pass --workspace <UUID>. Run 'partiri workspaces list' to see them."
                .into(),
        );
    }

    // Disambiguate by UUID — `email` is only populated on the user's primary workspace
    // and would render as "Name ()" for the others, breaking the find lookup.
    let options: Vec<String> = workspaces
        .iter()
        .map(|w| format!("{} ({})", w.name, w.id))
        .collect();
    let choice = Select::new("Select workspace:", options.clone())
        .prompt()
        .map_err(|e| format!("Selection cancelled: {e}"))?;
    let (_, workspace) = options
        .into_iter()
        .zip(workspaces.into_iter())
        .find(|(label, _)| label == &choice)
        .ok_or("Selected workspace not found in list")?;
    Ok(workspace.id)
}

fn normalise_environment(input: &str) -> Result<String> {
    match input.to_ascii_lowercase().as_str() {
        "dev" | "development" => Ok("dev".to_string()),
        "staging" | "stage" => Ok("staging".to_string()),
        "prod" | "production" => Ok("prod".to_string()),
        _ => Err(format!(
            "invalid --environment '{}'. Allowed: dev, staging, prod.",
            input
        )
        .into()),
    }
}