mod error;
mod fs;
mod markup;
mod settings;
mod templated;

use std::{os::unix::fs::MetadataExt, str::FromStr};

use axum::{
	body::Body,
	extract::Path,
	http::{header, StatusCode},
	response::Response,
	routing::get,
	Extension, Router,
};
use bempline::{Document, Options};
use camino::Utf8PathBuf;
pub use error::RuntimeError;
use fs::Filesystem;
use settings::Settings;
use tokio_util::io::ReaderStream;

use crate::{
	fs::{PathResolution, Webpath},
	templated::Templated,
};

#[tokio::main]
async fn main() {
	let fs = Filesystem::new("../inf/served");

	let settings = Settings {
		template_dir: Utf8PathBuf::from("../inf/templates"),
	};

	let app = Router::new()
		.route("/", get(index_handler))
		.route("/*path", get(handler))
		.layer(Extension(fs))
		.layer(Extension(settings));

	let listener = tokio::net::TcpListener::bind("0.0.0.0:2560").await.unwrap();
	axum::serve(listener, app).await.unwrap()
}

async fn index_handler(fse: Extension<Filesystem>, se: Extension<Settings>) -> Response {
	handler(fse, se, Path(String::from("/"))).await
}

async fn handler(
	Extension(fs): Extension<Filesystem>,
	Extension(settings): Extension<Settings>,
	Path(path): Path<String>,
) -> Response {
	match falible_handler(fs, settings, path).await {
		Ok(resp) => resp,
		Err(re) => Response::builder()
			.body(Body::from(re.to_string()))
			.unwrap(),
	}
}

async fn falible_handler(
	fs: Filesystem,
	settings: Settings,
	path: String,
) -> Result<Response, RuntimeError> {
	println!("raw = {path}");

	let webpath: Webpath = path.parse()?;

	println!("path = {path}");

	let PathResolution {
		filepath,
		is_dirfile,
	} = fs.resolve(&webpath)?;

	if !webpath.is_dir() && is_dirfile {
		println!("as_dir = {}", webpath.as_dir());
		return Ok(redirect(webpath.as_dir()));
	}

	let ext = filepath.extension().unwrap_or_default();

	if ext != "html" {
		send_file(filepath).await
	} else {
		let content = Filesystem::read_to_string(&filepath).await?;
		match Templated::from_str(&content) {
			Ok(templated) => send_template(templated, filepath, settings).await,
			Err(_) => Ok(Response::builder()
				.header(header::CONTENT_TYPE, "text/html")
				.body(Body::from(content))
				.unwrap()),
		}
	}
}

fn redirect<S: Into<String>>(redirection: S) -> Response {
	let location = redirection.into();
	println!("redirecting to {location}");
	Response::builder()
		.status(StatusCode::TEMPORARY_REDIRECT)
		.header(header::LOCATION, &location)
		.body(Body::new(format!("redirecting to {location}")))
		.unwrap()
}

// 20 megabytes
const STREAM_AFTER: u64 = 20 * 1024 * 1024;

async fn send_file(filepath: Utf8PathBuf) -> Result<Response, RuntimeError> {
	let ext = filepath.extension().unwrap_or_default();

	let mime = match ext {
		// Text
		"css" => "text/css",
		"html" => "text/html",
		"js" => "text/javascript",
		"txt" => "txt/plain",

		// Multimedia
		"gif" => "image/gif",
		"jpg" | "jpeg" => "image/jpeg",
		"mp4" => "video/mp4",
		"png" => "image/png",
		_ => "",
	};

	let mut response = Response::builder();
	if !mime.is_empty() {
		response = response.header(header::CONTENT_TYPE, mime);
	}

	let metadata = Filesystem::metadata(&filepath)?;
	if metadata.size() > STREAM_AFTER {
		let file = Filesystem::open(filepath).await?;
		let stream = ReaderStream::new(file);
		Ok(response.body(Body::from_stream(stream)).unwrap())
	} else {
		let content = Filesystem::read(filepath).await?;
		Ok(response.body(Body::from(content)).unwrap())
	}
}

async fn send_template(
	templated: Templated,
	path: Utf8PathBuf,
	settings: Settings,
) -> Result<Response, RuntimeError> {
	let template_stem = templated.frontmatter.get("template").expect("no template");
	let template_name = Utf8PathBuf::from(format!("{template_stem}.html"));
	let template_path = settings.template_dir.join(template_name);

	let filename = path.file_name().expect("template has no filename");

	let mut template = Document::from_file(
		template_path,
		Options::default().include_path(bempline::options::IncludeMethod::Path(
			settings.template_dir.as_std_path().to_owned(),
		)),
	)
	.unwrap();

	template.set(
		"title",
		templated.frontmatter.get("title").unwrap_or(filename),
	);

	let style_pattern = template.get_pattern("styles").unwrap();
	for style in templated.frontmatter.get_many("style") {
		let mut pat = style_pattern.clone();
		pat.set("style", style);
		template.set_pattern("styles", pat);
	}

	template.set("main", templated.content);

	Ok(Response::builder()
		.header(header::CONTENT_TYPE, "text/html")
		.body(Body::from(template.compile()))
		.unwrap())
}