//! API key validation with disk caching and asymmetric signature verification.
//!
//! Validates once at engine startup, caches the result to disk,
//! and never touches the hot path again. The cache is ECDSA-signed
//! (secp256k1) by the server to prevent tampering. The client only
//! embeds the public key — extracting it from the binary does NOT
//! allow forging signatures.

use crate::error::HorizonError;
use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use std::sync::OnceLock;
use tracing::{debug, warn};

static CURRENT_TIER: OnceLock<String> = OnceLock::new();

/// 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;

// ---------------------------------------------------------------------------
// Server public key for ECDSA (secp256k1) signature verification.
//
// This is the COMPRESSED public key (33 bytes, hex-encoded).
// The corresponding private key is held exclusively by the license server.
// Extracting this public key from the binary does NOT allow forging
// signatures — only verification.
//
// TODO: Replace with the production server's public key before release.
// ---------------------------------------------------------------------------
const SERVER_PUBLIC_KEY_HEX: &str =
    "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";

/// Parse the embedded server public key.
fn server_verifying_key() -> VerifyingKey {
    let bytes = hex::decode(SERVER_PUBLIC_KEY_HEX).expect("invalid embedded public key hex");
    VerifyingKey::from_sec1_bytes(&bytes).expect("invalid embedded public key")
}

/// Determine if a hex signature is a legacy HMAC-SHA256 value (32 bytes = 64 hex chars)
/// or empty. Legacy/unsigned caches are never trusted.
fn is_legacy_or_empty_sig(sig: &str) -> bool {
    if sig.is_empty() {
        return true;
    }
    // Old HMAC-SHA256 signatures are exactly 64 hex chars (32 bytes).
    // New ECDSA signatures are 128 hex chars (64 bytes).
    // Reject anything that isn't exactly 128 hex chars as legacy.
    let decoded = match hex::decode(sig) {
        Ok(b) => b,
        Err(_) => return true, // Not valid hex → treat as legacy/invalid
    };
    decoded.len() != 64 // ECDSA signature is exactly 64 bytes
}

/// Verify an ECDSA (secp256k1) signature on a license cache entry.
///
/// The payload is: `"{key_hash}|{tier}|{expires_at_u64}"` (same format as before).
/// The `sig` field must be a hex-encoded 64-byte ECDSA signature.
///
/// Returns `false` for empty, legacy HMAC, or invalid signatures.
fn verify_sig(key_hash: &str, tier: &str, expires_at: f64, sig: &str) -> bool {
    if is_legacy_or_empty_sig(sig) {
        return false;
    }

    let sig_bytes = match hex::decode(sig) {
        Ok(b) => b,
        Err(_) => return false,
    };

    let signature = match Signature::from_slice(&sig_bytes) {
        Ok(s) => s,
        Err(_) => return false,
    };

    let payload = format!("{}|{}|{}", key_hash, tier, expires_at as u64);
    let vk = server_verifying_key();
    vk.verify(payload.as_bytes(), &signature).is_ok()
}

/// On-disk license cache. Raw key is never stored — only the SHA-256 hash.
/// The `sig` field is an ECDSA (secp256k1) signature over "key_hash|tier|expires_at"
/// produced by the server, preventing local tampering.
#[derive(Debug, Serialize, Deserialize)]
struct LicenseCache {
    key_hash: String,
    validated_at: f64,
    expires_at: f64,
    tier: String,
    /// ECDSA signature from the server. Missing/empty = legacy unsigned cache.
    #[serde(default)]
    sig: String,
}

/// Set the current tier. Called once at Engine init.
pub fn set_tier(tier: String) {
    let _ = CURRENT_TIER.set(tier);
}

/// Get the current validated tier. Auto-initializes from env/cache if not yet set.
pub fn current_tier() -> &'static str {
    if CURRENT_TIER.get().is_none() {
        ensure_tier_initialized();
    }
    CURRENT_TIER.get().map(|s| s.as_str()).unwrap_or("free")
}

/// Bootstrap the tier from HORIZON_API_KEY env var or disk cache.
/// Called lazily when require_pro/require_ultra is invoked before Engine::new().
fn ensure_tier_initialized() {
    // Already set by Engine::new() — nothing to do
    if CURRENT_TIER.get().is_some() {
        return;
    }

    // Try env var first
    if let Ok(key) = std::env::var("HORIZON_API_KEY") {
        if !key.is_empty() {
            match validate_api_key(&key) {
                Ok(_) => return,
                Err(e) => {
                    debug!("auto-init from env failed: {}", e);
                }
            }
        }
    }

    // Fall back to valid, signed disk cache
    if let Some(cache) = read_cache() {
        if cache.expires_at > now_f64() {
            if !cache.sig.is_empty()
                && verify_sig(&cache.key_hash, &cache.tier, cache.expires_at, &cache.sig)
            {
                set_tier(cache.tier);
                return;
            }
            debug!("cache signature invalid or missing, ignoring cached tier");
        }
    }

    // No key and no valid cache — tier stays unset (will default to "free")
}

