use std::str::FromStr;

use rocket::{
	http::{CookieJar, Status},
	outcome::try_outcome,
	request::{FromRequest, Outcome},
	response::Redirect,
	Request,
};
use rocket_dyn_templates::{context, Template};
use uuid::Uuid;

use crate::db::{
	schema::{DollTagsDb, User},
	user,
};

pub fn login(cookies: &CookieJar<'_>, user_id: &Uuid) {
	cookies.add_private(("user_id", user_id.to_string()));
}

pub fn check_login(cookies: &CookieJar<'_>) -> Option<Uuid> {
	cookies
		.get_private("user_id")
		.and_then(|v| Uuid::from_str(&v.value()).ok())
}

pub fn logout(cookies: &CookieJar<'_>) {
	cookies.remove_private("user_id");
}

#[derive(Debug)]
pub struct SessionInternalFailure();

/// A struct that checks the presence of a session in cookie
///
/// This guard can be used to know the probable session status,
/// e.g. for rendering pages (to determine if a login button may be displayed).
///
/// In no way this guard should be trusted for an actual authentication check as
/// it does not verify the authenticity of the session against the user database.
/// For authentication, use the [`User`] guard instead.
///
/// The Uuid is the currently logged-in account's UUID.
#[derive(Debug)]
pub struct Session(pub Option<Uuid>);

#[rocket::async_trait]
impl<'a> FromRequest<'a> for Session {
	type Error = ();

	async fn from_request(req: &'a Request<'_>) -> Outcome<Self, Self::Error> {
		let cookies = req.cookies();

		Outcome::Success(Session(check_login(&cookies)))
	}
}

/// Guard that checks that the user has an active session cookie and,
/// using the UUID from this cookie checks in the DB if the user is enabled
#[rocket::async_trait]
impl<'a> FromRequest<'a> for User {
	type Error = SessionInternalFailure;

	async fn from_request(req: &'a Request<'_>) -> Outcome<Self, Self::Error> {
		let cookies = req.cookies();

		if let Some(id) = check_login(&cookies) {
			let db = DollTagsDb::from_request(req)
				.await
				.expect("User::from_request cannot get DB connection");

			match user::get_by_id(db, &id).await {
				Err(err) => {
					error!("User::from_request internal error: {:?}", err);
					Outcome::Error((Status::InternalServerError, SessionInternalFailure()))
				}
				Ok(Some(user)) => {
					if user.enabled {
						Outcome::Success(user)
					} else {
						Outcome::Forward(Status::Unauthorized)
					}
				}
				Ok(None) => Outcome::Forward(Status::Unauthorized),
			}
		} else {
			Outcome::Forward(Status::Unauthorized)
		}
	}
}

/// A specialization of User as a [`FromRequest`] guard to only trigger when the user is logged in,
/// checked as okay in DB, and is marked as an admin user.
#[derive(Debug)]
pub struct Admin(pub User);

#[rocket::async_trait]
impl<'a> FromRequest<'a> for Admin {
	type Error = SessionInternalFailure;

	async fn from_request(req: &'a Request<'_>) -> Outcome<Self, Self::Error> {
		let user = try_outcome!(req.guard::<User>().await);

		if user.is_admin {
			Outcome::Success(Admin(user))
		} else {
			Outcome::Forward(Status::Forbidden)
		}
	}
}

#[catch(401)]
pub fn unauthorized(req: &Request) -> Redirect {
	let next = req.uri().to_string();
	Redirect::to(uri!(crate::routes::form::accounts::show_login(Some(&next))))
}

#[catch(403)]
pub fn forbidden(_: &Request) -> Template {
	Template::render("error/forbidden", context! {})
}
