//! The `cambium` CLI binary. Parses argv through the grammar shared with
//! the desktop console panel (`cambium_core::commands`), builds the
//! execution context (config dir, interactivity), runs the command through
//! `cambium_core::executor::run`, and prints exactly one line:
//! human-readable by default, or a single JSON object with `--json`.
//!
//! Interactive prompting (trust-on-first-use y/N, password entry) happens
//! ONLY here, never in `cambium-core` (which is also linked into the
//! desktop app, where there is no console to prompt on). `login`, and
//! `trust` with no `--fingerprint`, get a dedicated interactive loop when
//! this process is attached to a terminal; every other invocation —
//! including those same two verbs when piped/scripted/CI — goes straight
//! through `executor::run`, which resolves auth non-interactively and
//! reports `NeedsTrust`/`NeedsPassword` as actionable errors instead of
//! prompting.

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

use cambium_core::auth::{self, AuthOutcome, AuthRequest};
use cambium_core::commands::{Cli, Cmd, CommandOutput};
use cambium_core::config::{CambiumDirs, DeviceIdentity};
use cambium_core::executor::{self, ExecContext};
use clap::Parser;

#[tokio::main]
async fn main() {
    // clap's default `Cli::parse()` behavior on a usage error: print the
    // rendered error to stderr and exit(2). We rely on that default here
    // rather than reimplementing it — see commands::parse_line for the
    // console's equivalent, which returns the error instead of exiting.
    let cli = Cli::parse();

    let dirs = match CambiumDirs::resolve() {
        Ok(d) => d,
        Err(e) => {
            eprintln!("{e}");
            std::process::exit(5);
        }
    };

    // Interactive only when BOTH ends of this process are a real terminal —
    // a non-TTY CLI run (piped, scripted, CI) must never block on a prompt.
    let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();

    let mut ctx = ExecContext {
        dirs,
        api: None,
        me: None,
        project: None,
        workspace: None,
        interactive,
        // No progress UI in the CLI yet — Task 13's note in executor.rs still
        // stands; this is the desktop console's hook, not the terminal's.
        progress: None,
    };

    // Captured before `cli` is potentially moved into `executor::run` below.
    let want_json = cli.json;

    let output = match &cli.cmd {
        Cmd::Login { .. } if ctx.interactive => interactive_login(&cli, &mut ctx).await,
        Cmd::Trust { fingerprint: None, .. } if ctx.interactive => {
            interactive_trust(&cli, &mut ctx).await
        }
        // Changing your OWN password needs the current one. On a terminal we
        // prompt for it and hand it to the executor through the same env var
        // the non-interactive path reads (CAMBIUM_CURRENT_PASSWORD). An admin
        // resetting someone else's password never needs this, so it's gated on
        // is_own_handle.
        Cmd::Password { handle, .. } if ctx.interactive && executor::is_own_handle(&ctx, handle) => {
            match rpassword::prompt_password("Current password: ") {
                Ok(current) => {
                    std::env::set_var("CAMBIUM_CURRENT_PASSWORD", current);
                    executor::run(cli, &mut ctx).await
                }
                Err(e) => executor::output_error(3, format!("failed to read current password: {e}")),
            }
        }
        // Destroying a project is the one irreversible verb, so on a terminal
        // it asks for the project name rather than accepting a bare `-f`
        // typo. Confirming sets `force` and hands the SAME command to the
        // executor, which is what the console reaches with `-f` — the gate
        // moves, the behavior doesn't.
        Cmd::Delproject { force: false, .. } if ctx.interactive => {
            interactive_delproject(cli, &mut ctx).await
        }
        _ => executor::run(cli, &mut ctx).await,
    };

    if want_json {
        println!("{}", output.to_json_line());
    } else {
        println!("{}", output.human);
    }

    std::process::exit(output.code);
}

