//! FS scanner impl
use std::path::{PathBuf, Path};

use crate::util;


/// Primary entry point; scan a set of paths with a given control struct.
///
/// We take ownership of the list of PathBuf's because we're going to
/// need the PathBuf in our Res struct, so we might as well just use the
/// same memory.
pub(super) fn rayon(basedir: &Path, paths: Vec<PathBuf>, hash: bool)
		-> ScanResult
{
	let pb = util::mk_progress(paths.len().try_into().unwrap());


	// Setup a rayon pool, and loop through the paths with it
	use rayon::ThreadPoolBuilder as TPB;
	let njobs = crate::core::jobs::cpu();
	let pool = TPB::new().num_threads(njobs as usize)
			.build().unwrap();

	// It feels like in_place_scope() would be the thing to do here, but
	// that apparently doesn't actually spawn the parallel iterators into
	// the thread pool, so all the work we did to specify num_threads
	// above is wasted?
	let (oks, errs, missing) = pool.scope(|_s| {
		use rayon::prelude::*;

		// It seems like the ParallelIterator::flat_map_iter() family of
		// things should be applicable here, but I can't make it DTRT
		let sruns = paths.into_par_iter().map(|p| {
			let r = scan_path(basedir, p, hash);
			pb.inc(1);
			r
		});

		// Now collect 'em up
		let mut oks = Vec::new();
		let mut errs = Vec::new();
		let mut missing = Vec::new();
		for sress in sruns.collect_vec_list()
		{
			for sres in sress
			{
				match sres
				{
					Ok(s) => oks.push(s),
					Err(e) => match e {
						ScanErr::Nonexistent(p) => missing.push(p),
						_ => errs.push(e),
					},
				}
			}
		}

		(oks, errs, missing)
	});



	// Finalize
	pb.finish();

	// Put together the return, and return it.
	let errs = match errs.len() {
		0 => None,
		_ => Some(ScanErrs { errs }),
	};
	let ret = ScanResult { oks, missing, errs };
	ret
}



/// The final cumulative result of a scan run
#[derive(Debug)]
pub(super) struct ScanResult
{
	/// Successful scan results
	pub(super) oks: Vec<Res>,

	/// Missing files
	pub(super) missing: Vec<PathBuf>,

	/// Errors
	pub(super) errs: Option<ScanErrs>,
}


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


/// Error in scanning a path
#[derive(Debug)]
#[derive(thiserror::Error)]
pub(super) enum ScanErr
{
	/// No such file.  Technically this would be Io too, but for our
	/// purposes, this is a normal-ish expected thing to be handled;
	/// other I/O errors mostly mean "WTF, something's broken"...
	///
	/// This is a bit of a wart being here, since we'd never have one of
	/// these in the ScanResult.errs that gets returned; they get turned
	/// into missing's.  But since we're sharing the type with the
	/// lower-down impl...   well, maybe we should fix that sometime.
	#[error("No such file")]
	Nonexistent(PathBuf),

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

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

// We use our util::lstat() for looking up stuff in the FS.
impl From<util::LstatErr> for ScanErr
{
	fn from(e: util::LstatErr) -> Self
	{
		use util::LstatErr as LE;
		match e {
			LE::CString(e)     => Self::Misc(format!("CString error: {e}")),
			LE::Nonexistent(p) => Self::Nonexistent(p),
			LE::Lstat(_, estr) => Self::Misc(format!("lstat(2): {estr}")),
		}
	}
}


/// The result of a single path's scan
#[derive(Debug, Clone, Default)]
pub(super) struct Res
{
	/// The scanned file
	pub(super) path: PathBuf,

	/// What type we found
	pub(super) ftype: FileType,

	/// SHA256 hash of the contents.  Only meaningful for regular files,
	/// not dirs or symlinks or the like.
	pub(super) sha256: Option<util::hash::Sha256Hash>,

	/// Symlinks would have a target
	pub(super) symlink: Option<PathBuf>,

	// Misc FS metadata
	pub(super) dev:   u64,
	pub(super) ino:   u64,
	pub(super) nlink: u64,
	pub(super) uid:   u32,
	pub(super) gid:   u32,
	pub(super) mode:  u32,
	pub(super) flags: u32,
}


/// Internal helper for the type of a file
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) enum FileType {
	#[default]
	File,
	Dir,
	SymLink,
}
impl TryFrom<std::fs::FileType> for FileType
{
	type Error = std::fs::FileType;

