use std::path::{Path, PathBuf};

use miette::{IntoDiagnostic, Result, miette};

#[derive(Debug, Clone)]
pub struct Snippets {
	savedir: Option<PathBuf>,
	pub dirs: Vec<PathBuf>,
}

impl Default for Snippets {
	fn default() -> Self {
		Self::new()
	}
}

impl Snippets {
	pub fn new() -> Self {
		let mut savedir = None;
		let mut dirs = Vec::new();
		if let Some(dir) = dirs::data_local_dir() {
			let dir = dir.join("snippets");
			savedir = Some(dir.clone());
			dirs.push(dir);
		}
		if let Some(dir) = dirs::data_dir() {
			let dir = dir.join("snippets");
			savedir = Some(dir.clone());
			dirs.push(dir);
		}
		if let Some(dir) = dirs::config_local_dir() {
			dirs.push(dir.join("snippets"));
		}
		if let Some(dir) = dirs::config_dir() {
			dirs.push(dir.join("snippets"));
		}
		dirs.push({
			let dir = PathBuf::from("/tamanu/snippets");
			if dir.exists() {
				savedir = Some(dir.clone());
			}
			dir
		});
		dirs.push({
			let dir = PathBuf::from("/snippets");
			if dir.exists() {
				savedir = Some(dir.clone());
			}
			dir
		});
		dirs.push("/etc/bestool/snippets".into());
		Self { savedir, dirs }
	}

	pub fn empty() -> Self {
		Self {
			savedir: None,
			dirs: Vec::new(),
		}
	}

	#[cfg(test)]
	pub fn with_savedir(savedir: PathBuf) -> Self {
		Self {
			savedir: Some(savedir.clone()),
			dirs: vec![savedir],
		}
	}

	fn try_path(dir: &Path, name: &str) -> Option<PathBuf> {
		let path = dir.join(format!("{name}.sql"));
		if path.exists() { Some(path) } else { None }
	}

	pub fn path(&self, name: &str) -> Result<PathBuf> {
		for dir in &self.dirs {
			if let Some(path) = Self::try_path(dir, name) {
				return Ok(path);
			}
		}

		Err(miette!("Snippet '{name}' not found"))
	}

	pub fn lookup_with_fallback(
		&self,
		name: &str,
		lookup: Option<&crate::config::SnippetLookup>,
	) -> Result<String> {
		for dir in &self.dirs {
			if let Some(path) = Self::try_path(dir, name)
				&& let Ok(content) = std::fs::read_to_string(&path)
			{
				return Ok(content);
			}
		}

		if let Some(lookup_provider) = lookup
			&& let Some(content) = lookup_provider.lookup(name)
		{
			return Ok(content);
		}

		Err(miette!("Snippet '{name}' not found"))
	}

