use crate::agent::runloop::unified::state::CtrlCState;
use anyhow::{Context, Result, anyhow};
use std::future::Future;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Notify;
use vtcode_auth::{
    AuthCallbackOutcome, AuthCodeCallbackServer, AuthCredentialsStoreMode, AuthStatus,
    OAuthCallbackPage, OAuthProvider, OpenAIChatGptAuthStatus, OpenAIChatGptSession,
    OpenRouterToken, PkceChallenge, clear_oauth_token_with_mode, clear_openai_chatgpt_session,
    exchange_code_for_token, exchange_openai_chatgpt_code_for_tokens, generate_openai_oauth_state,
    generate_pkce_challenge, get_auth_status_with_mode, get_auth_url,
    get_openai_chatgpt_auth_status_with_mode, get_openai_chatgpt_auth_url,
    load_openai_chatgpt_session_with_mode, parse_openai_chatgpt_manual_callback_input,
    save_oauth_token_with_mode, save_openai_chatgpt_session_with_mode,
    start_auth_code_callback_server,
};
use vtcode_config::VTCodeConfig;
use vtcode_core::config::api_keys::{ApiKeySources, get_api_key};
use vtcode_core::copilot::{
    COPILOT_AUTH_DOC_PATH, CopilotAuthEvent, CopilotAuthStatus, CopilotAuthStatusKind,
    login_with_events, logout_with_events, probe_auth_status,
};

use crate::agent::runloop::unified::url_guard::{UrlGuardPrompt, open_external_url};
use crate::codex_app_server::{
    CODEX_PROVIDER, CodexAccount, CodexAccountLoginCompleted, CodexAccountReadResponse,
    CodexAppServerClient, CodexLoginAccountResponse, CodexMcpServerStatus, ServerEvent,
    is_codex_cli_unavailable,
};

pub(crate) const OPENAI_PROVIDER: &str = "openai";
pub(crate) const OPENROUTER_PROVIDER: &str = "openrouter";
pub(crate) const COPILOT_PROVIDER: &str = "copilot";
const DEFAULT_OPENROUTER_CALLBACK_PORT: u16 = 8484;
const DEFAULT_FLOW_TIMEOUT_SECS: u64 =
    vtcode_config::constants::execution::DEFAULT_AUTH_FLOW_TIMEOUT_SECS;
const OPENAI_MANUAL_PLACEHOLDER: &str = "http://localhost:1455/auth/callback?code=...&state=...";

#[derive(Debug, Clone)]
pub(crate) struct PreparedOpenRouterLogin {
    pub(crate) auth_url: String,
    callback_port: u16,
    timeout_secs: u64,
    storage_mode: AuthCredentialsStoreMode,
    pkce: PkceChallenge,
}

#[derive(Debug, Clone)]
pub(crate) struct PreparedOpenAiLogin {
    pub(crate) auth_url: String,
    pub(crate) callback_port: u16,
    timeout_secs: u64,
    storage_mode: AuthCredentialsStoreMode,
    pkce: PkceChallenge,
    state: String,
}

pub(crate) struct StartedOpenRouterLogin {
    prepared: PreparedOpenRouterLogin,
    callback_server: AuthCodeCallbackServer,
}

pub(crate) struct StartedOpenAiLogin {
    prepared: PreparedOpenAiLogin,
    callback_server: AuthCodeCallbackServer,
}

pub(crate) fn supports_oauth_provider(provider: &str) -> bool {
    provider.parse::<OAuthProvider>().is_ok()
}

pub(crate) fn supports_auth_provider(provider: &str) -> bool {
    provider.eq_ignore_ascii_case(COPILOT_PROVIDER)
        || provider.eq_ignore_ascii_case(CODEX_PROVIDER)
        || supports_oauth_provider(provider)
}

pub(crate) fn prepare_openrouter_login(
    vt_cfg: Option<&VTCodeConfig>,
) -> Result<PreparedOpenRouterLogin> {
    let callback_port = vt_cfg
        .map(|cfg| cfg.auth.openrouter.callback_port)
        .unwrap_or(DEFAULT_OPENROUTER_CALLBACK_PORT);
    let timeout_secs = vt_cfg
        .map(|cfg| cfg.auth.openrouter.flow_timeout_secs)
        .unwrap_or(DEFAULT_FLOW_TIMEOUT_SECS);
    let storage_mode = credential_storage_mode(vt_cfg);
    let pkce = generate_pkce_challenge()?;
    let auth_url = get_auth_url(&pkce, callback_port);

    Ok(PreparedOpenRouterLogin {
        auth_url,
        callback_port,
        timeout_secs,
        storage_mode,
        pkce,
    })
}

pub(crate) async fn begin_openrouter_login(
    prepared: PreparedOpenRouterLogin,
) -> Result<StartedOpenRouterLogin> {
    let callback_server = start_auth_code_callback_server(
        prepared.callback_port,
        prepared.timeout_secs,
        OAuthCallbackPage::new(OAuthProvider::OpenRouter),
        None,
    )
    .await?;
    Ok(StartedOpenRouterLogin {
        prepared,
        callback_server,
    })
}

pub(crate) async fn complete_openrouter_login(started: StartedOpenRouterLogin) -> Result<String> {
    let StartedOpenRouterLogin {
        prepared,
        callback_server,
    } = started;

    match callback_server.wait().await? {
        AuthCallbackOutcome::Code(code) => persist_openrouter_login_code(prepared, &code).await,
        AuthCallbackOutcome::Cancelled => Err(anyhow!("OAuth flow was cancelled")),
        AuthCallbackOutcome::Error(error) => Err(anyhow!(error)),
    }
}

