use crate::{config::*, db::*, helpers, queries::*, templates::*};

use log::error;
use std::sync::Arc;
use tera::Context;

pub async fn start_server(config: Config, dbs: Dbs, templates: Templates) {
	tide::log::start();

	let templates = Arc::new(templates);
	let config = Arc::new(config);

	let mut app = tide::new();
	app.at(&format!("{}t/:topic", config.root_url)).get({
		let config = config.clone();
		let templates = templates.clone();
		let dbs = dbs.clone();
		move |req: tide::Request<()>| {
			serve_comments(req, config.clone(), templates.clone(), dbs.clone())
		}
	});
	app.at(&format!("{}t/:topic", config.root_url)).post({
		let config = config.clone();
		let templates = templates.clone();
		let dbs = dbs.clone();
		move |req: tide::Request<()>| {
			handle_post_comments(req, config.clone(), templates.clone(), dbs.clone())
		}
	});
	app.at(&format!("{}admin", config.root_url)).get({
		let config = config.clone();
		let templates = templates.clone();
		move |req: tide::Request<()>| serve_admin_login(req, config.clone(), templates.clone())
	});
	app.at(&format!("{}admin", config.root_url)).post({
		let config = config.clone();
		let templates = templates.clone();
		let dbs = dbs.clone();
		move |req: tide::Request<()>| {
			handle_post_admin(req, config.clone(), templates.clone(), dbs.clone())
		}
	});
	app.listen(config.listen).await.unwrap();
}

async fn serve_comments<'a>(
	req: tide::Request<()>,
	config: Arc<Config>,
	templates: Arc<Templates>,
	dbs: Dbs,
) -> tide::Result<tide::Response> {
	let Ok(topic) = req.param("topic") else {
		return Err(tide::Error::from_str(404, "No topic"))
	};

	let admin = req.cookie("admin").map_or(false, |psw| {
		config.admin_passwords.contains(&String::from(psw.value()))
	});

	let topic_hash = TopicHash::from_topic(topic);

	let mut context = Context::new();
	context.insert("config", &config);
	context.insert("admin", &admin);

	if admin {
		if let Ok(query) = req.query::<ApproveQuery>() {
			if let Ok(comment_id) = CommentId::from_base64(&query.approve) {
				helpers::approve_comment(comment_id, &dbs)
					.map_err(|e| error!("Approving comment: {:?}", e))
					.ok();
			}
		}
		if let Ok(query) = req.query::<RemoveQuery>() {
			if let Ok(comment_id) = CommentId::from_base64(&query.remove) {
				helpers::remove_pending_comment(comment_id, &dbs)
					.map_err(|e| error!("Removing comment: {:?}", e))
					.ok();
			}
		}

		context.insert(
			"comments_pending",
			&helpers::iter_pending_comments_by_topic(topic_hash.clone(), &dbs)
				.map(|(comment_id, comment)| CommentWithId {
					author: comment.author,
					id: comment_id.to_base64(),
					needs_approval: true,
					post_time: comment.post_time,
					text: comment.text,
				})
				.collect::<Vec<CommentWithId>>(),
		);
	}

	context.insert(
		"comments",
		&helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
			.map(|(comment_id, comment)| CommentWithId {
				author: comment.author,
				id: comment_id.to_base64(),
				needs_approval: false,
				post_time: comment.post_time,
				text: comment.text,
			})
			.collect::<Vec<CommentWithId>>(),
	);

	Ok(tide::Response::builder(200)
		.content_type(tide::http::mime::HTML)
		.body(templates.tera.render("comments.html", &context)?)
		.build())
}

