//! Claims file persistence (TOML).
//!
//! Stores human-authored claims in `.aphoria/claims.toml`, following
//! the same pattern as `ack_file.rs` for acknowledgments.
//!
//! ## File Format
//!
//! ```toml
//! # Aphoria Claims - version controlled
//! #
//! # Human-authored claims with provenance, invariants, and consequences.
//! # Manage with: aphoria claims create|list|explain|update|supersede|deprecate
//!
//! [[claim]]
//! id = "wallet-seqcst-001"
//! concept_path = "maxwell/wallet/atomics/ordering"
//! predicate = "required_ordering"
//! value = "SeqCst"
//! provenance = "Safety analysis by lead developer"
//! invariant = "All wallet atomics MUST use SeqCst"
//! consequence = "Double-spend race condition"
//! authority_tier = "expert"
//! evidence = ["wallet ADR-003", "Intel SDM Vol 4"]
//! category = "safety"
//! status = "active"
//! created_by = "jml"
//! created_at = "2026-02-08T12:00:00Z"
//! ```

use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::types::authored_claim::{AuthoredClaim, ClaimStatus};
use crate::AphoriaError;

/// Default path for the claims file relative to project root.
pub const CLAIMS_FILE_PATH: &str = ".aphoria/claims.toml";

/// Container for all authored claims in the file.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ClaimsFile {
    /// List of authored claims.
    #[serde(default, rename = "claim")]
    pub claims: Vec<AuthoredClaim>,
}

impl ClaimsFile {
    /// Create an empty claims file.
    pub fn new() -> Self {
        Self { claims: Vec::new() }
    }

    /// Add a claim entry, deduplicating by ID.
    pub fn add(&mut self, claim: AuthoredClaim) {
        if !self.claims.iter().any(|c| c.id == claim.id) {
            self.claims.push(claim);
        }
    }

    /// Load from a TOML file.
    pub fn load(path: &Path) -> Result<Self, AphoriaError> {
        if !path.exists() {
            return Ok(Self::new());
        }

        let content = std::fs::read_to_string(path)
            .map_err(|e| AphoriaError::Io(std::io::Error::new(e.kind(), format!("{e}"))))?;

        toml::from_str(&content)
            .map_err(|e| AphoriaError::Claims(format!("Failed to parse claims file: {e}")))
    }

    /// Save to a TOML file.
    pub fn save(&self, path: &Path) -> Result<(), AphoriaError> {
        if let Some(parent) = path.parent() {
            if !parent.exists() {
                std::fs::create_dir_all(parent)?;
            }
        }

        let header = r#"# Aphoria Claims - version controlled
#
# Human-authored claims with provenance, invariants, and consequences.
# Each claim represents a deliberate architectural decision or safety invariant.
#
# Manage with: aphoria claims create|list|explain|update|supersede|deprecate

"#;

        let content =
            toml::to_string_pretty(self).map_err(|e| AphoriaError::Claims(e.to_string()))?;

        std::fs::write(path, format!("{header}{content}"))
            .map_err(|e| AphoriaError::Io(std::io::Error::new(e.kind(), format!("{e}"))))?;

        Ok(())
    }

    /// Get the default path for the claims file.
    pub fn default_path(project_root: &Path) -> PathBuf {
        project_root.join(CLAIMS_FILE_PATH)
    }

    /// Check if a claims file exists at the default location.
    pub fn exists(project_root: &Path) -> bool {
        Self::default_path(project_root).exists()
    }

    /// Get the number of claims.
    pub fn len(&self) -> usize {
        self.claims.len()
    }

    /// Check if empty.
    pub fn is_empty(&self) -> bool {
        self.claims.is_empty()
    }

    /// Find a claim by ID.
    pub fn find_by_id(&self, id: &str) -> Option<&AuthoredClaim> {
        self.claims.iter().find(|c| c.id == id)
    }

    /// Find a claim by ID (mutable).
    pub fn find_by_id_mut(&mut self, id: &str) -> Option<&mut AuthoredClaim> {
        self.claims.iter_mut().find(|c| c.id == id)
    }

    /// Find claims by category.
    pub fn find_by_category(&self, category: &str) -> Vec<&AuthoredClaim> {
        self.claims.iter().filter(|c| c.category == category).collect()
    }

    /// Find claims by status.
    pub fn find_by_status(&self, status: &ClaimStatus) -> Vec<&AuthoredClaim> {
        self.claims.iter().filter(|c| &c.status == status).collect()
    }

    /// Update a claim's fields. Returns error if claim not found.
    pub fn update<F>(&mut self, id: &str, updater: F) -> Result<(), AphoriaError>
    where
        F: FnOnce(&mut AuthoredClaim),
    {
        let claim = self
            .find_by_id_mut(id)
            .ok_or_else(|| AphoriaError::Claims(format!("Claim not found: {id}")))?;
        updater(claim);
        Ok(())
    }

    /// Mark a claim as superseded and add the superseding claim.
    pub fn supersede(
        &mut self,
        old_id: &str,
        new_claim: AuthoredClaim,
    ) -> Result<(), AphoriaError> {
        // Mark old claim as superseded
        let now = new_claim.created_at.clone();
        self.update(old_id, |c| {
            c.status = ClaimStatus::Superseded;
            c.updated_at = Some(now);
        })?;

        // Add new claim
        self.add(new_claim);
        Ok(())
    }