pub(crate) async fn complete_openrouter_login_with_tui_cancel(
    started: StartedOpenRouterLogin,
    ctrl_c_state: &Arc<CtrlCState>,
    ctrl_c_notify: &Arc<Notify>,
) -> Result<String> {
    let StartedOpenRouterLogin {
        prepared,
        callback_server,
    } = started;

    let outcome =
        wait_for_callback_or_cancel(callback_server.wait(), ctrl_c_state, ctrl_c_notify).await?;

    match outcome {
        AuthCallbackOutcome::Code(code) => persist_openrouter_login_code(prepared, &code).await,
        AuthCallbackOutcome::Cancelled => Err(anyhow!("OAuth flow was cancelled")),
        AuthCallbackOutcome::Error(error) => Err(anyhow!(error)),
    }
}

pub(crate) fn prepare_openai_login(vt_cfg: Option<&VTCodeConfig>) -> Result<PreparedOpenAiLogin> {
    let callback_port = vt_cfg
        .map(|cfg| cfg.auth.openai.callback_port)
        .unwrap_or(vtcode_auth::OpenAIAuthConfig::default().callback_port);
    let timeout_secs = vt_cfg
        .map(|cfg| cfg.auth.openai.flow_timeout_secs)
        .unwrap_or(DEFAULT_FLOW_TIMEOUT_SECS);
    let storage_mode = credential_storage_mode(vt_cfg);
    let pkce = generate_pkce_challenge()?;
    let state = generate_openai_oauth_state()?;
    let auth_url = get_openai_chatgpt_auth_url(&pkce, callback_port, &state);

    Ok(PreparedOpenAiLogin {
        auth_url,
        callback_port,
        timeout_secs,
        storage_mode,
        pkce,
        state,
    })
}

pub(crate) async fn complete_openai_login(
    started: StartedOpenAiLogin,
) -> Result<OpenAIChatGptSession> {
    complete_openai_login_with_manual_future(
        started,
        None::<std::future::Ready<Result<Option<String>>>>,
    )
    .await
}

pub(crate) async fn begin_openai_login(
    prepared: PreparedOpenAiLogin,
) -> Result<StartedOpenAiLogin> {
    let callback_server = start_auth_code_callback_server(
        prepared.callback_port,
        prepared.timeout_secs,
        OAuthCallbackPage::new(OAuthProvider::OpenAi),
        Some(prepared.state.clone()),
    )
    .await?;
    Ok(StartedOpenAiLogin {
        prepared,
        callback_server,
    })
}

pub(crate) async fn complete_openai_login_with_manual_future<ManualFut>(
    started: StartedOpenAiLogin,
    manual_input: Option<ManualFut>,
) -> Result<OpenAIChatGptSession>
where
    ManualFut: Future<Output = Result<Option<String>>>,
{
    let StartedOpenAiLogin {
        prepared,
        callback_server,
    } = started;
    let code =
        resolve_openai_authorization_code(callback_server.wait(), &prepared.state, manual_input)
            .await?;
    persist_openai_login_code(prepared, &code).await
}

pub(crate) async fn complete_openai_login_with_tui_cancel(
    started: StartedOpenAiLogin,
    ctrl_c_state: &Arc<CtrlCState>,
    ctrl_c_notify: &Arc<Notify>,
) -> Result<OpenAIChatGptSession> {
    let StartedOpenAiLogin {
        prepared,
        callback_server,
    } = started;

    let outcome =
        wait_for_callback_or_cancel(callback_server.wait(), ctrl_c_state, ctrl_c_notify).await?;

    let code = callback_outcome_to_openai_code(outcome)?;
    persist_openai_login_code(prepared, &code).await
}

pub(crate) async fn complete_openai_login_from_manual_future<ManualFut>(
    prepared: PreparedOpenAiLogin,
    manual_input: ManualFut,
) -> Result<OpenAIChatGptSession>
where
    ManualFut: Future<Output = Result<Option<String>>>,
{
    let input = manual_input
        .await?
        .ok_or_else(|| anyhow!("No redirected URL or query string provided"))?;
    let code = parse_openai_chatgpt_manual_callback_input(&input, &prepared.state)?;
    persist_openai_login_code(prepared, &code).await
}

async fn persist_openai_login_code(
    prepared: PreparedOpenAiLogin,
    code: &str,
) -> Result<OpenAIChatGptSession> {
    tracing::info!("received openai oauth authorization code; exchanging tokens");
    let session =
        exchange_openai_chatgpt_code_for_tokens(code, &prepared.pkce, prepared.callback_port)
            .await?;
    tracing::info!("openai oauth token exchange completed; persisting session");
    save_openai_chatgpt_session_with_mode(&session, prepared.storage_mode)?;
    tracing::info!("openai oauth session persisted; verifying load");
    load_openai_chatgpt_session_with_mode(prepared.storage_mode)?
        .ok_or_else(|| anyhow!("OpenAI ChatGPT session was not persisted correctly"))
}

async fn persist_openrouter_login_code(
    prepared: PreparedOpenRouterLogin,
    code: &str,
) -> Result<String> {
    let api_key = exchange_code_for_token(code, &prepared.pkce).await?;
    let token = OpenRouterToken {
        api_key: api_key.clone(),
        obtained_at: now_secs(),
        expires_at: None,
        label: Some("VT Code OAuth".to_string()),
    };
    save_oauth_token_with_mode(&token, prepared.storage_mode)?;
    Ok(api_key)
}

