//! Hash checking pool
//!
//! The goal here is to check the hashes of a bunch of downloaded files
//! (usually in tmpdir), and if they're good, save them off into another
//! location (usually filesdir).
use std::path::{PathBuf, Path};

use crate::util::hash::{self, Sha256HashBuf};


/// Enter point: given a set of hashfiles in a tmpdir, check the hashes
/// and (on success) move them into the filesdir.
pub(crate) fn rayon<'a>(hashes: &'a [Sha256HashBuf], tdir: &Path,
		fdir: &Path, keep: bool)
		-> (Vec<&'a Sha256HashBuf>, HashCheckErrs)
{
	let nhash: u64 = hashes.len().try_into().unwrap();
	let pb = crate::util::mk_progress(nhash);


	// Setup rayon pool, and start bouncing over the requests
	use rayon::ThreadPoolBuilder as TPB;
	let njobs = crate::core::jobs::cpu();
	let pool = TPB::new().num_threads(njobs as usize)
			.build().unwrap();

	let (oks, errs) = pool.scope(|_s| {
		use rayon::prelude::*;

		// Run 'em all through
		let hruns = hashes.into_par_iter().map(|hash| {
			let r = hashcheck_worker(hash, tdir, fdir, keep);
			pb.inc(1);
			r
		});

		// Collect
		let mut oks = Vec::new();
		let mut errs = Vec::new();
		for hress in hruns.collect_vec_list()
		{
			for hres in hress
			{
				match hres
				{
					Ok(f)  => oks.push(f),
					Err(e) => errs.push(e),
				}
			}
		}

		(oks, errs)
	});


	// And return
	let errs = HashCheckErrs { errs };
	pb.finish();
	(oks, errs)
}


/// Errors found in scanning
#[derive(Debug)]
#[derive(thiserror::Error)]
#[error("{errs:?}")]  // kinda fugly...
pub(crate) struct HashCheckErrs
{
	/// Some number of individual errors
	pub(crate) errs: Vec<HashCheckErr>,
}


/// Error in the checking
#[derive(Debug)]
#[derive(thiserror::Error)]
pub(crate) enum HashCheckErr
{
	/// Missing file
	#[error("No such file: {0}")]
	Missing(PathBuf),

	/// Filesystem IO error of some kind.
	#[error("I/O error: {0}")]
	Io(#[from] std::io::Error),

	/// Compression error.
	#[error("Compression error: {0}")]
	Compress(anyhow::Error),

	/// Hashing error
	#[error("Hashing error: {0}")]
	Hashing(#[from] hash::Sha256ReaderErr),
}



/// HashChecking a single file
fn hashcheck_worker<'a>(hash: &'a Sha256HashBuf, tmpdir: &Path,
		filesdir: &Path, keep: bool)
		-> Result<&'a Sha256HashBuf, HashCheckErr>
{
	use HashCheckErr as SE;
	use std::fs;
	use crate::util::path_join;
	use crate::util::compress;

	// We deal with .gz files
	let gzfile = format!("{hash}.gz");

	// The input file to look at.  In theory; sometimes, this gets called
	// with the source .gz in tmpdir, and sometimes with the decompressed
	// version already there.
	let srcpath = path_join(tmpdir, &gzfile);

	// The final location
	let dstpath = path_join(filesdir, &gzfile);

	// Decompress it into the same dir; this will give us our
	// decompressed path too.  This silently succeeds if the decompressed file
	// is already there, which sometimes it is.
	let decpath = compress::decompress_gz_dirs(tmpdir, tmpdir,
			gzfile)
			.map_err(SE::Compress)?;

	// OK, well, it's there now, right?  Shouldn't be fallible...
	if !decpath.is_file() { Err(SE::Missing(srcpath.to_path_buf()))?; }


	// Check the hash
	hash::check_sha256_file(&decpath, hash.as_ref())?;

	// OK, it was good, move it into the final location.  Though if the
	// compressed version wasn't already there, we need to make it.
	match srcpath.is_file() {
		true => fs::rename(&srcpath, &dstpath)?,
		false => {
			compress::compress_gz(&decpath, &srcpath)?;
			fs::rename(&srcpath, &dstpath)?;
		},
	};

	// And maybe remove the decompressed file.
	if !keep { fs::remove_file(&decpath)?; }

	// Well OK then
	Ok(hash)
}
