//! `pic api-keys` — long-lived bearer-key management.
//!
//! Server semantics (mirrored from `manager-core/src/api_keys_api.rs`):
//!   * `raw_token` is returned **once** on mint and never again.
//!   * `app_id` (optional `--app`) binds the key to one app; instance
//!     scopes (`instance:*`) are rejected when `--app` is also set.
//!   * `scopes` is a `text[]` in the wire form (`script:read`, …).

use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use picloud_shared::Scope;

use crate::client::{Client, MintApiKeyBody};
use crate::config;
use crate::output::{KvBlock, OutputMode, Table};

pub async fn mint(
    name: &str,
    scope_strs: &[String],
    app_ident: Option<&str>,
    expires: Option<&str>,
    mode: OutputMode,
) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;

    let scopes = parse_scopes(scope_strs)?;
    let expires_at = expires.map(parse_expires).transpose()?;
    let app_id = match app_ident {
        Some(ident) => Some(client.apps_get(ident).await?.app.id),
        None => None,
    };

    let body = MintApiKeyBody {
        name,
        scopes: &scopes,
        app_id,
        expires_at,
    };
    let resp = client.apikeys_mint(&body).await?;

    let mut block = KvBlock::new();
    block
        .field("id", resp.key.id.to_string())
        .field("name", resp.key.name.clone())
        .field("prefix", resp.key.prefix.clone())
        .field(
            "scopes",
            resp.key
                .scopes
                .iter()
                .map(|s| s.as_str())
                .collect::<Vec<_>>()
                .join(","),
        )
        .field(
            "app_id",
            resp.key
                .app_id
                .map(|a| a.to_string())
                .unwrap_or_else(|| "-".into()),
        )
        .field(
            "expires_at",
            resp.key
                .expires_at
                .map(|t| t.to_rfc3339())
                .unwrap_or_else(|| "-".into()),
        )
        .field("token", resp.raw_token.clone());
    block.print(mode);
    if matches!(mode, OutputMode::Tsv) {
        // The token row is human-easy-to-miss in a wall of metadata;
        // call it out exactly once on the human path. Skip on JSON
        // since machine consumers don't need the nudge.
        eprintln!("Save this token — it will not be shown again.");
    }
    Ok(())
}

pub async fn ls(mode: OutputMode) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let keys = client.apikeys_list().await?;
    let mut table = Table::new([
        "id",
        "name",
        "prefix",
        "scopes",
        "app_id",
        "expires_at",
        "last_used_at",
        "created_at",
    ]);
    for k in keys {
        table.row([
            k.id.to_string(),
            k.name,
            k.prefix,
            k.scopes
                .iter()
                .map(|s| s.as_str())
                .collect::<Vec<_>>()
                .join(","),
            k.app_id
                .map(|a| a.to_string())
                .unwrap_or_else(|| "-".into()),
            k.expires_at
                .map(|t| t.to_rfc3339())
                .unwrap_or_else(|| "-".into()),
            k.last_used_at
                .map(|t| t.to_rfc3339())
                .unwrap_or_else(|| "-".into()),
            k.created_at.to_rfc3339(),
        ]);
    }
    table.print(mode);
    Ok(())
}

pub async fn rm(id: &str) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    client.apikeys_delete(id).await?;
    println!("Revoked api-key {id}");
    Ok(())
}

fn parse_scopes(raw: &[String]) -> Result<Vec<Scope>> {
    if raw.is_empty() {
        return Err(anyhow!(
            "at least one `--scope` is required (e.g. --scope script:read)"
        ));
    }
    raw.iter()
        .map(|s| Scope::from_wire(s).ok_or_else(|| anyhow!("unknown scope: {s}")))
        .collect()
}

/// `--expires` accepts either RFC 3339 (`2026-12-31T23:59:59Z`) or a
/// shorthand `<N>d` / `<N>h` / `<N>m` (days / hours / minutes from now).
/// Shorthand wins for the common "key good for 30 days" case; full
/// RFC 3339 keeps the door open for precise cutoffs.
fn parse_expires(raw: &str) -> Result<DateTime<Utc>> {
    if let Some(spec) = raw.strip_suffix('d') {
        let days: i64 = spec.parse().map_err(|_| anyhow!("bad days: {raw}"))?;
        return Ok(Utc::now() + chrono::Duration::days(days));
    }
    if let Some(spec) = raw.strip_suffix('h') {
        let hours: i64 = spec.parse().map_err(|_| anyhow!("bad hours: {raw}"))?;
        return Ok(Utc::now() + chrono::Duration::hours(hours));
    }
    if let Some(spec) = raw.strip_suffix('m') {
        let mins: i64 = spec.parse().map_err(|_| anyhow!("bad minutes: {raw}"))?;
        return Ok(Utc::now() + chrono::Duration::minutes(mins));
    }
    DateTime::parse_from_rfc3339(raw)
        .map(|d| d.with_timezone(&Utc))
        .map_err(|e| anyhow!("expected RFC 3339 or `<N>d/h/m`, got {raw:?}: {e}"))
}

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

    #[test]
    fn parse_scopes_accepts_wire_form() {
        let scopes = parse_scopes(&["script:read".into(), "log:read".into()]).unwrap();
        assert_eq!(scopes, vec![Scope::ScriptRead, Scope::LogRead]);
    }

    #[test]
    fn parse_scopes_rejects_empty() {
        let err = parse_scopes(&[]).unwrap_err();
        assert!(format!("{err}").contains("at least one"));
    }

    #[test]
    fn parse_scopes_rejects_unknown() {
        let err = parse_scopes(&["script:nope".into()]).unwrap_err();
        assert!(format!("{err}").contains("unknown scope"));
    }

    #[test]
    fn parse_expires_days_shorthand() {
        let d = parse_expires("7d").unwrap();
        let diff = (d - Utc::now()).num_days();
        assert!((6..=7).contains(&diff), "got {diff}");
    }

    #[test]
    fn parse_expires_rfc3339_passes_through() {
        let d = parse_expires("2030-01-01T00:00:00Z").unwrap();
        assert_eq!(d.timestamp(), 1893456000);
    }

    #[test]
    fn parse_expires_garbage_errors() {
        assert!(parse_expires("tomorrow").is_err());
    }
}