async fn resolve_openai_authorization_code<CallbackFut, ManualFut>(
    callback_future: CallbackFut,
    expected_state: &str,
    manual_input: Option<ManualFut>,
) -> Result<String>
where
    CallbackFut: Future<Output = Result<AuthCallbackOutcome>>,
    ManualFut: Future<Output = Result<Option<String>>>,
{
    tokio::pin!(callback_future);

    let Some(manual_input) = manual_input else {
        return callback_outcome_to_openai_code(callback_future.await?);
    };

    tokio::pin!(manual_input);

    match tokio::select! {
        callback = &mut callback_future => OpenAiAuthRace::Callback(callback),
        manual = &mut manual_input => OpenAiAuthRace::Manual(manual),
    } {
        OpenAiAuthRace::Manual(Ok(Some(input))) => {
            match parse_openai_chatgpt_manual_callback_input(&input, expected_state) {
                Ok(code) => Ok(code),
                Err(manual_err) => match callback_future.await {
                    Ok(AuthCallbackOutcome::Code(code)) => Ok(code),
                    Ok(_) | Err(_) => Err(manual_err),
                },
            }
        }
        OpenAiAuthRace::Manual(Ok(None)) => callback_outcome_to_openai_code(callback_future.await?),
        OpenAiAuthRace::Manual(Err(err)) => Err(err),
        OpenAiAuthRace::Callback(Ok(outcome)) => match callback_outcome_to_openai_code(outcome) {
            Ok(code) => Ok(code),
            Err(callback_err) => match manual_input.await? {
                Some(input) => parse_openai_chatgpt_manual_callback_input(&input, expected_state),
                None => Err(callback_err),
            },
        },
        OpenAiAuthRace::Callback(Err(callback_err)) => match manual_input.await? {
            Some(input) => parse_openai_chatgpt_manual_callback_input(&input, expected_state),
            None => Err(callback_err),
        },
    }
}

async fn wait_for_callback_or_cancel<CallbackFut>(
    callback_future: CallbackFut,
    ctrl_c_state: &Arc<CtrlCState>,
    ctrl_c_notify: &Arc<Notify>,
) -> Result<AuthCallbackOutcome>
where
    CallbackFut: Future<Output = Result<AuthCallbackOutcome>>,
{
    let cancel_future = wait_for_ctrl_c_cancel(ctrl_c_state, ctrl_c_notify);
    tokio::pin!(callback_future);
    tokio::pin!(cancel_future);

    tokio::select! {
        biased;
        _ = &mut cancel_future => {
            if ctrl_c_state.is_cancel_requested() {
                ctrl_c_state.mark_cancel_handled();
            }
            Ok(AuthCallbackOutcome::Cancelled)
        }
        outcome = &mut callback_future => outcome,
    }
}

async fn wait_for_ctrl_c_cancel(ctrl_c_state: &Arc<CtrlCState>, ctrl_c_notify: &Arc<Notify>) {
    if ctrl_c_state.is_cancel_requested() || ctrl_c_state.is_exit_requested() {
        return;
    }
    ctrl_c_notify.notified().await;
}

enum OpenAiAuthRace {
    Callback(Result<AuthCallbackOutcome>),
    Manual(Result<Option<String>>),
}

fn callback_outcome_to_openai_code(outcome: AuthCallbackOutcome) -> Result<String> {
    match outcome {
        AuthCallbackOutcome::Code(code) => Ok(code),
        AuthCallbackOutcome::Cancelled => Err(oauth_flow_cancelled_error()),
        AuthCallbackOutcome::Error(error) => Err(anyhow!(error)),
    }
}

#[derive(Debug, thiserror::Error)]
#[error("OAuth flow was cancelled")]
struct OAuthFlowCancelled;

#[cold]
pub(crate) fn oauth_flow_cancelled_error() -> anyhow::Error {
    anyhow!(OAuthFlowCancelled)
}

pub(crate) fn is_oauth_flow_cancelled(err: &anyhow::Error) -> bool {
    err.is::<OAuthFlowCancelled>()
}

pub(crate) fn clear_openrouter_login(vt_cfg: Option<&VTCodeConfig>) -> Result<()> {
    clear_oauth_token_with_mode(credential_storage_mode(vt_cfg))
}

pub(crate) fn clear_openai_login(_vt_cfg: Option<&VTCodeConfig>) -> Result<()> {
    clear_openai_chatgpt_session()
}

pub(crate) fn openrouter_auth_status(vt_cfg: Option<&VTCodeConfig>) -> Result<AuthStatus> {
    get_auth_status_with_mode(credential_storage_mode(vt_cfg))
}

pub(crate) fn openai_auth_status(vt_cfg: Option<&VTCodeConfig>) -> Result<OpenAIChatGptAuthStatus> {
    get_openai_chatgpt_auth_status_with_mode(credential_storage_mode(vt_cfg))
}

pub(crate) fn load_openai_session(
    vt_cfg: Option<&VTCodeConfig>,
) -> Result<Option<OpenAIChatGptSession>> {
    load_openai_chatgpt_session_with_mode(credential_storage_mode(vt_cfg))
}

pub(crate) fn refresh_openai_login(
    vt_cfg: Option<&VTCodeConfig>,
) -> impl Future<Output = Result<OpenAIChatGptSession>> + '_ {
    vtcode_auth::refresh_openai_chatgpt_session_with_mode(credential_storage_mode(vt_cfg))
}

pub(crate) async fn refresh_openai_login_if_available(
    vt_cfg: Option<&VTCodeConfig>,
) -> Result<Option<OpenAIChatGptSession>> {
    if load_openai_session(vt_cfg)?.is_none() {
        return Ok(None);
    }

    refresh_openai_login(vt_cfg).await.map(Some)
}

