gitswitch 0.1.0

Switch between GitHub accounts (git identity + gh auth) on the fly
//! One function per command, orchestrating the config / git / gh modules.

use std::io::{self, Write};
use std::path::Path;

use anyhow::{Context, Result, bail};

use crate::config::{Account, Config, is_reserved};
use crate::{gh, git};

/// `gitswitch <alias>` — apply an account switch globally.
pub fn switch(config_path: &Path, alias: &str) -> Result<()> {
    let cfg = Config::load(config_path)?;
    if cfg.accounts.is_empty() {
        bail!("no accounts configured yet — run `gitswitch add <alias>` first");
    }
    let account = cfg
        .get(alias)
        .with_context(|| format!("no account named '{alias}' (see `gitswitch list`)"))?;

    // Identity first — this always applies.
    git::set_identity(&account.name, &account.email)?;
    println!(
        "Switched git identity to {} <{}>",
        account.name, account.email
    );

    // Then gh; a failure here is a warning, not a hard error.
    switch_gh(&account.gh_user)?;
    Ok(())
}

/// Switch the active gh account, offering an interactive `gh auth login` and a
/// retry if the account isn't logged in yet.
fn switch_gh(gh_user: &str) -> Result<()> {
    match gh::switch(gh_user)? {
        gh::SwitchResult::Ok => {
            println!("Switched gh account to '{gh_user}'");
            return Ok(());
        }
        gh::SwitchResult::NotInstalled => {
            eprintln!("warning: `gh` is not installed, so the GitHub account was not switched.");
            eprintln!("  install it from https://cli.github.com to switch auth automatically.");
            return Ok(());
        }
        gh::SwitchResult::Failed(err) => {
            eprintln!("The gh account '{gh_user}' isn't logged in yet.");
            if !err.is_empty() {
                eprintln!("  ({err})");
            }
        }
    }

    // The account isn't authenticated. Offer to walk through `gh auth login`.
    if !prompt_yes_no(&format!("Log in to GitHub with gh now as '{gh_user}'?"), true)? {
        eprintln!("  skipped — run `gh auth login` yourself, then `gitswitch` again.");
        return Ok(());
    }

    if !gh::login()? {
        eprintln!("warning: `gh auth login` did not complete; gh account not switched.");
        return Ok(());
    }

    // Retry the switch now that a login exists.
    match gh::switch(gh_user)? {
        gh::SwitchResult::Ok => println!("Switched gh account to '{gh_user}'"),
        gh::SwitchResult::Failed(err) => {
            eprintln!("warning: still could not switch to gh account '{gh_user}'.");
            if !err.is_empty() {
                eprintln!("  {err}");
            }
            eprintln!(
                "  you may have logged in as a different account — run `gh auth status` to check."
            );
        }
        gh::SwitchResult::NotInstalled => unreachable!("gh was invoked moments ago"),
    }
    Ok(())
}

/// `gitswitch add <alias>` — interactive profile creation.
pub fn add(config_path: &Path, alias: &str) -> Result<()> {
    if is_reserved(alias) {
        bail!("'{alias}' is a reserved command name and cannot be used as an account alias");
    }

    let mut cfg = Config::load(config_path)?;
    let existing = cfg.get(alias).cloned();
    if existing.is_some() {
        println!("Account '{alias}' already exists; updating it.");
    }

    let name = prompt("Full name", existing.as_ref().map(|a| a.name.as_str()))?;
    let email = prompt("Email", existing.as_ref().map(|a| a.email.as_str()))?;
    let gh_user = prompt(
        "GitHub username",
        existing.as_ref().map(|a| a.gh_user.as_str()),
    )?;

    cfg.set_account(alias, Account { name, email, gh_user })?;
    cfg.save(config_path)?;
    println!("Saved account '{alias}'. Switch to it with: gitswitch {alias}");
    Ok(())
}

/// `gitswitch list` — show all profiles, marking the active one.
pub fn list(config_path: &Path) -> Result<()> {
    let cfg = Config::load(config_path)?;
    if cfg.accounts.is_empty() {
        println!("No accounts configured. Add one with: gitswitch add <alias>");
        return Ok(());
    }
    let (_, current_email) = git::get_identity();
    for (alias, account) in &cfg.accounts {
        let marker = if current_email.as_deref() == Some(account.email.as_str()) {
            "* "
        } else {
            "  "
        };
        println!(
            "{marker}{alias}  {} <{}>  (gh: {})",
            account.name, account.email, account.gh_user
        );
    }
    Ok(())
}

/// `gitswitch current` — show the active identity and gh user.
pub fn current(config_path: &Path) -> Result<()> {
    let cfg = Config::load(config_path)?;
    let (name, email) = git::get_identity();
    match (name, email.clone()) {
        (Some(n), Some(e)) => {
            let alias = cfg
                .accounts
                .iter()
                .find(|(_, a)| a.email == e)
                .map(|(alias, _)| alias.as_str());
            match alias {
                Some(a) => println!("git identity: {n} <{e}>  (account '{a}')"),
                None => println!("git identity: {n} <{e}>  (no matching account)"),
            }
        }
        _ => println!("git identity: not set"),
    }
    match gh::current_user() {
        Some(user) => println!("gh account:   {user}"),
        None => println!("gh account:   unknown (gh not installed or not logged in)"),
    }
    Ok(())
}

/// `gitswitch remove <alias>` — delete a profile.
pub fn remove(config_path: &Path, alias: &str) -> Result<()> {
    let mut cfg = Config::load(config_path)?;
    cfg.remove_account(alias)?;
    cfg.save(config_path)?;
    println!("Removed account '{alias}'.");
    Ok(())
}

/// Ask a yes/no question. On EOF (non-interactive), returns `default`.
fn prompt_yes_no(question: &str, default: bool) -> Result<bool> {
    let hint = if default { "[Y/n]" } else { "[y/N]" };
    print!("{question} {hint}: ");
    io::stdout().flush().ok();

    let mut line = String::new();
    let n = io::stdin().read_line(&mut line)?;
    if n == 0 {
        return Ok(default); // EOF / non-interactive
    }
    match line.trim().to_lowercase().as_str() {
        "" => Ok(default),
        "y" | "yes" => Ok(true),
        _ => Ok(false),
    }
}

/// Prompt for a line of input, showing an optional current value as default.
fn prompt(label: &str, default: Option<&str>) -> Result<String> {
    loop {
        match default {
            Some(d) => print!("{label} [{d}]: "),
            None => print!("{label}: "),
        }
        io::stdout().flush().ok();

        let mut line = String::new();
        let n = io::stdin().read_line(&mut line)?;
        if n == 0 {
            // EOF
            bail!("input closed before all fields were entered");
        }
        let trimmed = line.trim();
        if !trimmed.is_empty() {
            return Ok(trimmed.to_string());
        }
        if let Some(d) = default {
            return Ok(d.to_string());
        }
        eprintln!("  {label} cannot be empty.");
    }
}