use camino::{Utf8Path, Utf8PathBuf};
use std::{fmt, io, ops::Deref, path::Display, str::FromStr};

use crate::RuntimeError;

/// Webpath is the path we get from HTTP requests. It's garunteed to not fall
/// below the webroot and will never start or end with a slash.
#[derive(Clone, Debug, PartialEq)]
pub struct Webpath {
	pub webcanon: Utf8PathBuf,
	is_dir: bool,
}

impl FromStr for Webpath {
	type Err = RuntimeError;

	fn from_str(raw: &str) -> Result<Self, Self::Err> {
		let is_dir = raw.trim_end().ends_with('/');
		let mut curr = Utf8PathBuf::new();

		for component in raw.split('/') {
			match component {
				"." => continue,
				".." => {
					if !curr.pop() {
						return Err(RuntimeError::PathTooLow {
							path: raw.to_owned(),
						});
					}
				}
				comp => curr.push(comp),
			}
		}

		Ok(Self {
			webcanon: curr,
			is_dir,
		})
	}
}

impl Webpath {
	/// Return whether or not this Webpath is empty which would indicate it's
	/// the homepage.
	pub fn is_index(&self) -> bool {
		self.webcanon == ""
	}

	/// Whether or not this Webpath points to a directory (has a trailing slash)
	pub fn is_dir(&self) -> bool {
		self.is_dir
	}

	/// Return a String that interprets this Webpath as a directory, adding a
	/// trailing slash
	pub fn as_dir(&self) -> String {
		format!("/{}/", self.webcanon)
	}

	/// Returns the first directory of the Webpath. Assumes the path is a
	/// directory if it ends in a `/`, otherwise pops the last component.
	pub fn first_dir(&self) -> String {
		if self.is_dir {
			self.webcanon.to_string()
		} else {
			let mut dir = self.webcanon.clone();
			dir.pop();
			dir.to_string()
		}
	}
}

impl Deref for Webpath {
	type Target = Utf8Path;

	fn deref(&self) -> &Self::Target {
		&self.webcanon
	}
}

impl AsRef<Utf8Path> for Webpath {
	fn as_ref(&self) -> &Utf8Path {
		&self.webcanon
	}
}

impl PartialEq<str> for Webpath {
	fn eq(&self, other: &str) -> bool {
		self.webcanon.eq(other)
	}
}

impl fmt::Display for Webpath {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		write!(f, "/{}", self.webcanon)
	}
}

const ROOT_INDEX: &str = "home.html";
const DIRFILE_EXT: &str = "html";

pub struct PathResolution {
	pub filepath: Utf8PathBuf,
	pub is_dirfile: bool,
}

impl Deref for PathResolution {
	type Target = Utf8Path;

	fn deref(&self) -> &Self::Target {
		self.filepath.as_path()
	}
}

impl fmt::Display for PathResolution {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		write!(f, "{}", self.deref())
	}
}

#[derive(Clone, Debug)]
pub struct Filesystem {
	webroot: Utf8PathBuf,
}

impl Filesystem {
	pub fn new<P: Into<Utf8PathBuf>>(root: P) -> Self {
		Self {
			webroot: root.into(),
		}
	}

	pub fn resolve(&self, webpath: &Webpath) -> Result<PathResolution, RuntimeError> {
		if webpath.is_index() || webpath == ROOT_INDEX {
			return Ok(PathResolution {
				filepath: self.webroot.join(ROOT_INDEX),
				is_dirfile: false,
			});
		}

		let path = self.webroot.join(webpath);
		let metadata = Self::metadata(&path)?;

		// If it's a file then return the path immediatly
		if metadata.is_file() {
			Ok(PathResolution {
				filepath: path,
				is_dirfile: false,
			})
		} else {
			// we check this above. but still..
			//TODO: gen- probably don't unwrap
			let filename = path.file_name().unwrap();
			let dirfile = path.join(format!("{filename}.{DIRFILE_EXT}"));

			if dirfile.exists() {
				Ok(PathResolution {
					filepath: dirfile,
					is_dirfile: true,
				})
			} else {
				Err(RuntimeError::NotFound {
					source: io::ErrorKind::NotFound.into(),
					path: dirfile,
				})
			}
		}
	}

