//! Secret (API key) storage, per decision #7.
//!
//! Retrieval order: env var (incl. `.env`) → OS keychain (when built with the
//! `keychain` feature) → gitignored `.hephaes/credentials.toml` (chmod 600).
//! Keys are never written to `config.toml`.

use crate::config::{Paths, ProviderId};
use anyhow::{Context, Result};
use std::collections::BTreeMap;

#[allow(dead_code)] // used only by the optional `keychain` feature
const KEYCHAIN_SERVICE: &str = "hephaes";

/// Look up the API key for a provider, honoring the precedence chain.
pub fn get_key(paths: &Paths, provider: ProviderId) -> Option<String> {
    if let Some(var) = provider.env_key_var() {
        if let Ok(v) = std::env::var(var) {
            if !v.is_empty() {
                return Some(v);
            }
        }
    }
    #[cfg(feature = "keychain")]
    {
        if let Some(v) = keychain_get(provider) {
            return Some(v);
        }
    }
    file_get(paths, provider)
}

/// Where a currently-available key is sourced from (for `providers`/`status`).
pub fn key_source(paths: &Paths, provider: ProviderId) -> Option<&'static str> {
    if let Some(var) = provider.env_key_var() {
        if std::env::var(var).map(|v| !v.is_empty()).unwrap_or(false) {
            return Some("env");
        }
    }
    #[cfg(feature = "keychain")]
    {
        if keychain_get(provider).is_some() {
            return Some("keychain");
        }
    }
    if file_get(paths, provider).is_some() {
        return Some("file");
    }
    None
}

/// Persist a pasted key to the most secure available backend.
pub fn set_key(paths: &Paths, provider: ProviderId, key: &str) -> Result<()> {
    #[cfg(feature = "keychain")]
    {
        let _ = paths;
        return keychain_set(provider, key);
    }
    #[allow(unreachable_code)]
    file_set(paths, provider, key)
}

/// Human-readable name of the active secret backend (for messaging).
pub fn backend_name() -> &'static str {
    #[cfg(feature = "keychain")]
    {
        "OS keychain"
    }
    #[cfg(not(feature = "keychain"))]
    {
        "gitignored credentials file"
    }
}

// ---- file backend -------------------------------------------------------

fn file_get(paths: &Paths, provider: ProviderId) -> Option<String> {
    let map = read_file(paths).ok()?;
    map.get(provider.as_str()).cloned()
}

fn file_set(paths: &Paths, provider: ProviderId, key: &str) -> Result<()> {
    std::fs::create_dir_all(&paths.hephaes_dir)
        .with_context(|| format!("creating {}", paths.hephaes_dir.display()))?;
    let mut map = read_file(paths).unwrap_or_default();
    map.insert(provider.as_str().to_string(), key.to_string());
    let toml = toml::to_string_pretty(&map).context("serializing credentials")?;
    write_secure(&paths.credentials, &toml)
        .with_context(|| format!("writing {}", paths.credentials.display()))?;
    // Also chmod an already-existing file (mode at creation only applies to new files).
    restrict_permissions(&paths.credentials);
    Ok(())
}

/// Write a file, creating it with 0600 from the start on Unix (no 0644 window).
#[cfg(unix)]
fn write_secure(path: &std::path::Path, contents: &str) -> std::io::Result<()> {
    use std::io::Write;
    use std::os::unix::fs::OpenOptionsExt;
    let mut f = std::fs::OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .mode(0o600)
        .open(path)?;
    f.write_all(contents.as_bytes())
}

#[cfg(not(unix))]
fn write_secure(path: &std::path::Path, contents: &str) -> std::io::Result<()> {
    std::fs::write(path, contents)
}

fn read_file(paths: &Paths) -> Result<BTreeMap<String, String>> {
    if !paths.credentials.exists() {
        return Ok(BTreeMap::new());
    }
    let s = std::fs::read_to_string(&paths.credentials)?;
    Ok(toml::from_str(&s).unwrap_or_default())
}

#[cfg(unix)]
fn restrict_permissions(path: &std::path::Path) {
    use std::os::unix::fs::PermissionsExt;
    if let Ok(meta) = std::fs::metadata(path) {
        let mut perms = meta.permissions();
        perms.set_mode(0o600);
        let _ = std::fs::set_permissions(path, perms);
    }
}

#[cfg(not(unix))]
fn restrict_permissions(_path: &std::path::Path) {}

// ---- keychain backend (optional) ----------------------------------------

#[cfg(feature = "keychain")]
fn keychain_get(provider: ProviderId) -> Option<String> {
    keyring::Entry::new(KEYCHAIN_SERVICE, provider.as_str())
        .ok()?
        .get_password()
        .ok()
}

#[cfg(feature = "keychain")]
fn keychain_set(provider: ProviderId, key: &str) -> Result<()> {
    keyring::Entry::new(KEYCHAIN_SERVICE, provider.as_str())
        .context("opening keychain entry")?
        .set_password(key)
        .context("writing to keychain")
}
