//! File system watcher for incremental indexing.
//!
//! Uses the `notify` crate to watch the workspace directory for file changes
//! and feeds them to Elrond's `handle_file_event` for incremental updates
//! to the knowledge graph and vector store.

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::mpsc;
use tokio::time::Instant;
use tracing::{debug, error, info, warn};

use crate::agents::elrond::{Elrond, FileEvent};

/// Minimum quiet period before a file event is forwarded to Elrond.
/// Events for the same path within this window are coalesced (last wins).
const DEBOUNCE_MS: u64 = 300;

/// Directories to ignore from file watching (same as Elrond's SKIP_DIRS).
const SKIP_DIRS: &[&str] = &[
    ".git",
    "node_modules",
    "target",
    "dist",
    "build",
    ".next",
    "__pycache__",
    ".venv",
    "venv",
    ".tox",
    "vendor",
    ".idea",
    ".vscode",
];

/// Start watching the workspace directory and forward file events to Elrond.
///
/// This spawns a background task that runs until the returned `WatcherHandle`
/// is dropped.
pub fn start_watching(
    workspace_root: PathBuf,
    elrond: Arc<Elrond>,
) -> anyhow::Result<WatcherHandle> {
    let (tx, mut rx) = mpsc::channel::<notify::Result<Event>>(256);

    // Create the filesystem watcher.
    let mut watcher = RecommendedWatcher::new(
        move |res| {
            // Send from the notify thread to the async task.
            if let Err(e) = tx.blocking_send(res) {
                warn!("Watcher channel closed, event dropped: {e}");
            }
        },
        Config::default().with_poll_interval(Duration::from_secs(2)),
    )?;

    watcher.watch(&workspace_root, RecursiveMode::Recursive)?;
    info!(path = %workspace_root.display(), "File watcher started");

    // Spawn the event-processing task with debouncing.
    // Events for the same path within DEBOUNCE_MS are coalesced (last event wins).
    let handle = tokio::spawn(async move {
        let debounce = Duration::from_millis(DEBOUNCE_MS);
        let tick_interval = Duration::from_millis(DEBOUNCE_MS / 2);
        let mut pending: HashMap<PathBuf, (FileEvent, Instant)> = HashMap::new();
        let mut tick = tokio::time::interval(tick_interval);
        tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

        loop {
            tokio::select! {
                event_result = rx.recv() => {
                    match event_result {
                        Some(Ok(event)) => {
                            if let Some(file_events) = convert_event(&event) {
                                for fe in file_events {
                                    let path = event_path(&fe).to_path_buf();
                                    pending.insert(path, (fe, Instant::now()));
                                }
                            }
                        }
                        Some(Err(e)) => {
                            error!(error = %e, "File watcher error");
                        }
                        None => {
                            // Channel closed — flush remaining events and exit
                            for (_, (fe, _)) in pending.drain() {
                                if let Err(e) = elrond.handle_file_event(fe).await {
                                    warn!(error = %e, "Failed to process file event");
                                }
                            }
                            debug!("File watcher event loop ended");
                            return;
                        }
                    }
                }
                _ = tick.tick() => {
                    let now = Instant::now();
                    let ready: Vec<PathBuf> = pending.iter()
                        .filter(|(_, (_, ts))| now.duration_since(*ts) >= debounce)
                        .map(|(p, _)| p.clone())
                        .collect();

                    for path in ready {
                        if let Some((fe, _)) = pending.remove(&path) {
                            if let Err(e) = elrond.handle_file_event(fe).await {
                                warn!(error = %e, "Failed to process file event");
                            }
                        }
                    }
                }
            }
        }
    });

    Ok(WatcherHandle {
        _watcher: watcher,
        _task: handle,
    })
}

/// Handle that keeps the watcher alive. Drop it to stop watching.
pub struct WatcherHandle {
    _watcher: RecommendedWatcher,
    _task: tokio::task::JoinHandle<()>,
}

/// Extract the path from a `FileEvent`.
fn event_path(event: &FileEvent) -> &std::path::Path {
    match event {
        FileEvent::Created(p) | FileEvent::Modified(p) | FileEvent::Deleted(p) => p,
    }
}

/// Convert a `notify::Event` into zero or more `FileEvent`s.
fn convert_event(event: &Event) -> Option<Vec<FileEvent>> {
    let mut file_events = Vec::new();

    for path in &event.paths {
        // Skip directories.
        if path.is_dir() {
            continue;
        }

        // Skip paths in ignored directories.
        if should_skip(path) {
            continue;
        }

        match event.kind {
            EventKind::Create(_) => {
                file_events.push(FileEvent::Created(path.clone()));
            }
            EventKind::Modify(_) => {
                file_events.push(FileEvent::Modified(path.clone()));
            }
            EventKind::Remove(_) => {
                file_events.push(FileEvent::Deleted(path.clone()));
            }
            _ => {}
        }
    }

    if file_events.is_empty() {
        None
    } else {
        Some(file_events)
    }
}

/// Check if a path should be skipped based on directory names.
fn should_skip(path: &std::path::Path) -> bool {
    for component in path.components() {
        if let std::path::Component::Normal(name) = component {
            if let Some(name_str) = name.to_str() {
                if SKIP_DIRS.contains(&name_str) {
                    return true;
                }
            }
        }
    }
    false
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn test_should_skip_git() {
        assert!(should_skip(&PathBuf::from("/project/.git/objects/abc")));
    }

    #[test]
    fn test_should_skip_node_modules() {
        assert!(should_skip(&PathBuf::from("/project/node_modules/lodash/index.js")));
    }

    #[test]
    fn test_should_not_skip_src() {
        assert!(!should_skip(&PathBuf::from("/project/src/main.rs")));
    }

    #[test]
    fn test_should_skip_target() {
        assert!(should_skip(&PathBuf::from("/project/target/debug/furnace")));
    }

    #[test]
    fn test_convert_create_event() {
        let event = Event {
            kind: EventKind::Create(notify::event::CreateKind::File),
            paths: vec![PathBuf::from("/project/src/new_file.rs")],
            attrs: Default::default(),
        };
        let result = convert_event(&event).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0], FileEvent::Created(PathBuf::from("/project/src/new_file.rs")));
    }

    #[test]
    fn test_convert_modify_event() {
        let event = Event {
            kind: EventKind::Modify(notify::event::ModifyKind::Data(notify::event::DataChange::Content)),
            paths: vec![PathBuf::from("/project/src/main.rs")],
            attrs: Default::default(),
        };
        let result = convert_event(&event).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0], FileEvent::Modified(PathBuf::from("/project/src/main.rs")));
    }

    #[test]
    fn test_convert_delete_event() {
        let event = Event {
            kind: EventKind::Remove(notify::event::RemoveKind::File),
            paths: vec![PathBuf::from("/project/src/old.rs")],
            attrs: Default::default(),
        };
        let result = convert_event(&event).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0], FileEvent::Deleted(PathBuf::from("/project/src/old.rs")));
    }

    #[test]
    fn test_convert_ignored_path() {
        let event = Event {
            kind: EventKind::Create(notify::event::CreateKind::File),
            paths: vec![PathBuf::from("/project/target/debug/build/file.rs")],
            attrs: Default::default(),
        };
        let result = convert_event(&event);
        assert!(result.is_none());
    }
}