	/// Resolve a file system path to a webpath.
	///
	/// Paths are not checked to see if they exist and are not checked to be within the webroot.
	///
	/// Paths attempt to resolve to the simplest form. For example, if the path is [`ROOT_INDEX`]
	/// it will come out as `/` and if it is a dirfile, it will come out as a directory.
	///
	/// **FOOTGUN** currently assumes every path is a file...
	pub fn reverse_resolve<P: AsRef<Utf8Path>>(&self, path: P) -> Result<Webpath, RuntimeError> {
		let path = path.as_ref();
		let relpath = match path.strip_prefix(&self.webroot) {
			Ok(relpath) => relpath,
			Err(_e) => {
				if path.is_relative() {
					path
				} else {
					//FIXME: gen- This error is not strictly correct, but it'll do for now.
					// 2024-10-12
					return Err(RuntimeError::PathTooLow {
						path: path.to_string(),
					});
				}
			}
		};

		if relpath == "" || relpath == ROOT_INDEX {
			return Ok(Webpath {
				webcanon: Utf8PathBuf::new(),
				is_dir: true,
			});
		}

		todo!();

		Ok(Webpath {
			webcanon: relpath.into(),
			is_dir: false,
		})
	}

	pub fn metadata<P: AsRef<Utf8Path>>(path: P) -> Result<std::fs::Metadata, RuntimeError> {
		path.as_ref()
			.metadata()
			.map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned()))
	}

	pub async fn open<P: AsRef<Utf8Path>>(path: P) -> Result<tokio::fs::File, RuntimeError> {
		tokio::fs::File::open(path.as_ref())
			.await
			.map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned()))
	}

	pub async fn read<P: AsRef<Utf8Path>>(path: P) -> Result<Vec<u8>, RuntimeError> {
		tokio::fs::read(path.as_ref())
			.await
			.map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned()))
	}

	pub async fn read_to_string<P: AsRef<Utf8Path>>(path: P) -> Result<String, RuntimeError> {
		tracing::trace!("read_to_string {}", path.as_ref());

		tokio::fs::read_to_string(path.as_ref())
			.await
			.map_err(|ioe| RuntimeError::from_io(ioe, path.as_ref().to_owned()))
	}
}

#[cfg(test)]
mod test {
	use std::str::FromStr;

	use crate::fs::{Webpath, ROOT_INDEX};

	use super::Filesystem;

	macro_rules! webpath {
		($location:expr) => {
			Webpath::from_str($location).unwrap()
		};
	}

	#[test]
	fn webpath_finds_too_low() {
		assert!(Webpath::from_str("/..").is_err());
		assert!(Webpath::from_str("/one/..").is_ok());
		assert!(Webpath::from_str("/one/../..").is_err());
		assert!(Webpath::from_str("/one/../two").is_ok())
	}

	#[test]
	fn webpath_path_correct() {
		assert_eq!(Webpath::from_str("/one").unwrap().webcanon, "one");
		assert_eq!(Webpath::from_str("/one/..").unwrap().webcanon, "");
		assert_eq!(Webpath::from_str("/one/two/..").unwrap().webcanon, "one");
		assert_eq!(Webpath::from_str("/one/../two").unwrap().webcanon, "two");
		assert_eq!(
			Webpath::from_str("/one/two/three/..").unwrap().webcanon,
			"one/two"
		);
		assert_eq!(
			Webpath::from_str("/one/../home.html").unwrap().webcanon,
			"home.html"
		);
	}

	const TESTROOT: &str = "test/serve";

	#[test]
	fn filesystem_resolves_index() {
		let fs = Filesystem::new(TESTROOT);
		assert_eq!(
			fs.resolve(&webpath!("/")).unwrap().filepath,
			format!("{TESTROOT}/{ROOT_INDEX}")
		);
		assert_eq!(
			fs.resolve(&webpath!("/one/..")).unwrap().filepath,
			format!("{TESTROOT}/{ROOT_INDEX}")
		);
	}

	#[test]
	fn filesystem_resolves_dirfile() {
		let fs = Filesystem::new(TESTROOT);
		assert_eq!(
			fs.resolve(&webpath!("/one")).unwrap().filepath,
			format!("{TESTROOT}/one/one.html")
		);
		assert_eq!(
			fs.resolve(&webpath!("/one/eleven/")).unwrap().filepath,
			format!("{TESTROOT}/one/eleven/eleven.html")
		);
		assert_eq!(
			fs.resolve(&webpath!("/one/eleven/..")).unwrap().filepath,
			format!("{TESTROOT}/one/one.html")
		);
	}

	#[test]
	fn filesystem_reverse_resolves_index() {
		let fs = Filesystem::new(TESTROOT);

		assert_eq!(fs.reverse_resolve(TESTROOT).unwrap(), webpath!("/"));
		assert_eq!(
			fs.reverse_resolve(format!("{TESTROOT}/{ROOT_INDEX}"))
				.unwrap(),
			webpath!("/")
		)
	}

	#[test]
	fn filesystem_reverse_resolves_directories() {
		let fs = Filesystem::new(TESTROOT);

		assert_eq!(fs.reverse_resolve("test/").unwrap(), webpath!("test/"))
	}
}