//! Pending marker persistence (TOML).
//!
//! Stores detected inline claim markers before formalization in
//! `.aphoria/pending_markers.toml`, keeping them separate from
//! `claims.toml` to avoid pollution.
//!
//! ## File Format
//!
//! ```toml
//! # Aphoria Pending Markers
//! #
//! # Detected claim markers awaiting formalization.
//! # Manage with: aphoria claims list-markers|formalize-marker|reject-marker
//!
//! [[marker]]
//! id = "marker-20260208-a1b2c3"
//! file = "src/pool/manager.rs"
//! line = 42
//! invariant = "Pool size MUST NOT exceed 50"
//! consequence = "OOM under sustained load"
//! category = "safety"
//! status = "pending"
//! detected_at = "2026-02-08T14:30:00Z"
//! formalized_to = "myapp-pool-max-001"  # After formalization
//! rejected_reason = ""                  # After rejection
//! ```

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

use serde::{Deserialize, Serialize};

use crate::AphoriaError;

/// Default path for pending markers file relative to project root.
pub const PENDING_MARKERS_FILE_PATH: &str = ".aphoria/pending_markers.toml";

/// Status of a pending marker.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum MarkerStatus {
    /// Marker detected, awaiting formalization.
    #[default]
    Pending,
    /// Marker formalized to a full claim.
    Formalized,
    /// Marker rejected (not worth a claim).
    Rejected,
}

/// A pending claim marker detected in code.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendingMarker {
    /// Unique identifier (auto-generated).
    pub id: String,

    /// File path where marker was found.
    pub file: String,

    /// Line number (1-indexed).
    pub line: usize,

    /// The invariant statement.
    pub invariant: String,

    /// Optional consequence (what breaks if violated).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub consequence: Option<String>,

    /// Optional category (safety, security, architecture, etc.).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub category: Option<String>,

    /// Current status.
    #[serde(default)]
    pub status: MarkerStatus,

    /// When this marker was first detected (ISO 8601).
    pub detected_at: String,

    /// Claim ID if formalized.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub formalized_to: Option<String>,

    /// Reason if rejected.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rejected_reason: Option<String>,
}

impl PendingMarker {
    /// Create a new pending marker with auto-generated ID.
    pub fn new(
        file: String,
        line: usize,
        invariant: String,
        consequence: Option<String>,
        category: Option<String>,
    ) -> Self {
        let id = Self::generate_id(&file, line, &invariant);
        let detected_at = chrono::Utc::now().to_rfc3339();

        Self {
            id,
            file,
            line,
            invariant,
            consequence,
            category,
            status: MarkerStatus::Pending,
            detected_at,
            formalized_to: None,
            rejected_reason: None,
        }
    }

    /// Generate a unique marker ID from file/line/content hash.
    fn generate_id(file: &str, line: usize, invariant: &str) -> String {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        let mut hasher = DefaultHasher::new();
        file.hash(&mut hasher);
        line.hash(&mut hasher);
        invariant.hash(&mut hasher);
        let hash = hasher.finish();

        format!("marker-{:x}", hash)
    }
}

/// Container for all pending markers in the file.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PendingMarkersFile {
    /// List of pending markers.
    #[serde(default, rename = "marker")]
    pub markers: Vec<PendingMarker>,
}

impl PendingMarkersFile {
    /// Create an empty pending markers file.
    pub fn new() -> Self {
        Self { markers: Vec::new() }
    }

    /// Add a marker, deduplicating by ID.
    pub fn add(&mut self, marker: PendingMarker) {
        if !self.markers.iter().any(|m| m.id == marker.id) {
            self.markers.push(marker);
        }
    }

    /// 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 pending markers: {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 Pending Markers
#
# Detected claim markers awaiting formalization.
# Each marker represents an inline annotation in code that should become a full claim.
#
# Manage with: aphoria claims list-markers|formalize-marker|reject-marker

"#;

        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 pending markers file.
    pub fn default_path(project_root: &Path) -> PathBuf {
        project_root.join(PENDING_MARKERS_FILE_PATH)
    }

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

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

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

    /// Find a marker by ID.
    pub fn find_by_id(&self, id: &str) -> Option<&PendingMarker> {
        self.markers.iter().find(|m| m.id == id)
    }

    /// Find a marker by ID (mutable).
    pub fn find_by_id_mut(&mut self, id: &str) -> Option<&mut PendingMarker> {
        self.markers.iter_mut().find(|m| m.id == id)
    }

    /// Find a marker by file and line.
    pub fn find_by_location(&self, file: &str, line: usize) -> Option<&PendingMarker> {
        self.markers.iter().find(|m| m.file == file && m.line == line)
    }

    /// Get all markers with a specific status.
    pub fn filter_by_status(&self, status: &MarkerStatus) -> Vec<&PendingMarker> {
        self.markers.iter().filter(|m| &m.status == status).collect()
    }

    /// Get all pending markers (status = Pending).
    pub fn pending_markers(&self) -> Vec<&PendingMarker> {
        self.filter_by_status(&MarkerStatus::Pending)
    }

