use notify_debouncer_full::{new_debouncer, notify::RecommendedWatcher, DebounceEventResult};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{mpsc::channel, Arc, Mutex, RwLock};

use crate::builder::builder_options::BuilderOptions;
use crate::builder::{OptionsRegistryBuilder, OptionsWatcherBuilder};
use crate::provider::{
    CacheOptions, Features, GetOptionsPreferences, OptionsProvider, OptionsRegistry, WatcherOptions,
};
use crate::schema::metadata::OptionsMetadata;

pub type OptionsWatcherListener = Arc<dyn Fn(&HashSet<PathBuf>) + Send + Sync>;

/// A registry which changes the underlying when files are changed.
/// This is mainly meant to use for local development.
pub struct OptionsWatcher {
    current_provider: Arc<RwLock<OptionsProvider>>,
    last_modified: Arc<Mutex<std::time::SystemTime>>,
    watched_directories: Vec<PathBuf>,
    // The watcher needs to be held to continue watching files for changes.
    #[allow(dead_code)]
    debouncer_watcher: notify_debouncer_full::Debouncer<
        RecommendedWatcher,
        notify_debouncer_full::RecommendedCache,
    >,
    listeners: Arc<Mutex<Vec<OptionsWatcherListener>>>,
}

impl OptionsWatcher {
    pub fn build_with_options(
        directory: impl AsRef<Path>,
        watcher_options: WatcherOptions,
    ) -> Result<Self, String> {
        let mut builder = OptionsWatcherBuilder::new();
        builder.with_watcher_options(watcher_options);
        builder.add_directory(directory.as_ref())?;
        builder.build()
    }

    pub fn build_with_schema_and_options(
        directory: impl AsRef<Path>,
        schema_path: impl AsRef<Path>,
        watcher_options: WatcherOptions,
    ) -> Result<Self, String> {
        let mut builder = OptionsWatcherBuilder::new();
        builder.with_schema(schema_path.as_ref())?;
        builder.add_directory(directory.as_ref())?;
        builder.with_watcher_options(watcher_options);
        builder.build()
    }

    pub fn build_from_directories_with_options(
        directories: &[impl AsRef<Path>],
        watcher_options: WatcherOptions,
    ) -> Result<Self, String> {
        let mut builder = OptionsWatcherBuilder::new();
        builder.add_directories(directories)?;
        builder.with_watcher_options(watcher_options);
        builder.build()
    }

    pub fn build_from_directories_with_schema_and_options(
        directories: &[impl AsRef<Path>],
        schema_path: impl AsRef<Path>,
        watcher_options: WatcherOptions,
    ) -> Result<Self, String> {
        let mut builder = OptionsWatcherBuilder::new();
        builder.with_watcher_options(watcher_options);
        builder.with_schema(schema_path.as_ref())?;
        builder.add_directories(directories)?;
        builder.build()
    }

    pub(crate) fn new(
        watched_directories: &[impl AsRef<Path>],
        watcher_options: WatcherOptions,
        builder_options: BuilderOptions,
    ) -> Result<Self, String> {
        // Set up the watcher before building in case the files change before building.
        let (tx, rx) = channel();
        let mut debouncer_watcher = new_debouncer(
            watcher_options.debounce_duration,
            None,
            move |result: DebounceEventResult| match result {
                Ok(events) => {
                    let paths = events
                        .iter()
                        .filter(|event| !event.kind.is_access())
                        .filter(|event| {
                            // Ignore metadata changes such as the modified time.
                            match event.kind {
                                notify::EventKind::Modify(modify_kind) => {
                                    !matches!(modify_kind, notify::event::ModifyKind::Metadata(_))
                                }
                                _ => true,
                            }
                        })
                        .flat_map(|event| event.paths.clone())
                        .collect::<HashSet<_>>();

                    if paths.is_empty() {
                        return;
                    }

                    eprintln!(
                        "[optify] Rebuilding OptionsProvider because contents at these path(s) changed: {paths:?}"
                    );

                    tx.send(paths).unwrap();
                }
                Err(errors) => errors
                    .iter()
                    .for_each(|error| eprintln!("\x1b[31m[optify] {error:?}\x1b[0m")),
            },
        )
        .map_err(|e| format!("Failed to create debouncer: {e}"))?;
        for dir in watched_directories {
            debouncer_watcher
                .watch(dir, notify::RecursiveMode::Recursive)
                .map_err(|e| format!("Failed to watch directory {:?}: {e}", dir.as_ref()))?;
        }

        let provider = OptionsProvider::build_from_directories_with_options(
            watched_directories,
            builder_options.clone(),
        )
        .map_err(|e| format!("Failed to build provider: {e}"))?;
        let last_modified = Arc::new(Mutex::new(std::time::SystemTime::now()));

        let self_ = Self {
            current_provider: Arc::new(RwLock::new(provider)),
            last_modified,
            watched_directories: watched_directories
                .iter()
                .map(|dir| dir.as_ref().to_path_buf())
                .collect(),
            debouncer_watcher,
            listeners: Arc::new(Mutex::new(Vec::new())),
        };

        let current_provider = self_.current_provider.clone();
        let watched_directories = self_.watched_directories.clone();
        let last_modified = self_.last_modified.clone();
        let listeners = self_.listeners.clone();

        std::thread::spawn(move || {
            for paths in rx {
                let result = std::panic::catch_unwind(|| {
                    match OptionsProvider::build_from_directories_with_options(
                        &watched_directories,
                        builder_options.clone(),
                    ) {
                        Ok(new_provider) => match current_provider.write() {
                            Ok(mut provider) => {
                                *provider = new_provider;
                                *last_modified.lock().unwrap() = std::time::SystemTime::now();
                                eprintln!("\x1b[32m[optify] Successfully rebuilt the OptionsProvider.\x1b[0m");
                                let listeners_guard = listeners.lock().unwrap();
                                for listener in listeners_guard.iter() {
                                    listener(&paths);
                                }
                            }
                            Err(err) => {
                                eprintln!(
                                    "\x1b[31m[optify] Error rebuilding provider: {err}\nWill not change the provider until the files are fixed.\x1b[0m"
                                );
                            }
                        },
                        Err(err) => {
                            eprintln!("\x1b[31m[optify] Error rebuilding provider: {err}\x1b[0m");
                        }
                    }
                });

                if result.is_err() {
                    eprintln!("\x1b[31m[optify] Error rebuilding the provider. Will not change the provider until the files are fixed.\x1b[0m");
                }
            }
        });

        Ok(self_)
    }

