auberge 0.8.5

CLI tool for managing self-hosted infrastructure with Ansible
use crate::hosts::{Host, HostManager};
use crate::output;
use crate::selector::select_item;
use clap::Subcommand;
use dialoguer::{Confirm, Input, theme::ColorfulTheme};
use eyre::Result;
use std::path::PathBuf;
use tabled::Tabled;

pub struct AddHostArgs {
    pub name: Option<String>,
    pub address: Option<String>,
    pub user: Option<String>,
    pub port: u16,
    pub ssh_key: Option<String>,
    pub tags: Option<String>,
    pub description: Option<String>,
    pub no_input: bool,
}

#[derive(Tabled)]
struct HostDisplay {
    #[tabled(rename = "NAME")]
    name: String,
    #[tabled(rename = "ADDRESS")]
    address: String,
    #[tabled(rename = "USER")]
    user: String,
    #[tabled(rename = "PORT")]
    port: u16,
    #[tabled(rename = "TAGS")]
    tags: String,
}

impl From<&Host> for HostDisplay {
    fn from(host: &Host) -> Self {
        Self {
            name: host.name.clone(),
            address: host.address.clone(),
            user: host.user.clone(),
            port: host.port,
            tags: host.tags.join(", "),
        }
    }
}

#[derive(Subcommand)]
pub enum HostCommands {
    #[command(alias = "a", about = "Add a new host")]
    Add {
        #[arg(help = "Host name")]
        name: Option<String>,
        #[arg(help = "Host address (IP or hostname)")]
        address: Option<String>,
        #[arg(short, long, help = "SSH user")]
        user: Option<String>,
        #[arg(short, long, help = "SSH port", default_value = "22")]
        port: u16,
        #[arg(long, help = "Path to SSH key")]
        ssh_key: Option<String>,
        #[arg(short, long, help = "Tags (comma-separated)")]
        tags: Option<String>,
        #[arg(short, long, help = "Description")]
        description: Option<String>,
        #[arg(long, help = "Disable interactive prompts")]
        no_input: bool,
    },
    #[command(alias = "l", about = "List all hosts")]
    List {
        #[arg(short, long, help = "Filter by tags (comma-separated)")]
        tags: Option<String>,
        #[arg(short, long, help = "Output format: table, json, yaml")]
        output: Option<String>,
    },
    #[command(alias = "rm", about = "Remove a host")]
    Remove {
        #[arg(help = "Host name")]
        name: String,
        #[arg(short, long, help = "Skip confirmation")]
        yes: bool,
    },
    #[command(alias = "s", about = "Show host details")]
    Show {
        #[arg(help = "Host name")]
        name: String,
        #[arg(short, long, help = "Output format: yaml, json")]
        output: Option<String>,
    },
    #[command(alias = "e", about = "Edit a host")]
    Edit {
        #[arg(help = "Host name")]
        name: String,
    },
}

pub fn run_host_add(args: AddHostArgs) -> Result<()> {
    let is_tty = HostManager::is_tty();
    let interactive = is_tty && !args.no_input;

    let ssh_config_hosts = if interactive {
        match crate::ssh_config::SshConfigParser::new().and_then(|p| p.parse()) {
            Ok(hosts) if !hosts.is_empty() => {
                let existing_hosts = HostManager::list_hosts_filtered(None).unwrap_or_default();
                let existing_names: Vec<String> =
                    existing_hosts.iter().map(|h| h.name.clone()).collect();

                let available_hosts: Vec<_> = hosts
                    .into_iter()
                    .filter(|h| !existing_names.contains(&h.name))
                    .collect();

                if available_hosts.is_empty() {
                    None
                } else {
                    Some(available_hosts)
                }
            }
            Ok(_) => None,
            Err(e) => {
                output::info(&format!("Could not parse SSH config: {}", e));
                None
            }
        }
    } else {
        None
    };

    let imported_host = if let Some(ref ssh_hosts) = ssh_config_hosts {
        output::info(&format!(
            "Found {} new host(s) in ~/.ssh/config",
            ssh_hosts.len()
        ));

        let mut options: Vec<crate::ssh_config::SshConfigHost> =
            vec![crate::ssh_config::SshConfigHost {
                name: "Enter manually".to_string(),
                hostname: None,
                user: None,
                port: None,
                identity_file: None,
            }];
        options.extend(ssh_hosts.clone());

        select_item(
            &options,
            |h: &crate::ssh_config::SshConfigHost| {
                if h.hostname.is_none() {
                    "Enter manually".to_string()
                } else {
                    let addr = h.hostname.as_ref().unwrap();
                    let port = h.port.unwrap_or(22);
                    format!("{} ({}:{})", h.name, addr, port)
                }
            },
            "Import from SSH config or enter manually?",
        )?
        .and_then(|h| if h.hostname.is_some() { Some(h) } else { None })
    } else {
        None
    };

    let (name, address, user, port, ssh_key) = if let Some(imported) = imported_host {
        let name = imported.name;
        let address = imported.hostname.unwrap();
        let default_user = std::env::var("USER").unwrap_or_else(|_| "root".to_string());
        let user = imported.user.unwrap_or(default_user);
        let port = imported.port.unwrap_or(22);

        let ssh_key = imported.identity_file.and_then(|path| {
            let expanded = shellexpand::tilde(&path).into_owned();
            let key_path = PathBuf::from(&expanded);
            if !key_path.exists() {
                output::info(&format!(
                    "SSH key not found: {} (will use default derivation)",
                    expanded
                ));
                None
            } else {
                Some(expanded)
            }
        });

        output::info(&format!(
            "Importing: {} -> {}@{}:{}",
            name, user, address, port
        ));
        (name, address, user, port, ssh_key.or(args.ssh_key))
    } else {
        let name = if let Some(n) = args.name {
            n
        } else if interactive {
            Input::<String>::with_theme(&ColorfulTheme::default())
                .with_prompt("Host name")
                .interact_text()?
        } else {
            eyre::bail!("Host name is required (use --no-input in non-interactive mode)");
        };

        let address = if let Some(a) = args.address {
            a
        } else if interactive {
            Input::<String>::with_theme(&ColorfulTheme::default())
                .with_prompt("Host address (IP or hostname)")
                .interact_text()?
        } else {
            eyre::bail!("Host address is required");
        };

        let default_user = std::env::var("USER").unwrap_or_else(|_| "root".to_string());
        let user = if let Some(u) = args.user {
            u
        } else if interactive {
            Input::<String>::with_theme(&ColorfulTheme::default())
                .with_prompt("SSH user")
                .default(default_user)
                .interact_text()?
        } else {
            default_user
        };

        (name, address, user, args.port, args.ssh_key)
    };

    let tags_vec = args
        .tags
        .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
        .unwrap_or_default();

    let host = Host {
        name: name.clone(),
        address,
        user,
        port,
        ssh_key,
        tags: tags_vec,
        description: args.description,
        python_interpreter: None,
        become_method: "sudo".to_string(),
    };

    HostManager::add_host(host)?;

    let config_path = HostManager::config_path()?;
    output::success(&format!(
        "Host '{}' added to {}",
        name,
        config_path.display()
    ));

    Ok(())
}