    /// Update a marker's status and related fields.
    pub fn update_status(
        &mut self,
        id: &str,
        status: MarkerStatus,
        formalized_to: Option<String>,
        rejected_reason: Option<String>,
    ) -> Result<(), AphoriaError> {
        let marker = self
            .find_by_id_mut(id)
            .ok_or_else(|| AphoriaError::Claims(format!("Marker not found: {id}")))?;

        marker.status = status;
        marker.formalized_to = formalized_to;
        marker.rejected_reason = rejected_reason;

        Ok(())
    }
}

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

    fn sample_marker(id: &str, file: &str, line: usize) -> PendingMarker {
        PendingMarker {
            id: id.to_string(),
            file: file.to_string(),
            line,
            invariant: "Test invariant".to_string(),
            consequence: Some("Test consequence".to_string()),
            category: Some("safety".to_string()),
            status: MarkerStatus::Pending,
            detected_at: "2026-02-08T12:00:00Z".to_string(),
            formalized_to: None,
            rejected_reason: None,
        }
    }

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

        let mut file = PendingMarkersFile::new();
        file.add(sample_marker("marker-001", "src/test.rs", 42));
        file.add(sample_marker("marker-002", "src/test.rs", 50));

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

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

    #[test]
    fn test_no_duplicates() {
        let mut file = PendingMarkersFile::new();
        file.add(sample_marker("marker-001", "src/test.rs", 42));
        file.add(sample_marker("marker-001", "src/test.rs", 42));
        assert_eq!(file.len(), 1);
    }

    #[test]
    fn test_find_by_id() {
        let mut file = PendingMarkersFile::new();
        file.add(sample_marker("marker-001", "src/test.rs", 42));
        file.add(sample_marker("marker-002", "src/test.rs", 50));

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

    #[test]
    fn test_find_by_location() {
        let mut file = PendingMarkersFile::new();
        file.add(sample_marker("marker-001", "src/test.rs", 42));
        file.add(sample_marker("marker-002", "src/other.rs", 42));

        assert!(file.find_by_location("src/test.rs", 42).is_some());
        assert!(file.find_by_location("src/test.rs", 99).is_none());
    }

    #[test]
    fn test_filter_by_status() {
        let mut file = PendingMarkersFile::new();
        let mut marker1 = sample_marker("marker-001", "src/test.rs", 42);
        marker1.status = MarkerStatus::Formalized;
        file.add(marker1);
        file.add(sample_marker("marker-002", "src/test.rs", 50)); // Pending

        assert_eq!(file.filter_by_status(&MarkerStatus::Pending).len(), 1);
        assert_eq!(file.filter_by_status(&MarkerStatus::Formalized).len(), 1);
        assert_eq!(file.filter_by_status(&MarkerStatus::Rejected).len(), 0);
    }

    #[test]
    fn test_pending_markers() {
        let mut file = PendingMarkersFile::new();
        let mut marker1 = sample_marker("marker-001", "src/test.rs", 42);
        marker1.status = MarkerStatus::Formalized;
        file.add(marker1);
        file.add(sample_marker("marker-002", "src/test.rs", 50)); // Pending

        assert_eq!(file.pending_markers().len(), 1);
    }

    #[test]
    fn test_update_status() {
        let mut file = PendingMarkersFile::new();
        file.add(sample_marker("marker-001", "src/test.rs", 42));

        file.update_status(
            "marker-001",
            MarkerStatus::Formalized,
            Some("myapp-test-001".to_string()),
            None,
        )
        .expect("update status");

        let marker = file.find_by_id("marker-001").expect("find marker");
        assert_eq!(marker.status, MarkerStatus::Formalized);
        assert_eq!(marker.formalized_to.as_deref(), Some("myapp-test-001"));
    }

    #[test]
    fn test_update_status_not_found() {
        let mut file = PendingMarkersFile::new();
        assert!(
            file.update_status("nonexistent", MarkerStatus::Rejected, None, Some("reason".to_string()))
                .is_err()
        );
    }

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

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

    #[test]
    fn test_generate_id_consistency() {
        let marker1 = PendingMarker::new(
            "src/test.rs".to_string(),
            42,
            "Test invariant".to_string(),
            None,
            None,
        );

        let marker2 = PendingMarker::new(
            "src/test.rs".to_string(),
            42,
            "Test invariant".to_string(),
            None,
            None,
        );

        assert_eq!(marker1.id, marker2.id, "Same file/line/invariant should generate same ID");
    }

    #[test]
    fn test_generate_id_different_line() {
        let marker1 = PendingMarker::new(
            "src/test.rs".to_string(),
            42,
            "Test invariant".to_string(),
            None,
            None,
        );

        let marker2 = PendingMarker::new(
            "src/test.rs".to_string(),
            43,
            "Test invariant".to_string(),
            None,
            None,
        );

        assert_ne!(marker1.id, marker2.id, "Different lines should generate different IDs");
    }

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

        let mut file = PendingMarkersFile::new();
        let marker = PendingMarker {
            id: "marker-001".to_string(),
            file: "src/test.rs".to_string(),
            line: 42,
            invariant: "Test invariant".to_string(),
            consequence: None, // No consequence
            category: None,    // No category
            status: MarkerStatus::Pending,
            detected_at: "2026-02-08T12:00:00Z".to_string(),
            formalized_to: None,
            rejected_reason: None,
        };
        file.add(marker);

        file.save(&path).expect("save");
        let loaded = PendingMarkersFile::load(&path).expect("load");

        assert_eq!(loaded.markers[0].consequence, None);
        assert_eq!(loaded.markers[0].category, None);
    }
}
