/*************************************************************************/
/*  smtp_client.cpp                                                      */
/*************************************************************************/
/*                         This file is part of:                         */
/*                   PANDEMONIUM ENGINE'S SMTP MODULE                    */
/*             https://github.com/Relintai/pandemonium_engine            */
/*************************************************************************/
/* Copyright (c) 2022-present Péter Magyar.                              */
/* Copyright (c) 2021-2024 Nicolò Santilio                               */
/*                                                                       */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the       */
/* "Software"), to deal in the Software without restriction, including   */
/* without limitation the rights to use, copy, modify, merge, publish,   */
/* distribute, sublicense, and/or sell copies of the Software, and to    */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions:                                             */
/*                                                                       */
/* The above copyright notice and this permission notice shall be        */
/* included in all copies or substantial portions of the Software.       */
/*                                                                       */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
/*************************************************************************/

#include "smtp_client.h"

#include "core/bind/core_bind.h"
#include "core/config/engine.h"
#include "core/io/stream_peer_ssl.h"
#include "core/io/stream_peer_tcp.h"
#include "core/log/logger.h"
#include "core/os/os.h"

#include "email.h"

String SMTPClient::get_client_id() const {
	return client_id;
}
void SMTPClient::set_client_id(const String &p_value) {
	client_id = p_value;
}

String SMTPClient::get_host() const {
	return host;
}
void SMTPClient::set_host(const String &p_value) {
	host = p_value;
}

int SMTPClient::get_port() const {
	return port;
}
void SMTPClient::set_port(const int p_value) {
	port = p_value;
}

SMTPClient::TLSMethod SMTPClient::get_tls_method() const {
	return tls_method;
}
void SMTPClient::set_tls_method(const TLSMethod p_value) {
	tls_method = p_value;
}

String SMTPClient::get_server_auth_username() const {
	return server_auth_username;
}
void SMTPClient::set_server_auth_username(const String &p_value) {
	server_auth_username = p_value;
}

String SMTPClient::get_server_auth_password() const {
	return server_auth_password;
}
void SMTPClient::set_server_auth_password(const String &p_value) {
	server_auth_password = p_value;
}

SMTPClient::ServerAuthMethod SMTPClient::get_server_auth_method() const {
	return server_auth_method;
}
void SMTPClient::set_server_auth_method(const ServerAuthMethod p_value) {
	server_auth_method = p_value;
}

String SMTPClient::get_email_default_sender_email() const {
	return email_default_sender_email;
}
void SMTPClient::set_email_default_sender_email(const String &p_value) {
	email_default_sender_email = p_value;
}

String SMTPClient::get_email_default_sender_name() const {
	return email_default_sender_name;
}
void SMTPClient::set_email_default_sender_name(const String &p_value) {
	email_default_sender_name = p_value;
}

bool SMTPClient::get_use_threads() const {
	return _use_threads;
}
void SMTPClient::set_use_threads(const bool p_value) {
	if (_use_threads == p_value) {
		return;
	}

	if (Engine::get_singleton()->is_editor_hint()) {
		_use_threads = p_value;
		return;
	}

	if (is_inside_tree()) {
		if (should_use_threading()) {
			//currently using threads, will no longer be the case

			if (_worker_thread) {
				_worker_thread_running = false;
				//slow quit = let the current mail finish processing
				_worker_thread_fast_quit = false;

				_worker_semaphore.post();
				_worker_thread->wait_to_finish();
				memdelete(_worker_thread);
				_worker_thread = NULL;
			}

		} else {
			//currently not using threads, we want to

			if (should_use_threading()) {
				_worker_thread_running = true;
				_worker_thread = memnew(Thread);
				_worker_thread->start(_worker_thread_func, this);
			}
		}
	}

	_use_threads = p_value;
}

int SMTPClient::get_thread_sleep_usec() const {
	return thread_sleep_usec;
}
void SMTPClient::set_thread_sleep_usec(const int p_value) {
	thread_sleep_usec = p_value;
}

void SMTPClient::send_email(const Ref<EMail> &p_email) {
	ERR_FAIL_COND(Engine::get_singleton()->is_editor_hint());
	ERR_FAIL_COND(!is_inside_tree());
	ERR_FAIL_COND(!p_email.is_valid());

	if (should_use_threading()) {
		_mail_queue_mutex.lock();
		_mail_queue.push_back(p_email);
		_mail_queue_mutex.unlock();

		_worker_semaphore.post();
	} else {
		if (_current_session_email.is_valid()) {
			_mail_queue.push_back(p_email);
			return;
		}

		_send_email(p_email);
	}
}

