//! Guideline tracking for best practices ingestion.
//!
//! Tracks which architectural/security guidelines have been imported as claims,
//! enabling change detection and compliance filtering.

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

use serde::{Deserialize, Serialize};

use crate::AphoriaError;

#[cfg(test)]
use chrono::Utc;

/// Tracks a guideline that has been ingested as claims.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)] // Used in handlers and tests
pub struct GuidelineMetadata {
    /// Unique guideline ID (e.g., "hexagonal-arch", "owasp-top-10")
    pub id: String,

    /// Human-readable name
    pub name: String,

    /// Path to the source document (relative to project root)
    pub source_path: Option<PathBuf>,

    /// BLAKE3 hash of the source document (for change detection)
    pub document_hash: Option<String>,

    /// When this guideline was first ingested
    pub ingested_at: String,

    /// How many claims were created from this guideline
    pub claims_count: usize,

    /// Authority tier applied to claims (e.g., "team_policy")
    pub authority_tier: String,

    /// Category (e.g., "architecture", "security")
    pub category: String,

    /// Claim IDs associated with this guideline
    pub claim_ids: Vec<String>,
}

/// Manages the ingested guidelines registry (.aphoria/ingested_guides.toml).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[allow(dead_code)] // Used in handlers and tests
pub struct IngestedGuidesFile {
    /// List of tracked guidelines.
    #[serde(default)]
    pub guide: Vec<GuidelineMetadata>,
}

#[allow(dead_code)] // Methods used by handlers and tests
impl IngestedGuidesFile {
    /// Default path for the ingested guides file.
    pub fn default_path(project_root: &Path) -> PathBuf {
        project_root.join(".aphoria/ingested_guides.toml")
    }

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

        let content = std::fs::read_to_string(path)?;

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

    /// Save to TOML file.
    pub fn save(&self, path: &Path) -> Result<(), AphoriaError> {
        let content = toml::to_string_pretty(self).map_err(|e| {
            AphoriaError::Claims(format!("Failed to serialize ingested guides: {e}"))
        })?;

        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        std::fs::write(path, content)?;

        Ok(())
    }

    /// Add or update a guideline.
    ///
    /// If a guideline with the same ID exists, it is replaced.
    /// Otherwise, the guideline is appended to the list.
    pub fn upsert(&mut self, guideline: GuidelineMetadata) {
        if let Some(idx) = self.guide.iter().position(|g| g.id == guideline.id) {
            self.guide[idx] = guideline;
        } else {
            self.guide.push(guideline);
        }
    }

    /// Get a guideline by ID.
    ///
    /// Returns None if the guideline doesn't exist.
    pub fn get(&self, id: &str) -> Option<&GuidelineMetadata> {
        self.guide.iter().find(|g| g.id == id)
    }

    /// Remove a guideline by ID.
    ///
    /// Returns true if the guideline was removed, false if it didn't exist.
    pub fn remove(&mut self, id: &str) -> bool {
        if let Some(idx) = self.guide.iter().position(|g| g.id == id) {
            self.guide.remove(idx);
            true
        } else {
            false
        }
    }

    /// List all guidelines, optionally filtered by category.
    ///
    /// Pass None to get all guidelines, or Some(category) to filter.
    pub fn list(&self, category: Option<&str>) -> Vec<&GuidelineMetadata> {
        self.guide.iter().filter(|g| category.map_or(true, |c| g.category == c)).collect()
    }
}

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

    #[test]
    fn test_guideline_upsert() {
        let mut file = IngestedGuidesFile::default();

        let guide = GuidelineMetadata {
            id: "test-guide".to_string(),
            name: "Test Guide".to_string(),
            source_path: None,
            document_hash: None,
            ingested_at: Utc::now().to_rfc3339(),
            claims_count: 5,
            authority_tier: "team_policy".to_string(),
            category: "architecture".to_string(),
            claim_ids: vec!["claim-1".to_string(), "claim-2".to_string()],
        };

        file.upsert(guide.clone());
        assert_eq!(file.guide.len(), 1);

        // Update
        let mut updated = guide.clone();
        updated.claims_count = 10;
        file.upsert(updated);
        assert_eq!(file.guide.len(), 1);
        assert_eq!(file.guide[0].claims_count, 10);
    }

    #[test]
    fn test_guideline_get_and_remove() {
        let mut file = IngestedGuidesFile::default();

        let guide = GuidelineMetadata {
            id: "test-guide".to_string(),
            name: "Test Guide".to_string(),
            source_path: None,
            document_hash: None,
            ingested_at: Utc::now().to_rfc3339(),
            claims_count: 5,
            authority_tier: "team_policy".to_string(),
            category: "architecture".to_string(),
            claim_ids: vec![],
        };

        file.upsert(guide);

        assert!(file.get("test-guide").is_some());
        assert!(file.remove("test-guide"));
        assert!(file.get("test-guide").is_none());
        assert!(!file.remove("test-guide"));
    }

    #[test]
    fn test_guideline_list_filter() {
        let mut file = IngestedGuidesFile::default();

        file.upsert(GuidelineMetadata {
            id: "arch-1".to_string(),
            name: "Arch Guide 1".to_string(),
            source_path: None,
            document_hash: None,
            ingested_at: Utc::now().to_rfc3339(),
            claims_count: 5,
            authority_tier: "team_policy".to_string(),
            category: "architecture".to_string(),
            claim_ids: vec![],
        });

        file.upsert(GuidelineMetadata {
            id: "sec-1".to_string(),
            name: "Security Guide 1".to_string(),
            source_path: None,
            document_hash: None,
            ingested_at: Utc::now().to_rfc3339(),
            claims_count: 3,
            authority_tier: "team_policy".to_string(),
            category: "security".to_string(),
            claim_ids: vec![],
        });

        assert_eq!(file.list(None).len(), 2);
        assert_eq!(file.list(Some("architecture")).len(), 1);
        assert_eq!(file.list(Some("security")).len(), 1);
    }
}
