//! Acknowledgment file export/import.
//!
//! This module handles serializing acknowledgments to a TOML file for
//! version control, and importing them back into the local Episteme.
//!
//! ## File Format
//!
//! The `.aphoria/acks.toml` file contains:
//!
//! ```toml
//! # Aphoria Acknowledgments - version controlled
//! #
//! # This file records intentional exceptions to security policies.
//! # To regenerate: aphoria ack export
//! # To import: aphoria ack import
//!
//! [[ack]]
//! path = "code://rust/myapp/tls/cert_verification"
//! reason = "Self-signed certs in dev environment"
//! expires = "2026-12-31T00:00:00Z"  # Optional
//! created = "2026-02-07T10:30:00Z"
//! by = "jordan@example.com"  # Optional
//! ```

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

use serde::{Deserialize, Serialize};

use crate::AphoriaError;

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

/// A serialized acknowledgment entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AckEntry {
    /// The concept path being acknowledged.
    pub path: String,

    /// Reason for the acknowledgment.
    pub reason: String,

    /// Optional expiry timestamp (ISO 8601 format).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires: Option<String>,

    /// When the acknowledgment was created (ISO 8601 format).
    pub created: String,

    /// Who created the acknowledgment.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub by: Option<String>,
}

/// Container for all acknowledgments in the file.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AckFile {
    /// List of acknowledgments.
    #[serde(default, rename = "ack")]
    pub acks: Vec<AckEntry>,
}

impl AckFile {
    /// Create an empty ack file.
    pub fn new() -> Self {
        Self { acks: Vec::new() }
    }

    /// Add an acknowledgment entry.
    pub fn add(&mut self, entry: AckEntry) {
        // Check for duplicates by path
        if !self.acks.iter().any(|a| a.path == entry.path) {
            self.acks.push(entry);
        }
    }

    /// 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::Config(format!("Failed to parse ack file: {e}")))
    }

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

        let header = r#"# Aphoria Acknowledgments - version controlled
#
# This file records intentional exceptions to security policies.
# Each entry represents a finding that has been reviewed and accepted.
#
# To regenerate from database: aphoria ack export
# To import into database: aphoria ack import
#
# Glob patterns in paths are expanded during import.
# Example: "code://*/vulnbank/**" matches all vulnbank paths.

"#;

        let content =
            toml::to_string_pretty(self).map_err(|e| AphoriaError::Config(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 ack file.
    pub fn default_path(project_root: &Path) -> PathBuf {
        project_root.join(ACK_FILE_PATH)
    }

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

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

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

/// Parse the JSON payload stored in an acknowledgment assertion.
///
/// Acknowledgments are stored as assertions with a JSON object containing:
/// - "reason": string
/// - "expires_at": optional timestamp
///
/// Falls back to plain text for legacy acks.
#[derive(Debug, Clone, Deserialize)]
pub struct AckPayload {
    /// The reason for the acknowledgment.
    pub reason: String,
    /// Optional expiry timestamp (Unix seconds).
    #[serde(default)]
    pub expires_at: Option<i64>,
}

impl AckPayload {
    /// Parse from the assertion's object value.
    pub fn parse(text: &str) -> Self {
        // Try JSON first
        if let Ok(payload) = serde_json::from_str::<AckPayload>(text) {
            return payload;
        }

        // Fall back to plain text reason
        Self { reason: text.to_string(), expires_at: None }
    }

    /// Format expiry as ISO 8601 string.
    pub fn expires_iso(&self) -> Option<String> {
        self.expires_at.map(|ts| {
            chrono::DateTime::from_timestamp(ts, 0)
                .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
                .unwrap_or_else(|| format!("{ts}"))
        })
    }
}

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

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

        let mut ack_file = AckFile::new();
        ack_file.add(AckEntry {
            path: "code://rust/myapp/tls/cert_verification".to_string(),
            reason: "Self-signed certs in dev".to_string(),
            expires: Some("2026-12-31T00:00:00Z".to_string()),
            created: "2026-02-07T10:00:00Z".to_string(),
            by: Some("jordan@example.com".to_string()),
        });
        ack_file.add(AckEntry {
            path: "code://rust/myapp/secrets/api_key".to_string(),
            reason: "Test API key".to_string(),
            expires: None,
            created: "2026-02-07T10:00:00Z".to_string(),
            by: None,
        });

        ack_file.save(&path).expect("save ack file");

        let loaded = AckFile::load(&path).expect("load ack file");
        assert_eq!(loaded.len(), 2);
        assert_eq!(loaded.acks[0].path, "code://rust/myapp/tls/cert_verification");
        assert_eq!(loaded.acks[1].reason, "Test API key");
    }

    #[test]
    fn test_ack_file_no_duplicates() {
        let mut ack_file = AckFile::new();

        ack_file.add(AckEntry {
            path: "code://rust/test".to_string(),
            reason: "First".to_string(),
            expires: None,
            created: "2026-02-07T10:00:00Z".to_string(),
            by: None,
        });

        ack_file.add(AckEntry {
            path: "code://rust/test".to_string(),
            reason: "Duplicate".to_string(),
            expires: None,
            created: "2026-02-07T10:00:00Z".to_string(),
            by: None,
        });

        assert_eq!(ack_file.len(), 1);
        assert_eq!(ack_file.acks[0].reason, "First");
    }

    #[test]
    fn test_ack_payload_parse_json() {
        let json = r#"{"reason":"Test reason","expires_at":1735689600}"#;
        let payload = AckPayload::parse(json);
        assert_eq!(payload.reason, "Test reason");
        assert_eq!(payload.expires_at, Some(1735689600));
    }

    #[test]
    fn test_ack_payload_parse_plain_text() {
        let text = "Plain text reason";
        let payload = AckPayload::parse(text);
        assert_eq!(payload.reason, "Plain text reason");
        assert!(payload.expires_at.is_none());
    }

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

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