bool SMTPClient::should_use_threading() const {
#if !defined(NO_THREADS)
	return _use_threads;
#else
	return false;
#endif
}

void SMTPClient::_send_email(const Ref<EMail> &p_email) {
	_current_session_email = p_email;

	if (!_current_session_email.is_valid()) {
		return;
	}

	IP_Address ip;

	if (host.is_valid_ip_address()) {
		ip = host;
	} else {
		ip = IP::get_singleton()->resolve_hostname(host);
	}

	Error err = _tcp_client->connect_to_host(ip, port);
	if (err != OK) {
		PLOG_ERR("Could not connect! " + itos(err));

		Dictionary error_body;

		error_body["message"] = "Error connecting to host.";
		error_body["code"] = err;

		emit_signal("error", error_body);

		Dictionary result;
		result["success"] = false;
		result["error"] = error_body;

		emit_signal("result", result);

		if (!should_use_threading()) {
			_no_thread_next_email();
		}
	}

	_current_session_status = SESSION_STATUS_HELO;

	if (!should_use_threading()) {
		set_process(true);
	}
}

Error SMTPClient::poll_client() {
	if (_current_tls_started || _current_tls_established) {
		_tls_client->poll();
		return OK;
	} else {
		return OK;
	}
}

bool SMTPClient::client_get_status() {
	if (_current_tls_started) {
		return _tls_client->get_status() == StreamPeerSSL::STATUS_CONNECTED;
	}

	return _tcp_client->get_status() == StreamPeerTCP::STATUS_CONNECTED;
}

int SMTPClient::client_get_available_bytes() {
	if (_current_tls_started) {
		return _tls_client->get_available_bytes();
	}

	return _tcp_client->get_available_bytes();
}

String SMTPClient::client_get_string(int bytes) {
	if (_current_tls_started) {
		return _tls_client->get_string(bytes);
	}

	return _tcp_client->get_string(bytes);
}

bool SMTPClient::start_auth() {
	if (server_auth_method == SERVER_AUTH_PLAIN) {
		_current_session_status = SESSION_STATUS_AUTHENTICATED;
		return true;
	}

	if (!write_command("AUTH LOGIN")) {
		return false;
	}

	_current_session_status = SESSION_STATUS_AUTH_LOGIN;
	return true;
}
bool SMTPClient::start_hello() {
	//_current_session_status = SESSION_STATUS_HELO
	if (!write_command("HELO " + client_id)) {
		return false;
	}

	_current_session_status = SESSION_STATUS_HELO_ACK;
	return true;
}

Error SMTPClient::client_put_data(const String &data) {
	Vector<uint8_t> buffer = data.to_utf8_buffer();

	if (_current_tls_established) {
		return _tls_client->put_data(buffer.ptr(), buffer.size());
	}

	return _tcp_client->put_data(buffer.ptr(), buffer.size());
}

bool SMTPClient::write_command(const String &command) {
	//PLOG_ERR("COMMAND: " + command + "\n");
	Error err = client_put_data(command + "\n");

	if (err != OK) {
		_current_session_status = SESSION_STATUS_COMMAND_NOT_SENT;

		Dictionary error_body;
		error_body["message"] = "Session error on command: " + command;
		error_body["code"] = err;

		emit_signal("error", error_body);

		Dictionary result;
		result["success"] = false;
		result["error"] = error_body;

		emit_signal("result", result);
	}

	return (err == OK);
}

Error SMTPClient::write_data(const String &data) {
	return client_put_data(data + "\r\n.\r\n");
}

void SMTPClient::close_connection() {
	_current_session_status = SESSION_STATUS_NONE;
	_tls_client->disconnect_from_stream();
	_tcp_client->disconnect_from_host();
	_current_session_email.unref();
	_current_to_index = 0;
	_current_tls_started = false;
	_current_tls_established = false;

	if (!should_use_threading()) {
		_no_thread_next_email();
	}
}

String SMTPClient::encode_username() {
	return _Marshalls::get_singleton()->utf8_to_base64(server_auth_username);
}
String SMTPClient::encode_password() {
	return _Marshalls::get_singleton()->utf8_to_base64(server_auth_password);
}