pub(crate) async fn handle_login_command(
    vt_cfg: Option<&VTCodeConfig>,
    provider: &str,
    device_code: bool,
) -> Result<()> {
    let provider = provider.trim().to_ascii_lowercase();
    if device_code && provider != CODEX_PROVIDER {
        return Err(anyhow!(
            "`--device-code` is currently supported only for `vtcode login codex`."
        ));
    }

    if provider == CODEX_PROVIDER {
        let client = CodexAppServerClient::connect(vt_cfg).await?;
        let status = client.account_read().await?;
        if status.account.is_some() {
            print_existing_codex_auth_summary(&status);
            return Ok(());
        }
        if !status.requires_openai_auth {
            println!("Codex does not currently require OpenAI authentication for this setup.");
            return Ok(());
        }

        let response = if device_code {
            client.account_login_chatgpt_device_code().await?
        } else {
            client.account_login_chatgpt().await?
        };
        let login_id = match response {
            CodexLoginAccountResponse::ChatGpt { auth_url, login_id } => {
                println!("Starting Codex ChatGPT authentication...");
                open_browser_or_print_url(&auth_url)?;
                login_id
            }
            CodexLoginAccountResponse::ChatGptDeviceCode {
                verification_url,
                user_code,
                login_id,
            } => {
                println!("Starting Codex ChatGPT device-code authentication...");
                println!("Enter this code after signing in:");
                println!("{user_code}");
                vtcode_commons::ansi_codes::notify_attention(
                    true,
                    Some(&format!("Codex device code: {user_code}")),
                );
                open_browser_or_print_url(&verification_url)?;
                login_id
            }
            CodexLoginAccountResponse::ApiKey => {
                println!("Codex is configured with API key authentication.");
                return Ok(());
            }
            CodexLoginAccountResponse::ChatGptAuthTokens => {
                return Err(anyhow!(
                    "Codex requested externally managed ChatGPT tokens; `vtcode login codex` supports only Codex-managed ChatGPT login flows"
                ));
            }
        };
        let mut events = client.subscribe();
        let completion =
            wait_for_codex_account_login_completion(&mut events, Some(&login_id)).await?;
        if !completion.success {
            return Err(anyhow!(
                completion
                    .error
                    .unwrap_or_else(|| "Codex ChatGPT login failed".to_string())
            ));
        }
        let updated = client.account_read().await?;
        print_codex_login_summary(&updated);
        return Ok(());
    }

    if provider == COPILOT_PROVIDER {
        let workspace = current_auth_workspace();
        let auth_cfg = vt_cfg
            .map(|cfg| cfg.auth.copilot.clone())
            .unwrap_or_default();
        println!("Starting GitHub Copilot authentication via the official `copilot` CLI...");
        login_with_events(&auth_cfg, &workspace, print_copilot_auth_event).await?;
        println!("GitHub Copilot authentication complete.");
        return Ok(());
    }

    match provider.parse::<OAuthProvider>() {
        Ok(OAuthProvider::OpenRouter) => {
            let prepared = prepare_openrouter_login(vt_cfg)?;
            let auth_url = prepared.auth_url.clone();
            let started = begin_openrouter_login(prepared).await?;
            println!("Starting OpenRouter OAuth authentication...");
            open_browser_or_print_url(&auth_url)?;
            let api_key = complete_openrouter_login(started).await?;
            println!("OpenRouter authentication complete.");
            println!(
                "Stored secure OAuth token. Key preview: {}...",
                &api_key[..api_key.len().min(8)]
            );
            Ok(())
        }
        Ok(OAuthProvider::OpenAi) => {
            let prepared = prepare_openai_login(vt_cfg)?;
            let started = begin_openai_login(prepared.clone()).await;
            println!("Starting OpenAI ChatGPT authentication...");
            open_browser_or_print_url(&prepared.auth_url)?;
            print_openai_manual_guidance();
            let session = complete_openai_login_with_cli_fallback(prepared, started).await?;
            println!("OpenAI ChatGPT authentication complete.");
            if let Some(email) = session.email.as_deref() {
                println!("Account: {}", email);
            }
            if let Some(plan) = session.plan.as_deref() {
                println!("Plan: {}", plan);
            }
            Ok(())
        }
        Err(()) => Err(anyhow!(
            "Authentication is not supported for provider '{}'. Supported providers: openai, openrouter, copilot, codex",
            provider
        )),
    }
}

pub(crate) async fn handle_logout_command(
    vt_cfg: Option<&VTCodeConfig>,
    provider: &str,
) -> Result<()> {
    let provider = provider.trim().to_ascii_lowercase();
    if provider == CODEX_PROVIDER {
        let client = CodexAppServerClient::connect(vt_cfg).await?;
        let status = client.account_read().await?;
        if status.account.is_none() {
            println!("Codex account credentials already cleared.");
            return Ok(());
        }
        client.account_logout().await?;
        println!("Codex account credentials cleared.");
        return Ok(());
    }

    if provider == COPILOT_PROVIDER {
        let workspace = current_auth_workspace();
        let auth_cfg = vt_cfg
            .map(|cfg| cfg.auth.copilot.clone())
            .unwrap_or_default();
        logout_with_events(&auth_cfg, &workspace, print_copilot_auth_event).await?;
        println!("GitHub Copilot authentication cleared.");
        return Ok(());
    }

    match provider.parse::<OAuthProvider>() {
        Ok(OAuthProvider::OpenRouter) => {
            if matches!(
                openrouter_auth_status(vt_cfg)?,
                AuthStatus::NotAuthenticated
            ) {
                println!("OpenRouter OAuth token already cleared.");
                return Ok(());
            }
            clear_openrouter_login(vt_cfg)?;
            println!("OpenRouter OAuth token cleared.");
            Ok(())
        }
        Ok(OAuthProvider::OpenAi) => {
            if matches!(
                openai_auth_status(vt_cfg)?,
                OpenAIChatGptAuthStatus::NotAuthenticated
            ) {
                println!("OpenAI ChatGPT session already cleared.");
                return Ok(());
            }
            clear_openai_login(vt_cfg)?;
            println!("OpenAI ChatGPT session cleared.");
            Ok(())
        }
        Err(()) => Err(anyhow!(
            "Authentication is not supported for provider '{}'. Supported providers: openai, openrouter, copilot, codex",
            provider
        )),
    }
}