	fn try_from(ft: std::fs::FileType) -> Result<Self, Self::Error>
	{
		if ft.is_file()    { return Ok(Self::File); }
		if ft.is_dir()     { return Ok(Self::Dir); }
		if ft.is_symlink() { return Ok(Self::SymLink); }
		Err(ft)
	}
}

impl TryFrom<u16> for FileType
{
	type Error = u16;

	fn try_from(st_mode: u16) -> Result<Self, Self::Error>
	{
		use libc::{S_IFMT, S_IFDIR, S_IFREG, S_IFLNK};
		let ft = st_mode & S_IFMT;
		match ft {
			S_IFREG => Ok(Self::File),
			S_IFDIR => Ok(Self::Dir),
			S_IFLNK => Ok(Self::SymLink),
			_ => Err(ft)
		}
	}
}



/*
 * The remainder here is purely internal impl stuff, you shouldn't need
 * to look at any of it unless you're working on the scan code itself.
 */




/// Scanning a single path
fn scan_path(basedir: &Path, path: PathBuf, hash: bool) -> Result<Res, ScanErr>
{
	use ScanErr as SE;

	// Where's the actual file to check?
	let realpath = util::path_join(basedir, &path);

	// Most of the info we care about is in the extended metadata stuff.
	// In fact, almost all of it.  Except the flags.  Apparently we have
	// to use the libc crate for that...
	let use_libc = true;
	let statres = match use_libc {
		true  => stat_libc(&realpath),
		false => stat_metadata(&realpath),
	};
	let mut res = match statres {
		Ok(s) => s,
		Err(e) => {
			use StatErr as StE;
			let err = match e {
				StE::Missing => SE::Nonexistent(path),
				StE::Io(e)   => SE::Io(e),
				StE::Misc(e) => SE::Misc(e),
			};
			return Err(err);
		},
	};

	// Now we can move this in
	res.path = path;



	// Now the extra bits beyond basic stat()-ish.
	//
	// If it's a regular file, calculate the sha256 of it.  If it's a
	// symlink, we need to see what it's pointing to.
	match res.ftype
	{
		FileType::File => {
			if hash
			{
				let fh = match util::hash::sha256_file(&realpath) {
					Ok(h) => h,
					Err(e) => {
						use util::hash::Sha256ReaderErr as HE;
						let ne = match e {
							HE::IO(e) => e.into(),
							x => SE::Misc(x.to_string()),
						};
						return Err(ne);
					},
				};
				res.sha256 = Some(fh);
			}
		},
		FileType::SymLink => {
			let sl = std::fs::read_link(&realpath)?;
			res.symlink = Some(sl);
		},
		_ => (),
	}


	// OK, if we made it here, we succeeded at stating a thing.
	Ok(res)
}



/// Low-level stat-ish info about a single path
#[derive(Debug)]
enum StatErr
{
	/// Nothing at the path
	Missing,

	/// Filesystem I/O error
	Io(std::io::Error),

	/// Something else not characterized
	Misc(String),
}


/// Looking up low-level stat-ish info about a single path (libc lstat
/// variant).
///
/// Sadly, it looks like this is our only choice to get the flags, so we
/// use our wrappers around the raw ffi to libc.
fn stat_libc(p: &Path) -> Result<Res, StatErr>
{
	let mut res: Res = Default::default();

	// OK, we've wrapped up the grimiest of these details...
	let (myst, _) = util::lstat(p)
			.map_err(|e| {
				use util::LstatErr::Nonexistent as NE;
				match e {
					NE(_) => StatErr::Missing,
					e => StatErr::Misc(e.to_string()),
				}
			})?;

	// If we got this far, it's successful, so pull out the bits we
	// want.
	res.ftype = myst.mode.try_into().map_err(|e| {
			let ep = p.display();
			let estr = format!("Unknown mode filetype for {ep}: {e}");
			StatErr::Misc(estr)
		})?;
	res.dev   = myst.dev;
	res.ino   = myst.ino;
	res.nlink = myst.nlink;
	res.uid   = myst.uid;
	res.gid   = myst.gid;
	res.mode  = myst.perms.into();
	res.flags = myst.flags;

	Ok(res)
}


