use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::time::Duration;

use notify::{Event, EventKind as NotifyEventKind, RecommendedWatcher, RecursiveMode, Watcher};

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

/// Type of file system event.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventKind {
    /// File was created.
    Created,
    /// File was modified.
    Modified,
    /// File was deleted.
    Removed,
    /// File was renamed.
    Renamed,
    /// Access event (read).
    Accessed,
    /// Other/unknown event.
    Other,
}

impl From<&NotifyEventKind> for EventKind {
    fn from(kind: &NotifyEventKind) -> Self {
        match kind {
            NotifyEventKind::Create(_) => EventKind::Created,
            NotifyEventKind::Modify(notify::event::ModifyKind::Name(_)) => EventKind::Renamed,
            NotifyEventKind::Modify(_) => EventKind::Modified,
            NotifyEventKind::Remove(_) => EventKind::Removed,
            NotifyEventKind::Access(_) => EventKind::Accessed,
            _ => EventKind::Other,
        }
    }
}

/// A file system change event.
#[derive(Debug, Clone)]
pub struct FileEvent {
    /// The kind of event.
    pub kind: EventKind,

    /// Paths affected by this event.
    pub paths: Vec<PathBuf>,
}

impl FileEvent {
    /// Get the primary path (first path in the event).
    pub fn path(&self) -> Option<&Path> {
        self.paths.first().map(|p| p.as_path())
    }

    /// Check if any of the paths match a given extension.
    pub fn has_extension(&self, ext: &str) -> bool {
        self.paths.iter().any(|p| {
            p.extension()
                .and_then(|e| e.to_str())
                .map(|e| e == ext)
                .unwrap_or(false)
        })
    }
}

/// Watches the filesystem for changes.
pub struct FileWatcher {
    #[allow(dead_code)]
    watcher: RecommendedWatcher,
    receiver: Receiver<Result<FileEvent>>,
    config: Config,
}

impl FileWatcher {
    /// Create a new file watcher for the given path.
    pub fn new<P: AsRef<Path>>(path: P, config: Config) -> Result<Self> {
        let (tx, rx) = mpsc::channel();

        let event_tx = tx.clone();
        let mut watcher =
            notify::recommended_watcher(move |res: std::result::Result<Event, notify::Error>| {
                let event = match res {
                    Ok(event) => Ok(FileEvent {
                        kind: EventKind::from(&event.kind),
                        paths: event.paths,
                    }),
                    Err(e) => Err(Error::Watcher(e)),
                };
                let _ = event_tx.send(event);
            })?;

        let mode = if config.max_depth == Some(1) {
            RecursiveMode::NonRecursive
        } else {
            RecursiveMode::Recursive
        };

        watcher.watch(path.as_ref(), mode)?;

        Ok(Self {
            watcher,
            receiver: rx,
            config,
        })
    }

    /// Get the next event, blocking until one is available.
    pub fn next(&self) -> Option<Result<FileEvent>> {
        self.receiver.recv().ok()
    }

    /// Try to get the next event without blocking.
    pub fn try_next(&self) -> Option<Result<FileEvent>> {
        match self.receiver.try_recv() {
            Ok(event) => Some(event),
            Err(TryRecvError::Empty) => None,
            Err(TryRecvError::Disconnected) => None,
        }
    }

    /// Get the next event with a timeout.
    pub fn next_timeout(&self, timeout: Duration) -> Option<Result<FileEvent>> {
        self.receiver.recv_timeout(timeout).ok()
    }

    /// Iterate over events.
    pub fn events(&self) -> impl Iterator<Item = Result<FileEvent>> + '_ {
        std::iter::from_fn(|| self.next())
    }

    /// Filter events to only include files matching the config.
    pub fn filtered_events(&self) -> impl Iterator<Item = Result<FileEvent>> + '_ {
        self.events().filter(|event| {
            if let Ok(e) = event {
                self.should_include(e)
            } else {
                true // Pass through errors
            }
        })
    }

    fn should_include(&self, event: &FileEvent) -> bool {
        // Check extension filters
        if !self.config.extensions.is_empty() {
            let has_matching_ext = event.paths.iter().any(|p| {
                p.extension()
                    .and_then(|e| e.to_str())
                    .map(|e| self.config.extensions.iter().any(|ext| ext == e))
                    .unwrap_or(false)
            });
            if !has_matching_ext {
                return false;
            }
        }

        // Check hidden files
        if !self.config.include_hidden {
            let has_hidden = event.paths.iter().any(|p| {
                p.components().any(|c| {
                    c.as_os_str()
                        .to_str()
                        .map(|s| s.starts_with('.'))
                        .unwrap_or(false)
                })
            });
            if has_hidden {
                return false;
            }
        }

        true
    }
}

impl Iterator for FileWatcher {
    type Item = Result<FileEvent>;

    fn next(&mut self) -> Option<Self::Item> {
        FileWatcher::next(self)
    }
}
