//! File-system watcher that forwards config-path change events to a channel.

use crate::events::{ChangeKind, ConfigChangedEvent};
use chrono::Utc;
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::PathBuf;
use tokio::sync::mpsc;

pub type WatchSender = mpsc::Sender<ConfigChangedEvent>;
pub type WatchReceiver = mpsc::Receiver<ConfigChangedEvent>;

/// Create a watcher that sends `ConfigChangedEvent`s when any of `paths` change.
///
/// Returns `(WatchReceiver, RecommendedWatcher)`. Caller must retain the
/// `RecommendedWatcher` for the watch to stay active — drop it to stop.
pub fn spawn_watcher(paths: Vec<PathBuf>) -> anyhow::Result<(WatchReceiver, RecommendedWatcher)> {
    let (tx, rx) = mpsc::channel::<ConfigChangedEvent>(64);

    let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
        let Ok(event) = res else { return };
        let kind = classify(event.kind);
        for path in event.paths {
            let ev = ConfigChangedEvent { path, change: kind, at: Utc::now() };
            // Non-blocking send — drop if channel is full rather than blocking notify callback.
            let _ = tx.try_send(ev);
        }
    })?;

    for path in paths {
        if path.exists() {
            watcher.watch(&path, RecursiveMode::NonRecursive)?;
        }
    }

    Ok((rx, watcher))
}

fn classify(kind: EventKind) -> ChangeKind {
    match kind {
        EventKind::Create(_) => ChangeKind::Create,
        EventKind::Remove(_) => ChangeKind::Remove,
        _ => ChangeKind::Modify,
    }
}

/// Consume config-change events, emit NATS events and mark domain state.
///
/// Runs until sender side is dropped (watcher dropped or channel closed).
pub async fn run_config_watch_loop(
    mut rx: WatchReceiver,
    state: crate::domain_state::DomainState,
    bus: Option<crate::events::EventBus>,
) {
    while let Some(ev) = rx.recv().await {
        tracing::info!(path = %ev.path.display(), change = ?ev.change, "config file changed");
        state.mark_config_reload();
        if let Some(ref b) = bus {
            let nats_ev = crate::events::DaemonEvent::ConfigChanged(ev);
            if let Err(e) = b.publish(&nats_ev).await {
                tracing::warn!("failed to publish config-changed event: {e}");
            }
        }
    }
    tracing::info!("config watcher loop exited");
}

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

    #[tokio::test]
    async fn watcher_creates_receiver() {
        let dir = TempDir::new().unwrap();
        let (mut rx, _watcher) = spawn_watcher(vec![dir.path().to_path_buf()]).unwrap();
        // Write a file into the watched directory
        std::fs::write(dir.path().join("test.ncl"), b"{}").unwrap();
        // Give the OS watcher a moment to fire
        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
        // We may or may not receive depending on OS event timing — just verify
        // the channel is still open (not disconnected).
        let _ = rx.try_recv(); // ok if empty
    }

    #[test]
    fn classify_maps_correctly() {
        use notify::event::{CreateKind, ModifyKind, RemoveKind};
        assert_eq!(classify(EventKind::Create(CreateKind::File)), ChangeKind::Create);
        assert_eq!(classify(EventKind::Remove(RemoveKind::File)), ChangeKind::Remove);
        assert_eq!(classify(EventKind::Modify(ModifyKind::Any)), ChangeKind::Modify);
    }
}