pub(crate) async fn handle_show_auth_command(
    vt_cfg: Option<&VTCodeConfig>,
    provider: Option<&str>,
) -> Result<()> {
    println!("Authentication Status");
    println!();

    match provider.map(str::trim).filter(|value| !value.is_empty()) {
        Some(value) if value.eq_ignore_ascii_case(CODEX_PROVIDER) => {
            let client = CodexAppServerClient::connect(vt_cfg).await?;
            render_codex_auth_status(
                client.account_read().await?,
                Some(client.mcp_server_status_list().await?.data.as_slice()),
            );
        }
        Some(value) if value.eq_ignore_ascii_case(COPILOT_PROVIDER) => {
            let workspace = current_auth_workspace();
            let auth_cfg = vt_cfg
                .map(|cfg| cfg.auth.copilot.clone())
                .unwrap_or_default();
            render_copilot_auth_status(probe_auth_status(&auth_cfg, Some(&workspace)).await);
        }
        Some(value) => match value.parse::<OAuthProvider>() {
            Ok(OAuthProvider::OpenRouter) => {
                render_openrouter_auth_status(openrouter_auth_status(vt_cfg)?)
            }
            Ok(OAuthProvider::OpenAi) => render_openai_auth_status(openai_auth_status(vt_cfg)?),
            Err(()) => {
                return Err(anyhow!(
                    "Authentication is not supported for provider '{}'. Supported providers: openai, openrouter, copilot, codex",
                    value
                ));
            }
        },
        None => {
            match CodexAppServerClient::connect(vt_cfg).await {
                Ok(client) => {
                    let mcp_statuses = client.mcp_server_status_list().await?;
                    render_codex_auth_status(
                        client.account_read().await?,
                        Some(mcp_statuses.data.as_slice()),
                    );
                }
                Err(err) if is_codex_cli_unavailable(&err) => {
                    render_codex_unavailable_status();
                }
                Err(err) => return Err(err),
            }
            println!();
            render_openrouter_auth_status(openrouter_auth_status(vt_cfg)?);
            println!();
            render_openai_auth_status(openai_auth_status(vt_cfg)?);
            println!();
            let workspace = current_auth_workspace();
            let auth_cfg = vt_cfg
                .map(|cfg| cfg.auth.copilot.clone())
                .unwrap_or_default();
            render_copilot_auth_status(probe_auth_status(&auth_cfg, Some(&workspace)).await);
            println!();
            println!("Use `vtcode login <provider>` to authenticate.");
            println!("Use `vtcode logout <provider>` to clear stored credentials.");
        }
    }

    Ok(())
}

fn credential_storage_mode(vt_cfg: Option<&VTCodeConfig>) -> AuthCredentialsStoreMode {
    vt_cfg
        .map(|cfg| cfg.agent.credential_storage_mode)
        .unwrap_or_default()
}

fn current_auth_workspace() -> PathBuf {
    std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}

pub(crate) fn openai_manual_placeholder(callback_port: u16) -> String {
    OPENAI_MANUAL_PLACEHOLDER.replace("1455", &callback_port.to_string())
}

fn print_openai_manual_guidance() {
    println!(
        "If the browser cannot reach localhost or you are on SSH/headless, paste the redirected URL or raw query string here."
    );
}

async fn complete_openai_login_with_cli_fallback(
    prepared: PreparedOpenAiLogin,
    started: Result<StartedOpenAiLogin>,
) -> Result<OpenAIChatGptSession> {
    match started {
        Ok(started) => match complete_openai_login(started).await {
            Ok(session) => Ok(session),
            Err(err) if should_prompt_manual_openai_input(&err) => {
                let Some(input) = prompt_openai_manual_input_cli_once()? else {
                    return Err(err);
                };
                complete_openai_login_from_manual_future(
                    prepared,
                    std::future::ready(Ok(Some(input))),
                )
                .await
            }
            Err(err) => Err(err),
        },
        Err(err) if should_prompt_manual_openai_input(&err) => {
            let Some(input) = prompt_openai_manual_input_cli_once()? else {
                return Err(err);
            };
            complete_openai_login_from_manual_future(prepared, std::future::ready(Ok(Some(input))))
                .await
        }
        Err(err) => Err(err),
    }
}

pub(crate) fn should_prompt_manual_openai_input(err: &anyhow::Error) -> bool {
    let message = err.to_string();
    message.contains("failed to bind localhost callback server") || message.contains("timed out")
}

fn prompt_openai_manual_input_cli_once() -> Result<Option<String>> {
    tokio::task::block_in_place(|| -> Result<Option<String>> {
        println!("Paste the redirected URL/query and press Enter when ready.");
        print!("Redirect URL or query: ");
        vtcode_core::ui::terminal::flush_stdout();
        let input = vtcode_core::ui::terminal::read_line()
            .map_err(|err| anyhow!("stdin read failed: {err}"))?;
        let trimmed = input.trim();
        if trimmed.is_empty() {
            Ok(None)
        } else {
            Ok(Some(trimmed.to_string()))
        }
    })
}