	pub async fn save(&self, name: &str, content: &str) -> Result<PathBuf> {
		let savedir = self.savedir.as_ref().ok_or(miette!("No savedir"))?;
		tokio::fs::create_dir_all(savedir).await.into_diagnostic()?;
		let path = savedir.join(format!("{name}.sql"));
		tokio::fs::write(&path, content).await.into_diagnostic()?;
		Ok(path)
	}
}

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

	#[test]
	fn test_empty_snippets() {
		let snippets = Snippets::empty();
		let result = snippets.path("nonexistent");
		assert!(result.is_err());
		assert_eq!(
			result.unwrap_err().to_string(),
			"Snippet 'nonexistent' not found"
		);
	}

	#[tokio::test]
	async fn test_save_creates_savedir() {
		let temp_dir = TempDir::new().unwrap();
		let savedir = temp_dir.path().join("snippets");
		assert!(!savedir.exists());

		let snippets = Snippets::with_savedir(savedir.clone());
		let saved_path = snippets.save("test_snippet", "SELECT 1;").await.unwrap();

		assert!(savedir.exists());
		let saved_file = savedir.join("test_snippet.sql");
		assert!(saved_file.exists());
		assert_eq!(saved_path, saved_file);

		let content = tokio::fs::read_to_string(&saved_file).await.unwrap();
		assert_eq!(content, "SELECT 1;");
	}

	#[tokio::test]
	async fn test_save_overwrites_existing_snippet() {
		let temp_dir = TempDir::new().unwrap();
		let savedir = temp_dir.path().to_path_buf();

		let snippets = Snippets::with_savedir(savedir.clone());
		let path1 = snippets.save("test_snippet", "SELECT 1;").await.unwrap();
		let path2 = snippets.save("test_snippet", "SELECT 2;").await.unwrap();

		assert_eq!(path1, path2);
		let saved_file = savedir.join("test_snippet.sql");
		let content = tokio::fs::read_to_string(&saved_file).await.unwrap();
		assert_eq!(content, "SELECT 2;");
	}

	#[tokio::test]
	async fn test_save_multiple_snippets() {
		let temp_dir = TempDir::new().unwrap();
		let savedir = temp_dir.path().to_path_buf();

		let snippets = Snippets::with_savedir(savedir.clone());
		let path1 = snippets.save("snippet1", "SELECT 1;").await.unwrap();
		let path2 = snippets.save("snippet2", "SELECT 2;").await.unwrap();
		let path3 = snippets.save("snippet3", "SELECT 3;").await.unwrap();

		assert_eq!(path1, savedir.join("snippet1.sql"));
		assert_eq!(path2, savedir.join("snippet2.sql"));
		assert_eq!(path3, savedir.join("snippet3.sql"));

		assert!(savedir.join("snippet1.sql").exists());
		assert!(savedir.join("snippet2.sql").exists());
		assert!(savedir.join("snippet3.sql").exists());

		let content1 = tokio::fs::read_to_string(savedir.join("snippet1.sql"))
			.await
			.unwrap();
		assert_eq!(content1, "SELECT 1;");
	}

	#[tokio::test]
	async fn test_save_no_savedir_fails() {
		let snippets = Snippets::empty();
		let result = snippets.save("test_snippet", "SELECT 1;").await;
		assert!(result.is_err());
		assert_eq!(result.unwrap_err().to_string(), "No savedir");
	}

	#[test]
	fn test_path_finds_existing_snippet() {
		let temp_dir = TempDir::new().unwrap();
		let savedir = temp_dir.path().to_path_buf();

		let snippet_file = savedir.join("test.sql");
		std::fs::write(&snippet_file, "SELECT 1;").unwrap();

		let snippets = Snippets::with_savedir(savedir);
		let result = snippets.path("test").unwrap();
		assert_eq!(result, snippet_file);
	}

	#[test]
	fn test_path_returns_error_for_missing_snippet() {
		let temp_dir = TempDir::new().unwrap();
		let savedir = temp_dir.path().to_path_buf();

		let snippets = Snippets::with_savedir(savedir);
		let result = snippets.path("nonexistent");
		assert!(result.is_err());
	}

	#[tokio::test]
	async fn test_save_with_special_characters_in_name() {
		let temp_dir = TempDir::new().unwrap();
		let savedir = temp_dir.path().to_path_buf();

		let snippets = Snippets::with_savedir(savedir.clone());
		let saved_path = snippets
			.save("test_snippet-123", "SELECT 1;")
			.await
			.unwrap();

		let saved_file = savedir.join("test_snippet-123.sql");
		assert!(saved_file.exists());
		assert_eq!(saved_path, saved_file);
	}

	#[tokio::test]
	async fn test_save_with_multiline_content() {
		let temp_dir = TempDir::new().unwrap();
		let savedir = temp_dir.path().to_path_buf();

		let snippets = Snippets::with_savedir(savedir.clone());
		let content = "SELECT *\nFROM users\nWHERE id = 1;";
		let saved_path = snippets.save("multiline", content).await.unwrap();

		let saved_file = savedir.join("multiline.sql");
		assert_eq!(saved_path, saved_file);
		let read_content = tokio::fs::read_to_string(&saved_file).await.unwrap();
		assert_eq!(read_content, content);
	}

	#[tokio::test]
	async fn test_save_preserves_content_exactly() {
		let temp_dir = TempDir::new().unwrap();
		let savedir = temp_dir.path().to_path_buf();

		let snippets = Snippets::with_savedir(savedir.clone());
		let content = "-- Comment\nSELECT 1; -- inline comment\n";
		let saved_path = snippets.save("with_comments", content).await.unwrap();

		let saved_file = savedir.join("with_comments.sql");
		assert_eq!(saved_path, saved_file);
		let read_content = tokio::fs::read_to_string(&saved_file).await.unwrap();
		assert_eq!(read_content, content);
	}
}