    pub fn add_listener(&mut self, listener: OptionsWatcherListener) {
        self.listeners.lock().unwrap().push(listener);
    }

    /// Returns the time when the provider was finished building.
    pub fn last_modified(&self) -> std::time::SystemTime {
        *self.last_modified.lock().unwrap()
    }
}

impl OptionsRegistry for OptionsWatcher {
    fn build(directory: impl AsRef<Path>) -> Result<OptionsWatcher, String> {
        let mut builder = OptionsWatcherBuilder::new();
        builder.add_directory(directory.as_ref())?;
        builder.build()
    }

    fn build_from_directories(directories: &[impl AsRef<Path>]) -> Result<OptionsWatcher, String> {
        let mut builder = OptionsWatcherBuilder::new();
        builder.add_directories(directories)?;
        builder.build()
    }

    fn build_from_directories_with_options(
        directories: &[impl AsRef<Path>],
        options: BuilderOptions,
    ) -> Result<Self, String> {
        let mut builder = OptionsWatcherBuilder::new();
        builder.with_options(options)?;
        builder.add_directories(directories)?;
        builder.build()
    }

    fn build_with_options(
        directory: impl AsRef<Path>,
        options: BuilderOptions,
    ) -> Result<Self, String> {
        let mut builder = OptionsWatcherBuilder::new();
        builder.with_options(options)?;
        builder.add_directory(directory.as_ref())?;
        builder.build()
    }

    fn get_aliases(&self) -> Vec<String> {
        self.current_provider.read().unwrap().get_aliases()
    }

    fn get_features_and_aliases(&self) -> Vec<String> {
        self.current_provider
            .read()
            .unwrap()
            .get_features_and_aliases()
    }

    fn get_all_options(
        &self,
        feature_names: &[impl AsRef<str>],
        cache_options: Option<&CacheOptions>,
        preferences: Option<&GetOptionsPreferences>,
    ) -> std::result::Result<serde_json::Value, String> {
        self.current_provider.read().unwrap().get_all_options(
            feature_names,
            cache_options,
            preferences,
        )
    }

    fn get_canonical_feature_name(
        &self,
        feature_name: &str,
    ) -> std::result::Result<String, String> {
        self.current_provider
            .read()
            .unwrap()
            .get_canonical_feature_name(feature_name)
    }

    fn get_canonical_feature_names(
        &self,
        feature_names: &[impl AsRef<str>],
    ) -> std::result::Result<Vec<String>, String> {
        self.current_provider
            .read()
            .unwrap()
            .get_canonical_feature_names(feature_names)
    }

    fn get_feature_metadata(&self, canonical_feature_name: &str) -> Option<OptionsMetadata> {
        self.current_provider
            .read()
            .unwrap()
            .get_feature_metadata(canonical_feature_name)
    }

    fn get_features(&self) -> Vec<String> {
        self.current_provider.read().unwrap().get_features()
    }

    fn get_features_referencing_file(&self, relative_path: &str) -> Option<Vec<String>> {
        self.current_provider
            .read()
            .unwrap()
            .get_features_referencing_file(relative_path)
    }

    fn get_features_with_metadata(&self) -> Features {
        self.current_provider
            .read()
            .unwrap()
            .get_features_with_metadata()
    }

    fn get_filtered_feature_names(
        &self,
        feature_names: &[impl AsRef<str>],
        preferences: Option<&GetOptionsPreferences>,
    ) -> std::result::Result<Vec<String>, String> {
        self.current_provider
            .read()
            .unwrap()
            .get_filtered_feature_names(feature_names, preferences)
    }

    fn get_options(
        &self,
        key: &str,
        feature_names: &[impl AsRef<str>],
    ) -> std::result::Result<serde_json::Value, String> {
        self.current_provider
            .read()
            .unwrap()
            .get_options(key, feature_names)
    }

    fn get_options_with_preferences(
        &self,
        key: &str,
        feature_names: &[impl AsRef<str>],
        cache_options: Option<&CacheOptions>,
        preferences: Option<&GetOptionsPreferences>,
    ) -> std::result::Result<serde_json::Value, String> {
        self.current_provider
            .read()
            .unwrap()
            .get_options_with_preferences(key, feature_names, cache_options, preferences)
    }

    fn has_conditions(&self, canonical_feature_name: &str) -> bool {
        self.current_provider
            .read()
            .unwrap()
            .has_conditions(canonical_feature_name)
    }

    fn map_feature_names(
        &self,
        feature_names: &[impl AsRef<str>],
        preferences: Option<&GetOptionsPreferences>,
    ) -> std::result::Result<Vec<Option<String>>, String> {
        self.current_provider
            .read()
            .unwrap()
            .map_feature_names(feature_names, preferences)
    }
}
