//! Scroll file export: writes `.scroll` files to `<workspace>/.furnace/scrolls/`.
//!
//! The `.scroll` file format is JSON containing the scroll definition, tasks,
//! and agent activities. IDE extensions (VS Code, IntelliJ) watch this directory
//! to display scroll status and agent activity.

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

use anyhow::Result;
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};

use crate::db::SqliteDb;
use crate::types::{AgentActivity, Scroll, Task};

/// The on-disk representation of a `.scroll` file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScrollFile {
    /// Format version for forwards compatibility.
    pub version: u32,
    /// The scroll definition.
    pub scroll: Scroll,
    /// Tasks belonging to this scroll.
    pub tasks: Vec<Task>,
    /// Agent activities recorded during execution.
    pub agent_activities: Vec<AgentActivity>,
}

/// Handles exporting scroll state to `.scroll` files on disk.
pub struct ScrollExporter {
    scrolls_dir: PathBuf,
    sqlite: Arc<SqliteDb>,
}

impl ScrollExporter {
    pub fn new(scrolls_dir: PathBuf, sqlite: Arc<SqliteDb>) -> Self {
        Self { scrolls_dir, sqlite }
    }

    /// Export a scroll (with tasks and activities) to a `.scroll` file.
    ///
    /// File path: `<scrolls_dir>/<scroll_id>.scroll`
    pub async fn export_scroll(&self, scroll_id: &str) -> Result<PathBuf> {
        let scroll = self.sqlite.get_scroll(scroll_id).await?;
        let tasks = self.sqlite.get_tasks_for_scroll(scroll_id).await?;
        let activities = self.sqlite.get_agent_activities_for_scroll(scroll_id).await?;

        let scroll_file = ScrollFile {
            version: 1,
            scroll,
            tasks,
            agent_activities: activities,
        };

        std::fs::create_dir_all(&self.scrolls_dir)?;

        let file_path = self.scroll_path(scroll_id);
        let json = serde_json::to_string_pretty(&scroll_file)?;
        std::fs::write(&file_path, json)?;

        debug!(scroll_id, path = %file_path.display(), "Scroll exported to file");

        Ok(file_path)
    }

    /// Export a scroll, logging errors rather than propagating them.
    /// Used in fire-and-forget contexts (e.g., after status changes).
    pub async fn export_scroll_quiet(&self, scroll_id: &str) {
        if let Err(e) = self.export_scroll(scroll_id).await {
            warn!(scroll_id, error = %e, "Failed to export scroll file");
        }
    }

    /// Return the path where a scroll file would be written.
    pub fn scroll_path(&self, scroll_id: &str) -> PathBuf {
        self.scrolls_dir.join(format!("{}.scroll", scroll_id))
    }

    /// Delete the `.scroll` file for a given scroll (e.g., on cleanup).
    pub fn delete_scroll_file(&self, scroll_id: &str) {
        let path = self.scroll_path(scroll_id);
        if path.exists() {
            if let Err(e) = std::fs::remove_file(&path) {
                warn!(scroll_id, error = %e, "Failed to delete scroll file");
            } else {
                debug!(scroll_id, "Scroll file deleted");
            }
        }
    }

    /// Return the scrolls directory path.
    pub fn scrolls_dir(&self) -> &Path {
        &self.scrolls_dir
    }
}

