#include "connection.hpp"
#include <utility>

#include <qcontainerfwd.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlocalsocket.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtenvironmentvariables.h>
#include <qtmetamacros.h>
#include <qtypes.h>

#include "../../core/generation.hpp"
#include "../../core/logcat.hpp"

namespace {
QS_LOGGING_CATEGORY(logGreetd, "quickshell.service.greetd");
}

QString GreetdState::toString(GreetdState::Enum value) {
	switch (value) {
	case GreetdState::Inactive: return "Inactive";
	case GreetdState::Authenticating: return "Authenticating";
	case GreetdState::ReadyToLaunch: return "Ready to Launch";
	case GreetdState::Launching: return "Launching";
	case GreetdState::Launched: return "Launched";
	default: return "Invalid";
	}
}

GreetdConnection::GreetdConnection() {
	auto socket = qEnvironmentVariable("GREETD_SOCK");

	if (socket.isEmpty()) {
		this->mAvailable = false;
		return;
	}

	this->mAvailable = true;

	// clang-format off
	QObject::connect(&this->socket, &QLocalSocket::connected, this, &GreetdConnection::onSocketConnected);
	QObject::connect(&this->socket, &QLocalSocket::readyRead, this, &GreetdConnection::onSocketReady);
	// clang-format on

	this->socket.connectToServer(socket, QLocalSocket::ReadWrite);
}

void GreetdConnection::createSession(QString user) {
	if (!this->mAvailable) {
		qCCritical(logGreetd) << "Greetd is not available.";
		return;
	}

	if (user != this->mUser) {
		this->mUser = std::move(user);
		emit this->userChanged();
	}

	this->setActive(true);
}

void GreetdConnection::cancelSession() { this->setActive(false); }

void GreetdConnection::respond(QString response) {
	if (!this->mResponseRequired) {
		qCCritical(logGreetd) << "Cannot respond to greetd as a response is not currently required.";
		return;
	}

	this->sendRequest({
	    {"type", "post_auth_message_response"},
	    {"response", response},
	});

	this->mResponseRequired = false;
}

void GreetdConnection::launch(
    const QList<QString>& command,
    const QList<QString>& environment,
    bool exit
) {
	if (this->mState != GreetdState::ReadyToLaunch) {
		qCCritical(logGreetd) << "Cannot call launch() as state is not currently ReadyToLaunch.";
		return;
	}

	this->mState = GreetdState::Launching;
	this->mExitAfterLaunch = exit;

	this->sendRequest({
	    {"type", "start_session"},
	    {"cmd", QJsonArray::fromStringList(command)},
	    {"env", QJsonArray::fromStringList(environment)},
	});
}

bool GreetdConnection::isAvailable() const { return this->mAvailable; }
GreetdState::Enum GreetdConnection::state() const { return this->mState; }

void GreetdConnection::setActive(bool active) {
	if (this->socket.state() == QLocalSocket::ConnectedState) {
		this->mTargetActive = active;
		if (active == (this->mState != GreetdState::Inactive)) return;

		if (active) {
			if (this->mUser.isEmpty()) {
				qCCritical(logGreetd) << "Cannot activate greetd with unset user.";
				this->setActive(false);
				return;
			}

			this->sendRequest({
			    {"type", "create_session"},
			    {"username", this->mUser},
			});

			this->mState = GreetdState::Authenticating;
			emit this->stateChanged();
		} else {
			this->sendRequest({
			    {"type", "cancel_session"},
			});

			this->setInactive();
		}
	} else {
		if (active != this->mTargetActive) {
			this->mTargetActive = active;
		}
	}
}

void GreetdConnection::setInactive() {
	this->mTargetActive = false;
	this->mResponseRequired = false;
	this->mState = GreetdState::Inactive;
	emit this->stateChanged();
}

QString GreetdConnection::user() const { return this->mUser; }

void GreetdConnection::onSocketConnected() {
	this->reader.setDevice(&this->socket);
	qCDebug(logGreetd) << "Connected to greetd socket.";

	if (this->mTargetActive) {
		this->setActive(true);
	}
}

void GreetdConnection::onSocketError(QLocalSocket::LocalSocketError error) {
	qCCritical(logGreetd) << "Greetd socket encountered an error and cannot continue:" << error;

	this->mAvailable = false;
	this->setActive(false);
}

void GreetdConnection::onSocketReady() {
	while (true) {
		this->reader.startTransaction();
		auto length = this->reader.readI32();
		auto text = this->reader.readBytes(length);
		if (!this->reader.commitTransaction()) return;

		auto json = QJsonDocument::fromJson(text).object();
		auto type = json.value("type").toString();

		qCDebug(logGreetd).noquote() << "Received greetd response:" << text;

		if (type == "success") {
			switch (this->mState) {
			case GreetdState::Authenticating:
				qCDebug(logGreetd) << "Authentication complete.";
				this->mState = GreetdState::ReadyToLaunch;
				emit this->stateChanged();
				emit this->readyToLaunch();
				break;
			case GreetdState::Launching:
				qCDebug(logGreetd) << "Target session set successfully.";
				this->mState = GreetdState::Launched;
				emit this->stateChanged();
				emit this->launched();

				if (this->mExitAfterLaunch) {
					qCDebug(logGreetd) << "Quitting.";
					EngineGeneration::currentGeneration()->quit();
				}

				break;
			default: goto unexpected;
			}
		} else if (type == "error") {
			auto errorType = json.value("error_type").toString();
			auto desc = json.value("description").toString();

			// Special case this error in case a session was already running.
			// This cancels and restarts the session.
			if (errorType == "error" && desc == "a session is already being configured") {
				qCDebug(
				    logGreetd
				) << "A session was already in progress, cancelling it and starting a new one.";
				this->setActive(false);
				this->setActive(true);
				return;
			}

			if (errorType == "auth_error") {
				emit this->authFailure(desc);
				this->setActive(false);
			} else if (errorType == "error") {
				qCWarning(logGreetd) << "Greetd error occurred" << desc;
				emit this->error(desc);
			} else goto unexpected;

			// errors terminate the session
			this->setInactive();
		} else if (type == "auth_message") {
			auto message = json.value("auth_message").toString();
			auto type = json.value("auth_message_type").toString();
			auto error = type == "error";
			auto responseRequired = type == "visible" || type == "secret";
			auto echoResponse = type != "secret";

			this->mResponseRequired = responseRequired;
			emit this->authMessage(message, error, responseRequired, echoResponse);

			if (!responseRequired) {
				this->sendRequest({{"type", "post_auth_message_response"}});
			}
		} else goto unexpected;

		continue;
	unexpected:
		qCCritical(logGreetd) << "Received unexpected greetd response" << text;
		this->setActive(false);
	}
}

void GreetdConnection::sendRequest(const QJsonObject& json) {
	auto text = QJsonDocument(json).toJson(QJsonDocument::Compact);
	auto length = static_cast<qint32>(text.length());

	if (logGreetd().isDebugEnabled()) {
		auto debugJson = json;

		if (json.value("type").toString() == "post_auth_message_response") {
			debugJson["response"] = "<CENSORED>";
		}

		qCDebug(logGreetd).noquote() << "Sending greetd request:"
		                             << QJsonDocument(debugJson).toJson(QJsonDocument::Compact);
	}

	this->socket.write(reinterpret_cast<char*>(&length), sizeof(qint32));

	this->socket.write(text);
	this->socket.flush();
}

GreetdConnection* GreetdConnection::instance() {
	static auto* instance = new GreetdConnection(); // NOLINT
	return instance;
}