void SMTPClient::_process_email() {
	if (_current_session_status == SESSION_STATUS_SERVER_ERROR) {
		close_connection();
	}

	if (poll_client() == OK) {
		bool connected = client_get_status();

		if (connected) {
			// We need to wait until the connection is properly set up before starting ssl
			if (tls_method == TLS_METHOD_SMTPS && !_current_tls_started) {
				Error err = _tls_client->connect_to_stream(_tcp_client, false, host);

				if (err != OK) {
					_current_session_status = SESSION_STATUS_SERVER_ERROR;

					Dictionary error_body;

					error_body["message"] = "Error connecting to TLS Stream.";
					error_body["code"] = err;

					emit_signal("error", error_body);

					Dictionary result;
					result["success"] = false;
					result["error"] = error_body;

					emit_signal("result", result);

					if (!should_use_threading()) {
						_no_thread_next_email();
					}

					return;
				}

				_current_tls_started = true;
				_current_tls_established = true;
			}

			int bytes = client_get_available_bytes();

			if (bytes > 0) {
				String msg = client_get_string(bytes);
				//PLOG_ERR("RECEIVED: " + msg)
				String code = msg.left(3);

				if (code == "220") {
					if (_current_session_status == SESSION_STATUS_HELO) {
						start_hello();
					} else if (_current_session_status == SESSION_STATUS_STARTTLS) {
						Error err = _tls_client->connect_to_stream(_tcp_client, false, host);

						if (err != OK) {
							_current_session_status = SESSION_STATUS_SERVER_ERROR;

							Dictionary error_body;
							error_body["message"] = "Error connecting to TLS Stream.";
							error_body["code"] = err;

							emit_signal("error", error_body);

							Dictionary result;
							result["success"] = false;
							result["error"] = error_body;

							emit_signal("result", result);

							return;
						}

						_current_tls_started = true;
						_current_tls_established = true;

						// We need to do HELO + EHLO again
						_current_session_status = SESSION_STATUS_HELO;
						start_hello();
					}

				} else if (code == "250") {
					if (_current_session_status == SESSION_STATUS_HELO_ACK) {
						if (!write_command("EHLO " + client_id)) {
							return;
						}

						_current_session_status = SESSION_STATUS_EHLO_ACK;

					} else if (_current_session_status == SESSION_STATUS_EHLO_ACK) {
						if (tls_method == TLS_METHOD_STARTTLS) {
							if (_current_tls_started) {
								// second round of HELO + EHLO done
								if (!start_auth()) {
									return;
								}

							} else {
								if (!write_command("STARTTLS")) {
									return;
								}

								_current_session_status = SESSION_STATUS_STARTTLS;
							}
						} else {
							if (!start_auth()) {
								return;
							}
						}

					} else if (_current_session_status == SESSION_STATUS_MAIL_FROM) {
						if (_current_to_index < _current_session_email->get_recipient_count()) {
							if (!write_command("RCPT TO: <" + _current_session_email->get_recipient_address(_current_to_index) + ">")) {
								return;
							}

							_current_to_index += 1;
						}

						if (_current_cc_index < _current_session_email->get_cc_count()) {
							if (!write_command("RCPT TO: <" + _current_session_email->get_cc_address(_current_cc_index) + ">")) {
								return;
							}

							_current_cc_index += 1;
						}

						_current_session_status = SESSION_STATUS_RCPT_TO;

					} else if (_current_session_status == SESSION_STATUS_RCPT_TO) {
						if (_current_to_index < _current_session_email->get_recipient_count()) {
							_current_session_status = SESSION_STATUS_MAIL_FROM;
							return;
						}

						if (_current_cc_index < _current_session_email->get_cc_count()) {
							_current_session_status = SESSION_STATUS_MAIL_FROM;
							return;
						}

						if (!write_command("DATA")) {
							return;
						}

						_current_session_status = SESSION_STATUS_DATA;

					} else if (_current_session_status == SESSION_STATUS_DATA_ACK) {
						if (!write_command("QUIT")) {
							return;
						}

						_current_session_status = SESSION_STATUS_QUIT;
					}

				} else if (code == "221") {
					if (_current_session_status == SESSION_STATUS_QUIT) {
						close_connection();
						emit_signal("email_sent");

						Dictionary result;
						result["success"] = true;

						emit_signal("result", result);
					}
				} else if (code == "235") {
					// Authentication Succeeded

					if (_current_session_status == SESSION_STATUS_PASSWORD) {
						_current_session_status = SESSION_STATUS_AUTHENTICATED;
					}

				} else if (code == "334") {
					if (_current_session_status == SESSION_STATUS_AUTH_LOGIN) {
						if (msg.begins_with("334 VXNlcm5hbWU6")) {
							if (!write_command(encode_username())) {
								return;
							}

							_current_session_status = SESSION_STATUS_USERNAME;
						}
					} else if (_current_session_status == SESSION_STATUS_USERNAME) {
						if (msg.begins_with("334 UGFzc3dvcmQ6")) {
							if (!write_command(encode_password())) {
								return;
							}

							_current_session_status = SESSION_STATUS_PASSWORD;
						}
					}
				} else if (code == "354") {
					if (_current_session_status == SESSION_STATUS_DATA) {
						if (!(write_data(_current_session_email->get_email_data_string(email_default_sender_name, email_default_sender_email)) == OK)) {
							_current_session_status = SESSION_STATUS_SERVER_ERROR;
							return;
						}

						_current_session_status = SESSION_STATUS_DATA_ACK;
					}
				} else {
					PLOG_ERR(msg);
				}
			}
		}

		if (_current_session_email.is_valid() && (_current_session_status == SESSION_STATUS_AUTHENTICATED)) {
			_current_session_status = SESSION_STATUS_MAIL_FROM;

			String sender_address = _current_session_email->get_sender_address();

			String fn;

			if (sender_address.size() > 0) {
				fn = "<" + sender_address + ">";
			} else {
				fn = "<" + email_default_sender_email + ">";
			}

			if (!write_command("MAIL FROM: " + fn)) {
				return;
			}
		} else {
			return;
		}
	} else {
		PLOG_ERR("Couldn't poll!")
	}
}

