//! Handler for `mcptest login` plus its YAML resolver helpers.

use std::io;
use std::path::PathBuf;

use anyhow::Result;

use crate::cli::args::LoginArgs;
use crate::cli::GlobalArgs;
use crate::commands;

/// Handle `mcptest login`. Resolves one or more server URLs from the
/// CLI args + YAML config, then hands off to [`commands::run_login`]
/// per target. The resolver order is:
///
/// 1. `--all`: every URL server declared in `mcptest.yml`.
/// 2. positional `[server]`: the named entry in `servers:` (or `server:`
///    singular) of the YAML config.
/// 3. `--url <URL>` or the global `--server-url`: ad-hoc URL.
/// 4. otherwise: if the YAML has exactly one URL server, use it; if
///    multiple, run the interactive picker over their names.
pub(crate) fn login_command(global: &GlobalArgs, args: LoginArgs) -> Result<i32> {
    let targets = resolve_login_targets(global, &args)?;
    // Tests point the token cache at a `tempfile::TempDir` via this
    // env var so they do not need to mutate `$HOME`. Production runs
    // leave it unset and the default `~/.mcptest/auth/` root is used.
    let cache_root = std::env::var_os("MCPTEST_AUTH_DIR").map(PathBuf::from);
    let mut last_code = 0;
    for target in targets {
        let label = target.label.clone();
        let url = target.url.clone();
        println!("logging in to {label} ({url})");
        let code = commands::run_login(commands::LoginRunArgs {
            server_url: url,
            client_id: args.client_id.clone(),
            config_path: global.config.clone(),
            no_browser: args.no_browser,
            device: args.device,
            force: args.force,
            cache_root: cache_root.clone(),
            timeout: std::time::Duration::from_secs(300),
        })?;
        if code != 0 {
            last_code = code;
            eprintln!("login for {label} exited with code {code}; continuing");
        }
    }
    Ok(last_code)
}

/// One resolved server target for the login flow.
#[derive(Debug, Clone)]
struct LoginTarget {
    /// Display label for log output (server name or "url=...").
    label: String,
    /// Server URL the login flow authenticates against.
    url: String,
}

/// Build the list of [`LoginTarget`]s the operator asked to log in to.
/// Returns an error when the user's flags cannot be satisfied (no
/// servers configured, named server is stdio, etc.).
fn resolve_login_targets(global: &GlobalArgs, args: &LoginArgs) -> Result<Vec<LoginTarget>> {
    if args.all {
        return load_yaml_url_servers(global.config.as_deref()).and_then(|servers| {
            if servers.is_empty() {
                Err(anyhow::anyhow!(
                    "--all: no URL servers found in mcptest.yml; only `command:` (stdio) entries are present"
                ))
            } else {
                Ok(servers
                    .into_iter()
                    .map(|s| LoginTarget {
                        label: s.name,
                        url: s.url,
                    })
                    .collect())
            }
        });
    }

    if let Some(name) = &args.server {
        let servers = load_yaml_url_servers(global.config.as_deref())?;
        let entry = servers
            .into_iter()
            .find(|s| &s.name == name)
            .ok_or_else(|| {
                anyhow::anyhow!(
                "no URL server named `{name}` in mcptest.yml (check spelling, or pass --url <URL>)"
            )
            })?;
        return Ok(vec![LoginTarget {
            label: entry.name,
            url: entry.url,
        }]);
    }

    if let Some(url) = &args.url {
        return Ok(vec![LoginTarget {
            label: format!("url={url}"),
            url: url.clone(),
        }]);
    }

    if let Some(url) = &global.server_url {
        return Ok(vec![LoginTarget {
            label: format!("url={url}"),
            url: url.clone(),
        }]);
    }

    let servers = load_yaml_url_servers(global.config.as_deref()).unwrap_or_default();
    match servers.len() {
        0 => Err(anyhow::anyhow!(
            "mcptest login needs a server: pass a server name, --url <URL>, or configure one in mcptest.yml"
        )),
        1 => {
            let only = servers.into_iter().next().expect("len == 1");
            Ok(vec![LoginTarget {
                label: only.name,
                url: only.url,
            }])
        }
        _ => pick_server_interactive(&servers).map(|chosen| {
            vec![LoginTarget {
                label: chosen.name,
                url: chosen.url,
            }]
        }),
    }
}

/// One URL-bearing server entry parsed out of `mcptest.yml`.
#[derive(Debug, Clone)]
struct YamlUrlServer {
    /// Name from the `servers:` map key or `server:` singular.
    name: String,
    /// Resolved `url:` value.
    url: String,
}

/// Walk the YAML config and return every server entry that has a `url:`.
/// `command:` (stdio) entries are silently skipped; the singular
/// `server:` block is treated as a server named "default" so legacy
/// configs still work with `mcptest login`.
fn load_yaml_url_servers(config: Option<&std::path::Path>) -> Result<Vec<YamlUrlServer>> {
    let path = match config {
        Some(p) => p.to_path_buf(),
        // WOR-1350: shared default resolution (`mcptest.yaml`, then
        // `mcptest.yml`) so login reads the same file run would.
        None => {
            let cwd = std::env::current_dir()?;
            crate::cli::default_config::resolve_default_config(&cwd)
                .map_err(|ambiguous| anyhow::anyhow!("{}", ambiguous.message()))?
                .into_path()
        }
    };
    let raw = std::fs::read_to_string(&path)
        .map_err(|e| anyhow::anyhow!("could not read {}: {e}", path.display()))?;
    let yaml: serde_yaml::Value = serde_yaml::from_str(&raw)
        .map_err(|e| anyhow::anyhow!("could not parse {}: {e}", path.display()))?;

    let mut out: Vec<YamlUrlServer> = Vec::new();

    if let Some(singular) = yaml.get("server") {
        if let Some(url) = singular.get("url").and_then(|u| u.as_str()) {
            out.push(YamlUrlServer {
                name: "default".to_string(),
                url: url.to_string(),
            });
        }
    }

    if let Some(plural) = yaml.get("servers") {
        if let Some(map) = plural.as_mapping() {
            for (k, v) in map {
                let Some(name) = k.as_str() else { continue };
                if let Some(url) = v.get("url").and_then(|u| u.as_str()) {
                    out.push(YamlUrlServer {
                        name: name.to_string(),
                        url: url.to_string(),
                    });
                }
            }
        }
    }

    Ok(out)
}

/// Render a numbered list of configured URL servers and read a 1-based
/// index from stdin. Used when the operator runs bare `mcptest login`
/// against a multi-server config and we cannot pick on their behalf.
fn pick_server_interactive(servers: &[YamlUrlServer]) -> Result<YamlUrlServer> {
    use std::io::{BufRead, Write};
    println!("Select a server to log in to:");
    for (i, s) in servers.iter().enumerate() {
        println!("  [{}] {} ({})", i + 1, s.name, s.url);
    }
    print!("Enter a number 1..{}: ", servers.len());
    io::stdout().flush().ok();
    let mut line = String::new();
    let stdin = io::stdin();
    let mut handle = stdin.lock();
    handle
        .read_line(&mut line)
        .map_err(|e| anyhow::anyhow!("read stdin: {e}"))?;
    let trimmed = line.trim();
    let idx: usize = trimmed
        .parse()
        .map_err(|e| anyhow::anyhow!("could not parse `{trimmed}` as a number: {e}"))?;
    if idx == 0 || idx > servers.len() {
        return Err(anyhow::anyhow!(
            "selection {idx} out of range 1..{}",
            servers.len()
        ));
    }
    Ok(servers[idx - 1].clone())
}
