//! File system watcher for detecting changes.

use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;

use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use parking_lot::Mutex;
use tokio::sync::mpsc;

use crate::error::{Error, Result};

/// File system watcher for a workspace.
pub struct FileWatcher {
    watcher: RecommendedWatcher,
    root_path: PathBuf,
}

impl FileWatcher {
    /// Start watching a directory for changes.
    ///
    /// Returns a receiver that will receive batches of changed file paths.
    pub fn watch(root: &Path) -> Result<mpsc::Receiver<Vec<PathBuf>>> {
        let (tx, rx) = mpsc::channel(100);
        let pending = Arc::new(Mutex::new(HashSet::<PathBuf>::new()));
        let pending_clone = pending.clone();
        let tx_clone = tx.clone();

        // Debounce timer - collect changes and send in batches
        let debounce_duration = Duration::from_millis(500);

        let mut watcher = notify::recommended_watcher(move |res: std::result::Result<Event, notify::Error>| {
            match res {
                Ok(event) => {
                    // Filter to relevant event kinds
                    use notify::EventKind::*;
                    match event.kind {
                        Create(_) | Modify(_) | Remove(_) => {
                            let mut pending = pending_clone.lock();
                            for path in event.paths {
                                if path.is_file() {
                                    pending.insert(path);
                                }
                            }
                        }
                        _ => {}
                    }
                }
                Err(e) => {
                    tracing::error!("Watch error: {:?}", e);
                }
            }
        })
        .map_err(|e| Error::internal(format!("Failed to create watcher: {}", e)))?;

        watcher
            .watch(root, RecursiveMode::Recursive)
            .map_err(|e| Error::internal(format!("Failed to watch path: {}", e)))?;

        // Spawn debounce task
        tokio::spawn(async move {
            loop {
                tokio::time::sleep(debounce_duration).await;

                let changed: Vec<PathBuf> = {
                    let mut pending = pending.lock();
                    if pending.is_empty() {
                        continue;
                    }
                    pending.drain().collect()
                };

                if !changed.is_empty() {
                    if tx_clone.send(changed).await.is_err() {
                        break; // Receiver dropped
                    }
                }
            }
        });

        Ok(rx)
    }
}

/// Simple polling-based file change detector.
///
/// This is a fallback for systems where file notifications are unreliable.
pub struct PollingWatcher {
    root_path: PathBuf,
    file_hashes: Mutex<std::collections::HashMap<PathBuf, String>>,
    poll_interval: Duration,
}

impl PollingWatcher {
    /// Create a new polling watcher.
    pub fn new(root_path: PathBuf, poll_interval: Duration) -> Self {
        Self {
            root_path,
            file_hashes: Mutex::new(std::collections::HashMap::new()),
            poll_interval,
        }
    }

    /// Initialize with current file state.
    pub fn initialize(&self, files: &[PathBuf]) -> Result<()> {
        let mut hashes = self.file_hashes.lock();
        for file in files {
            if let Ok(hash) = compute_file_hash(file) {
                hashes.insert(file.clone(), hash);
            }
        }
        Ok(())
    }

    /// Check for changes since last poll.
    pub fn check_changes(&self, files: &[PathBuf]) -> Vec<PathBuf> {
        let mut hashes = self.file_hashes.lock();
        let mut changed = Vec::new();

        for file in files {
            let new_hash = match compute_file_hash(file) {
                Ok(h) => h,
                Err(_) => continue,
            };

            match hashes.get(file) {
                Some(old_hash) if old_hash == &new_hash => {
                    // No change
                }
                Some(_) => {
                    // File changed
                    hashes.insert(file.clone(), new_hash);
                    changed.push(file.clone());
                }
                None => {
                    // New file
                    hashes.insert(file.clone(), new_hash);
                    changed.push(file.clone());
                }
            }
        }

        // Check for deleted files
        let current_files: HashSet<_> = files.iter().collect();
        let deleted: Vec<_> = hashes
            .keys()
            .filter(|k| !current_files.contains(k))
            .cloned()
            .collect();

        for path in &deleted {
            hashes.remove(path);
            changed.push(path.clone());
        }

        changed
    }

    /// Get the poll interval.
    pub fn poll_interval(&self) -> Duration {
        self.poll_interval
    }
}

/// Compute hash of a file for change detection.
fn compute_file_hash(path: &Path) -> Result<String> {
    let content = std::fs::read(path)?;
    let hash = blake3::hash(&content);
    Ok(hash.to_hex().to_string())
}

/// Change event types.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChangeType {
    Created,
    Modified,
    Deleted,
}

/// A file change event.
#[derive(Debug, Clone)]
pub struct FileChange {
    pub path: PathBuf,
    pub change_type: ChangeType,
}

impl FileChange {
    pub fn created(path: PathBuf) -> Self {
        Self {
            path,
            change_type: ChangeType::Created,
        }
    }

    pub fn modified(path: PathBuf) -> Self {
        Self {
            path,
            change_type: ChangeType::Modified,
        }
    }

    pub fn deleted(path: PathBuf) -> Self {
        Self {
            path,
            change_type: ChangeType::Deleted,
        }
    }
}

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

    #[test]
    fn test_polling_watcher() {
        let temp_dir = TempDir::new().unwrap();
        let file1 = temp_dir.path().join("test1.rs");
        let file2 = temp_dir.path().join("test2.rs");

        std::fs::write(&file1, "content1").unwrap();
        std::fs::write(&file2, "content2").unwrap();

        let watcher = PollingWatcher::new(
            temp_dir.path().to_path_buf(),
            Duration::from_secs(1),
        );

        let files = vec![file1.clone(), file2.clone()];
        watcher.initialize(&files).unwrap();

        // No changes yet
        let changes = watcher.check_changes(&files);
        assert!(changes.is_empty());

        // Modify a file
        std::fs::write(&file1, "modified content").unwrap();

        let changes = watcher.check_changes(&files);
        assert!(changes.contains(&file1));
        assert!(!changes.contains(&file2));
    }
}
