//! # API-key loader (spec §5.3)
//!
//! Two-source config: `OPENAI_API_KEY` env var default + optional
//! `OPENAI_API_KEY_FILE` path override. KMS/Vault integration is deferred
//! to step 6 (spec §1.2); the file-path override makes secret-manager
//! injection (k8s secrets, Vault agent) trivial when that layer exists.
//!
//! # Why file-path override (not env-only)
//!
//! A k8s pod mounts secrets at file paths (e.g. `/var/secrets/openai_key`).
//! Env-var secrets are also fine, but file-path is the standard pattern for
//! k8s + Vault agent + sidecar secret rotators. The dev loop uses the env
//! var path (single-source, simplest).

use std::env;
use std::fs;
use std::path::PathBuf;

use thiserror::Error;

#[derive(Debug, Error)]
pub enum ApiKeyError {
    #[error("no API key: set OPENAI_API_KEY or OPENAI_API_KEY_FILE")]
    NotFound,
    #[error("OPENAI_API_KEY_FILE path is not valid UTF-8: {0:?}")]
    PathNotUtf8(PathBuf),
    #[error("failed to read OPENAI_API_KEY_FILE at {path}: {source}")]
    FileRead {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
}

/// Load the OpenAI API key per the env-var + file-path posture (spec §5.3).
///
/// Precedence: `OPENAI_API_KEY_FILE` wins over `OPENAI_API_KEY` (a file
/// path is a more specific override; this matches k8s-secret patterns where
/// an operator mounts a file to override the default env var).
///
/// Trims trailing whitespace from either source (some k8s secret mounts
/// add trailing newlines).
pub fn load_api_key() -> Result<String, ApiKeyError> {
    if let Ok(path_str) = env::var("OPENAI_API_KEY_FILE") {
        let path = PathBuf::from(&path_str);
        let raw = fs::read_to_string(&path).map_err(|source| ApiKeyError::FileRead {
            path: path.clone(),
            source,
        })?;
        return Ok(raw.trim().to_string());
    }
    if let Ok(raw) = env::var("OPENAI_API_KEY") {
        return Ok(raw.trim().to_string());
    }
    Err(ApiKeyError::NotFound)
}

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

    // NOTE: edition 2024 makes env::set_var / env::remove_var `unsafe` (they
    // can race with concurrent reads of the process environ). The brief was
    // written against edition 2021 syntax; we wrap each mutation in
    // `unsafe { ... }` rather than reverting to a pre-2024 edition. The
    // unsafety is local to the test process and accepted by the stdlib's
    // own test helpers; no threads here read these env vars concurrently.

    // The process environ is process-global state. Even with the save/restore
    // pattern below, running these tests in parallel would let one test's
    // `set_var` race against another's `remove_var` for the same variable,
    // producing flaky failures. This mutex serializes every test in this
    // module so the env-var mutations are atomic across tests.
    static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());

    /// Acquire the env mutex for the test body. Panics if the lock is held
    /// (only happens if a test panics while held — fine, fail fast).
    fn lock_env() -> std::sync::MutexGuard<'static, ()> {
        ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner())
    }

    #[test]
    fn returns_error_when_neither_env_nor_file_set() {
        let _guard = lock_env();
        // Save then clear both env vars to make the test order-independent.
        let saved_key = env::var_os("OPENAI_API_KEY").map(|v| ("OPENAI_API_KEY".to_string(), v));
        let saved_file =
            env::var_os("OPENAI_API_KEY_FILE").map(|v| ("OPENAI_API_KEY_FILE".to_string(), v));
        unsafe {
            env::remove_var("OPENAI_API_KEY");
            env::remove_var("OPENAI_API_KEY_FILE");
        }

        let r = load_api_key();
        assert!(matches!(r, Err(ApiKeyError::NotFound)));

        // Restore for any other test in the same process.
        if let Some((k, v)) = saved_key {
            unsafe {
                env::set_var(k, v);
            }
        }
        if let Some((k, v)) = saved_file {
            unsafe {
                env::set_var(k, v);
            }
        }
    }

    #[test]
    fn reads_from_env_var() {
        let _guard = lock_env();
        let saved = env::var_os("OPENAI_API_KEY").map(|v| ("OPENAI_API_KEY".to_string(), v));
        let saved_file =
            env::var_os("OPENAI_API_KEY_FILE").map(|v| ("OPENAI_API_KEY_FILE".to_string(), v));
        unsafe {
            env::set_var("OPENAI_API_KEY", "sk-test-12345");
            env::remove_var("OPENAI_API_KEY_FILE");
        }

        let r = load_api_key().unwrap();
        assert_eq!(r, "sk-test-12345");

        if let Some((k, v)) = saved {
            unsafe {
                env::set_var(k, v);
            }
        } else {
            unsafe {
                env::remove_var("OPENAI_API_KEY");
            }
        }
        if let Some((k, v)) = saved_file {
            unsafe {
                env::set_var(k, v);
            }
        }
    }

    #[test]
    fn trim_trailing_newline_from_env() {
        let _guard = lock_env();
        let saved = env::var_os("OPENAI_API_KEY").map(|v| ("OPENAI_API_KEY".to_string(), v));
        unsafe {
            env::set_var("OPENAI_API_KEY", "sk-test-with-newline\n");
            env::remove_var("OPENAI_API_KEY_FILE");
        }

        let r = load_api_key().unwrap();
        assert_eq!(r, "sk-test-with-newline");

        if let Some((k, v)) = saved {
            unsafe {
                env::set_var(k, v);
            }
        } else {
            unsafe {
                env::remove_var("OPENAI_API_KEY");
            }
        }
    }

    #[test]
    fn file_path_overrides_env() {
        let _guard = lock_env();
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("key.txt");
        std::fs::write(&path, "sk-from-file-98765\n").unwrap();

        unsafe {
            env::set_var("OPENAI_API_KEY", "sk-from-env-should-be-overridden");
            env::set_var("OPENAI_API_KEY_FILE", path.to_str().unwrap());
        }

        let r = load_api_key().unwrap();
        assert_eq!(r, "sk-from-file-98765");

        unsafe {
            env::remove_var("OPENAI_API_KEY");
            env::remove_var("OPENAI_API_KEY_FILE");
        }
    }

    #[test]
    fn file_path_missing_returns_file_read_error() {
        let _guard = lock_env();
        unsafe {
            env::set_var("OPENAI_API_KEY_FILE", "/nonexistent/path/to/key.txt");
            env::remove_var("OPENAI_API_KEY");
        }

        let r = load_api_key();
        assert!(matches!(r, Err(ApiKeyError::FileRead { .. })));

        unsafe {
            env::remove_var("OPENAI_API_KEY_FILE");
        }
    }
}
