//! Device-code authentication flow + local credential storage.
//!
//! `kranth login` POSTs `/v1/cli/auth/start { device_name }` → receives
//! `{ request_id, pairing_code, verification_url, expires_in }` → prints the
//! pairing code in big text, opens the browser to the verification URL, and
//! polls `/v1/cli/auth/poll/{request_id}` every second until the user clicks
//! Authorize on the SPA's `/cli` page. On approval, the API returns a fresh
//! `kr_live_…` key which we stash in the OS keyring under
//! `service = "kranth-cli"`, `username = "<api_base_url>"`.

use std::time::Duration;

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

const KEYRING_SERVICE: &str = "kranth-cli";

#[derive(Serialize)]
struct StartRequest {
    device_name: String,
}

#[derive(Deserialize)]
struct StartResponse {
    request_id: String,
    pairing_code: String,
    verification_url: String,
    expires_in: u64,
}

#[derive(Deserialize)]
struct PollResponse {
    status: String, // "pending" | "approved" | "expired" | "denied"
    api_key: Option<String>,
    user_email: Option<String>,
}

pub async fn login(api_url: &str) -> anyhow::Result<()> {
    let device_name = hostname_or_fallback();
    let http = reqwest::Client::builder()
        .timeout(Duration::from_secs(10))
        .build()?;

    let start: StartResponse = http
        .post(format!("{api_url}/v1/cli/auth/start"))
        .json(&StartRequest { device_name: device_name.clone() })
        .send()
        .await
        .context("contacting Kranth auth server")?
        .error_for_status()
        .context("auth start rejected")?
        .json()
        .await?;

    print_pairing_screen(&device_name, &start);

    if let Err(err) = webbrowser::open(&start.verification_url) {
        eprintln!(
            "Could not open the browser automatically ({err}).\nOpen this URL manually:\n  {}",
            start.verification_url
        );
    }

    let deadline = std::time::Instant::now() + Duration::from_secs(start.expires_in.max(60));
    let mut interval = tokio::time::interval(Duration::from_secs(1));
    interval.tick().await; // burn the immediate first tick

    loop {
        if std::time::Instant::now() >= deadline {
            bail!("Pairing code expired. Run `kranth login` again.");
        }

        let resp: PollResponse = http
            .get(format!("{api_url}/v1/cli/auth/poll/{}", start.request_id))
            .send()
            .await?
            .error_for_status()?
            .json()
            .await?;

        match resp.status.as_str() {
            "pending" => { interval.tick().await; }
            "approved" => {
                let key = resp
                    .api_key
                    .ok_or_else(|| anyhow!("server approved but returned no api_key"))?;
                store_key(api_url, &key)?;
                let who = resp.user_email.as_deref().unwrap_or("your account");
                println!("\n✓ Signed in as {who}.");
                return Ok(());
            }
            "denied" => bail!("Authorization denied. Run `kranth login` again."),
            "expired" => bail!("Pairing code expired. Run `kranth login` again."),
            other => bail!("Unexpected status from server: {other}"),
        }
    }
}

pub async fn logout() -> anyhow::Result<()> {
    let removed = clear_all_keys();
    if removed {
        println!("✓ Signed out.");
    } else {
        println!("No stored credentials to clear.");
    }
    Ok(())
}

pub async fn whoami(api_url: &str, api_key_override: Option<&str>) -> anyhow::Result<()> {
    let key = resolve_key(api_url, api_key_override)?
        .ok_or_else(|| anyhow!("Not signed in. Run `kranth login` first."))?;

    let http = reqwest::Client::builder()
        .timeout(Duration::from_secs(10))
        .build()?;

    let resp: serde_json::Value = http
        .get(format!("{api_url}/v1/me"))
        .bearer_auth(&key)
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    println!("{}", serde_json::to_string_pretty(&resp)?);
    Ok(())
}

pub fn resolve_key(api_url: &str, override_: Option<&str>) -> anyhow::Result<Option<String>> {
    if let Some(k) = override_ {
        return Ok(Some(k.to_string()));
    }
    match keyring::Entry::new(KEYRING_SERVICE, api_url) {
        Ok(entry) => match entry.get_password() {
            Ok(secret) => Ok(Some(secret)),
            Err(keyring::Error::NoEntry) => Ok(None),
            Err(err) => Err(anyhow!("keyring read failed: {err}")),
        },
        Err(err) => Err(anyhow!("keyring init failed: {err}")),
    }
}

fn store_key(api_url: &str, key: &str) -> anyhow::Result<()> {
    let entry = keyring::Entry::new(KEYRING_SERVICE, api_url)
        .context("opening OS keyring")?;
    entry.set_password(key).context("writing key to OS keyring")?;
    Ok(())
}

fn clear_all_keys() -> bool {
    // We don't know which api_url was used; clear the default prod URL plus
    // anything pointed at the staging/local hosts. Best-effort.
    let candidates = [
        "https://api.kranth.com",
        "http://localhost:3000",
        "http://localhost:3020",
    ];
    let mut removed = false;
    for url in candidates {
        if let Ok(entry) = keyring::Entry::new(KEYRING_SERVICE, url) {
            if entry.delete_credential().is_ok() {
                removed = true;
            }
        }
    }
    removed
}

fn hostname_or_fallback() -> String {
    std::env::var("KRANTH_DEVICE_NAME")
        .ok()
        .or_else(|| hostname::get().ok().and_then(|h| h.into_string().ok()))
        .unwrap_or_else(|| "this device".to_string())
}

fn print_pairing_screen(device_name: &str, start: &StartResponse) {
    // Big-text rendering is reserved for the ratatui REPL; in CLI login mode
    // we keep it as plain ANSI so it works in any shell.
    println!();
    println!("  Pairing code:  {}", start.pairing_code);
    println!("  For device:    {}", device_name);
    println!("  Verify at:     {}", start.verification_url);
    println!();
    println!("  Waiting for you to approve in the browser…");
}

// Standalone hostname module — `hostname` crate is intentionally tiny;
// pulling it in keeps `keyring`/`webbrowser` light.
mod hostname {
    pub fn get() -> std::io::Result<std::ffi::OsString> {
        // Try $HOSTNAME first (cheap on most Unix shells), fall back to `uname -n`.
        if let Ok(name) = std::env::var("HOSTNAME") {
            if !name.is_empty() {
                return Ok(name.into());
            }
        }
        #[cfg(unix)]
        {
            let output = std::process::Command::new("uname").arg("-n").output()?;
            let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
            Ok(name.into())
        }
        #[cfg(not(unix))]
        {
            Ok(std::env::var_os("COMPUTERNAME").unwrap_or_else(|| "this device".into()))
        }
    }
}
