//! File stashing pool
use std::path::{PathBuf, Path};

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



/// Entry point: given a set of <paths> under a <basedir>, stash up
/// compressed hashfiles in a <filesdir> (using a <tmpdir> for
/// intermediate products).
pub(crate) fn rayon(files: &[(&Path, Sha256HashBuf)], basedir: &Path,
		tmpdir: &Path, filesdir: &Path)
		-> Result<usize, StashErrs>
{
	let nfiles: u64 = files.len().try_into().unwrap();
	let pb = crate::util::mk_progress(nfiles);

	// Various paths the worker will use
	let ctrl = Control { basedir, filesdir, tmpdir };

	// 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 (num_ok, errs) = pool.scope(|_s| {
		use rayon::prelude::*;

		// Run 'em all through
		let fruns = files.into_par_iter().map(|f| {
			let r = stash_worker(&ctrl, f.0, &f.1);
			pb.inc(1);
			r
		});

		// Collect
		let mut num_ok = 0;
		let mut errs = Vec::new();
		for fress in fruns.collect_vec_list()
		{
			for fres in fress
			{
				match fres
				{
					Ok(_)  => num_ok += 1,
					Err(e) => errs.push(e),
				}
			}
		}

		(num_ok, errs)
	});


	// And return
	pb.finish();
	match errs.len() {
		0 => Ok(num_ok),
		_ => Err(StashErrs { errs }),
	}
}


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

/// Error in stashing
#[derive(Debug)]
#[derive(thiserror::Error)]
pub(crate) enum StashErr
{
	/// Not a file.  Sorta an Io, but it means it's some other type,
	/// which means we lost a race since our scan.
	#[error("Not a file")]
	BadType(PathBuf),

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

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

	/// Hash didn't match; this indicates a race, since we already stored
	/// up the hash in our scanning just moments ago
	#[error("Hash didn't match: expected {0}, got {1}")]
	HashMismatch(String, String),

	/// Some other misc thing that doesn't fit that.
	#[error("Internal error: {0}")]
	Misc(anyhow::Error),
}



/// Control for scanning; we're under a basedir
#[derive(Debug, Clone)]
struct Control<'a>
{
	/// The basedir where source files live under
	pub(crate) basedir: &'a Path,

	/// The dir we store the hashfiles into
	pub(crate) filesdir: &'a Path,

	/// The tempdir we use in the process
	pub(crate) tmpdir: &'a Path,
}


/// Stashing a single file
fn stash_worker(ctrl: &Control, path: &Path, hash: &Sha256HashBuf)
		-> Result<(), StashErr>
{
	use StashErr as SE;
	use std::fs;

	// Where's the actual file to check?
	use crate::util::path_join;
	let srcpath = path_join(ctrl.basedir, path);

	// Temp location
	let hstr = hash.as_ref();
	let tmppath = ctrl.tmpdir.join(hstr);

	// Final location
	let hgzstr = format!("{}.gz", hstr);
	let finalpath = ctrl.filesdir.join(&hgzstr);


	// Trivial check.  Of course, this is racy too, but it's already a
	// tiny race since the scan, and if you're messing with system files
	// while you're running a system upgrade util, you deserve to get
	// grumpy errors.
	if !srcpath.is_file() { Err(SE::BadType(srcpath.to_path_buf()))?; }

	// Copy it
	fs::copy(&srcpath, &tmppath)?;

	// Check the hash
	use hash::check_sha256_file;
	if let Err(e) = check_sha256_file(&tmppath, hstr)
	{
		use hash::Sha256ReaderErr as HE;
		Err(match e {
			HE::Hash(exp, got) => SE::HashMismatch(exp, got),
			HE::IO(e) => SE::Io(e),
			HE::Expected(s) => SE::Misc(s),
		})?;
	}

	// Compress it (still in the temp loc, just to be crash safe-ish).
	use crate::util::compress;
	let tmpgz = ctrl.tmpdir.join(&hgzstr);
	compress::compress_gz(&srcpath, &tmpgz)?;

	// And move over to final
	fs::rename(&tmpgz, &finalpath)?;

	// And we're done
	Ok(())
}