pub fn run_host_list(tags: Option<String>, output: Option<String>) -> Result<()> {
    let filter_tags = tags.map(|t| t.split(',').map(|s| s.trim().to_string()).collect());

    let hosts = HostManager::list_hosts_filtered(filter_tags)?;

    if hosts.is_empty() {
        output::info("No hosts configured yet");
        println!("\nAdd a host with:");
        println!("  auberge host add <name> <address>");
        return Ok(());
    }

    match output.as_deref() {
        Some("json") => {
            println!("{}", serde_json::to_string_pretty(&hosts)?);
        }
        Some("yaml") => {
            println!("{}", serde_yaml::to_string(&hosts)?);
        }
        _ => {
            let display_hosts: Vec<HostDisplay> = hosts.iter().map(HostDisplay::from).collect();
            output::print_table(&display_hosts);
        }
    }

    Ok(())
}

pub fn run_host_remove(name: String, yes: bool) -> Result<()> {
    let is_tty = HostManager::is_tty();

    if !yes && is_tty {
        let confirm = Confirm::with_theme(&ColorfulTheme::default())
            .with_prompt(format!("Remove host '{}'?", name))
            .default(false)
            .interact()?;

        if !confirm {
            println!("Cancelled.");
            return Ok(());
        }
    }

    HostManager::remove_host(&name)?;
    output::success(&format!("Host '{}' removed", name));

    Ok(())
}

pub fn run_host_show(name: String, output: Option<String>) -> Result<()> {
    let host = HostManager::get_host(&name)?;

    match output.as_deref() {
        Some("json") => {
            println!("{}", serde_json::to_string_pretty(&host)?);
        }
        _ => {
            println!("{}", serde_yaml::to_string(&host)?);
        }
    }

    Ok(())
}

pub fn run_host_edit(name: String) -> Result<()> {
    let host = HostManager::get_host(&name)?;

    let address = Input::<String>::with_theme(&ColorfulTheme::default())
        .with_prompt("Host address")
        .default(host.address.clone())
        .interact_text()?;

    let user = Input::<String>::with_theme(&ColorfulTheme::default())
        .with_prompt("SSH user")
        .default(host.user.clone())
        .interact_text()?;

    let port = Input::<u16>::with_theme(&ColorfulTheme::default())
        .with_prompt("SSH port")
        .default(host.port)
        .interact_text()?;

    let tags_str = host.tags.join(", ");
    let new_tags_str = Input::<String>::with_theme(&ColorfulTheme::default())
        .with_prompt("Tags (comma-separated)")
        .default(tags_str)
        .allow_empty(true)
        .interact_text()?;

    let tags_vec: Vec<String> = if new_tags_str.is_empty() {
        Vec::new()
    } else {
        new_tags_str
            .split(',')
            .map(|s| s.trim().to_string())
            .collect()
    };

    let description = Input::<String>::with_theme(&ColorfulTheme::default())
        .with_prompt("Description")
        .default(host.description.clone().unwrap_or_default())
        .allow_empty(true)
        .interact_text()?;

    let updated_host = Host {
        name: name.clone(),
        address,
        user,
        port,
        ssh_key: host.ssh_key,
        tags: tags_vec,
        description: if description.is_empty() {
            None
        } else {
            Some(description)
        },
        python_interpreter: host.python_interpreter,
        become_method: host.become_method,
    };

    HostManager::update_host(&name, updated_host)?;
    output::success(&format!("Host '{}' updated", name));

    Ok(())
}