/// Tier numeric level: free=0, pro=1, ultra=2.
fn tier_level(tier: &str) -> u8 {
    match tier {
        "ultra" => 2,
        "pro" => 1,
        _ => 0,
    }
}

/// Require at least Pro tier. Returns error for Free tier.
/// Always succeeds when the `skip-auth` feature is enabled (test/dev builds).
pub fn require_pro() -> Result<(), HorizonError> {
    #[cfg(feature = "skip-auth")]
    return Ok(());

    #[cfg(not(feature = "skip-auth"))]
    {
        if tier_level(current_tier()) >= 1 {
            Ok(())
        } else {
            Err(HorizonError::Auth(
                "This feature requires a Pro or Ultra plan. Upgrade at api.mathematicalcompany.com".to_string()
            ))
        }
    }
}

/// Require Ultra tier. Returns error for Free and Pro tiers.
/// Always succeeds when the `skip-auth` feature is enabled (test/dev builds).
pub fn require_ultra() -> Result<(), HorizonError> {
    #[cfg(feature = "skip-auth")]
    return Ok(());

    #[cfg(not(feature = "skip-auth"))]
    {
        if tier_level(current_tier()) >= 2 {
            Ok(())
        } else {
            Err(HorizonError::Auth(
                "This feature requires an Ultra plan. Upgrade at api.mathematicalcompany.com".to_string()
            ))
        }
    }
}

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

    #[cfg(not(feature = "skip-auth"))]
    {
        // If tier is already validated (e.g. by a previous Engine or cache auto-init),
        // skip re-validation — just return the current tier.
        if let Some(tier) = CURRENT_TIER.get() {
            return Ok(tier.clone());
        }

        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 api.mathematicalcompany.com"
                    .to_string(),
            ));
        }

        let hash = sha256_hex(key);

        // Check disk cache — require valid ECDSA signature
        if let Some(cache) = read_cache() {
            if cache.key_hash == hash && cache.expires_at > now_f64() {
                if !cache.sig.is_empty()
                    && verify_sig(&cache.key_hash, &cache.tier, cache.expires_at, &cache.sig)
                {
                    set_tier(cache.tier.clone());
                    debug!(tier = %cache.tier, "API key validated (signed cache hit)");
                    return Ok(cache.tier);
                }
                debug!("cache signature invalid, falling through to remote validation");
            }
        }

        // Remote validation
        match validate_remote(key) {
            Ok(result) => {
                let now = now_f64();
                // Use server-provided expiry if available, otherwise compute locally
                let expires = if result.expires_at > now {
                    result.expires_at
                } else {
                    now + CACHE_TTL_SECS
                };
                // Server MUST provide the ECDSA signature. If not provided,
                // store empty sig — cache will be treated as unsigned/untrusted
                // on next read, forcing re-validation.
                let cache = LicenseCache {
                    key_hash: hash,
                    validated_at: now,
                    expires_at: expires,
                    tier: result.tier.clone(),
                    sig: result.sig,
                };
                write_cache(&cache);
                set_tier(result.tier.clone());
                debug!("API key validated (remote), tier={}", result.tier);
                Ok(result.tier)
            }
            Err(HorizonError::Auth(msg)) => Err(HorizonError::Auth(msg)),
            Err(_) => {
                // Network failure — allow if signed stale cache exists for same hash
                if let Some(cache) = read_cache() {
                    if cache.key_hash == hash
                        && !cache.sig.is_empty()
                        && verify_sig(
                            &cache.key_hash,
                            &cache.tier,
                            cache.expires_at,
                            &cache.sig,
                        )
                    {
                        set_tier(cache.tier.clone());
                        warn!("Remote validation failed, using signed stale cache (grace period)");
                        return Ok(cache.tier);
                    }
                }
                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");
            }
        }
    }
}

/// Remote validation result.
struct RemoteResult {
    tier: String,
    sig: String,
    /// Server-provided expiry (unix timestamp). 0 = not provided.
    expires_at: f64,
}