void SMTPClient::_no_thread_next_email() {
	if (should_use_threading()) {
		return;
	}

	Ref<EMail> mail;

	int size = _mail_queue.size();
	if (size > 0) {
		mail = _mail_queue[0];
		_mail_queue.remove(0);
	} else {
		set_process(false);
		return;
	}

	if (mail.is_valid()) {
		_send_email(mail);
	}
}

void SMTPClient::_worker_thread_func(void *user_data) {
	SMTPClient *self = (SMTPClient *)user_data;

	while (self->_worker_thread_running) {
		Ref<EMail> _mail;

		self->_mail_queue_mutex.lock();

		int size = self->_mail_queue.size();
		if (size > 0) {
			_mail = self->_mail_queue[0];
			self->_mail_queue.remove(0);
		}

		self->_mail_queue_mutex.unlock();

		if (_mail.is_valid()) {
			self->_send_email(_mail);
		}

		while (self->_current_session_email.is_valid()) {
			OS::get_singleton()->delay_usec(self->thread_sleep_usec);

			// Early return if we want to quit
			if (!self->_worker_thread_running && self->_worker_thread_fast_quit) {
				self->close_connection();
				return;
			}

			self->_process_email();
		}

		if (!self->_worker_thread_running) {
			return;
		}

		if (self->_mail_queue.size() == 0) {
			self->_worker_semaphore.wait();
		}
	}
}

SMTPClient::SMTPClient() {
	client_id = "smtp.pandemoniumengine.org";
	port = 465;

	tls_method = TLS_METHOD_SMTPS;

	server_auth_method = SERVER_AUTH_LOGIN;

	_use_threads = true;
	_worker_thread = NULL;
	// 10 msec
	thread_sleep_usec = 10000;

	// Networking
	_tcp_client.instance();
	_tls_client = Ref<StreamPeerSSL>(StreamPeerSSL::create());

	//SessionStatus
	_current_session_status = SESSION_STATUS_NONE;
	_current_to_index = 0;
	_current_cc_index = 0;

	_current_tls_started = false;
	_current_tls_established = false;

	// Threading
	_worker_thread_running = false;
	_worker_thread_fast_quit = false;

	set_process(false);
}
SMTPClient::~SMTPClient() {
}

void SMTPClient::_notification(int p_what) {
	switch (p_what) {
		case NOTIFICATION_PROCESS: {
			if (Engine::get_singleton()->is_editor_hint()) {
				set_process(false);
				return;
			}

			if (should_use_threading()) {
				set_process(false);
				return;
			}

			_process_email();
		} break;
		case NOTIFICATION_ENTER_TREE: {
			if (Engine::get_singleton()->is_editor_hint()) {
				set_process(false);
				return;
			}

			set_process(false);

			_worker_thread_fast_quit = false;

			if (should_use_threading()) {
				_worker_thread_running = true;
				_worker_thread = memnew(Thread);
				_worker_thread->start(_worker_thread_func, this);
			}

		} break;
		case NOTIFICATION_EXIT_TREE: {
			if (Engine::get_singleton()->is_editor_hint()) {
				set_process(false);
				return;
			}

			if (_worker_thread) {
				_worker_thread_running = false;
				_worker_thread_fast_quit = true;
				_worker_semaphore.post();
				_worker_thread->wait_to_finish();
				memdelete(_worker_thread);
				_worker_thread = NULL;
			}

		} break;
	}
}

