use std::net::IpAddr;

use regex::Regex;
use rocket::{
	form::{self, Contextual, Form},
	http::CookieJar,
	response::Redirect,
	tokio::task,
};
use rocket_dyn_templates::{context, Template};

use crate::{
	auth::{pw, session},
	db::{
		schema::{DollTagsDb, User},
		user,
	},
	pages::CommonTemplateState,
	routes::error_handlers::{PageResponse, PageResult},
};

#[derive(Debug, FromForm)]
pub struct AuthForm<'a> {
	#[field(validate=len(2..=256))]
	pub username: &'a str,
	#[field(validate=len(8..))]
	pub password: &'a str,
}

// needed cuz the var. name changes how the query string is parsed
#[allow(unused_variables)]
#[get("/login?<next>")]
pub fn show_login(
	next: Option<&str>,
	maybe_loggedin: Option<User>,
	meta: CommonTemplateState,
) -> PageResult {
	if maybe_loggedin.is_some() {
		let next = String::from(next.unwrap_or("/"));
		Ok(Redirect::to(next).into())
	} else {
		Ok(Template::render("account/login", context! {meta}).into())
	}
}

#[post("/login?<next>", data = "<form>")]
pub async fn handle_login(
	db: DollTagsDb,
	next: Option<&str>,
	form: Form<Contextual<'_, AuthForm<'_>>>,
	cookies: &CookieJar<'_>,
	maybe_loggedin: Option<User>,
	meta: CommonTemplateState,
	client_ip: IpAddr,
) -> PageResult {
	if maybe_loggedin.is_some() {
		let next = String::from(next.unwrap_or("/"));
		return Ok(Redirect::to(next).into());
	}

	let values = match &form.value {
		None => {
			return Ok(Template::render("account/login", context! {failure: true, meta}).into())
		}
		Some(v) => v,
	};

	let miss = (|username: String| {
		move || {
			warn!("[audit|{}] login failure ({})", client_ip, username);
			PageResponse::Page(Template::render(
				"account/login",
				context! {failure: true, meta},
			))
		}
	})(String::from(values.username));

	warn!("[audit|{}] login attempt ({})", client_ip, &values.username);

	let user_in_db = user::get(db, &values.username).await?;
	let user = match user_in_db {
		None => return Ok(miss()),
		Some(v) => v,
	};

	let password = String::from(values.password);
	let right_password =
		task::spawn_blocking(move || pw::verify(&password, &user.password)).await??;

	if right_password && user.enabled {
		session::login(cookies, &user.id);
		warn!(
			"[audit|{}] [{}] login successful ({})",
			client_ip,
			user.id.to_string(),
			&values.username
		);
		let next_url = String::from(next.unwrap_or("/"));
		Ok(Redirect::to(next_url).into())
	} else {
		Ok(miss())
	}
}

#[get("/register")]
pub fn show_register(maybe_loggedin: Option<User>, meta: CommonTemplateState) -> PageResult {
	if maybe_loggedin.is_some() {
		return Ok(Redirect::to("/").into());
	}

	Ok(Template::render(
		"account/register",
		context! {
			previous: form::Context::default(),
			meta
		},
	)
	.into())
}

#[derive(Debug, FromForm)]
pub struct RegisterForm<'a> {
	#[field(validate=len(1..=256))]
	pub username: &'a str,
	#[field(validate=len(8..))]
	pub password: &'a str,
	#[field(validate=validate_email())]
	pub email: &'a str,

	// if it's set to true, a (bad) bot is submitting the form since it's a hidden field
	pub anti_bot: bool,
	// if it isn't set to true, we re-request it to be set
	pub inhuman: bool,
}

fn validate_email<'v>(email: &str) -> form::Result<'v, ()> {
	let email_re = Regex::new(r".+@.+").unwrap();
	if email.len() == 0 {
		return Ok(());
	}

	if email.len() >= 3 && email.len() <= 254 && email_re.is_match(email) {
		Ok(())
	} else {
		Err(form::Error::validation("this doesn't appear to be a valid email").into())
	}
}

#[post("/register", data = "<form>")]
pub async fn handle_register(
	db: DollTagsDb,
	form: Form<Contextual<'_, RegisterForm<'_>>>,
	cookies: &CookieJar<'_>,
	maybe_loggedin: Option<User>,
	meta: CommonTemplateState,
	client_ip: IpAddr,
) -> PageResult {
	if maybe_loggedin.is_some() {
		return Ok(Redirect::to("/").into());
	}

	let values = match form.value {
		Some(ref v) => v,
		None => {
			debug!(
				"account registration form invalid, context: {:?}",
				&form.context
			);

			return Ok(Template::render(
				"account/register",
				context! {
					meta,
					previous: &form.context,
				},
			)
			.into());
		}
	};

	let password = String::from(values.password);
	let hashed_password = task::spawn_blocking(move || pw::hash(&password)).await??;

	let account_id = user::create(
		db,
		values.username,
		&hashed_password,
		if values.email.len() != 0 {
			Some(values.email)
		} else {
			None
		},
	)
	.await?;

	warn!(
		"[audit|{}] [{}] account creation",
		client_ip,
		&account_id.to_string()
	);
	session::login(cookies, &account_id);

	Ok(Redirect::to(uri!("/")).into())
}

#[get("/logout")]
pub fn logout(cookies: &CookieJar<'_>) -> Redirect {
	session::logout(cookies);

	Redirect::to("/")
}