pub(crate) fn open_browser_or_print_url(url: &str) -> Result<()> {
    println!("Open this URL to continue:");
    println!(
        "{}{}{}",
        vtcode_commons::ansi_codes::hyperlink_open(url),
        url,
        vtcode_commons::ansi_codes::hyperlink_close(),
    );

    if !confirm_cli_browser_open(url)? {
        println!(
            "Automatic browser launch skipped. Open the URL manually if you want to continue."
        );
        return Ok(());
    }

    if let Err(err) = open_external_url(url) {
        eprintln!("warning: {}", err);
    }

    Ok(())
}

async fn wait_for_codex_account_login_completion(
    events: &mut tokio::sync::broadcast::Receiver<ServerEvent>,
    login_id: Option<&str>,
) -> Result<CodexAccountLoginCompleted> {
    loop {
        let event = match events.recv().await {
            Ok(event) => event,
            Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
            Err(tokio::sync::broadcast::error::RecvError::Closed) => {
                return Err(anyhow!(
                    "lost connection while waiting for Codex login completion"
                ));
            }
        };

        if event.method != "account/login/completed" {
            continue;
        }

        let completion: CodexAccountLoginCompleted =
            serde_json::from_value(event.params).context("invalid Codex login completion event")?;
        if login_id.is_none() || completion.login_id.as_deref() == login_id {
            return Ok(completion);
        }
    }
}

fn render_codex_auth_status(
    status: CodexAccountReadResponse,
    mcp_statuses: Option<&[CodexMcpServerStatus]>,
) {
    println!("Codex");
    match status.account {
        Some(CodexAccount::ApiKey) => {
            println!("  Auth: API key");
        }
        Some(CodexAccount::ChatGpt { email, plan_type }) => {
            println!("  Auth: ChatGPT");
            println!("  Account: {email}");
            println!("  Plan: {plan_type}");
        }
        None if status.requires_openai_auth => {
            println!("  Auth: not authenticated");
        }
        None => {
            println!("  Auth: not required");
        }
    }

    if let Some(mcp_statuses) = mcp_statuses
        && !mcp_statuses.is_empty()
    {
        println!("  MCP:");
        for server in mcp_statuses {
            println!("    {}: {}", server.name, server.auth_status);
        }
    }
}

fn render_codex_unavailable_status() {
    println!("Codex");
    println!("  Auth: sidecar unavailable");
    println!(
        "  Help: {}",
        crate::codex_app_server::codex_sidecar_requirement_note()
    );
}

fn print_codex_login_summary(status: &CodexAccountReadResponse) {
    println!("Codex authentication complete.");
    if let Some(CodexAccount::ChatGpt { email, plan_type }) = &status.account {
        println!("Account: {email}");
        println!("Plan: {plan_type}");
    }
}

fn print_existing_codex_auth_summary(status: &CodexAccountReadResponse) {
    match &status.account {
        Some(CodexAccount::ApiKey) => println!("Codex is already authenticated with an API key."),
        Some(CodexAccount::ChatGpt { email, plan_type }) => {
            println!("Codex is already authenticated.");
            println!("Account: {email}");
            println!("Plan: {plan_type}");
        }
        None => {}
    }
}

fn confirm_cli_browser_open(url: &str) -> Result<bool> {
    let Some(prompt) = UrlGuardPrompt::parse(url.to_string()) else {
        return Ok(false);
    };

    if vtcode_core::ui::terminal::is_piped_input() || vtcode_core::ui::terminal::is_piped_output() {
        println!(
            "Automatic browser launch skipped because approval requires an interactive terminal."
        );
        return Ok(false);
    }

    for line in prompt.cli_lines() {
        println!("{}", line);
    }

    print!("Open in browser now? [y/N]: ");
    vtcode_core::ui::terminal::flush_stdout();
    let input = vtcode_core::ui::terminal::read_line()
        .map_err(|err| anyhow!("stdin read failed: {err}"))?;
    Ok(cli_browser_open_approved(&input))
}

fn cli_browser_open_approved(input: &str) -> bool {
    matches!(input.trim().to_ascii_lowercase().as_str(), "y" | "yes")
}

fn render_openrouter_auth_status(status: AuthStatus) {
    match status {
        AuthStatus::Authenticated {
            label,
            age_seconds,
            expires_in,
        } => {
            println!("OpenRouter: authenticated (OAuth)");
            if let Some(label) = label {
                println!("  Label: {}", label);
            }
            println!("  Token obtained: {}", format_auth_duration(age_seconds));
            if let Some(expires_in) = expires_in {
                println!("  Expires in: {}", format_auth_duration(expires_in));
            }
        }
        AuthStatus::NotAuthenticated => {
            if get_api_key(OPENROUTER_PROVIDER, &ApiKeySources::default()).is_ok() {
                println!("OpenRouter: using OPENROUTER_API_KEY");
            } else {
                println!("OpenRouter: not authenticated");
            }
        }
    }
}

fn render_openai_auth_status(status: OpenAIChatGptAuthStatus) {
    match status {
        OpenAIChatGptAuthStatus::Authenticated {
            label,
            age_seconds,
            expires_in,
        } => {
            println!("OpenAI: authenticated (ChatGPT)");
            if let Some(label) = label {
                println!("  Label: {}", label);
            }
            println!("  Session obtained: {}", format_auth_duration(age_seconds));
            if let Some(expires_in) = expires_in {
                println!("  Expires in: {}", format_auth_duration(expires_in));
            }
        }
        OpenAIChatGptAuthStatus::NotAuthenticated => {
            if get_api_key(OPENAI_PROVIDER, &ApiKeySources::default()).is_ok() {
                println!("OpenAI: using OPENAI_API_KEY");
            } else {
                println!("OpenAI: not authenticated");
            }
        }
    }
}

