//! On-disk credentials store.
//!
//! Path is resolved via `directories::ProjectDirs` so the file lives in
//! the platform-appropriate config dir (XDG on Linux, Library on macOS,
//! AppData on Windows). On POSIX the file is forced to mode 0600 so the
//! pasted bearer token isn't world-readable.

use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Credentials {
    pub url: String,
    pub token: String,
    pub username: String,
}

/// Resolve the credentials file path. Honors `PICLOUD_CONFIG_DIR` as an
/// override (used by tests to redirect to a tempdir) before falling
/// back to the platform default.
pub fn credentials_path() -> Result<PathBuf> {
    if let Ok(dir) = std::env::var("PICLOUD_CONFIG_DIR") {
        return Ok(PathBuf::from(dir).join("credentials"));
    }
    let dirs = ProjectDirs::from("dev", "picloud", "picloud")
        .ok_or_else(|| anyhow!("could not determine config directory"))?;
    Ok(dirs.config_dir().join("credentials"))
}

pub fn load() -> Result<Credentials> {
    let path = credentials_path()?;
    let body = fs::read_to_string(&path).with_context(|| {
        format!(
            "no credentials at {}. run `pic login` first",
            path.display()
        )
    })?;
    toml::from_str(&body).with_context(|| format!("failed to parse {}", path.display()))
}

/// Resolution order used by every non-login command:
///   1. If both `PICLOUD_URL` and `PICLOUD_TOKEN` are set (and non-empty),
///      use them directly. Matches gcloud/aws/kubectl semantics — env
///      wins so CI never accidentally reads a developer's stale file.
///   2. Otherwise fall back to the on-disk credentials file.
///
/// Username is best-effort: env mode has no way to know the real one
/// (no round-trip to `/auth/me`), so it shows as `"-"` in `whoami`
/// output. Callers that need the canonical username re-fetch via
/// `Client::auth_me`.
pub fn resolve() -> Result<Credentials> {
    if let (Ok(url), Ok(token)) = (std::env::var("PICLOUD_URL"), std::env::var("PICLOUD_TOKEN")) {
        if !url.is_empty() && !token.is_empty() {
            return Ok(Credentials {
                url,
                token,
                username: "-".to_string(),
            });
        }
    }
    load()
}

/// Delete the on-disk credentials file. Idempotent — silently succeeds
/// if the file is already gone (the user already logged out, or never
/// logged in to begin with).
pub fn delete() -> Result<()> {
    let path = credentials_path()?;
    match fs::remove_file(&path) {
        Ok(()) => Ok(()),
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(err) => Err(err).with_context(|| format!("removing {}", path.display())),
    }
}

pub fn save(creds: &Credentials) -> Result<()> {
    let path = credentials_path()?;
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
    }
    let body = toml::to_string(creds).context("serializing credentials")?;
    write_private(&path, body.as_bytes())?;
    Ok(())
}

#[cfg(unix)]
fn write_private(path: &Path, bytes: &[u8]) -> Result<()> {
    use std::os::unix::fs::OpenOptionsExt;
    let mut f = fs::OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .mode(0o600)
        .open(path)
        .with_context(|| format!("opening {}", path.display()))?;
    f.write_all(bytes)
        .with_context(|| format!("writing {}", path.display()))?;
    // Belt-and-suspenders: re-set perms in case the file already existed
    // with a wider mode (mode() on create doesn't downgrade existing).
    let mut perms = fs::metadata(path)?.permissions();
    use std::os::unix::fs::PermissionsExt;
    perms.set_mode(0o600);
    fs::set_permissions(path, perms)?;
    Ok(())
}

#[cfg(not(unix))]
fn write_private(path: &Path, bytes: &[u8]) -> Result<()> {
    fs::write(path, bytes).with_context(|| format!("writing {}", path.display()))?;
    Ok(())
}

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

    #[test]
    fn roundtrip_toml() {
        let creds = Credentials {
            url: "http://localhost:8000".to_string(),
            token: "pic_abc".to_string(),
            username: "admin".to_string(),
        };
        let serialized = toml::to_string(&creds).unwrap();
        let parsed: Credentials = toml::from_str(&serialized).unwrap();
        assert_eq!(creds, parsed);
    }

    #[cfg(unix)]
    #[test]
    fn posix_mode_is_0600() {
        use std::os::unix::fs::PermissionsExt;

        let dir = tempdir().unwrap();
        std::env::set_var("PICLOUD_CONFIG_DIR", dir.path());
        let creds = Credentials {
            url: "http://localhost:8000".to_string(),
            token: "pic_secret".to_string(),
            username: "admin".to_string(),
        };
        save(&creds).unwrap();
        let path = credentials_path().unwrap();
        let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
        assert_eq!(mode, 0o600, "credentials must be readable only by owner");
        std::env::remove_var("PICLOUD_CONFIG_DIR");
    }
}
