#[derive(clap::Args)]
pub struct LoginArgs {
    /// Override OAuth 2 grant type to use.
    #[arg(long)]
    pub oauth_grant_type: Option<crate::config::OAuthGrantType>,

    /// Credential server ID or URL to use.
    pub server_name: String,
}

#[tokio::main]
pub async fn run(args: &LoginArgs) -> Result<(), anyhow::Error> {
    let mut agent = crate::cmd::agent::connect_or_start().await?;
    login(&mut agent, args).await
}

pub async fn login(
    agent: &mut crate::agent::AgentConn,
    args: &LoginArgs,
) -> Result<(), anyhow::Error> {
    let mut server = get_server(agent, &args.server_name).await?;
    if server.aws_sso.is_some() && server.oauth.is_none() {
        agent
            .refresh_aws_sso_client_registration(
                crate::proto::RefreshAwsSsoClientRegistrationRequest {
                    server_id: server.id().to_owned(),
                    force: false,
                },
            )
            .await?;
        server = get_server(agent, &args.server_name).await?;
    }
    server.validate()?;

    let oauth_grant_type = match args.oauth_grant_type {
        Some(x) => Ok(x),
        // XXX: this must live in config.rs
        None if server.aws_sso.is_some() => Ok(crate::config::OAuthGrantType::Code),
        None => {
            let oauth = server
                .oauth
                .as_ref()
                .ok_or_else(|| crate::Error::ConfigError("server missing oauth".to_string()))?;
            oauth.default_grant_type()
        }
    }?;

    tracing::debug!(oauth_grant_type = ?oauth_grant_type, server = ?server, "Using OAuth");

    match oauth_grant_type {
        crate::config::OAuthGrantType::Code => do_oauth_code(agent, server).await,
        crate::config::OAuthGrantType::DeviceCode => do_oauth_device_code(agent, server).await,
    }
}

pub async fn do_oauth_code(
    agent: &mut crate::agent::AgentConn,
    server: crate::config::Server,
) -> Result<(), anyhow::Error> {
    let (path, mut local_port, use_localhost) = if let Some(ref aws_sso) = server.aws_sso {
        ("/oauth/callback", aws_sso.local_port, false)
    } else {
        let (_oauth, code_grant) = server.try_oauth_code_grant()?;
        (
            "/oauth2callback",
            code_grant.local_port,
            code_grant.use_localhost,
        )
    };

    if let Ok(x) = std::env::var("MAIRU_LOCAL_PORT")
        && let Ok(i) = x.parse()
    {
        local_port = Some(i);
    }

    let (listener, url) = match crate::oauth_code::bind_tcp_for_callback(
        path,
        local_port,
        use_localhost,
    )
    .await
    {
        Ok(t) => t,
        Err(e) => anyhow::bail!(
            "Failed to bind TCP server for OAuth 2.0 callback acceptance, perhaps there is concurrent mairu-exec call waitng for login, or occupied by oher process; {}",
            e
        ),
    };

    let session = agent
        .initiate_oauth_code(crate::proto::InitiateOAuthCodeRequest {
            server_id: server.id().to_owned(),
            redirect_url: url.to_string(),
        })
        .await?
        .into_inner();
    tracing::debug!(session = ?session, "Initiated OAuth 2.0 authorization code flow");

    let product = env!("CARGO_PKG_NAME");
    let server_id = server.id();
    let server_url = &server.url;

    let (auth_route, short_authorize_url) =
        crate::oauth_code::generate_short_authorize_url(&listener, use_localhost)?;

    crate::terminal::send(&indoc::formatdoc! {"
        :: {product} :: Login to {server_id} ({server_url}) ::::::::
        :: {product} ::
        :: {product} ::
        :: {product} :: Open the following URL to continue
        :: {product} :: >   {short_authorize_url}
        :: {product} ::
        :: {product} ::
    "})
    .await;

    crate::oauth_code::listen_for_callback(listener, session, agent, auth_route).await?;
    tracing::info!("Logged in");
    Ok(())
}

pub async fn do_oauth_device_code(
    agent: &mut crate::agent::AgentConn,
    server: crate::config::Server,
) -> Result<(), anyhow::Error> {
    if server.aws_sso.is_none() {
        server.try_oauth_device_code_grant()?;
    }

    let session = agent
        .initiate_oauth_device_code(crate::proto::InitiateOAuthDeviceCodeRequest {
            server_id: server.id().to_owned(),
        })
        .await?
        .into_inner();
    tracing::debug!(session = ?session, "Initiated flow");

    let product = env!("CARGO_PKG_NAME");
    let server_id = server.id();
    let server_url = &server.url;
    let user_code = &session.user_code;
    let mut authorize_url = &session.verification_uri_complete;
    if authorize_url.is_empty() {
        authorize_url = &session.verification_uri;
    }

    crate::terminal::send(&indoc::formatdoc! {"
        :: {product} :: Login to {server_id} ({server_url}) ::::::::
        :: {product} ::
        :: {product} ::   Your Verification Code: {user_code}
        :: {product} ::      To authorize, visit: {authorize_url}
        :: {product} ::
    "})
    .await;

    let mut interval = session.interval as u64;
    loop {
        tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
        let completion = agent
            .complete_oauth_device_code(crate::proto::CompleteOAuthDeviceCodeRequest {
                handle: session.handle.clone(),
            })
            .await;

        match completion {
            Ok(_) => break,
            Err(e) if e.code() == tonic::Code::ResourceExhausted => {
                interval += 5;
                tracing::debug!(interval = ?interval, "Received slow_down request");
            }
            Err(e) if e.code() == tonic::Code::FailedPrecondition => {
                // continue
            }
            Err(e) => {
                anyhow::bail!(e);
            }
        }
    }

    tracing::info!("Logged in");
    Ok(())
}

async fn get_server(
    agent: &mut crate::agent::AgentConn,
    query: &str,
) -> anyhow::Result<crate::config::Server> {
    Ok(agent
        .get_server(tonic::Request::new(crate::proto::GetServerRequest {
            query: query.to_owned(),
            no_cache: true,
            check_session: false,
        }))
        .await?
        .into_inner()
        .try_into()?)
}