    /// Mark a claim as deprecated.
    pub fn deprecate(&mut self, id: &str, timestamp: &str) -> Result<(), AphoriaError> {
        self.update(id, |c| {
            c.status = ClaimStatus::Deprecated;
            c.updated_at = Some(timestamp.to_string());
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::authored_claim::AuthoredValue;
    use tempfile::TempDir;

    fn sample_claim(id: &str) -> AuthoredClaim {
        AuthoredClaim {
            id: id.to_string(),
            concept_path: "test/concept".to_string(),
            predicate: "test_pred".to_string(),
            value: AuthoredValue::Text("test_value".to_string()),
            comparison: Default::default(),
            provenance: "Test provenance".to_string(),
            invariant: "Test invariant".to_string(),
            consequence: "Test consequence".to_string(),
            authority_tier: "expert".to_string(),
            evidence: vec!["evidence-1".to_string()],
            category: "safety".to_string(),
            status: ClaimStatus::Active,
            supersedes: None,
            created_by: "tester".to_string(),
            created_at: "2026-02-08T12:00:00Z".to_string(),
            updated_at: None,
        }
    }

    #[test]
    fn test_claims_file_roundtrip() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let path = temp_dir.path().join(".aphoria/claims.toml");

        let mut file = ClaimsFile::new();
        file.add(sample_claim("claim-001"));
        file.add(sample_claim("claim-002"));

        file.save(&path).expect("save claims file");

        let loaded = ClaimsFile::load(&path).expect("load claims file");
        assert_eq!(loaded.len(), 2);
        assert_eq!(loaded.claims[0].id, "claim-001");
        assert_eq!(loaded.claims[1].id, "claim-002");
    }

    #[test]
    fn test_no_duplicates() {
        let mut file = ClaimsFile::new();
        file.add(sample_claim("claim-001"));
        file.add(sample_claim("claim-001"));
        assert_eq!(file.len(), 1);
    }

    #[test]
    fn test_find_by_id() {
        let mut file = ClaimsFile::new();
        file.add(sample_claim("claim-001"));
        file.add(sample_claim("claim-002"));

        assert!(file.find_by_id("claim-001").is_some());
        assert!(file.find_by_id("nonexistent").is_none());
    }

    #[test]
    fn test_find_by_category() {
        let mut file = ClaimsFile::new();
        let mut arch_claim = sample_claim("arch-001");
        arch_claim.category = "architecture".to_string();

        file.add(sample_claim("safety-001"));
        file.add(arch_claim);

        assert_eq!(file.find_by_category("safety").len(), 1);
        assert_eq!(file.find_by_category("architecture").len(), 1);
        assert_eq!(file.find_by_category("imports").len(), 0);
    }

    #[test]
    fn test_find_by_status() {
        let mut file = ClaimsFile::new();
        let mut dep_claim = sample_claim("dep-001");
        dep_claim.status = ClaimStatus::Deprecated;

        file.add(sample_claim("active-001"));
        file.add(dep_claim);

        assert_eq!(file.find_by_status(&ClaimStatus::Active).len(), 1);
        assert_eq!(file.find_by_status(&ClaimStatus::Deprecated).len(), 1);
    }

    #[test]
    fn test_update() {
        let mut file = ClaimsFile::new();
        file.add(sample_claim("claim-001"));

        file.update("claim-001", |c| {
            c.provenance = "Updated provenance".to_string();
            c.updated_at = Some("2026-02-08T13:00:00Z".to_string());
        })
        .expect("update claim");

        assert_eq!(
            file.find_by_id("claim-001").map(|c| c.provenance.as_str()),
            Some("Updated provenance")
        );
    }

    #[test]
    fn test_update_not_found() {
        let mut file = ClaimsFile::new();
        assert!(file.update("nonexistent", |_| {}).is_err());
    }

    #[test]
    fn test_supersede() {
        let mut file = ClaimsFile::new();
        file.add(sample_claim("claim-001"));

        let mut new_claim = sample_claim("claim-002");
        new_claim.supersedes = Some("claim-001".to_string());
        new_claim.provenance = "Updated analysis".to_string();

        file.supersede("claim-001", new_claim).expect("supersede");

        assert_eq!(file.find_by_id("claim-001").map(|c| &c.status), Some(&ClaimStatus::Superseded));
        assert_eq!(
            file.find_by_id("claim-002").map(|c| c.supersedes.as_deref()),
            Some(Some("claim-001"))
        );
    }

    #[test]
    fn test_deprecate() {
        let mut file = ClaimsFile::new();
        file.add(sample_claim("claim-001"));

        file.deprecate("claim-001", "2026-02-08T14:00:00Z").expect("deprecate");

        let claim = file.find_by_id("claim-001").expect("find");
        assert_eq!(claim.status, ClaimStatus::Deprecated);
        assert_eq!(claim.updated_at.as_deref(), Some("2026-02-08T14:00:00Z"));
    }

    #[test]
    fn test_load_nonexistent() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let path = temp_dir.path().join("nonexistent.toml");

        let file = ClaimsFile::load(&path).expect("load should succeed");
        assert!(file.is_empty());
    }
}