fn render_copilot_auth_status(status: CopilotAuthStatus) {
    match status.kind {
        CopilotAuthStatusKind::Authenticated => {
            println!("GitHub Copilot: authenticated (managed auth via Copilot CLI)")
        }
        CopilotAuthStatusKind::Unauthenticated => println!("GitHub Copilot: not authenticated"),
        CopilotAuthStatusKind::ServerUnavailable => println!("GitHub Copilot: CLI unavailable"),
        CopilotAuthStatusKind::AuthFlowFailed => println!("GitHub Copilot: auth flow failed"),
    }

    if let Some(message) = status.message.as_deref()
        && !message.trim().is_empty()
    {
        println!("  Details: {}", message);
    }

    if matches!(status.kind, CopilotAuthStatusKind::ServerUnavailable) {
        println!(
            "  Help: install `copilot` or configure `[auth.copilot].command`; see {}",
            COPILOT_AUTH_DOC_PATH
        );
    }
}

fn print_copilot_auth_event(event: CopilotAuthEvent) -> Result<()> {
    match event {
        CopilotAuthEvent::VerificationCode { url, user_code } => {
            println!("GitHub device code (copy this): {}", user_code);
            vtcode_commons::ansi_codes::notify_attention(
                true,
                Some(&format!("GitHub device code: {user_code}")),
            );
            println!("Open this URL to continue:");
            println!("{}", url);
        }
        CopilotAuthEvent::Progress { message } => println!("{}", message),
        CopilotAuthEvent::Success { account } => {
            if let Some(account) = account {
                println!("GitHub Copilot account: {}", account);
            }
        }
        CopilotAuthEvent::Failure { message } => {
            eprintln!("GitHub Copilot auth failed: {}", message)
        }
    }

    Ok(())
}

fn format_auth_duration(seconds: u64) -> String {
    if seconds < 60 {
        format!("{seconds}s")
    } else if seconds < 3600 {
        format!("{}m {}s", seconds / 60, seconds % 60)
    } else if seconds < 86_400 {
        format!("{}h {}m", seconds / 3600, (seconds % 3600) / 60)
    } else {
        format!("{}d {}h", seconds / 86_400, (seconds % 86_400) / 3600)
    }
}

fn now_secs() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}

#[cfg(test)]
mod tests {
    use super::{
        clear_openai_login, clear_openrouter_login, cli_browser_open_approved,
        handle_login_command, resolve_openai_authorization_code, wait_for_callback_or_cancel,
    };
    use crate::agent::runloop::unified::state::{CtrlCSignal, CtrlCState};
    use anyhow::anyhow;
    use serial_test::serial;
    use std::future;
    use std::sync::Arc;
    use tokio::sync::Notify;
    use vtcode_auth::{
        AuthCallbackOutcome, AuthCredentialsStoreMode, OpenAIAuthConfig, OpenAIChatGptSession,
        OpenAIResolvedAuth, OpenRouterToken, clear_oauth_token_with_mode,
        clear_openai_chatgpt_session, clear_openai_chatgpt_session_with_mode,
        load_oauth_token_with_mode, load_openai_chatgpt_session_with_mode, resolve_openai_auth,
        save_oauth_token_with_mode, save_openai_chatgpt_session_with_mode,
    };
    use vtcode_config::VTCodeConfig;

    fn config_with_storage_mode(mode: AuthCredentialsStoreMode) -> VTCodeConfig {
        let mut config = VTCodeConfig::default();
        config.agent.credential_storage_mode = mode;
        config
    }

    fn sample_openai_session(api_key: &str) -> OpenAIChatGptSession {
        OpenAIChatGptSession {
            openai_api_key: api_key.to_string(),
            id_token: "id-token".to_string(),
            access_token: "access-token".to_string(),
            refresh_token: "refresh-token".to_string(),
            account_id: Some("account-123".to_string()),
            email: Some("user@example.com".to_string()),
            plan: Some("plus".to_string()),
            obtained_at: 1,
            refreshed_at: 1,
            expires_at: Some(2),
        }
    }

    fn sample_openrouter_token(api_key: &str) -> OpenRouterToken {
        OpenRouterToken {
            api_key: api_key.to_string(),
            obtained_at: 1,
            expires_at: None,
            label: Some("test-token".to_string()),
        }
    }

    #[test]
    fn cli_browser_open_approved_accepts_yes_only() {
        assert!(cli_browser_open_approved("y"));
        assert!(cli_browser_open_approved("YES"));
        assert!(!cli_browser_open_approved("n"));
        assert!(!cli_browser_open_approved(""));
    }

    #[tokio::test]
    async fn openai_code_resolution_accepts_manual_query_input() {
        let code = resolve_openai_authorization_code(
            future::pending::<Result<AuthCallbackOutcome, anyhow::Error>>(),
            "test-state",
            Some(future::ready(Ok(Some(
                "code=manual-code&state=test-state".to_string(),
            )))),
        )
        .await
        .expect("manual input should resolve");
        assert_eq!(code, "manual-code");
    }

    #[tokio::test]
    async fn openai_code_resolution_falls_back_to_manual_when_callback_fails() {
        let code = resolve_openai_authorization_code(
            future::ready(Err(anyhow!("failed to bind localhost callback server"))),
            "test-state",
            Some(future::ready(Ok(Some(
                "http://localhost:1455/auth/callback?code=manual-code&state=test-state".to_string(),
            )))),
        )
        .await
        .expect("manual fallback should resolve");
        assert_eq!(code, "manual-code");
    }

    #[tokio::test]
    async fn openai_code_resolution_rejects_manual_bare_code() {
        let error = resolve_openai_authorization_code(
            future::ready(Err(anyhow!("OAuth flow timed out after 300 seconds"))),
            "test-state",
            Some(future::ready(Ok(Some("manual-code".to_string())))),
        )
        .await
        .expect_err("bare code should fail");
        assert!(
            error
                .to_string()
                .contains("full redirect url or query string")
        );
    }

