//! API key validation with disk caching.
//!
//! Validates once at engine startup, caches the result to disk,
//! and never touches the hot path again.

use crate::error::HorizonError;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use tracing::{debug, warn};

/// Supabase RPC endpoint for key validation.
const SUPABASE_RPC_URL: &str =
    "https://pjhydvnnptabkiiluoji.supabase.co/rest/v1/rpc/validate_sdk_key";

/// Supabase anon key (public by design — RLS is the security layer).
const SUPABASE_ANON_KEY: &str =
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBqaHlkdm5ucHRhYmtpaWx1b2ppIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA3NTMwMDQsImV4cCI6MjA4NjMyOTAwNH0.vwa7fwDIwDX9rJkWDq9K_mgWyCMsGqnKE3OMNOR8KOM";

/// Cache validity period: 72 hours.
const CACHE_TTL_SECS: f64 = 72.0 * 3600.0;

/// On-disk license cache. Raw key is never stored — only the SHA-256 hash.
#[derive(Debug, Serialize, Deserialize)]
struct LicenseCache {
    key_hash: String,
    validated_at: f64,
    expires_at: f64,
    tier: String,
}

/// Validate an API key. Called once at `Engine::new()`.
///
/// Resolution order:
/// 1. `skip-auth` feature → always Ok (test builds).
/// 2. Empty key → error with signup URL.
/// 3. Disk cache hit (hash matches + not expired) → Ok.
/// 4. Remote validation via Supabase PostgREST (sdk_keys table).
/// 5. On network failure: stale cache for same hash → Ok (grace period).
pub fn validate_api_key(key: &str) -> Result<(), HorizonError> {
    #[cfg(feature = "skip-auth")]
    return Ok(());

    #[cfg(not(feature = "skip-auth"))]
    {
        if key.is_empty() {
            return Err(HorizonError::Auth(
                "No Horizon API key provided. Set HORIZON_API_KEY or pass \
                 api_key to hz.run(). Get your key at horizon.mathematicalcompany.com"
                    .to_string(),
            ));
        }

        let hash = sha256_hex(key);

        // Check disk cache
        if let Some(cache) = read_cache() {
            if cache.key_hash == hash && cache.expires_at > now_f64() {
                debug!(tier = %cache.tier, "API key validated (cache hit)");
                return Ok(());
            }
        }

        // Remote validation
        match validate_remote(key) {
            Ok(tier) => {
                let now = now_f64();
                let cache = LicenseCache {
                    key_hash: hash,
                    validated_at: now,
                    expires_at: now + CACHE_TTL_SECS,
                    tier,
                };
                write_cache(&cache);
                debug!("API key validated (remote)");
                Ok(())
            }
            Err(HorizonError::Auth(msg)) => Err(HorizonError::Auth(msg)),
            Err(_) => {
                // Network failure — allow if stale cache exists for same hash
                if let Some(cache) = read_cache() {
                    if cache.key_hash == hash {
                        warn!("Remote validation failed, using stale cache (grace period)");
                        return Ok(());
                    }
                }
                Err(HorizonError::Auth(
                    "Could not validate API key (network error) and no cached \
                     validation found. Check your internet connection."
                        .to_string(),
                ))
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn sha256_hex(input: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(input.as_bytes());
    hex::encode(hasher.finalize())
}

fn now_f64() -> f64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs_f64()
}

fn cache_path() -> Option<PathBuf> {
    dirs_next().map(|home| home.join(".horizon").join("license.json"))
}

/// Minimal home-dir resolution (avoids adding a dependency).
fn dirs_next() -> Option<PathBuf> {
    std::env::var_os("HOME")
        .or_else(|| std::env::var_os("USERPROFILE"))
        .map(PathBuf::from)
}

fn read_cache() -> Option<LicenseCache> {
    let path = cache_path()?;
    let data = std::fs::read_to_string(&path).ok()?;
    serde_json::from_str(&data).ok()
}

fn write_cache(cache: &LicenseCache) {
    if let Some(path) = cache_path() {
        if let Some(parent) = path.parent() {
            let _ = std::fs::create_dir_all(parent);
            // Set directory permissions to 0o700 (owner only) on Unix
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                let _ = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700));
            }
        }
        match serde_json::to_string_pretty(cache) {
            Ok(json) => {
                if let Err(e) = std::fs::write(&path, &json) {
                    warn!(error = %e, "failed to write license cache");
                } else {
                    // Set file permissions to 0o600 (owner read/write only) on Unix
                    #[cfg(unix)]
                    {
                        use std::os::unix::fs::PermissionsExt;
                        let _ = std::fs::set_permissions(
                            &path,
                            std::fs::Permissions::from_mode(0o600),
                        );
                    }
                }
            }
            Err(e) => {
                warn!(error = %e, "failed to serialize license cache");
            }
        }
    }
}

/// Call Supabase RPC function to validate a key hash.
/// Uses a one-shot current_thread tokio runtime (the engine's
/// multi-thread runtime doesn't exist yet).
fn validate_remote(key: &str) -> Result<String, HorizonError> {
    let hash = sha256_hex(key);

    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .map_err(|e| HorizonError::Internal(format!("failed to create auth runtime: {e}")))?;

    rt.block_on(async move {
        let client = reqwest::Client::builder()
            .connect_timeout(std::time::Duration::from_secs(5))
            .timeout(std::time::Duration::from_secs(10))
            .build()
            .map_err(|e| HorizonError::Internal(format!("http client error: {e}")))?;

        let resp = client
            .post(SUPABASE_RPC_URL)
            .header("apikey", SUPABASE_ANON_KEY)
            .header("Authorization", format!("Bearer {}", SUPABASE_ANON_KEY))
            .json(&serde_json::json!({ "p_key_hash": hash }))
            .send()
            .await
            .map_err(|e| {
                HorizonError::Internal(format!("validation request failed: {e}"))
            })?;

        let status = resp.status();
        if !status.is_success() {
            return Err(HorizonError::Internal(format!(
                "validation server returned {status}"
            )));
        }

        #[derive(Deserialize)]
        struct RpcResponse {
            valid: bool,
            tier: Option<String>,
            #[allow(dead_code)]
            reason: Option<String>,
        }

        let body: RpcResponse = resp.json().await.map_err(|e| {
            HorizonError::Internal(format!("invalid validation response: {e}"))
        })?;

        if !body.valid {
            return Err(HorizonError::Auth(
                "Invalid or expired API key. Check your key at horizon.mathematicalcompany.com"
                    .to_string(),
            ));
        }

        Ok(body.tier.unwrap_or_else(|| "free".to_string()))
    })
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn test_sha256_hex() {
        let hash = sha256_hex("hello");
        assert_eq!(hash.len(), 64);
        assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
        // Known SHA-256 of "hello"
        assert_eq!(
            hash,
            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
        );
    }

    #[test]
    fn test_empty_key_rejected() {
        let result = validate_api_key("");
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(err.contains("No Horizon API key"));
        assert!(err.contains("horizon.mathematicalcompany.com"));
    }

    #[test]
    fn test_cache_roundtrip() {
        let cache = LicenseCache {
            key_hash: sha256_hex("test_key_123"),
            validated_at: 1700000000.0,
            expires_at: 1700259200.0,
            tier: "free".to_string(),
        };
        let json = serde_json::to_string(&cache).unwrap();
        let parsed: LicenseCache = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.key_hash, cache.key_hash);
        assert_eq!(parsed.tier, "free");
        assert!((parsed.expires_at - cache.expires_at).abs() < f64::EPSILON);
    }
}
