//! Scroll file import: reads `.scroll` files from disk and reconciles with SQLite.
//!
//! This is the reverse of `scroll_export`. When a user edits a `.scroll` file
//! via an IDE or text editor, `ScrollImporter` detects the change and syncs
//! the modifications back into the SQLite database (the source of truth).

use std::path::PathBuf;
use std::sync::Arc;

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

use crate::db::SqliteDb;
use crate::scroll_export::ScrollFile;
use crate::types::Task;

/// Result of importing a `.scroll` file into SQLite.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ImportResult {
    pub scroll_updated: bool,
    pub tasks_created: usize,
    pub tasks_updated: usize,
    pub tasks_deleted: usize,
}

/// Handles importing scroll state from `.scroll` files on disk back into SQLite.
pub struct ScrollImporter {
    scrolls_dir: PathBuf,
    sqlite: Arc<SqliteDb>,
}

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

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

    /// Check if the `.scroll` file on disk is newer than the SQLite record.
    /// If so, import it. Returns `Ok(true)` if a sync was performed.
    pub async fn sync_if_needed(&self, scroll_id: &str) -> Result<bool> {
        let path = self.scroll_path(scroll_id);
        if !path.exists() {
            return Ok(false);
        }

        let file_mtime = std::fs::metadata(&path)
            .and_then(|m| m.modified())
            .context("Failed to read .scroll file metadata")?;

        let scroll = self.sqlite.get_scroll(scroll_id).await?;
        let db_updated = scroll
            .updated_at
            .timestamp()
            .max(0) as u64;

        let file_epoch = file_mtime
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0);

        if file_epoch > db_updated {
            info!(
                scroll_id,
                file_epoch,
                db_updated,
                "Scroll file is newer than DB — importing changes"
            );
            self.import_scroll(scroll_id).await?;
            Ok(true)
        } else {
            debug!(scroll_id, "Scroll file is not newer than DB — no import needed");
            Ok(false)
        }
    }

    /// Parse a `.scroll` file and reconcile its contents with SQLite.
    /// All database mutations are applied atomically in a single transaction.
    pub async fn import_scroll(&self, scroll_id: &str) -> Result<ImportResult> {
        let path = self.scroll_path(scroll_id);
        let content = tokio::fs::read_to_string(&path)
            .await
            .context("Failed to read .scroll file")?;

        let scroll_file: ScrollFile = serde_json::from_str(&content)
            .context("Failed to parse .scroll JSON")?;

        let mut result = ImportResult::default();

        // 1. Compute scroll field diff.
        let db_scroll = self.sqlite.get_scroll(scroll_id).await?;
        let file_scroll = &scroll_file.scroll;

        let title_changed = db_scroll.title != file_scroll.title;
        let intent_changed = db_scroll.intent != file_scroll.intent;
        let mode_changed = db_scroll.metadata.execution_mode != file_scroll.metadata.execution_mode;
        let afc_changed = db_scroll.metadata.auto_forge_config != file_scroll.metadata.auto_forge_config;

        let scroll_title = if title_changed { Some(file_scroll.title.as_str()) } else { None };
        let scroll_intent = if intent_changed { Some(file_scroll.intent.as_str()) } else { None };
        let scroll_mode = if mode_changed { Some(&file_scroll.metadata.execution_mode) } else { None };
        let scroll_afc = if afc_changed {
            Some(file_scroll.metadata.auto_forge_config.as_ref())
        } else {
            None
        };
        result.scroll_updated = title_changed || intent_changed || mode_changed || afc_changed;

        // 2. Compute task diff.
        let db_tasks = self.sqlite.get_tasks_for_scroll(scroll_id).await?;
        let file_tasks = &scroll_file.tasks;

        let db_map: std::collections::HashMap<&str, &Task> =
            db_tasks.iter().map(|t| (t.id.as_str(), t)).collect();
        let file_map: std::collections::HashMap<&str, &Task> =
            file_tasks.iter().map(|t| (t.id.as_str(), t)).collect();

        // Tasks to create (in file but not in DB).
        let mut tasks_to_create = Vec::new();
        for ft in file_tasks {
            if !db_map.contains_key(ft.id.as_str()) {
                tasks_to_create.push(ft.clone());
            }
        }
        result.tasks_created = tasks_to_create.len();

        // Tasks to update (in both, with changes).
        let mut task_updates = Vec::new();
        for ft in file_tasks {
            if let Some(dt) = db_map.get(ft.id.as_str()) {
                let desc_changed = dt.description != ft.description;
                let file_changed = dt.target_file != ft.target_file;
                let type_changed = dt.task_type != ft.task_type;
                let code_changed = dt.generated_code != ft.generated_code;
                let deps_changed = dt.dependencies != ft.dependencies;
                let group_changed = dt.group_label != ft.group_label;

                if desc_changed || file_changed || type_changed || code_changed || deps_changed || group_changed {
                    task_updates.push((
                        ft.id.clone(),
                        if desc_changed { Some(ft.description.clone()) } else { None },
                        if file_changed { Some(ft.target_file.clone()) } else { None },
                        if type_changed { Some(ft.task_type.clone()) } else { None },
                        if code_changed { Some(ft.generated_code.clone()) } else { None },
                        if deps_changed { Some(ft.dependencies.clone()) } else { None },
                        if group_changed { Some(ft.group_label.clone()) } else { None },
                    ));
                }
            }
        }
        result.tasks_updated = task_updates.len();

        // Tasks to delete (in DB but not in file).
        let to_delete: Vec<String> = db_tasks
            .iter()
            .filter(|dt| !file_map.contains_key(dt.id.as_str()))
            .map(|dt| dt.id.clone())
            .collect();
        result.tasks_deleted = to_delete.len();

        // 3. Apply all changes atomically.
        self.sqlite
            .apply_import_changes(
                scroll_id,
                scroll_title,
                scroll_intent,
                scroll_mode,
                scroll_afc,
                &tasks_to_create,
                &task_updates,
                &to_delete,
            )
            .await?;

        info!(
            scroll_id,
            scroll_updated = result.scroll_updated,
            tasks_created = result.tasks_created,
            tasks_updated = result.tasks_updated,
            tasks_deleted = result.tasks_deleted,
            "Scroll imported from file"
        );

        Ok(result)
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::scroll_export::{ScrollExporter, ScrollFile};
    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: "Original title".into(),
            intent: "Original intent".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,
        }
    }

    fn make_task(id: &str, scroll_id: &str, target: &str) -> Task {
        Task {
            id: id.into(),
            scroll_id: scroll_id.into(),
            description: format!("Task for {}", target),
            target_file: target.into(),
            task_type: TaskType::Create,
            status: TaskStatus::Pending,
            dependencies: vec![],
            generated_code: Some(format!("// code for {}", target)),
            diff: None,
            created_at: Utc::now(),
            group_label: None,
        }
    }

    #[tokio::test]
    async fn test_import_detects_title_change() {
        let (db, _tmp) = make_db().await;
        let dir = TempDir::new().unwrap();
        let scrolls_dir = dir.path().join("scrolls");

        db.create_scroll(&make_scroll("imp-1")).await.unwrap();

        // Export, then modify the file
        let exporter = ScrollExporter::new(scrolls_dir.clone(), Arc::clone(&db));
        exporter.export_scroll("imp-1").await.unwrap();

        let path = scrolls_dir.join("imp-1.scroll");
        let content = std::fs::read_to_string(&path).unwrap();
        let mut sf: ScrollFile = serde_json::from_str(&content).unwrap();
        sf.scroll.title = "User-edited title".into();
        std::fs::write(&path, serde_json::to_string_pretty(&sf).unwrap()).unwrap();

        // Import
        let importer = ScrollImporter::new(scrolls_dir, Arc::clone(&db));
        let result = importer.import_scroll("imp-1").await.unwrap();

        assert!(result.scroll_updated);
        let scroll = db.get_scroll("imp-1").await.unwrap();
        assert_eq!(scroll.title, "User-edited title");
    }

    #[tokio::test]
    async fn test_import_detects_task_update() {
        let (db, _tmp) = make_db().await;
        let dir = TempDir::new().unwrap();
        let scrolls_dir = dir.path().join("scrolls");

        db.create_scroll(&make_scroll("imp-2")).await.unwrap();
        db.create_task(&make_task("imp-2-T1", "imp-2", "main.rs")).await.unwrap();

        let exporter = ScrollExporter::new(scrolls_dir.clone(), Arc::clone(&db));
        exporter.export_scroll("imp-2").await.unwrap();

        // Modify task code in file
        let path = scrolls_dir.join("imp-2.scroll");
        let content = std::fs::read_to_string(&path).unwrap();
        let mut sf: ScrollFile = serde_json::from_str(&content).unwrap();
        sf.tasks[0].generated_code = Some("// updated by user".into());
        std::fs::write(&path, serde_json::to_string_pretty(&sf).unwrap()).unwrap();

        let importer = ScrollImporter::new(scrolls_dir, Arc::clone(&db));
        let result = importer.import_scroll("imp-2").await.unwrap();

        assert_eq!(result.tasks_updated, 1);
        let task = db.get_task_by_id("imp-2-T1").await.unwrap();
        assert_eq!(task.generated_code.as_deref(), Some("// updated by user"));
    }

    #[tokio::test]
    async fn test_import_detects_task_removal() {
        let (db, _tmp) = make_db().await;
        let dir = TempDir::new().unwrap();
        let scrolls_dir = dir.path().join("scrolls");

        db.create_scroll(&make_scroll("imp-3")).await.unwrap();
        db.create_task(&make_task("imp-3-T1", "imp-3", "a.rs")).await.unwrap();
        db.create_task(&make_task("imp-3-T2", "imp-3", "b.rs")).await.unwrap();

        let exporter = ScrollExporter::new(scrolls_dir.clone(), Arc::clone(&db));
        exporter.export_scroll("imp-3").await.unwrap();

        // Remove task T2 from file
        let path = scrolls_dir.join("imp-3.scroll");
        let content = std::fs::read_to_string(&path).unwrap();
        let mut sf: ScrollFile = serde_json::from_str(&content).unwrap();
        sf.tasks.retain(|t| t.id != "imp-3-T2");
        std::fs::write(&path, serde_json::to_string_pretty(&sf).unwrap()).unwrap();

        let importer = ScrollImporter::new(scrolls_dir, Arc::clone(&db));
        let result = importer.import_scroll("imp-3").await.unwrap();

        assert_eq!(result.tasks_deleted, 1);
        let tasks = db.get_tasks_for_scroll("imp-3").await.unwrap();
        assert_eq!(tasks.len(), 1);
        assert_eq!(tasks[0].id, "imp-3-T1");
    }

    #[tokio::test]
    async fn test_import_detects_new_task() {
        let (db, _tmp) = make_db().await;
        let dir = TempDir::new().unwrap();
        let scrolls_dir = dir.path().join("scrolls");

        db.create_scroll(&make_scroll("imp-4")).await.unwrap();

        let exporter = ScrollExporter::new(scrolls_dir.clone(), Arc::clone(&db));
        exporter.export_scroll("imp-4").await.unwrap();

        // Add a new task in the file
        let path = scrolls_dir.join("imp-4.scroll");
        let content = std::fs::read_to_string(&path).unwrap();
        let mut sf: ScrollFile = serde_json::from_str(&content).unwrap();
        sf.tasks.push(Task {
            id: "imp-4-T1".into(),
            scroll_id: "imp-4".into(),
            description: "User-added task".into(),
            target_file: "new_file.rs".into(),
            task_type: TaskType::Create,
            status: TaskStatus::Pending,
            dependencies: vec![],
            generated_code: Some("// user code".into()),
            diff: None,
            created_at: Utc::now(),
            group_label: None,
        });
        std::fs::write(&path, serde_json::to_string_pretty(&sf).unwrap()).unwrap();

        let importer = ScrollImporter::new(scrolls_dir, Arc::clone(&db));
        let result = importer.import_scroll("imp-4").await.unwrap();

        assert_eq!(result.tasks_created, 1);
        let tasks = db.get_tasks_for_scroll("imp-4").await.unwrap();
        assert_eq!(tasks.len(), 1);
        assert_eq!(tasks[0].description, "User-added task");
    }

    #[tokio::test]
    async fn test_import_detects_group_label_change() {
        let (db, _tmp) = make_db().await;
        let dir = TempDir::new().unwrap();
        let scrolls_dir = dir.path().join("scrolls");

        db.create_scroll(&make_scroll("imp-5")).await.unwrap();
        db.create_task(&make_task("imp-5-T1", "imp-5", "src/mod.rs")).await.unwrap();

        let exporter = ScrollExporter::new(scrolls_dir.clone(), Arc::clone(&db));
        exporter.export_scroll("imp-5").await.unwrap();

        // Add group_label in the scroll file
        let path = scrolls_dir.join("imp-5.scroll");
        let content = std::fs::read_to_string(&path).unwrap();
        let mut sf: ScrollFile = serde_json::from_str(&content).unwrap();
        sf.tasks[0].group_label = Some("auth-module".into());
        std::fs::write(&path, serde_json::to_string_pretty(&sf).unwrap()).unwrap();

        let importer = ScrollImporter::new(scrolls_dir, Arc::clone(&db));
        let result = importer.import_scroll("imp-5").await.unwrap();

        assert_eq!(result.tasks_updated, 1);
        let task = db.get_task_by_id("imp-5-T1").await.unwrap();
        assert_eq!(task.group_label.as_deref(), Some("auth-module"));
    }

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

        let importer = ScrollImporter::new(dir.path().join("scrolls"), Arc::clone(&db));
        let synced = importer.sync_if_needed("nonexistent").await.unwrap();
        assert!(!synced);
    }
}