void SMTPClient::_bind_methods() {
	ADD_SIGNAL(MethodInfo("error", PropertyInfo(Variant::DICTIONARY, "error")));
	ADD_SIGNAL(MethodInfo("email_sent"));
	ADD_SIGNAL(MethodInfo("result", PropertyInfo(Variant::DICTIONARY, "content")));

	ClassDB::bind_method(D_METHOD("get_client_id"), &SMTPClient::get_client_id);
	ClassDB::bind_method(D_METHOD("set_client_id", "val"), &SMTPClient::set_client_id);
	ADD_PROPERTY(PropertyInfo(Variant::STRING, "client_id"), "set_client_id", "get_client_id");

	ClassDB::bind_method(D_METHOD("get_host"), &SMTPClient::get_host);
	ClassDB::bind_method(D_METHOD("set_host", "val"), &SMTPClient::set_host);
	ADD_PROPERTY(PropertyInfo(Variant::STRING, "host"), "set_host", "get_host");

	ClassDB::bind_method(D_METHOD("get_port"), &SMTPClient::get_port);
	ClassDB::bind_method(D_METHOD("set_port", "val"), &SMTPClient::set_port);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "port"), "set_port", "get_port");

	ClassDB::bind_method(D_METHOD("get_tls_method"), &SMTPClient::get_tls_method);
	ClassDB::bind_method(D_METHOD("set_tls_method", "val"), &SMTPClient::set_tls_method);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "tls_method", PROPERTY_HINT_ENUM, "NONE,STARTTLS,SMTPS"), "set_tls_method", "get_tls_method");

	ClassDB::bind_method(D_METHOD("get_server_auth_username"), &SMTPClient::get_server_auth_username);
	ClassDB::bind_method(D_METHOD("set_server_auth_username", "val"), &SMTPClient::set_server_auth_username);
	ADD_PROPERTY(PropertyInfo(Variant::STRING, "server_auth_username"), "set_server_auth_username", "get_server_auth_username");

	ClassDB::bind_method(D_METHOD("get_server_auth_password"), &SMTPClient::get_server_auth_password);
	ClassDB::bind_method(D_METHOD("set_server_auth_password", "val"), &SMTPClient::set_server_auth_password);
	ADD_PROPERTY(PropertyInfo(Variant::STRING, "server_auth_password"), "set_server_auth_password", "get_server_auth_password");

	ClassDB::bind_method(D_METHOD("get_server_auth_method"), &SMTPClient::get_server_auth_method);
	ClassDB::bind_method(D_METHOD("set_server_auth_method", "val"), &SMTPClient::set_server_auth_method);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "server_auth_method", PROPERTY_HINT_ENUM, "Plain,Login"), "set_server_auth_method", "get_server_auth_method");

	ClassDB::bind_method(D_METHOD("get_email_default_sender_email"), &SMTPClient::get_email_default_sender_email);
	ClassDB::bind_method(D_METHOD("set_email_default_sender_email", "val"), &SMTPClient::set_email_default_sender_email);
	ADD_PROPERTY(PropertyInfo(Variant::STRING, "email_default_sender_email"), "set_email_default_sender_email", "get_email_default_sender_email");

	ClassDB::bind_method(D_METHOD("get_email_default_sender_name"), &SMTPClient::get_email_default_sender_name);
	ClassDB::bind_method(D_METHOD("set_email_default_sender_name", "val"), &SMTPClient::set_email_default_sender_name);
	ADD_PROPERTY(PropertyInfo(Variant::STRING, "email_default_sender_name"), "set_email_default_sender_name", "get_email_default_sender_name");

	ClassDB::bind_method(D_METHOD("get_use_threads"), &SMTPClient::get_use_threads);
	ClassDB::bind_method(D_METHOD("set_use_threads", "val"), &SMTPClient::set_use_threads);
	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_threads"), "set_use_threads", "get_use_threads");

	ClassDB::bind_method(D_METHOD("get_thread_sleep_usec"), &SMTPClient::get_thread_sleep_usec);
	ClassDB::bind_method(D_METHOD("set_thread_sleep_usec", "val"), &SMTPClient::set_thread_sleep_usec);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "thread_sleep_usec"), "set_thread_sleep_usec", "get_thread_sleep_usec");

	ClassDB::bind_method(D_METHOD("send_email", "email"), &SMTPClient::send_email);

	BIND_ENUM_CONSTANT(TLS_METHOD_NONE);
	BIND_ENUM_CONSTANT(TLS_METHOD_STARTTLS);
	BIND_ENUM_CONSTANT(TLS_METHOD_SMTPS);

	BIND_ENUM_CONSTANT(SERVER_AUTH_PLAIN);
	BIND_ENUM_CONSTANT(SERVER_AUTH_LOGIN);
}