//! Info about running system version.
//!
//! Details can get a bit touchy, and bad things can happen when things
//! get well out of sync, but we do the best we can...



/// Our idea of the current system version, as used through the system.
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct Version
{
	/// The release: "12.3", "14.0", etc.
	pub(crate) release: String,
	/// The release type: "RELEASE", "STABLE", "RC1", etc.
	pub(crate) reltype: String,
	/// The patch level: "12.3-RELEASE-p2" -> Some(2)
	pub(crate) patch: Option<u32>,
}


impl Version
{
	/// Generate a string of this version, but with a replaced patchnum.
	/// We do this in a few places where we use the server's patchnum in
	/// a display (the server doesn't have a full version, so...)
	pub(crate) fn str_patchnum(&self, patch: Option<u32>) -> String
	{
		let rel   = &self.release;
		let rtype = &self.reltype;
		match patch {
			Some(p) => format!("{rel}-{rtype}-p{p}"),
			None    => format!("{rel}-{rtype}"),
		}
	}
}


use std::fmt;
impl fmt::Display for Version
{
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
	{
		let vstr = self.str_patchnum(self.patch);
		write!(f, "{vstr}")
	}
}


/// Generate a fake version from a given string
pub(crate) fn fake(vstr: &str) -> Result<Version, anyhow::Error>
{
	parse_version_row(vstr, "fake")
}


/// Parse a single row of version info, with tweaked error messaging.
/// FromStr does all the actual work, this just gives us a convenient
/// wrapper.
fn parse_version_row(row: &str, rdesc: &str) -> Result<Version, anyhow::Error>
{
	row.parse().map_err(|e| anyhow::anyhow!("Error in {rdesc} version: {e}"))
}

impl std::str::FromStr for Version
{
	type Err = String;

	fn from_str(s: &str) -> Result<Self, Self::Err>
	{
		// <version>-p<patch>
		let mut riter = s.split("-p");
		let relbit = riter.next().ok_or("No release")?;
		let pat = riter.next();

		// <version> = 1.2-STABLE, etc
		let mut rsp = relbit.split("-");
		let release = rsp.next()
			.ok_or_else(|| "No version".to_string())?
			.trim().to_string();
		let reltype = rsp.next()
			.ok_or_else(|| "No version type".to_string())?
			.trim().to_string();

		let patch = match pat {
			None => None,
			Some(s) => {
				let pnum = s.trim().parse::<u32>()
					.map_err(|e| format!("Bad patch version: {}", e))?;
				Some(pnum)
			},
		};
		Ok(Version { release, reltype, patch })
	}
}




/// Get info about currently installed release stuff, from running
/// freebsd-version (out of the given basedir).
use std::path::Path;
pub(crate) fn get(bdir: &Path) -> Result<Version, anyhow::Error>
{
	let vs = run_freebsd_version(bdir)?;
	let rvers = match (vs.0, vs.1) {
		(None, user) => user,
		(Some(kern), user) => std::cmp::max(kern, user),
	};
	Ok(rvers)
}


/// Run freebsd-version to get kernel/user version info.  Out of a given
/// basedir, with a given env, f-v will DTRT by itself.
fn run_freebsd_version(bdir: &Path)
		-> Result<(Option<Version>, Version), anyhow::Error>
{
	use crate::util::path_join;
	let vpath = path_join(bdir, "/bin/freebsd-version");
	let vcmd = || {
		let mut c = std::process::Command::new(&vpath);
		c.env("ROOT", bdir);
		c
	};


	// Instead of running -ku, run -k and -u separately.  In cases
	// there's there's no kernel installed, -k will blow up right away
	// before doing anything else.  We could do various attempts at error
	// handling, but it's simpler to just run both independently, then
	// tweak stuff.  If we get nothing for the kernel, OK, we'll live
	// with that; that's normal.  If we get nothing for the user, now...
	// that would be bad.
	let kvbuf = vcmd().arg("-k").output()
			.map_err(|e| {
				anyhow::anyhow!("Running freebsd-version -k: {e}")
			})?.stdout;
	let uvbuf = vcmd().arg("-u").output()
			.map_err(|e| {
				anyhow::anyhow!("Running freebsd-version -u: {e}")
			})?.stdout;

	// There are more efficient ways of doing this, but what the heck...
	// we'll just assume it's all perfectly utf8-able
	use std::str::from_utf8;
	let kernel = match kvbuf.len() {
		0 => None,
		_ => Some(parse_version_row(from_utf8(&kvbuf)?, "kernel")?),
	};

	let user = match uvbuf.len() {
		0 => anyhow::bail!("No output from freebsd-version -u"),
		_ => parse_version_row(from_utf8(&uvbuf)?, "user")?,
	};

	Ok((kernel, user))
}




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

	/// Test fake version
	#[test]
	fn fake_version()
	{
		// Test a fake one
		let fver = "1.2.3-RC1-p1";
		let vers = super::fake(fver).unwrap();
		assert_eq!(vers.release, "1.2.3");
		assert_eq!(vers.reltype, "RC1");
		assert_eq!(vers.patch, Some(1));
	}

	/// Test slightly odd version parsing
	#[test]
	fn weird_version()
	{
		// freebsd-version output will have newlines
		let fver = "1.2-RELEASE\n";
		let vers: Version = fver.parse().unwrap();
		assert_eq!(vers.release, "1.2");
		assert_eq!(vers.reltype, "RELEASE");
		assert_eq!(vers.patch, None);

		let fver = "1.2-RELEASE-p34\n";
		let vers: Version = fver.parse().unwrap();
		assert_eq!(vers.release, "1.2");
		assert_eq!(vers.reltype, "RELEASE");
		assert_eq!(vers.patch, Some(34));
	}

	/// Test display
	#[test]
	fn display_version()
	{
		let fver = "12.3-RELEASE";
		let mut vers: Version = fver.parse().unwrap();
		assert_eq!(vers.to_string(), "12.3-RELEASE");
		vers.patch = Some(3);
		assert_eq!(vers.to_string(), "12.3-RELEASE-p3");
	}
}