/// Looking up low-level stat-ish info about a single path (rust std
/// variant).
///
/// Most of the info we care about is in the extended metadata stuff.
/// Almost all of it, in fact.  _Except_ the flags, which means we can't
/// actually really use this.  Mostly just left for reference.
fn stat_metadata(p: &Path) -> Result<Res, StatErr>
{
	use std::os::unix::fs::MetadataExt as _;

	let mut res: Res = Default::default();

	let md = std::fs::symlink_metadata(p)
			.map_err(|e| {
				// "Not found" is an "error", but we treat it separately
				// from other harder errors.
				use std::io::ErrorKind as EK;
				match e.kind()
				{
					EK::NotFound => StatErr::Missing,
					_ => StatErr::Io(e),
				}
			})?;

	res.ftype = md.file_type().try_into().map_err(|e| {
			let ep = p.display();
			StatErr::Misc(format!("Unknown type for {ep}: {e:?}"))
		})?;
	res.dev = md.dev();
	res.ino = md.ino();
	res.nlink = md.nlink();
	res.uid = md.uid();
	res.gid = md.gid();
	res.mode = md.mode() & 0o7777; // Only care about u/g/o and the setXid's

	// I mean, I s'pose we could just do all the libc::stat stuff
	// here to get at this, and at least have all the "normal" bits
	// above dealt with, right?
	res.flags = 0;  // Dangit...

	Ok(res)
}



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

	fn tempdir() -> TempDir
	{
		tempfile::tempdir().expect("You can't fail a tempdir!")
	}

	#[test]
	fn file()
	{
		use std::path::PathBuf;
		use std::fs;
		use crate::util::hash;

		// Test scanning a file
		let tmpdir = tempdir();
		let tpath = tmpdir.path();

		// We'll put some content in it
		let fcont = "Full fathom five thy father lies";
		let fchash = hash::sha256_reader(&mut fcont.as_bytes()).unwrap();
		assert_eq!(fchash.to_string(),
			"040f848bda0cc34f2c3161949a107073c83444328561599a7cf501b2dabb2de3",
			"expected hash");

		let fname: PathBuf = "testfile".into();
		let fpath = tpath.join(&fname);
		fs::write(fpath, fcont).expect("writing tempfile");

		// Now scan the sucker.  Test a missing file while we're at it.
		let mfile: PathBuf = "hahano".into();
		let files = vec![fname.clone(), mfile.clone()];
		let res = super::rayon(tpath, files, true);
		assert!(res.errs.is_none(), "No errs");

		// One missing file
		assert_eq!(res.missing.len(), 1, "One missing file");
		assert_eq!(res.missing[0], mfile, "Expected missing file");

		// One present
		assert_eq!(res.oks.len(), 1, "Got one file");
		let okf = &res.oks[0];
		assert_eq!(okf.path, fname, "Found expected file");
		assert_eq!(okf.ftype, super::FileType::File, "Is a file");
		assert_eq!(okf.sha256, Some(fchash), "Got the right hash");
		assert_eq!(okf.symlink, None, "Files aren't symlinks");
		assert_eq!(okf.nlink, 1, "I sure didn't make more links...");
	}

	#[test]
	fn symlink()
	{
		use std::path::PathBuf;

		let tmpdir = tempdir();
		let tpath = tmpdir.path();

		// Symlinks don't have to point to a real target, so we can just
		// pretent.
		let lname: PathBuf = "link".into();
		let ltarg: PathBuf = "target".into();

		use std::os::unix::fs::symlink;
		symlink(&ltarg, tpath.join(&lname)).unwrap();

		// Scan and check
		let files = vec![lname.clone()];
		let res = super::rayon(tpath, files, true);
		assert!(res.errs.is_none(), "No errs");
		assert_eq!(res.missing.len(), 0, "No missing");

		// One symlink present
		assert_eq!(res.oks.len(), 1, "Got one file");
		let okf = &res.oks[0];
		assert_eq!(okf.path, lname, "Found expected file");
		assert_eq!(okf.ftype, super::FileType::SymLink, "Is a symlink");
		assert_eq!(okf.sha256, None, "No hash on symlinks");
		assert_eq!(okf.symlink, Some(ltarg), "Right target");
		assert_eq!(okf.nlink, 1, "I sure didn't make more links...");
	}
}