/// Call Supabase RPC function to validate a key hash.
/// Returns tier, sig, and optionally expires_at from the server.
/// Uses a one-shot current_thread tokio runtime (the engine's
/// multi-thread runtime doesn't exist yet).
fn validate_remote(key: &str) -> Result<RemoteResult, 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>,
            #[serde(default)]
            sig: Option<String>,
            #[serde(default)]
            expires_at: Option<f64>,
            #[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 api.mathematicalcompany.com"
                    .to_string(),
            ));
        }

        Ok(RemoteResult {
            tier: body.tier.unwrap_or_else(|| "free".to_string()),
            sig: body.sig.unwrap_or_default(),
            expires_at: body.expires_at.unwrap_or(0.0),
        })
    })
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use k256::ecdsa::{signature::Signer, SigningKey};

    /// Helper: sign a payload with a given private key (simulates what the server does).
    fn test_sign(signing_key: &SigningKey, key_hash: &str, tier: &str, expires_at: f64) -> String {
        let payload = format!("{}|{}|{}", key_hash, tier, expires_at as u64);
        let sig: Signature = signing_key.sign(payload.as_bytes());
        hex::encode(sig.to_bytes())
    }

    /// The placeholder private key corresponding to SERVER_PUBLIC_KEY_HEX.
    /// This is the "generator" private key (scalar = 1) for secp256k1,
    /// whose public key is the well-known generator point G.
    /// DO NOT use in production — this is only for tests.
    fn test_signing_key() -> SigningKey {
        let mut privkey = [0u8; 32];
        privkey[31] = 1; // scalar = 1
        SigningKey::from_bytes((&privkey[..]).into()).unwrap()
    }

    #[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("api.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(),
            sig: String::new(),
        };
        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);
    }

    #[test]
    fn test_ecdsa_signature_roundtrip() {
        let sk = test_signing_key();
        let key_hash = sha256_hex("test_key");
        let tier = "ultra";
        let expires_at = 1700259200.0;

        let sig = test_sign(&sk, &key_hash, tier, expires_at);
        assert!(!sig.is_empty());
        assert_eq!(sig.len(), 128); // 64 bytes = 128 hex chars
        assert!(verify_sig(&key_hash, tier, expires_at, &sig));
    }

    #[test]
    fn test_ecdsa_rejects_tampered_tier() {
        let sk = test_signing_key();
        let key_hash = sha256_hex("test_key");
        let expires_at = 1700259200.0;

        // Sign as "free"
        let sig = test_sign(&sk, &key_hash, "free", expires_at);

        // Tampering: verify with "ultra" should fail
        assert!(!verify_sig(&key_hash, "ultra", expires_at, &sig));
    }

    #[test]
    fn test_ecdsa_rejects_tampered_expiry() {
        let sk = test_signing_key();
        let key_hash = sha256_hex("test_key");
        let tier = "pro";
        let expires_at = 1700259200.0;

        let sig = test_sign(&sk, &key_hash, tier, expires_at);

        // Tampering: extend expiry
        assert!(!verify_sig(&key_hash, tier, 9999999999.0, &sig));
    }

    #[test]
    fn test_ecdsa_rejects_forged_sig() {
        let key_hash = sha256_hex("test_key");
        // 128 hex chars (64 bytes) but random/wrong
        let fake_sig = "a".repeat(128);
        assert!(!verify_sig(&key_hash, "ultra", 9999999999.0, &fake_sig));
    }

    #[test]
    fn test_ecdsa_rejects_wrong_key_signature() {
        // Sign with a different private key — should fail verification
        // against the embedded public key.
        let mut wrong_key_bytes = [0u8; 32];
        wrong_key_bytes[31] = 2; // scalar = 2, different from our embedded key
        let wrong_sk = SigningKey::from_bytes((&wrong_key_bytes[..]).into()).unwrap();

        let key_hash = sha256_hex("test_key");
        let tier = "ultra";
        let expires_at = 1700259200.0;

        let sig = test_sign(&wrong_sk, &key_hash, tier, expires_at);
        assert!(!verify_sig(&key_hash, tier, expires_at, &sig));
    }

    #[test]
    fn test_legacy_hmac_sig_rejected() {
        // Old HMAC-SHA256 signatures are 64 hex chars (32 bytes) — should be
        // rejected as legacy format.
        let key_hash = sha256_hex("test_key");
        let legacy_sig = "ab".repeat(32); // 64 hex chars = 32 bytes
        assert!(!verify_sig(&key_hash, "ultra", 9999999999.0, &legacy_sig));
    }

    #[test]
    fn test_empty_sig_rejected() {
        let key_hash = sha256_hex("test_key");
        assert!(!verify_sig(&key_hash, "ultra", 9999999999.0, ""));
    }

    #[test]
    fn test_server_verifying_key_loads() {
        // Ensure the embedded public key constant is valid
        let vk = server_verifying_key();
        // Verify it produces a valid VerifyingKey (no panic)
        let _ = vk.to_encoded_point(true);
    }
}
