//! `pic login` — primary auth entry point.
//!
//! Two flows:
//!   * **username + password**: POST `/api/v1/admin/auth/login` with the
//!     credentials and persist the returned session token. Mirrors the
//!     dashboard's login form. Interactive by default; for CI pass
//!     `--username <U> --password-stdin` to read the password from stdin
//!     (inline passwords are never accepted — they leak into history/`ps`).
//!   * **paste-a-token** (`--token <T>`, or `PICLOUD_TOKEN` env): skip
//!     the credential exchange and persist a bearer string directly.
//!     Used by CI and by anyone using a long-lived API key minted via
//!     `pic api-keys mint`. Validated against `/auth/me` before save.
//!
//! `--url <U>` (or `PICLOUD_URL`) overrides the URL prompt non-interactively.

use std::io::{self, BufRead, Write};

use anyhow::{Context, Result};
use picloud_shared::InstanceRole;

use crate::client::{self, Client};
use crate::config::{save, Credentials};

const DEFAULT_URL: &str = "http://localhost:8000";

pub async fn run(
    url_arg: Option<&str>,
    token_arg: Option<&str>,
    username_arg: Option<&str>,
    password_stdin: bool,
) -> Result<()> {
    let url = resolve_url(url_arg)?;
    let token_from_env = std::env::var("PICLOUD_TOKEN")
        .ok()
        .filter(|s| !s.is_empty());
    // Audit 2026-06-11 (H2) — an inline `--token <T>` lands in shell
    // history, `ps aux`, and /proc/<pid>/cmdline. Accept only `--token -`
    // (stdin) or the PICLOUD_TOKEN env var (the CI path); reject inline.
    let token_from_arg = match token_arg {
        Some("-") => Some(read_token_from_stdin()?),
        Some(_) => {
            anyhow::bail!(
                "passing the token inline is not allowed — it leaks into shell history, \
                 `ps aux`, and /proc/<pid>/cmdline. Use `--token -` to read it from stdin, \
                 or set the PICLOUD_TOKEN environment variable."
            )
        }
        None => None,
    };
    let bearer_token = token_from_arg.or(token_from_env);

    let (token, username, role) = match bearer_token {
        Some(t) => login_with_bearer(&url, &t).await?,
        None => login_with_password(&url, username_arg, password_stdin).await?,
    };

    let creds = Credentials {
        url: url.clone(),
        token,
        username: username.clone(),
    };
    save(&creds)?;
    println!(
        "Logged in as {username} ({}) at {url}",
        instance_role_label(&role)
    );
    Ok(())
}

/// Read a bearer token from stdin. Interactive sessions get a no-echo
/// prompt (a token is a secret); piped input reads one line. Audit
/// 2026-06-11 (H2) — backs `pic login --token -`.
fn read_token_from_stdin() -> Result<String> {
    use std::io::IsTerminal;
    if io::stdin().is_terminal() {
        let t = rpassword::prompt_password("Token: ").context("read token from stdin")?;
        Ok(t.trim().to_string())
    } else {
        let mut buf = String::new();
        io::stdin()
            .lock()
            .read_line(&mut buf)
            .context("read token from stdin")?;
        Ok(buf.trim().to_string())
    }
}

async fn login_with_password(
    url: &str,
    username_arg: Option<&str>,
    password_stdin: bool,
) -> Result<(String, String, InstanceRole)> {
    let username = match username_arg {
        Some(u) => u.trim().to_string(),
        None => prompt_line("Username: ")?,
    };
    if username.is_empty() {
        anyhow::bail!("username is required");
    }
    // `--password-stdin` reads one line from stdin without prompting —
    // the CI path. The password is never accepted inline (it would leak
    // into shell history, `ps`, and /proc), mirroring the `--token` rule.
    let password = if password_stdin {
        read_password_from_stdin()?
    } else {
        read_password()?
    };
    let resp = client::auth_login(url, &username, &password).await?;
    Ok((resp.token, resp.user.username, resp.user.instance_role))
}

/// Read a password from stdin without prompting — backs
/// `--password-stdin`. Interactive sessions still get a no-echo prompt
/// (a password is a secret); piped input reads one line.
fn read_password_from_stdin() -> Result<String> {
    use std::io::IsTerminal;
    if io::stdin().is_terminal() {
        let p = rpassword::prompt_password("Password: ").context("read password from stdin")?;
        Ok(p.trim_end_matches(['\r', '\n']).to_string())
    } else {
        let mut buf = String::new();
        io::stdin()
            .lock()
            .read_line(&mut buf)
            .context("read password from stdin")?;
        Ok(buf.trim_end_matches(['\r', '\n']).to_string())
    }
}

/// Read a password without echoing it where possible. Falls back to a
/// plain stdin read when no controlling terminal is attached — CI
/// systems and `cargo test`'s piped stdin both land here, and dying
/// outright would block scripted use entirely. The fallback is louder
/// (visible characters), but it's that or no functioning login.
fn read_password() -> Result<String> {
    match rpassword::prompt_password("Password: ") {
        Ok(p) => Ok(p),
        Err(_) => {
            eprint!("Password: ");
            io::stderr().flush()?;
            let mut buf = String::new();
            io::stdin()
                .lock()
                .read_line(&mut buf)
                .context("reading password from stdin")?;
            Ok(buf.trim_end_matches(['\r', '\n']).to_string())
        }
    }
}

/// Bearer-token path: validate against `/auth/me` so a typo doesn't get
/// persisted, then trust the username the server reports rather than
/// whatever the user typed (which they didn't type at all in this mode).
async fn login_with_bearer(url: &str, token: &str) -> Result<(String, String, InstanceRole)> {
    let client = Client::new(url, token)?;
    let me = client.auth_me().await?;
    Ok((token.to_string(), me.username, me.instance_role))
}

fn instance_role_label(role: &InstanceRole) -> &'static str {
    match role {
        InstanceRole::Owner => "owner",
        InstanceRole::Admin => "admin",
        InstanceRole::Member => "member",
    }
}

fn resolve_url(url_arg: Option<&str>) -> Result<String> {
    if let Some(u) = url_arg {
        return Ok(u.trim_end_matches('/').to_string());
    }
    if let Ok(env_url) = std::env::var("PICLOUD_URL") {
        if !env_url.is_empty() {
            return Ok(env_url.trim_end_matches('/').to_string());
        }
    }
    let typed = prompt_with_default("PiCloud URL", DEFAULT_URL)?;
    Ok(typed.trim_end_matches('/').to_string())
}

fn prompt_line(label: &str) -> Result<String> {
    print!("{label}");
    io::stdout().flush()?;
    let mut buf = String::new();
    io::stdin().lock().read_line(&mut buf)?;
    Ok(buf.trim().to_string())
}

fn prompt_with_default(label: &str, default: &str) -> Result<String> {
    print!("{label} [{default}]: ");
    io::stdout().flush()?;
    let mut buf = String::new();
    io::stdin().lock().read_line(&mut buf)?;
    let trimmed = buf.trim();
    Ok(if trimmed.is_empty() {
        default.to_string()
    } else {
        trimmed.to_string()
    })
}