    #[tokio::test]
    async fn openai_code_resolution_uses_callback_when_manual_input_is_invalid() {
        let code = resolve_openai_authorization_code(
            future::ready(Ok(AuthCallbackOutcome::Code("callback-code".to_string()))),
            "test-state",
            Some(future::ready(Ok(Some("manual-code".to_string())))),
        )
        .await
        .expect("callback code should win after invalid manual input");
        assert_eq!(code, "callback-code");
    }

    #[tokio::test]
    async fn callback_wait_returns_cancelled_when_ctrl_c_is_pending() {
        let ctrl_c_state = Arc::new(CtrlCState::new());
        assert!(matches!(
            ctrl_c_state.register_signal(),
            CtrlCSignal::Cancel
        ));
        let ctrl_c_notify = Arc::new(Notify::new());

        let outcome = wait_for_callback_or_cancel(
            future::pending::<Result<AuthCallbackOutcome, anyhow::Error>>(),
            &ctrl_c_state,
            &ctrl_c_notify,
        )
        .await
        .expect("cancelled callback wait");

        assert!(matches!(outcome, AuthCallbackOutcome::Cancelled));
        assert!(!ctrl_c_state.is_cancel_requested());
    }

    #[tokio::test]
    async fn device_code_flag_is_rejected_for_non_codex_provider() {
        let error = handle_login_command(None, "openai", true)
            .await
            .expect_err("non-codex device-code login should fail");

        assert!(
            error
                .to_string()
                .contains("supported only for `vtcode login codex`")
        );
    }

    #[test]
    #[serial]
    fn openai_logout_clears_all_sessions_and_falls_back_to_api_key() {
        let _ = clear_openai_chatgpt_session();
        let _ = clear_openai_chatgpt_session_with_mode(AuthCredentialsStoreMode::File);
        let _ = clear_openai_chatgpt_session_with_mode(AuthCredentialsStoreMode::Keyring);

        let file_session = sample_openai_session("file-api-key");
        let keyring_session = sample_openai_session("keyring-api-key");
        if save_openai_chatgpt_session_with_mode(&file_session, AuthCredentialsStoreMode::File)
            .is_err()
        {
            return;
        }

        if save_openai_chatgpt_session_with_mode(
            &keyring_session,
            AuthCredentialsStoreMode::Keyring,
        )
        .is_err()
        {
            let _ = clear_openai_chatgpt_session_with_mode(AuthCredentialsStoreMode::File);
            return;
        }

        let config = config_with_storage_mode(AuthCredentialsStoreMode::File);
        clear_openai_login(Some(&config)).expect("clear openai login");

        assert!(
            load_openai_chatgpt_session_with_mode(AuthCredentialsStoreMode::File)
                .expect("load file session")
                .is_none()
        );
        assert!(
            load_openai_chatgpt_session_with_mode(AuthCredentialsStoreMode::Keyring)
                .expect("load keyring session")
                .is_none()
        );
        assert!(matches!(
            resolve_openai_auth(
                &OpenAIAuthConfig::default(),
                AuthCredentialsStoreMode::File,
                Some("env-api-key".to_string())
            )
            .expect("resolve auth after logout"),
            OpenAIResolvedAuth::ApiKey { api_key } if api_key == "env-api-key"
        ));

        let _ = clear_openai_chatgpt_session_with_mode(AuthCredentialsStoreMode::File);
        let _ = clear_openai_chatgpt_session_with_mode(AuthCredentialsStoreMode::Keyring);
    }

    #[test]
    #[serial]
    fn openrouter_logout_clears_only_configured_storage_mode() {
        let _ = clear_oauth_token_with_mode(AuthCredentialsStoreMode::File);
        let _ = clear_oauth_token_with_mode(AuthCredentialsStoreMode::Keyring);

        let file_token = sample_openrouter_token("file-token");
        let keyring_token = sample_openrouter_token("keyring-token");
        if save_oauth_token_with_mode(&file_token, AuthCredentialsStoreMode::File).is_err() {
            return;
        }

        if save_oauth_token_with_mode(&keyring_token, AuthCredentialsStoreMode::Keyring).is_err() {
            let _ = clear_oauth_token_with_mode(AuthCredentialsStoreMode::File);
            return;
        }

        let Some(loaded_keyring_token) =
            load_oauth_token_with_mode(AuthCredentialsStoreMode::Keyring)
                .expect("load keyring token after save")
        else {
            let _ = clear_oauth_token_with_mode(AuthCredentialsStoreMode::File);
            let _ = clear_oauth_token_with_mode(AuthCredentialsStoreMode::Keyring);
            return;
        };
        if loaded_keyring_token.api_key != "keyring-token" {
            let _ = clear_oauth_token_with_mode(AuthCredentialsStoreMode::File);
            let _ = clear_oauth_token_with_mode(AuthCredentialsStoreMode::Keyring);
            return;
        }

        let config = config_with_storage_mode(AuthCredentialsStoreMode::File);
        clear_openrouter_login(Some(&config)).expect("clear openrouter login");

        assert!(
            load_oauth_token_with_mode(AuthCredentialsStoreMode::File)
                .expect("load file token")
                .is_none()
        );
        assert_eq!(
            load_oauth_token_with_mode(AuthCredentialsStoreMode::Keyring)
                .expect("load keyring token")
                .expect("keyring token should remain")
                .api_key,
            "keyring-token"
        );

        let _ = clear_oauth_token_with_mode(AuthCredentialsStoreMode::File);
        let _ = clear_oauth_token_with_mode(AuthCredentialsStoreMode::Keyring);
    }
}
