use axum::{
	Extension, Router,
	extract::{Query, State},
	routing::{get, on, post},
};
use conduwuit_core::warn;
use conduwuit_service::{mailer::messages, threepid::session::ValidationSessions};
use lettre::{Address, message::Mailbox};
use ruma::{ClientSecret, OwnedClientSecret, OwnedSessionId};
use serde::{Deserialize, Serialize};

use crate::{
	WebError,
	extract::{Expect, PostForm},
	form,
	pages::{
		GET_POST, Result, TemplateContext,
		account::ThreepidQuery,
		components::{UserCard, form::Form},
	},
	response,
	session::{LoginTarget, User},
	template,
};

pub(crate) fn build() -> Router<crate::State> {
	Router::new()
		.route("/change/", on(GET_POST, route_change_email))
		.route("/change/validate", get(get_change_email_validate))
		.route("/change/delete", post(post_delete_email))
}

template! {
	struct ChangeEmail use "change_email.html.j2" {
		user_card: UserCard,
		email: Option<String>,
		form: Form<'static>,
		may_remove: bool
	}
}

form! {
	struct ChangeEmailForm {
		email: Address where {
			input_type: "email",
			label: "Email address"
		}

		submit: "Change email"
	}
}

template! {
	struct ChangeEmailValidate use "change_email_validate.html.j2" {
		user_card: UserCard,
		body: ChangeEmailValidateBody
	}
}

template! {
	struct DeleteEmail use "delete_email.html.j2" {
		user_card: UserCard
	}
}

#[derive(Debug)]
enum ChangeEmailValidateBody {
	ValidationPending {
		session_id: OwnedSessionId,
		client_secret: OwnedClientSecret,
		validation_error: bool,
	},
	Success,
}

async fn route_change_email(
	State(services): State<crate::State>,
	Extension(context): Extension<TemplateContext>,
	user: User,
	PostForm(form): PostForm<ChangeEmailForm>,
) -> Result {
	let user_id = user.expect_recent(LoginTarget::ChangeEmail)?;

	if !services.threepid.email_requirement().may_change() {
		return Err(WebError::Forbidden("You may not change your email address.".to_owned()));
	}

	let Some(form) = form else {
		return response!(ChangeEmail::new(
			context.clone(),
			UserCard::for_local_user(&services, user_id.clone()).await,
			services
				.threepid
				.get_email_for_localpart(user_id.localpart())
				.await
				.map(|address| address.to_string()),
			ChangeEmailForm::build(context),
			services.threepid.email_requirement().may_remove(),
		));
	};

	let client_secret = ClientSecret::new();

	let session_id = {
		let display_name = services.users.displayname(&user_id).await.ok();

		match services
			.threepid
			.send_validation_email(
				Mailbox::new(display_name, form.email.clone()),
				|verification_link| messages::ChangeEmail {
					server_name: services.globals.server_name().as_str(),
					user_id: Some(&user_id),
					verification_link,
				},
				&client_secret,
				0,
			)
			.await
		{
			| Ok(session_id) => session_id,
			| Err(err) => {
				// If we couldn't send an email, generate a random session ID to not give that
				// away
				warn!(
					"Failed to send email change message for {user_id} to {}: {err}",
					form.email
				);

				ValidationSessions::generate_session_id()
			},
		}
	};

	response!(ChangeEmailValidate::new(
		context,
		UserCard::for_local_user(&services, user_id).await,
		ChangeEmailValidateBody::ValidationPending {
			session_id,
			client_secret,
			validation_error: false
		}
	))
}

#[derive(Deserialize, Serialize)]
struct ChangeEmailQuery {
	#[serde(flatten)]
	threepid: ThreepidQuery,
}

async fn get_change_email_validate(
	State(services): State<crate::State>,
	Extension(context): Extension<TemplateContext>,
	Expect(Query(ChangeEmailQuery {
		threepid: ThreepidQuery { client_secret, session_id },
	})): Expect<Query<ChangeEmailQuery>>,
	user: User,
) -> Result {
	let user_id = user.expect(LoginTarget::ChangeEmail)?;
	let user_card = UserCard::for_local_user(&services, user_id.clone()).await;

	if !services.threepid.email_requirement().may_change() {
		return Err(WebError::Forbidden("You may not change your email address.".to_owned()));
	}

	let Ok(session) = services
		.threepid
		.get_valid_session(&session_id, &client_secret)
		.await
	else {
		return response!(ChangeEmailValidate::new(
			context,
			user_card,
			ChangeEmailValidateBody::ValidationPending {
				session_id,
				client_secret,
				validation_error: true
			}
		));
	};

	let new_email = session.consume();

	if let Err(err) = services
		.threepid
		.associate_localpart_email(user_id.localpart(), &new_email)
		.await
	{
		return response!(BadRequest(err.message()));
	}

	response!(ChangeEmailValidate::new(context, user_card, ChangeEmailValidateBody::Success))
}

async fn post_delete_email(
	State(services): State<crate::State>,
	Extension(context): Extension<TemplateContext>,
	user: User,
) -> Result {
	let user_id = user.expect(LoginTarget::ChangeEmail)?;
	let user_card = UserCard::for_local_user(&services, user_id.clone()).await;

	if !services.threepid.email_requirement().may_remove() {
		return Err(WebError::Forbidden("You may not remove your email address.".to_owned()));
	}

	let _ = services
		.threepid
		.disassociate_localpart_email(user_id.localpart())
		.await;

	response!(DeleteEmail::new(context, user_card))
}
