use std::{
	ffi::OsString,
	path::{Path, PathBuf, MAIN_SEPARATOR},
	sync::Arc,
};

use ignore_files::VCS_DIR_NAMES;
use miette::{IntoDiagnostic, Result};
use tokio::io::{AsyncBufReadExt, BufReader};
use tracing::{info, trace, trace_span};
use watchexec::{error::RuntimeError, filter::Filterer};
use watchexec_events::{
	filekind::{FileEventKind, ModifyKind},
	Event, Priority, Tag,
};
use watchexec_filterer_globset::GlobsetFilterer;

use crate::args::{filtering::FsEvent, Args};

pub mod parse;
mod proglib;
mod progs;
mod syncval;

/// A custom filterer that combines the library's Globset filterer and a switch for --no-meta
#[derive(Debug)]
pub struct WatchexecFilterer {
	inner: GlobsetFilterer,
	fs_events: Vec<FsEvent>,
	progs: Option<progs::FilterProgs>,
}

impl Filterer for WatchexecFilterer {
	fn check_dir(&self, path: &Path) -> Result<bool, RuntimeError> {
		self.inner.check_dir(path)
	}

	#[tracing::instrument(level = "trace", skip(self))]
	fn check_event(&self, event: &Event, priority: Priority) -> Result<bool, RuntimeError> {
		for tag in &event.tags {
			if let Tag::FileEventKind(fek) = tag {
				let normalised = match fek {
					FileEventKind::Access(_) => FsEvent::Access,
					FileEventKind::Modify(ModifyKind::Name(_)) => FsEvent::Rename,
					FileEventKind::Modify(ModifyKind::Metadata(_)) => FsEvent::Metadata,
					FileEventKind::Modify(_) => FsEvent::Modify,
					FileEventKind::Create(_) => FsEvent::Create,
					FileEventKind::Remove(_) => FsEvent::Remove,
					_ => continue,
				};

				trace!(allowed=?self.fs_events, this=?normalised, "check against fs event filter");
				if !self.fs_events.contains(&normalised) {
					return Ok(false);
				}
			}
		}

		trace!("check against original event");
		if !self.inner.check_event(event, priority)? {
			return Ok(false);
		}

		if let Some(progs) = &self.progs {
			trace!("check against program filters");
			if !progs.check(event)? {
				return Ok(false);
			}
		}

		Ok(true)
	}
}

impl WatchexecFilterer {
	/// Create a new filterer from the given arguments
	pub async fn new(args: &Args) -> Result<Arc<Self>> {
		let project_origin = args.filtering.project_origin.clone().unwrap();
		let workdir = args.command.workdir.clone().unwrap();

		let ignore_files = if args.filtering.no_discover_ignore {
			Vec::new()
		} else {
			let vcs_types = crate::dirs::vcs_types(&project_origin).await;
			crate::dirs::ignores(args, &vcs_types).await?
		};

		let mut ignores = Vec::new();

		if !args.filtering.no_default_ignore {
			ignores.extend([
				(format!("**{MAIN_SEPARATOR}.DS_Store"), None),
				(String::from("watchexec.*.log"), None),
				(String::from("*.py[co]"), None),
				(String::from("#*#"), None),
				(String::from(".#*"), None),
				(String::from(".*.kate-swp"), None),
				(String::from(".*.sw?"), None),
				(String::from(".*.sw?x"), None),
			]);
			for directory in VCS_DIR_NAMES {
				ignores.extend([
					(format!("**{MAIN_SEPARATOR}{directory}"), None),
					(
						format!("**{MAIN_SEPARATOR}{directory}{MAIN_SEPARATOR}**"),
						None,
					),
				]);
			}
		}

		let whitelist = args.filtering.paths.iter().map(std::convert::Into::into);

		let mut filters = args
			.filtering
			.filter_patterns
			.iter()
			.map(|f| (f.to_owned(), Some(workdir.clone())))
			.collect::<Vec<_>>();

		for filter_file in &args.filtering.filter_files {
			filters.extend(read_filter_file(filter_file).await?);
		}

		ignores.extend(
			args.filtering
				.ignore_patterns
				.iter()
				.map(|f| (f.to_owned(), Some(workdir.clone()))),
		);

		let exts = args
			.filtering
			.filter_extensions
			.iter()
			.map(|e| OsString::from(e.strip_prefix('.').unwrap_or(e)));

		info!("initialising Globset filterer");
		Ok(Arc::new(Self {
			inner: GlobsetFilterer::new(
				project_origin,
				filters,
				ignores,
				whitelist,
				ignore_files,
				exts,
			)
			.await
			.into_diagnostic()?,
			fs_events: args.filtering.filter_fs_events.clone(),
			progs: if args.filtering.filter_programs_parsed.is_empty() {
				None
			} else {
				Some(progs::FilterProgs::new(args)?)
			},
		}))
	}
}

async fn read_filter_file(path: &Path) -> Result<Vec<(String, Option<PathBuf>)>> {
	let _span = trace_span!("loading filter file", ?path).entered();

	let file = tokio::fs::File::open(path).await.into_diagnostic()?;

	let metadata_len = file
		.metadata()
		.await
		.map_or(Ok(0), |metadata| usize::try_from(metadata.len()))
		.into_diagnostic()?;
	let filter_capacity = if metadata_len == 0 {
		0
	} else {
		metadata_len / 20
	};
	let mut filters = Vec::with_capacity(filter_capacity);

	let reader = BufReader::new(file);
	let mut lines = reader.lines();
	while let Some(line) = lines.next_line().await.into_diagnostic()? {
		let line = line.trim();
		if line.is_empty() || line.starts_with('#') {
			continue;
		}

		trace!(?line, "adding filter line");
		filters.push((line.to_owned(), Some(path.to_owned())));
	}

	Ok(filters)
}

#[cfg(test)]
mod tests {
	use super::*;
	use watchexec_events::FileType;

	#[tokio::test]
	async fn normalised_watch_root_whitelist_overrides_event_ignore() {
		let origin = dunce::canonicalize(".").unwrap();
		let emitted_root = origin.join("ignored");
		let configured_root = origin.join("intermediate").join("..").join("ignored");
		assert!(configured_root.is_absolute());
		assert_ne!(configured_root, emitted_root);

		let inner = GlobsetFilterer::new(
			&origin,
			Vec::<(String, Option<PathBuf>)>::new(),
			vec![(String::from("**/ignored"), None)],
			vec![configured_root],
			Vec::<ignore_files::IgnoreFile>::new(),
			Vec::<OsString>::new(),
		)
		.await
		.unwrap();
		let filterer = WatchexecFilterer {
			inner,
			fs_events: Vec::new(),
			progs: None,
		};
		let event = Event {
			tags: vec![Tag::Path {
				path: emitted_root.clone(),
				file_type: Some(FileType::Dir),
			}],
			metadata: Default::default(),
		};

		assert!(filterer.check_event(&event, Priority::Normal).unwrap());
		assert!(!filterer.check_dir(&emitted_root).unwrap());
	}
}