/// `delproject` on a real terminal: the GUI makes you type the project name
/// before it will delete one, and this is the terminal's equivalent of that
/// gate. Only an exact match proceeds — deleting the wrong project because
/// `-f` was already on the line is precisely what this exists to prevent.
///
/// Typed visibly (not via rpassword): it's a project name, not a secret, and
/// seeing what you typed is the whole point.
async fn interactive_delproject(mut cli: Cli, ctx: &mut ExecContext) -> CommandOutput {
    let name = match &cli.cmd {
        Cmd::Delproject { project, .. } => project.trim().to_string(),
        _ => unreachable!("interactive_delproject() only called for Cmd::Delproject"),
    };
    if name.is_empty() {
        return executor::output_error(2, "specify a project name.".to_string());
    }

    println!("This irreversibly deletes {name} and its entire history.");
    print!("Type the project name to confirm: ");
    if io::stdout().flush().is_err() {
        return executor::output_error(3, "failed to write the confirmation prompt.".to_string());
    }

    let mut typed = String::new();
    if let Err(e) = io::stdin().read_line(&mut typed) {
        return executor::output_error(3, format!("failed to read confirmation: {e}"));
    }
    if typed.trim() != name {
        return executor::output_error(
            2,
            format!("that doesn't match {name}; nothing was deleted."),
        );
    }

    // Matched: promote to the same command the console runs with `-f`.
    if let Cmd::Delproject { force, .. } = &mut cli.cmd {
        *force = true;
    }
    executor::run(cli, ctx).await
}

/// `login` on a real terminal: resolves auth, prompting in place of the
/// `NeedsTrust`/`NeedsPassword` outcomes `executor::run` would otherwise
/// turn into errors. Retries after a successful trust so the same
/// connection attempt can proceed to the password step in one command.
async fn interactive_login(cli: &Cli, ctx: &mut ExecContext) -> CommandOutput {
    let (server, user) = executor::login_target(cli);

    loop {
        let outcome = {
            let req = AuthRequest { dirs: &ctx.dirs, server: server.clone(), username: user.clone() };
            auth::resolve(req).await
        };

        match outcome {
            Ok(AuthOutcome::Ready(client)) => {
                if !executor::login_remember(cli) {
                    return finish_login(ctx, client, executor::RememberOutcome::NotRequested);
                }
                // `Ready` here means auth resolved without this run ever
                // holding a plaintext password (a cached session token, an
                // already-stored credential, or a silent CAMBIUM_PASSWORD
                // login inside `auth::resolve` itself) — so `--remember`
                // still has nothing to seal. `decide_remember` is the same
                // decision `executor::login`'s non-interactive path
                // consults: if CAMBIUM_PASSWORD is set, use it directly
                // (validate-then-store, no prompt) instead of demanding a
                // keystroke a tty-attached automation run already answered
                // by setting the env var; only fall back to the interactive
                // prompt when it's absent.
                let env_password = std::env::var("CAMBIUM_PASSWORD").ok();
                let password = match executor::decide_remember(true, env_password.is_some(), ctx.interactive) {
                    executor::RememberDecision::UseEnvPassword => {
                        env_password.expect("UseEnvPassword implies env_password.is_some()")
                    }
                    executor::RememberDecision::Prompt => {
                        match rpassword::prompt_password("Password (to remember): ") {
                            Ok(p) => p,
                            Err(e) => {
                                return executor::output_error(3, format!("failed to read password: {e}"))
                            }
                        }
                    }
                    // Neither is reachable here: `remember=true` is passed
                    // literally above, and this whole function only runs
                    // when `ctx.interactive` is true (see main()'s dispatch),
                    // which rules out `Skip` and `Fail` respectively. Kept
                    // exhaustive (rather than `_ => unreachable!()`) so a
                    // future change to either precondition fails loudly
                    // instead of silently reporting success while storing
                    // nothing.
                    executor::RememberDecision::Skip | executor::RememberDecision::Fail => {
                        return executor::output_error(
                            3,
                            "--remember needs a password to store; set CAMBIUM_PASSWORD, or enter one \
                             at the prompt."
                                .to_string(),
                        );
                    }
                };
                // This branch is only reached when `executor::login_remember(cli)`
                // is true (checked above, which returns early otherwise), so
                // the user did ask to be remembered — the server should mint
                // a token. Whether it actually did, and whether that token
                // actually made it to local storage, is decided AFTER the
                // call below by `record_remember_outcome` — never assumed
                // from the request alone.
                let remember_requested = true;
                let req = AuthRequest { dirs: &ctx.dirs, server: server.clone(), username: user.clone() };
                return match auth::login_with(&req, &password, remember_requested).await {
                    Ok((fresh_client, response)) => {
                        // login_with has just written session.json, which
                        // record_remember_outcome's write reads for the
                        // canonical handle.
                        let outcome = executor::record_remember_outcome(
                            ctx,
                            remember_requested,
                            response.remember_token.as_deref(),
                        );
                        finish_login(ctx, fresh_client, outcome)
                    }
                    Err(e) => {
                        let (code, msg) = executor::map_api_error(&e);
                        executor::output_error(code, msg)
                    }
                };
            }
            Ok(AuthOutcome::NeedsTrust { fingerprint, subject }) => {
                let host = executor::resolve_trust_host(ctx, &server)
                    .unwrap_or_else(|| "the server".to_string());
                print!("Trust server {host}? Fingerprint: {fingerprint} [y/N] ");
                let _ = io::stdout().flush();
                let mut answer = String::new();
                if io::stdin().read_line(&mut answer).is_err() {
                    return executor::output_error(4, "failed to read trust confirmation.".to_string());
                }
                if !answer.trim().eq_ignore_ascii_case("y") {
                    return executor::output_error(
                        4,
                        format!("not trusted: {subject} ({fingerprint})."),
                    );
                }
                if let Err(e) = auth::trust(&ctx.dirs, &host, &fingerprint).await {
                    return executor::output_error(4, format!("failed to save trust: {e}"));
                }
                // Retry resolution now that the host is pinned.
                continue;
            }
            Ok(AuthOutcome::NeedsPassword) => {
                let password = match rpassword::prompt_password("Password: ") {
                    Ok(p) => p,
                    Err(e) => return executor::output_error(3, format!("failed to read password: {e}")),
                };
                let remember_requested = executor::login_remember(cli);
                let req = AuthRequest { dirs: &ctx.dirs, server: server.clone(), username: user.clone() };
                return match auth::login_with(&req, &password, remember_requested).await {
                    Ok((client, response)) => {
                        // login_with has just written session.json, which
                        // record_remember_outcome's write reads for the
                        // canonical handle.
                        let outcome = executor::record_remember_outcome(
                            ctx,
                            remember_requested,
                            response.remember_token.as_deref(),
                        );
                        finish_login(ctx, client, outcome)
                    }
                    Err(e) => {
                        let (code, msg) = executor::map_api_error(&e);
                        executor::output_error(code, msg)
                    }
                };
            }
            Err(e) => {
                let (code, msg) = executor::map_api_error(&e);
                return executor::output_error(code, msg);
            }
        }
    }
}

