use std::net::IpAddr;

use rocket::{
	form::{self, Contextual, Error, Form},
	http::CookieJar,
	response::Redirect,
	serde::{json::Json, Serialize},
	tokio::task,
};
use rocket_dyn_templates::{context, Template};
use sqlx::Acquire;

use crate::{
	auth::{pw, session},
	db::{
		doll,
		schema::{DollProfile, DollTagsDb, User},
		user,
	},
	pages::CommonTemplateState,
};

use super::error_handlers::{PageResult, RawResult};

#[get("/")]
pub async fn index(mut db: DollTagsDb, user: User, meta: CommonTemplateState) -> PageResult {
	let tags = doll::list(&mut *db, &user.id).await?;
	let archived_tags = doll::list_archived(&mut *db, &user.id).await?;

	Ok(Template::render(
		"account/index",
		context! {
			meta,
			user,
			tags,
			archived_tags,
		},
	)
	.into())
}

#[get("/settings")]
pub fn show_settings(user: User, meta: CommonTemplateState) -> Template {
	Template::render(
		"account/settings",
		context! {
			user,
			meta,
			prev_common: form::Context::default(),
			prev_password: form::Context::default(),
		},
	)
}

#[derive(FromForm)]
pub struct CommonSettings<'a> {
	#[field(validate=len(..=256))]
	pub username: &'a str,
	#[field(validate=len(..=254))]
	pub email: &'a str,
}

#[post("/settings/common", data = "<form>")]
pub async fn change_settings(
	mut db: DollTagsDb,
	user: User,
	meta: CommonTemplateState,
	mut form: Form<Contextual<'_, CommonSettings<'_>>>,
	client_ip: IpAddr,
) -> PageResult {
	let values = match form.value {
		Some(ref v) => v,
		None => {
			return Ok(Template::render(
				"account/settings",
				context! {
					user,
					meta,
					prev_common: &form.context,
					prev_password: form::Context::default(),
				},
			)
			.into())
		}
	};

	if user.username != values.username && (user::exists(&mut *db, values.username).await?) {
		form.context
			.push_error(Error::validation("username already taken").with_name("username"));
		return Ok(Template::render(
			"account/settings",
			context! {
				user,
				meta,
				prev_common: &form.context,
				prev_password: form::Context::default(),
			},
		)
		.into());
	}

	let new_username = if values.username.len() != 0 {
		values.username
	} else {
		&user.username
	};
	// TODO: this will not allow to unset the email
	let new_email = if values.email.len() != 0 {
		Some(values.email)
	} else {
		user.email.as_ref().map(|w| w.as_str())
	};

	user::update_info(&mut *db, &user.id, new_username, new_email).await?;

	warn!(
		"[audit|{}] [{}] changed username/email (from {})",
		client_ip,
		user.id.to_string(),
		user.username,
	);

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

#[derive(FromForm)]
pub struct PasswordSettings<'a> {
	#[field(validate=len(8..))]
	pub old_password: &'a str,
	#[field(validate=len(8..))]
	#[field(validate=validate_new_passwords(self.confirm_password))]
	pub password: &'a str,
	#[field(validate=len(8..))]
	pub confirm_password: &'a str,
}

pub fn validate_new_passwords<'v>(new: &str, confirm: &str) -> form::Result<'v, ()> {
	if new != confirm {
		Err(form::Error::validation("the two new passwords don't match"))?;
	}

	Ok(())
}

#[post("/settings/password", data = "<form>")]
pub async fn change_password(
	mut db: DollTagsDb,
	user: User,
	meta: CommonTemplateState,
	mut form: Form<Contextual<'_, PasswordSettings<'_>>>,
	client_ip: IpAddr,
) -> PageResult {
	let values = match form.value {
		Some(ref v) => v,
		None => {
			return Ok(Template::render(
				"account/settings",
				context! {
					user,
					meta,
					prev_common: form::Context::default(),
					prev_password: &form.context,
				},
			)
			.into())
		}
	};

	let password = String::from(values.old_password);
	let old_password = user.password.clone();
	let right_password =
		task::spawn_blocking(move || pw::verify(&password, &old_password)).await??;
	if !right_password {
		form.context
			.push_error(Error::validation("wrong password").with_name("old_password"));
		return Ok(Template::render(
			"account/settings",
			context! {
				user,
				meta,
				prev_common: form::Context::default(),
				prev_password: &form.context,
			},
		)
		.into());
	}

	let new_password = String::from(values.password);
	let new_password_hash = task::spawn_blocking(move || pw::hash(&new_password)).await??;
	user::update_password(&mut *db, &user.id, &new_password_hash).await?;

	warn!(
		"[audit|{}] [{}] changed password",
		client_ip,
		user.id.to_string()
	);

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

#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct DataDump {
	account: User,
	tags: Vec<DollProfile>,
	reserved_tags: Vec<i32>,
}

#[get("/data_dump")]
pub async fn export_data(
	mut db: DollTagsDb,
	user: User,
	client_ip: IpAddr,
) -> RawResult<Json<DataDump>> {
	let tags = doll::list(&mut *db, &user.id).await?;
	let reserved_tags = doll::list_archived(&mut *db, &user.id).await?;

	warn!(
		"[audit|{}] [{}] exported data",
		client_ip,
		user.id.to_string()
	);

	Ok(Json(DataDump {
		account: user,
		tags,
		reserved_tags,
	}))
}

#[get("/delete/<id>")]
pub async fn ask_delete(
	mut db: DollTagsDb,
	id: i32,
	_user: User,
	meta: CommonTemplateState,
) -> PageResult {
	let db_tag = doll::get(&mut *db, id, "", false).await?;

	if let Some(tag) = db_tag {
		Ok(Template::render(
			"tag/delete",
			context! {
				meta,
				tag,
			},
		)
		.into())
	} else {
		Ok(Redirect::to(uri!("/account", index)).into())
	}
}

#[get("/yes_delete_this/<id>")]
pub async fn confirm_delete(
	mut db: DollTagsDb,
	id: i32,
	user: User,
	client_ip: IpAddr,
) -> PageResult {
	let mut trx = db.begin().await?;
	doll::delete(&mut trx, id).await?;
	trx.commit().await?;

	warn!(
		"[audit|{}] [{}] deleted tag {:0>6}",
		client_ip,
		user.id.to_string(),
		id
	);

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

#[get("/terminate")]
pub fn ask_terminate_account(_user: User, meta: CommonTemplateState) -> Template {
	Template::render("account/terminate", context! {meta})
}

#[get("/termin@or")]
pub async fn confirm_terminate_account(
	mut db: DollTagsDb,
	user: User,
	cookies: &CookieJar<'_>,
	client_ip: IpAddr,
) -> PageResult {
	let mut trx = db.begin().await?;
	doll::delete_all_from_account(&mut trx, &user.id).await?;
	user::delete(&mut trx, &user.id).await?;
	session::logout(cookies);
	trx.commit().await?;

	warn!(
		"[audit|{}] [{}] deleted account",
		client_ip,
		user.id.to_string()
	);

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