//! Lucid Cloud sign-in.
//!
//! Logs the user into the hosted Lucid Cloud proxy with Google, using a
//! loopback flow like `gh`/Codex: we bind a short-lived `127.0.0.1` listener,
//! open the browser at the backend's `/auth/cli/start`, and wait for the web
//! `/auth` page to redirect the freshly minted CLI token back to our loopback.
//!
//! The token (`lucid_token`) is a durable bearer the agent stores in
//! `~/.lucid-train/config.toml` and sends to the Lucid Cloud `/v1` endpoints.
//! Google's OAuth redirect is registered as the web app (`/auth`), so the token
//! is routed back through that page rather than directly to our loopback.

use anyhow::{anyhow, bail, Context, Result};
use serde::Deserialize;

/// The result of a successful Lucid Cloud login.
#[derive(Debug, Clone, Default)]
pub struct LucidLogin {
    pub token: String,
    pub email: String,
    pub tier: String,
    pub status: String,
    pub balance_usd: f64,
}

/// Account/plan status fetched from `GET /v1/me`.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct MeResponse {
    #[serde(default)]
    pub email: String,
    #[serde(default)]
    pub tier: String,
    #[serde(default)]
    pub status: String,
    #[serde(default)]
    pub balance_usd: f64,
}

/// Strip a trailing `/v1` so we can build the backend root for auth/billing.
pub fn cloud_root(base_url: &str) -> String {
    base_url.trim_end_matches('/').trim_end_matches("/v1").trim_end_matches('/').to_string()
}

/// Run the browser login and return a usable token + account status.
pub async fn login(http: &reqwest::Client, base_url: &str) -> Result<LucidLogin> {
    let root = cloud_root(base_url);

    // Bind the loopback BEFORE opening the browser to avoid a race. Prefer a
    // stable port (nicer for any allow-listing); fall back to an ephemeral one.
    let listener = match tokio::net::TcpListener::bind(("127.0.0.1", 1456)).await {
        Ok(l) => l,
        Err(_) => tokio::net::TcpListener::bind(("127.0.0.1", 0))
            .await
            .context("cannot bind a localhost callback port")?,
    };
    let port = listener.local_addr()?.port();
    let cb = format!("http://127.0.0.1:{port}/callback");

    let start_url = {
        let mut u = url::Url::parse(&format!("{root}/auth/cli/start"))?;
        u.query_pairs_mut().append_pair("cb", &cb);
        u.to_string()
    };
    open_browser(&start_url);

    let token = tokio::time::timeout(std::time::Duration::from_secs(300), wait_for_token(&listener))
        .await
        .map_err(|_| anyhow!("login timed out after 5 minutes"))??;
    if token.is_empty() {
        bail!("login completed but no token was returned");
    }

    // Fetch account status so /status and the success notice can show the plan.
    let mut out = LucidLogin { token: token.clone(), ..Default::default() };
    if let Ok(resp) = http
        .get(format!("{root}/v1/me"))
        .bearer_auth(&token)
        .timeout(std::time::Duration::from_secs(15))
        .send()
        .await
    {
        if resp.status().is_success() {
            if let Ok(me) = resp.json::<MeResponse>().await {
                out.email = me.email;
                out.tier = me.tier;
                out.status = me.status;
                out.balance_usd = me.balance_usd;
            }
        }
    }
    Ok(out)
}

/// Fetch `GET /v1/me` for an already-saved token (used by /status, /credits).
pub async fn fetch_me(http: &reqwest::Client, base_url: &str, token: &str) -> Result<MeResponse> {
    let root = cloud_root(base_url);
    let resp = http
        .get(format!("{root}/v1/me"))
        .bearer_auth(token)
        .timeout(std::time::Duration::from_secs(15))
        .send()
        .await
        .context("could not reach Lucid Cloud")?;
    if !resp.status().is_success() {
        bail!("HTTP {}", resp.status());
    }
    resp.json::<MeResponse>().await.context("bad /v1/me response")
}

/// Wait for the loopback redirect `GET /callback?token=...`, reply with a small
/// page, and return the token.
async fn wait_for_token(listener: &tokio::net::TcpListener) -> Result<String> {
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    loop {
        let (mut sock, _) = listener.accept().await.context("callback accept failed")?;
        let mut buf = vec![0u8; 8192];
        let n = sock.read(&mut buf).await.unwrap_or(0);
        let req = String::from_utf8_lossy(&buf[..n]);
        let path = req.lines().next().and_then(|l| l.split_whitespace().nth(1)).unwrap_or("");
        if !path.starts_with("/callback") {
            let _ = sock.write_all(http_response("Lucid Train", "You can close this tab.")).await;
            continue;
        }
        let full = format!("http://localhost{path}");
        let mut token = None;
        let mut err = None;
        if let Ok(u) = url::Url::parse(&full) {
            for (k, v) in u.query_pairs() {
                match k.as_ref() {
                    "token" => token = Some(v.into_owned()),
                    "error" => err = Some(v.into_owned()),
                    _ => {}
                }
            }
        }
        if let Some(e) = err {
            let _ = sock.write_all(http_response("Login failed", &e)).await;
            bail!("login failed: {e}");
        }
        match token {
            Some(t) if !t.is_empty() => {
                let _ = sock
                    .write_all(http_response("Signed in to Lucid Train", "Return to your terminal."))
                    .await;
                return Ok(t);
            }
            _ => {
                let _ = sock.write_all(http_response("Login failed", "no token in callback")).await;
                bail!("no token in callback");
            }
        }
    }
}

fn http_response(title: &str, msg: &str) -> &'static [u8] {
    let body = format!(
        "<!doctype html><html><head><meta charset=utf-8><title>{title}</title>\
         <style>body{{background:#1a1b26;color:#c0caf5;font:16px/1.6 ui-monospace,Menlo,monospace;\
         display:flex;align-items:center;justify-content:center;height:100vh;margin:0}}\
         .c{{text-align:center}}b{{color:#7aa2f7}}</style></head>\
         <body><div class=c><h2><b>&gt;_</b> {title}</h2><p>{msg}</p></div></body></html>"
    );
    let resp = format!(
        "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
        body.len(),
        body
    );
    Box::leak(resp.into_bytes().into_boxed_slice())
}

fn open_browser(url: &str) {
    #[cfg(target_os = "macos")]
    let cmd = "open";
    #[cfg(target_os = "windows")]
    let cmd = "explorer";
    #[cfg(all(unix, not(target_os = "macos")))]
    let cmd = "xdg-open";
    let _ = std::process::Command::new(cmd)
        .arg(url)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cloud_root_strips_v1() {
        assert_eq!(cloud_root("http://localhost:8787/v1"), "http://localhost:8787");
        assert_eq!(cloud_root("http://localhost:8787/v1/"), "http://localhost:8787");
        assert_eq!(cloud_root("https://api.lucidtrain.com/v1"), "https://api.lucidtrain.com");
    }
}
