use eyre::{OptionExt, Result};
use owo_colors::OwoColorize as _;
use std::{
	fs::{OpenOptions, create_dir_all, read_to_string},
	io::{BufRead, BufReader, Write},
	path::{Path, PathBuf},
	sync::LazyLock,
};

static CONFIG_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
	let base_dirs = directories::BaseDirs::new().unwrap();
	let config_dir = base_dirs.config_dir();

	let hoopoe_config = config_dir.join("hoopoe");
	create_dir_all(&hoopoe_config).unwrap();

	hoopoe_config
});
static BUILTIN_DICT_DIR: &str = "builtin";
static BUILTIN_DICT_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
	let builtin_path = CONFIG_PATH.join(BUILTIN_DICT_DIR);
	create_dir_all(&builtin_path).unwrap();

	builtin_path
});

static USER_DICT_FILE_NAME: &str = "user-dict.txt";

pub fn dicts_cmd(mut args: pico_args::Arguments) -> Result<()> {
	if args.contains(["-h", "--help"]) {
		print_help(0);
	}

	let subcommand = args.subcommand()?;
	match subcommand.as_deref() {
		Some("add") => {
			let given_path = args.free_from_os_str(|s| {
				let path = PathBuf::from(s);
				if !path.is_file()
					|| path.extension()
						.is_none_or(|ext| ext != "dic" && ext != "aff")
				{
					eyre::bail!(
						"unrecognized dictionary path: {}",
						path.display()
					);
				}
				Ok(path)
			})?;
			let name =
				given_path.file_prefix().ok_or_eyre("undecipherable file name")?;
			let dict_path = CONFIG_PATH.join(name);

			let src_dic = given_path.with_extension("dic");
			let src_aff = given_path.with_extension("aff");

			eyre::ensure!(
				src_aff.is_file() && src_dic.is_file(),
				"You need two files with the same name and extensions .dic and .aff next to each other to add a dictionary."
			);

			create_dir_all(&dict_path)?;

			let dst_dic = dict_path.join(name).with_extension("dic");
			let dst_aff = dict_path.join(name).with_extension("aff");

			std::fs::copy(src_dic, &dst_dic)?;
			std::fs::copy(src_aff, &dst_aff)?;

			println!("Dictionary added in: {}", dict_path.display());
			println!("You can refer to it later with: {}", name.to_string_lossy());

			Ok(())
		}
		Some("list") => {
			println!("{}", "en_US (builtin)".green());

			for entry in std::fs::read_dir(&*CONFIG_PATH)? {
				let entry = entry?;

				if entry.file_type()?.is_dir() {
					let dir_name = entry.file_name();
					let name_str = dir_name.to_string_lossy();

					if name_str != BUILTIN_DICT_DIR {
						println!(
							"{:<12} {}",
							name_str.blue(),
							entry.path().display()
						);
					}
				}
			}

			Ok(())
		}
		Some("help") => print_help(0),
		Some(_) | None => print_help(1),
	}
}

pub(crate) struct Dictionary {
	book: spellbook::Dictionary,
	/// None for built in dictionary
	path: Option<PathBuf>,
}

impl Dictionary {
	pub fn init(name: Option<&str>) -> Result<Self> {
		let Some(name) = name else {
			return Self::init_builtin();
		};

		let dictionary_path = CONFIG_PATH.join(name);
		let aff_path = dictionary_path.join(name).with_added_extension("aff");
		let dic_path = dictionary_path.join(name).with_added_extension("dic");

		let aff = read_to_string(aff_path)?;
		let dic = read_to_string(dic_path)?;

		Self::init_from_parts(&aff, &dic, &dictionary_path, Some(dictionary_path.clone()))
	}

	fn init_builtin() -> Result<Self> {
		static EN_AFF: &str = include_str!("../dictionary/en_US.aff");
		static EN_DIC: &str = include_str!("../dictionary/en_US.dic");

		Self::init_from_parts(EN_AFF, EN_DIC, &BUILTIN_DICT_PATH, None)
	}

	fn init_from_parts(
		aff: &str,
		dic: &str,
		user_dict_dir: &Path,
		stored_path: Option<PathBuf>,
	) -> Result<Self> {
		let mut dict = spellbook::Dictionary::new(aff, dic)
			.map_err(|e| eyre::eyre!("dictionary parsing error: {e}"))?;

		let file = OpenOptions::new()
			.read(true)
			.append(true)
			.create(true)
			.open(user_dict_dir.join(USER_DICT_FILE_NAME))?;

		let reader = BufReader::new(file);
		for line in reader.lines() {
			let Ok(line) = line else { break };
			_ = dict.add(&line);
		}

		Ok(Self { book: dict, path: stored_path })
	}

	pub fn name(&self) -> String {
		self.path
			.as_deref()
			.and_then(|p| p.file_name())
			.map_or("en_US (builtin)".into(), |s| s.to_string_lossy().into())
	}

	pub fn check(
		&self,
		word: &str,
	) -> bool {
		self.book.check(word)
	}

	pub fn suggest(
		&self,
		word: &str,
	) -> Box<[String]> {
		let mut out = Vec::new();
		self.book.suggest(word, &mut out);
		out.into_boxed_slice()
	}

	pub fn add_word(
		&self,
		word: &str,
	) -> Result<()> {
		let mut file = match self.path {
			Some(ref path) => OpenOptions::new()
				.append(true)
				.create(true)
				.open(path.join(USER_DICT_FILE_NAME))?,
			None => OpenOptions::new()
				.append(true)
				.create(true)
				.open(BUILTIN_DICT_PATH.join(USER_DICT_FILE_NAME))?,
		};

		writeln!(file, "{word}")?;
		Ok(())
	}
}

fn print_help(code: i32) -> ! {
	eprintln!("{DICTS_CMD_HELP}");
	std::process::exit(code);
}

static DICTS_CMD_HELP: &str = "
Hoopoe uses Hunspell dictionaries
You may get the one you need from
\thttps://github.com/LibreOffice/dictionaries
\thttps://github.com/wooorm/dictionaries

USAGE: hoopoe dict [OPTIONS] <COMMAND>

COMMANDS:
	add <FILE>    add a dictionary. overwrites existing ones.
	              <FILE> must be a .dic/.aff path. The matching pair must exist together.
	list          list existing dictionaries

OPTIONS:
	-h, --help    display this message
";
