mod atom;
mod error;
mod fs;
mod ifc;
mod markup;
mod settings;
mod templated;
mod timeparse;
mod util;

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

use axum::{
	body::Body,
	extract::Path,
	http::{header, StatusCode},
	response::Response,
	routing::get,
	Extension, Router,
};
use bempline::{variables, Document, Options};
use camino::Utf8PathBuf;
use confindent::{Confindent, Node};
pub use error::RuntimeError;
use fs::Filesystem;
use settings::Settings;
use tokio_util::io::ReaderStream;
use tracing_subscriber::{fmt::time, prelude::*, EnvFilter};
use util::{Referer, RemoteIp, SessionId};

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

#[tokio::main]
async fn main() {
	match std::env::args().nth(1).as_deref() {
		Some("atomizer") => atom::main(),
		/* fallthrough*/
		Some("serve") => (),
		_ => (),
	}

	tracing_subscriber::registry()
		.with(tracing_subscriber::fmt::layer())
		.with(
			EnvFilter::try_from_default_env()
				.or_else(|_| EnvFilter::try_new("info"))
				.unwrap(),
		)
		.init();

	let conf = Confindent::from_file(std::env::args().nth(2).unwrap()).unwrap();
	let webroot: Utf8PathBuf = conf.child_parse("Webroot").unwrap();
	let templates = conf.child_value("Templates").unwrap();
	let hostname = conf.child_owned("Hostname").unwrap();

	let fs = Filesystem::new(&webroot);

	let settings = Settings {
		template_dir: Utf8PathBuf::from(webroot.join(templates))
			.canonicalize_utf8()
			.unwrap(),
		hostname,
	};

	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>,
	sid: SessionId,
	rfr: Option<Referer>,
) -> Response {
	handler(fse, se, sid, rfr, Path(String::from("/"))).await
}

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

async fn falible_handler(
	fs: Filesystem,
	settings: Settings,
	sid: SessionId,
	rfr: Option<Referer>,
	path: String,
) -> Result<Response, RuntimeError> {
	tracing::debug!("webpath = {path}");

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

	if !webpath.is_dir() && resolve.is_dirfile {
		return Ok(redirect(webpath.as_dir()));
	}

	match rfr {
		None => {
			tracing::info!("[{sid}] serving {webpath}");
		}
		Some(referer) => {
			tracing::info!("[{sid}] (refer {referer}) serving {webpath}");
		}
	}

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

	if ext != "html" {
		send_file(resolve.filepath).await
	} else {
		let content = Filesystem::read_to_string(&resolve.filepath).await?;

		let result = Templated::from_str(&content);
		tracing::trace!("full return from Templated::from_str");

		match result {
			Ok(templated) => {
				//tracing::trace!("sending template for {resolve}");

				std::io::stdout().write_all(b"meow meow meow!!").unwrap();
				std::io::stdout().flush().unwrap();

				send_template(templated, resolve, webpath, settings).await
			}
			Err(e) => {
				tracing::warn!("error sending template {e}");

				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();
	tracing::info!("redirect 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 stem = filepath.file_stem().unwrap_or_default();

	let mime = match ext {
		// Text
		"css" => "text/css",
		"html" => "text/html",
		"js" => "text/javascript",
		"txt" => "text/plain",
		"xml" if stem.ends_with("atom") => "application/atom+xml",
		"xml" => "application/xml",

		// 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 {
		tracing::debug!("large file, streaming to client");

		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,
	resolve: PathResolution,
	webpath: Webpath,
	settings: Settings,
) -> Result<Response, RuntimeError> {
	tracing::trace!("sending template");
	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 = resolve
		.filepath
		.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();

	let title = templated.frontmatter.get("title").unwrap_or(filename);

	template.set("title", title);

	tracing::trace!("doing opengraph stuff!");
	if let Some(og_description) = templated.frontmatter.get("description") {
		let og_title = title;
		let og_url = format!("https://{}{}", &settings.hostname, webpath);

		if let Some(art_relpath) = templated.frontmatter.get("art") {
			let serving_dir = Utf8PathBuf::from(webpath.first_dir());
			let art_path = serving_dir.join(art_relpath);

			let og_image_alt = match templated.frontmatter.get("art_alt") {
				Some(alt) => alt,
				None => {
					tracing::warn!("{} has art but no alt", resolve.filepath);
					""
				}
			};

			let og_image = format!("https://{}/{}", &settings.hostname, art_path);

			variables!(template, og_image, og_image_alt);
		}

		let og_site_name = &settings.hostname;

		variables!(template, og_title, og_url, og_description, og_site_name);
	}

	tracing::trace!("stylin'!");
	// styles the templated stuff wants
	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(pat);
	}

	tracing::trace!("poppin'!");
	// path to the file for navigation
	let mut path: Vec<&str> = webpath.webcanon.iter().collect();
	// we don't want the directory/filename itself
	path.pop();

	if let Some(path_pattern) = template.get_pattern("path") {
		let offset = match templated
			.frontmatter
			.get("path-offset")
			.map(|raw| raw.parse::<usize>())
		{
			Some(Ok(offset)) => offset,
			None => 0,
			Some(Err(_)) => {
				tracing::error!(
					"path-offset in template {} is not an integer",
					resolve.filepath
				);

				0
			}
		};

		for _ in 0..offset {
			path.pop();
		}

		let mut link = Utf8PathBuf::from("/");

		let mut pat = path_pattern.clone();
		pat.set("path_link", "/");
		pat.set("path_name", "home");
		template.set_pattern(pat);

		for part in path {
			link.push(part);

			let mut pat = path_pattern.clone();
			pat.set("path_link", &link);
			pat.set("path_name", part);
			template.set_pattern(pat);
		}
	}

	tracing::trace!("starting published block");
	'published: {
		if let Some(mut published_pattern) = template.get_pattern("published") {
			let publish_date_result = templated
				.frontmatter
				.get("published")
				.map(|ts| timeparse::parse(ts));

			match publish_date_result {
				None => break 'published,
				Some(Err(_e)) => {
					tracing::warn!("template {resolve} has malformed `published` frontmatter");
					break 'published;
				}
				Some(Ok(datetime)) => {
					let published_human = timeparse::format_long(&datetime);
					let published_machine = timeparse::iso8601(&datetime);

					variables!(published_pattern, published_human, published_machine);
					template.set_pattern(published_pattern);
				}
			}
		}
	}
	tracing::trace!("finished published block");

	// insert the page content itself
	let markedup = markup::process(&templated.content);
	template.set("main", markedup);

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