fn finish_login(
    ctx: &mut ExecContext,
    client: cambium_core::api::ApiClient,
    remember: executor::RememberOutcome,
) -> CommandOutput {
    ctx.api = Some(client);
    ctx.me = match DeviceIdentity::load_or_create(&ctx.dirs, None, None) {
        Ok(d) => Some(d),
        Err(e) => return executor::output_error(1, format!("failed to load device identity: {e}")),
    };
    executor::login_success(ctx, remember)
}

/// `trust` with no `--fingerprint` on a real terminal: fetches the
/// certificate, shows the fingerprint, and asks for y/N before pinning it —
/// the interactive half of the trust-on-first-use flow described in
/// the published command reference.
async fn interactive_trust(cli: &Cli, ctx: &mut ExecContext) -> CommandOutput {
    let host_flag = match &cli.cmd {
        Cmd::Trust { host, .. } => host,
        _ => unreachable!("interactive_trust only called for Cmd::Trust"),
    };
    let host = match executor::resolve_trust_host(ctx, host_flag) {
        Some(h) => h,
        None => {
            return executor::output_error(
                2,
                "specify a host, e.g. 'trust HOST:PORT', or run 'login' first.".to_string(),
            )
        }
    };

    let cert =
        match cambium_core::pinned_http::fetch_server_certificate(executor::bare_host(&host)).await {
            Ok(c) => c,
            Err(e) => {
                return executor::output_error(4, format!("could not fetch server certificate: {e}"))
            }
        };

    print!("Trust server {host}? Fingerprint: {} [y/N] ", cert.fingerprint);
    let _ = io::stdout().flush();
    let mut answer = String::new();
    if io::stdin().read_line(&mut answer).is_err() {
        return executor::output_error(4, "failed to read trust confirmation.".to_string());
    }
    if !answer.trim().eq_ignore_ascii_case("y") {
        return executor::output_error(4, format!("not trusted: {} ({}).", cert.subject, cert.fingerprint));
    }

    match auth::trust(&ctx.dirs, &host, &cert.fingerprint).await {
        Ok(()) => executor::output_ok(
            format!("Trusted {host} (fingerprint {}).", cert.fingerprint),
            serde_json::json!({"host": host, "fingerprint": cert.fingerprint}),
        ),
        Err(e) => executor::output_error(4, format!("failed to save trust: {e}")),
    }
}
