//! Local cache for remote claims (offline fallback).
//!
//! When remote mode is enabled, Aphoria caches fetched claims locally
//! in `.aphoria/cache.toml`. This allows scans to continue when the
//! remote server is unreachable.

use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};

use serde::{Deserialize, Serialize};

use crate::types::AuthoredClaim;
use crate::AphoriaError;

/// Local cache for claims fetched from remote server.
pub struct ClaimCache {
    cache_path: PathBuf,
}

/// Cache file structure (TOML).
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ClaimCacheFile {
    /// Timestamp when cache was last updated (Unix seconds).
    last_updated: u64,

    /// URL of the remote server (for staleness detection).
    remote_url: String,

    /// Cached claims.
    claims: Vec<AuthoredClaim>,
}

impl ClaimCache {
    /// Create a new claim cache.
    ///
    /// Defaults to `.aphoria/cache.toml` in the current directory.
    pub fn new() -> Self {
        Self { cache_path: PathBuf::from(".aphoria/cache.toml") }
    }

    /// Create a claim cache with a custom path.
    pub fn with_path(cache_path: PathBuf) -> Self {
        Self { cache_path }
    }

    /// Save claims to the cache.
    pub fn save(&self, claims: &[AuthoredClaim], remote_url: &str) -> Result<(), AphoriaError> {
        let now = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .map_err(|e| AphoriaError::Io(std::io::Error::other(e)))?
            .as_secs();

        let cache = ClaimCacheFile {
            last_updated: now,
            remote_url: remote_url.to_string(),
            claims: claims.to_vec(),
        };

        // Ensure .aphoria directory exists
        if let Some(parent) = self.cache_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let toml = toml::to_string_pretty(&cache)
            .map_err(|e| AphoriaError::Config(format!("Failed to serialize cache: {e}")))?;

        std::fs::write(&self.cache_path, toml)?;

        Ok(())
    }

    /// Load claims from the cache.
    ///
    /// Returns an empty vector if the cache doesn't exist.
    pub fn load(&self) -> Result<Vec<AuthoredClaim>, AphoriaError> {
        if !self.cache_path.exists() {
            return Ok(vec![]);
        }

        let toml = std::fs::read_to_string(&self.cache_path)?;
        let cache: ClaimCacheFile = toml::from_str(&toml)
            .map_err(|e| AphoriaError::Config(format!("Failed to parse cache: {e}")))?;

        Ok(cache.claims)
    }

    /// Check if the cache is stale (older than max_age).
    pub fn is_stale(&self, max_age: Duration) -> bool {
        let metadata = match self.cache_path.metadata() {
            Ok(m) => m,
            Err(_) => return true, // Missing cache is stale
        };

        let modified = match metadata.modified() {
            Ok(t) => t,
            Err(_) => return true, // Can't get modified time = stale
        };

        let age = match SystemTime::now().duration_since(modified) {
            Ok(d) => d,
            Err(_) => return true, // Clock skew = stale
        };

        age > max_age
    }

    /// Get the cache path.
    pub fn path(&self) -> &Path {
        &self.cache_path
    }

    /// Check if the cache exists.
    pub fn exists(&self) -> bool {
        self.cache_path.exists()
    }
}

impl Default for ClaimCache {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{AuthoredValue, ClaimStatus};
    use std::time::Duration;
    use tempfile::TempDir;

    fn make_test_claim(id: &str) -> AuthoredClaim {
        AuthoredClaim {
            id: id.to_string(),
            concept_path: "test/path".to_string(),
            predicate: "enabled".to_string(),
            value: AuthoredValue::Bool(true),
            comparison: Default::default(),
            provenance: "test".to_string(),
            invariant: "test invariant".to_string(),
            consequence: "test consequence".to_string(),
            authority_tier: "expert".to_string(),
            evidence: vec![],
            category: "test".to_string(),
            status: ClaimStatus::Active,
            supersedes: None,
            created_by: "test-user".to_string(),
            created_at: "2026-02-13T00:00:00Z".to_string(),
            updated_at: None,
        }
    }

    #[test]
    fn test_cache_save_and_load() {
        let temp_dir = TempDir::new().unwrap();
        let cache_path = temp_dir.path().join("cache.toml");
        let cache = ClaimCache::with_path(cache_path);

        let claims = vec![make_test_claim("test-001"), make_test_claim("test-002")];

        // Save
        cache.save(&claims, "https://example.com").unwrap();

        // Load
        let loaded = cache.load().unwrap();
        assert_eq!(loaded.len(), 2);
        assert_eq!(loaded[0].id, "test-001");
        assert_eq!(loaded[1].id, "test-002");
    }

    #[test]
    fn test_cache_load_missing_returns_empty() {
        let temp_dir = TempDir::new().unwrap();
        let cache_path = temp_dir.path().join("nonexistent.toml");
        let cache = ClaimCache::with_path(cache_path);

        let loaded = cache.load().unwrap();
        assert_eq!(loaded.len(), 0);
    }

    #[test]
    fn test_cache_staleness() {
        let temp_dir = TempDir::new().unwrap();
        let cache_path = temp_dir.path().join("cache.toml");
        let cache = ClaimCache::with_path(cache_path);

        // Save
        let claims = vec![make_test_claim("test-001")];
        cache.save(&claims, "https://example.com").unwrap();

        // Fresh cache is not stale
        assert!(!cache.is_stale(Duration::from_secs(3600)));

        // But it is stale if we set max_age to 0
        assert!(cache.is_stale(Duration::from_secs(0)));
    }

    #[test]
    fn test_cache_nonexistent_is_stale() {
        let temp_dir = TempDir::new().unwrap();
        let cache_path = temp_dir.path().join("nonexistent.toml");
        let cache = ClaimCache::with_path(cache_path);

        assert!(cache.is_stale(Duration::from_secs(3600)));
    }

    #[test]
    fn test_cache_accessors() {
        let cache_path = PathBuf::from("/tmp/test-cache.toml");
        let cache = ClaimCache::with_path(cache_path.clone());

        assert_eq!(cache.path(), cache_path.as_path());
        assert!(!cache.exists());
    }
}
