use notify::EventKind;
use serde::Serialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::mpsc::RecvTimeoutError;
use std::time::{Duration, Instant};
use tauri::{AppHandle, Emitter};

#[derive(Serialize, Clone)]
struct FileChangedPayload {
    path: String,
}

pub fn run_debounce_loop(
    rx: std::sync::mpsc::Receiver<notify::Result<notify::Event>>,
    app: AppHandle,
) {
    let mut pending: HashMap<PathBuf, Instant> = HashMap::new();

    loop {
        match rx.recv_timeout(Duration::from_millis(10)) {
            Ok(Ok(event)) => {
                if matches!(
                    event.kind,
                    EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
                ) {
                    for p in event.paths {
                        if is_relevant_path(&p) {
                            pending.insert(p, Instant::now());
                        }
                    }
                }
            }
            Ok(Err(_)) => {}
            Err(RecvTimeoutError::Timeout) => {}
            Err(RecvTimeoutError::Disconnected) => break,
        }

        let now = Instant::now();
        let ready: Vec<PathBuf> = pending
            .iter()
            .filter(|(_, t)| now.duration_since(**t) >= Duration::from_millis(80))
            .map(|(p, _)| p.clone())
            .collect();

        for p in ready {
            pending.remove(&p);
            let _ = app.emit(
                "file-changed",
                FileChangedPayload {
                    path: p.to_string_lossy().to_string(),
                },
            );
        }
    }
}

fn is_relevant_path(path: &Path) -> bool {
    if path
        .components()
        .any(|c| c.as_os_str().to_string_lossy().starts_with('.'))
    {
        return false;
    }
    path.extension().is_some_and(|e| e == "md")
}

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

    #[test]
    fn relevant_path_md_file() {
        assert!(is_relevant_path(Path::new("/docs/notes.md")));
    }

    #[test]
    fn relevant_path_nested_md() {
        assert!(is_relevant_path(Path::new("/wiki/section/page.md")));
    }

    #[test]
    fn irrelevant_path_txt_extension() {
        assert!(!is_relevant_path(Path::new("/docs/readme.txt")));
    }

    #[test]
    fn irrelevant_path_no_extension() {
        assert!(!is_relevant_path(Path::new("/docs/makefile")));
    }

    #[test]
    fn irrelevant_path_hidden_file() {
        assert!(!is_relevant_path(Path::new("/docs/.hidden.md")));
    }

    #[test]
    fn irrelevant_path_hidden_dir() {
        assert!(!is_relevant_path(Path::new("/docs/.git/file.md")));
    }

    #[test]
    fn irrelevant_path_hidden_dir_at_root() {
        assert!(!is_relevant_path(Path::new(".hidden/file.md")));
    }
}
