//! `mcptest login --device`: RFC 8628 device-code grant (WOR-1286).
//!
//! For SSH boxes and containers with no local browser. The flow prints
//! a verification URL plus a short user code, then polls the token
//! endpoint until a human approves on any other device. Discovery, DCR,
//! and the token-cache tail are shared with the sibling `login` module
//! so a device-grant token caches and refreshes exactly like a PKCE one.

use std::time::Duration;

use anyhow::{anyhow, Context, Result};
use mcptest_core::auth::{
    poll_device_token, request_device_authorization, DeviceAuthorization,
    DeviceAuthorizationRequest, DeviceTokenRequest, RegisterClientRequest,
};

use super::login::{
    finish_login, now_unix, resolve_client_id, resolve_token_cache, scope_of, LoginRunArgs,
    ServerMetadata,
};

/// Per-request timeout for each token poll. Each poll is a single short
/// POST; the long waits happen between polls, not inside them.
const POLL_TIMEOUT: Duration = Duration::from_secs(15);

/// Run the device-code login flow against already-discovered metadata.
/// Returns the process exit code. Called from `run_login` once `--device`
/// is set, so `--force` eviction and metadata discovery have already
/// happened.
pub(super) fn run_device_login(args: &LoginRunArgs, metadata: &ServerMetadata) -> Result<i32> {
    let endpoint = metadata
        .device_authorization_endpoint
        .as_deref()
        .ok_or_else(|| {
            anyhow!(
                "the IdP at {} does not advertise device_authorization_endpoint; \
                 device flow unsupported, use mcptest login --no-browser instead",
                args.server_url
            )
        })?;

    let token_cache = resolve_token_cache(args)?;
    let registration = RegisterClientRequest::device_public_client("mcptest");
    let client_id = resolve_client_id(
        &token_cache,
        metadata,
        &args.server_url,
        &registration,
        args.client_id.clone(),
    )?;

    let authz = request_device_authorization(&DeviceAuthorizationRequest {
        device_authorization_endpoint: endpoint.to_owned(),
        client_id: client_id.clone(),
        scope: scope_of(metadata),
    })
    .context(
        "request device authorization (if this client was registered for the \
         browser flow it may lack the device grant; re-run with --force to \
         register a fresh client)",
    )?;
    print_verification_instructions(&authz);

    let now_unix = now_unix();
    let http = reqwest::blocking::Client::builder()
        .timeout(POLL_TIMEOUT)
        .build()
        .context("build HTTP client for device token polling")?;
    let token = poll_device_token(
        &http,
        &DeviceTokenRequest {
            token_endpoint: metadata.token_endpoint.clone(),
            client_id: client_id.clone(),
            client_secret: None,
            device_code: authz.device_code.clone(),
        },
        &authz,
        now_unix,
        &mut |d| std::thread::sleep(d),
    )
    .context("poll token endpoint for device-code grant")?;

    finish_login(
        args,
        &token_cache,
        &metadata.token_endpoint,
        client_id,
        token,
        now_unix,
    )
}

/// Print the URL and code the human needs. Kept separate from the flow
/// so the operator-facing wording lives in one place; the integration
/// tests assert on these exact lines.
fn print_verification_instructions(authz: &DeviceAuthorization) {
    println!("To sign in, open this URL on any device:");
    println!("  {}", authz.verification_uri);
    println!("and enter the code: {}", authz.user_code);
    if let Some(complete) = &authz.verification_uri_complete {
        println!("or open: {complete}");
    }
}