// ===========================================================================
// Tests
// ===========================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::*;
    use chrono::Utc;
    use tempfile::{NamedTempFile, TempDir};

    async fn make_db() -> (Arc<SqliteDb>, NamedTempFile) {
        let tmp = NamedTempFile::new().expect("temp file");
        let db = Arc::new(SqliteDb::new(tmp.path()).expect("sqlite"));
        (db, tmp)
    }

    fn make_scroll(id: &str) -> Scroll {
        let now = Utc::now();
        Scroll {
            id: id.into(),
            title: "Test scroll".into(),
            intent: "testing export".into(),
            constraints: vec![],
            tasks: vec![],
            metadata: ScrollMetadata {
                execution_mode: ExecutionMode::Forge,
                auto_forge_config: None,
                recipe_used: None,
                search_context: vec![],
            },
            status: ScrollStatus::Draft,
            created_at: now,
            updated_at: now,
            cleanup_after: None,
        }
    }

    #[tokio::test]
    async fn test_export_creates_scroll_file() {
        let (db, _tmp) = make_db().await;
        let dir = TempDir::new().unwrap();

        db.create_scroll(&make_scroll("s1")).await.unwrap();

        let exporter = ScrollExporter::new(dir.path().join("scrolls"), Arc::clone(&db));
        let path = exporter.export_scroll("s1").await.unwrap();

        assert!(path.exists());
        assert!(path.to_string_lossy().ends_with("s1.scroll"));

        // Parse and verify
        let content = std::fs::read_to_string(&path).unwrap();
        let scroll_file: ScrollFile = serde_json::from_str(&content).unwrap();
        assert_eq!(scroll_file.version, 1);
        assert_eq!(scroll_file.scroll.id, "s1");
        assert!(scroll_file.tasks.is_empty());
        assert!(scroll_file.agent_activities.is_empty());
    }

    #[tokio::test]
    async fn test_export_includes_tasks_and_activities() {
        let (db, _tmp) = make_db().await;
        let dir = TempDir::new().unwrap();

        db.create_scroll(&make_scroll("s2")).await.unwrap();

        let task = Task {
            id: "t1".into(),
            scroll_id: "s2".into(),
            description: "test task".into(),
            target_file: "test.rs".into(),
            task_type: TaskType::Create,
            status: TaskStatus::Completed,
            dependencies: vec![],
            generated_code: Some("fn main() {}".into()),
            diff: None,
            created_at: Utc::now(),
            group_label: None,
        };
        db.create_task(&task).await.unwrap();

        let activity = AgentActivity {
            id: "act-1".into(),
            scroll_id: "s2".into(),
            task_id: "t1".into(),
            agent_name: "Gandalf".into(),
            action: "build_constraint_context".into(),
            detail: "Extracted 3 constraints".into(),
            started_at: Utc::now(),
            completed_at: Some(Utc::now()),
            status: AgentActivityStatus::Completed,
        };
        db.record_agent_activity(&activity).await.unwrap();

        let exporter = ScrollExporter::new(dir.path().join("scrolls"), Arc::clone(&db));
        let path = exporter.export_scroll("s2").await.unwrap();

        let content = std::fs::read_to_string(&path).unwrap();
        let scroll_file: ScrollFile = serde_json::from_str(&content).unwrap();
        assert_eq!(scroll_file.tasks.len(), 1);
        assert_eq!(scroll_file.tasks[0].id, "t1");
        assert_eq!(scroll_file.agent_activities.len(), 1);
        assert_eq!(scroll_file.agent_activities[0].agent_name, "Gandalf");
    }

    #[tokio::test]
    async fn test_delete_scroll_file() {
        let (db, _tmp) = make_db().await;
        let dir = TempDir::new().unwrap();

        db.create_scroll(&make_scroll("s3")).await.unwrap();

        let exporter = ScrollExporter::new(dir.path().join("scrolls"), Arc::clone(&db));
        let path = exporter.export_scroll("s3").await.unwrap();
        assert!(path.exists());

        exporter.delete_scroll_file("s3");
        assert!(!path.exists());
    }

    #[tokio::test]
    async fn test_export_overwrites_existing() {
        let (db, _tmp) = make_db().await;
        let dir = TempDir::new().unwrap();

        db.create_scroll(&make_scroll("s4")).await.unwrap();

        let exporter = ScrollExporter::new(dir.path().join("scrolls"), Arc::clone(&db));

        // First export
        exporter.export_scroll("s4").await.unwrap();

        // Update scroll status (valid transitions: Draft → Validated → InProgress)
        db.update_scroll_status("s4", &ScrollStatus::Validated).await.unwrap();
        db.update_scroll_status("s4", &ScrollStatus::InProgress).await.unwrap();

        // Re-export
        let path = exporter.export_scroll("s4").await.unwrap();
        let content = std::fs::read_to_string(&path).unwrap();
        let scroll_file: ScrollFile = serde_json::from_str(&content).unwrap();
        assert_eq!(scroll_file.scroll.status, ScrollStatus::InProgress);
    }
}