async fn serve_admin<'a>(
	_req: tide::Request<()>,
	config: Arc<Config>,
	templates: Arc<Templates>,
	dbs: Dbs,
) -> tide::Result<tide::Response> {
	let mut context = Context::new();
	context.insert("config", &config);
	context.insert("admin", &true);

	context.insert(
		"comments",
		&dbs.comment_pending
			.iter()
			.filter_map(|entry| {
				let ((_topic_hash, _time, comment_id), ()) = dbg!(entry
					.map_err(|e| error!("Reading comment_pending: {:?}", e))
					.ok()?);
				let comment = dbs
					.comment
					.get(&comment_id)
					.map_err(|e| error!("Reading comment: {:?}", e))
					.ok()?
					.or_else(|| {
						error!("Comment not found");
						None
					})?;
				Some(CommentWithId {
					author: comment.author,
					id: comment_id.to_base64(),
					needs_approval: true,
					post_time: comment.post_time,
					text: comment.text,
				})
			})
			.collect::<Vec<CommentWithId>>(),
	);

	Ok(tide::Response::builder(200)
		.content_type(tide::http::mime::HTML)
		.body(templates.tera.render("comments.html", &context)?)
		.build())
}

async fn serve_admin_login(
	_req: tide::Request<()>,
	config: Arc<Config>,
	templates: Arc<Templates>,
) -> tide::Result<tide::Response> {
	let mut context = Context::new();
	context.insert("config", &config);

	Ok(tide::Response::builder(200)
		.content_type(tide::http::mime::HTML)
		.body(templates.tera.render("admin_login.html", &context)?)
		.build())
}

async fn handle_post_comments(
	mut req: tide::Request<()>,
	config: Arc<Config>,
	templates: Arc<Templates>,
	dbs: Dbs,
) -> tide::Result<tide::Response> {
	match req.body_form::<CommentQuery>().await? {
		CommentQuery::NewComment(query) => {
			let Ok(topic) = req.param("topic") else {
				return Err(tide::Error::from_str(404, "No topic"))
			};

			if query.author.len() > config.comment_author_max_len {
				return Err(tide::Error::from_str(400, "Too long"));
			}
			if query.email.len() > config.comment_email_max_len {
				return Err(tide::Error::from_str(400, "Too long"));
			}
			if query.text.len() > config.comment_text_max_len {
				return Err(tide::Error::from_str(400, "Too long"));
			}

			let topic_hash = TopicHash::from_topic(topic);

			let time = std::time::SystemTime::now()
				.duration_since(std::time::UNIX_EPOCH)
				.unwrap()
				.as_secs();

			let comment = Comment {
				topic_hash,
				author: query.author,
				email: if query.email.is_empty() {
					None
				} else {
					Some(query.email)
				},
				last_edit_time: None,
				post_time: time,
				text: query.text,
			};
			helpers::new_pending_comment(&comment, &dbs)
				.map_err(|e| error!("Adding pending comment: {:?}", e))
				.ok();
		}
		_ => todo!(),
	}
	serve_comments(req, config, templates, dbs).await
}

async fn handle_post_admin(
	mut req: tide::Request<()>,
	config: Arc<Config>,
	templates: Arc<Templates>,
	dbs: Dbs,
) -> tide::Result<tide::Response> {
	if let Some(psw) = req.cookie("admin") {
		if config.admin_passwords.contains(&String::from(psw.value())) {
			match req.body_form::<AdminQuery>().await? {
				_ => serve_admin(req, config, templates, dbs).await,
			}
		} else {
			serve_admin_login(req, config, templates).await
		}
	} else if let AdminQuery::Login(query) = req.body_form::<AdminQuery>().await? {
		if config.admin_passwords.contains(&query.psw) {
			serve_admin(req, config.clone(), templates, dbs)
				.await
				.map(|mut r| {
					let mut cookie = tide::http::Cookie::new("admin", query.psw);
					cookie.set_http_only(Some(true));
					cookie.set_path(config.root_url.clone());
					if let Some(domain) = &config.cookies_domain {
						cookie.set_domain(domain.clone());
					}
					if config.cookies_https_only {
						cookie.set_secure(Some(true));
					}
					r.insert_cookie(cookie);
					r
				})
		} else {
			serve_admin_login(req, config, templates).await
		}
	} else {
		serve_admin_login(req, config, templates).await
	}
}