//! Various http stuff.
use std::path::Path;

use super::Server;
use crate::util::hash::Sha256HashBuf;

use url::Url;


impl Server
{
	/*
	 * External funcs
	 */

	/// Fetch a set of patch files into a dir (presumably a temp dir).
	/// This is used during fetching to try and get patches instead of
	/// whole files.
	///
	/// Returns the list of successfully fetched patches, or an error.
	/// Error is generally presumptively fatal; things like missing files
	/// or failed requests aren't "errors" in this meaning, they're just
	/// "whelp, didn't find that patch".
	pub(crate) fn fetch_patch_files(&self, patches: Vec<String>,
			tmpdir: &Path) -> Result<Vec<String>, anyhow::Error>
	{
		let agent = self.cache.agent()?;
		// XXX patchdir differs for upgrade?  Tackle this then...
		let baseurl = self.cache.burl()?.join("bp/")?;

		use crate::core::fetch;
		let fres = fetch::rayon(&baseurl, agent, tmpdir, patches);

		// We only care about what succeeded; any errors in the process
		// don't matter to us.
		Ok(fres.okfiles)
	}


	/// Fetch a set of complete files (named by hash), check the hashes,
	/// and move them into a final storage directory (our filesdir).
	///
	/// This is expected to fully succeed; while patching is an
	/// optimization, this is the last stop.  If we're asking for a file
	/// here, it's because we need it, and if we don't get it, that's
	/// probably going to be fatal.  So we just treat that as an error
	/// return.
	pub(crate) fn fetch_hash_files(&self, hashes: &[Sha256HashBuf],
			tdir: &Path, keep: bool)
			-> Result<(), anyhow::Error>
	{
		use crate::util::{plural, plural_es};
		let nhash = hashes.len();

		// We need the list of hashnames, not just the hashes.
		println!("Fetching {nhash} new file{}.", plural(nhash));
		let fnames: Vec<String> = hashes.iter()
				.map(|f| format!("{f}.gz")).collect();
		// Complete files are under <baseurl>/f
		let url = self.cache.burl()?.join("f/")?;
		let nf = self.fetch_files_from_to(url, fnames, tdir)?;
		// Should be impossible to fail, if it didn't fetch 'em all it
		// woulda Err()'d.
		assert_eq!(nf as usize, nhash);

		// OK, we presumably got 'em all.  Check the hashes and store
		// into files/.
		println!("Checking {nhash} hash{}.", plural_es(nhash));

		// Do the pool's work
		use crate::core::hashcheck as hc;
		let fdir = self.cache.filesdir()?;
		let (oks, errs) = hc::rayon(hashes, tdir, fdir, keep);

		// Any errors probably means we failed.
		if !errs.errs.is_empty() { return Err(errs)?; }

		// If there weren't errs, this better even out...
		let oklen = oks.len();
		if oklen != nhash
		{
			anyhow::bail!("Internal errr: should have checked {nhash}, \
					but only got {oklen}.");
		}

		Ok(())
	}




	/*
	 * Higher-level calling funcs
	 */

	/// Fetch a set of files from a base URL into a dir.  Mostly a
	/// backend-sorta thing that we build more special-purpose frontends
	/// onto.
	///
	/// This very much expects to succeed at getting every file, and if
	/// it doesn't, that's an error.  That's simpler for cases where we
	/// require that result, but not so suitable for cases where we're OK
	/// with not finding everything we're trying.
	pub(super) fn fetch_files_from_to(&self, baseurl: Url,
			files: Vec<String>, path: &Path)
			-> Result<u32, anyhow::Error>
	{
		let agent = self.cache.agent()?;

		use crate::core::fetch;
		let fres = fetch::rayon(&baseurl, agent, path, files);


		// Figure out smarter returns sometime.  For now, if there are
		// errors, just return them, else the file count.
		if let Some(errs) = fres.errs { return Err(errs)?; }
		if fres.nfiles as usize != fres.okfiles.len()
		{
			anyhow::bail!("Expected {} files, fetched {}",
					fres.nfiles, fres.okfiles.len());
		}

		Ok(fres.nfiles)
	}


	/*
	 * The lower-level actual fetching bits.
	 */

	/// Do a GET and dump the results into a Vec<u8>.  I guess there's some
	/// way to do this via ureq's http-interop feature and http::Response?
	/// But instead, we're doing this whole thing manually for the moment...
	///
	/// This is intended as a simple util for fetching "small" files (up to a
	/// few dozen k, maybe), that we're just going to be poking through for
	/// stuff.  It's not built for fetching big files, or saving the results
	/// out to disk; that'll go elsewhere.
	pub(in crate::server) fn get_bytes(&self, url: &url::Url)
			-> Result<Vec<u8>, anyhow::Error>
	{
		let agent = self.cache.agent()?;
		get_bytes(agent, url)
	}
}



/// Backend for Server::get_bytes().
///
/// This is kept separate so Server::get_key_tag() can call it directly
/// with an agent, because that's still in the process of choosing a
/// Server to use, so it would be annoying to potentially make extra
/// Agent's that hang around on those Servers we didn't choose without
/// being dropped until <some arbitrary time>.
///
/// This way we can do those checks _before_ deciding "we'll keep this
/// server" and holding that Agent around for the rest of the process.
pub(in crate::server) fn get_bytes(agent: &ureq::Agent, url: &url::Url)
		-> Result<Vec<u8>, anyhow::Error>
{
	// These are small files to directly poke at, so set a limit big
	// enough to easily fit anything we expect, but not blow out memory
	// if somebody messes with us.
	const LIMIT: u64 = 10 * 1024 * 1024;

	let resp = agent.request_url("GET", url)
		.call()?;
	let clen: Option<usize> = match resp.header("Content-Length") {
		Some(len) => match len.parse() {
			Ok(cl) => Some(cl),
			Err(_) => None,
		},
		None => None,
	};
	let mut data: Vec<u8> = match clen {
		Some(b) => Vec::with_capacity(b),
		None    => Vec::new(),
	};

	use std::io::Read;
	resp.into_reader().take(LIMIT).read_to_end(&mut data)?;
	Ok(data)
}



/// Creating an Agent for our use.  Centralize to make later adjustments
/// a little easier...
pub(in crate::server) fn mk_agent() -> ureq::Agent
{
	use std::time::Duration;

	ureq::AgentBuilder::new()
		.timeout_connect(Duration::from_secs(10))
		.timeout_read(Duration::from_secs(10))
		.build()
}
