// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2018 Konrad Twardowski

#include "plugins.h"

#include "commandline.h"
#include "config.h"
#include "log.h"
#include "mainwindow.h"
#include "password.h"
#include "uwidgets.h"
#include "actions/extras.h"
#include "actions/lock.h"
#include "actions/test.h"
#include "actions/unlock.h"
#include "triggers/filemonitor.h"
#include "triggers/idlemonitor.h"
#include "triggers/processmonitor.h"

#include <QDebug>
#include <QMessageBox>

#ifndef Q_OS_WIN32
	#include <csignal> // for ::kill
#endif // !Q_OS_WIN32

// Strategy

// public:

bool Strategy::notAvailable(const QString &method) {
	if (! m_cond)
		return false; // skip

	#ifdef QT_DBUS_LIB
	if (! initDBusInterface())
		return false;

	QDBusReply<bool> reply = m_dbus->call(method);

	return ! reply.isValid() || ! reply.value();
	#else
	Q_UNUSED(method)

	return false;
	#endif // QT_DBUS_LIB
}

QString Strategy::toString() const {
	QString result = m_cond.name() + ": ";

	#ifdef QT_DBUS_LIB
	if (! m_dbusService.isNull()) {
		QString s = (m_dbus == nullptr) ? "D-Bus (lazy)" : "D-Bus";
		result += s + " " + m_dbusService + " | " + m_dbusPath + " | " + m_dbusInterfaceString + " | " + m_dbusMethod;
	}
	else
	#endif // QT_DBUS_LIB
	/*else*/ if (! m_dummy.isNull())
		result += m_dummy;
	else if (m_pid != 0)
		result += "Terminate " + QString::number(m_pid) + " PID";
	else
		result += "Run " + m_program + " " + m_args.join(' ');

	return result;
}

#ifdef QT_DBUS_LIB
bool Strategy::initDBusInterface() {
	if (m_dbusService.isNull()) {
		//qDebug() << "No D-Bus service defined:" << toString();

		return false;
	}

	if (m_dbus == nullptr) {
		m_dbus = new QDBusInterface(m_dbusService, m_dbusPath, m_dbusInterfaceString);
		qDebug() << "Initialized D-Bus interface:" << toString();
	}

	return true;
}
#endif // QT_DBUS_LIB

// StrategyManager

// public:

void StrategyManager::dummy(const Cond &cond, const QString &text) {
	auto strategy = std::make_shared<Strategy>(cond);
	strategy->m_dummy = text;

	m_list += strategy;
}

void StrategyManager::program(const Cond &cond, const QString &program, const QStringList &args) {
	auto strategy = std::make_shared<Strategy>(cond);
	strategy->m_args = args;
	strategy->m_program = program;

	m_list += strategy;
}

bool StrategyManager::run() {
	for (auto &i : m_list) {
		if (i->m_cond) { // cppcheck-suppress useStlAlgorithm
			if (! i->m_dummy.isNull())
				continue; // for

			qCInfo(log, "%s", KS_S(i->toString()));

			#ifdef QT_DBUS_LIB
			if (i->initDBusInterface()) {
				if (! i->m_dbus->isValid())
					return false;

				QDBusReply<void> reply = i->m_dbus->call(i->m_dbusMethod);

				return reply.isValid();
			}
			#endif // QT_DBUS_LIB

			#ifndef Q_OS_WIN32
			if (i->m_pid != 0)
				return (::kill(i->m_pid, SIGTERM) == 0);
			#endif // !Q_OS_WIN32

			return Utils::run(i->m_program, i->m_args);
		}
	}

	qCWarning(log, "No suitable strategy found");

	return false;
}

#ifdef QT_DBUS_LIB
std::shared_ptr<Strategy> StrategyManager::lazySessionBus(const Cond &cond, const QString &service, const QString &path, const QString &interface, const QString &method) {
	auto strategy = std::make_shared<Strategy>(cond);
	strategy->m_dbusService = service;
	strategy->m_dbusPath = path;
	strategy->m_dbusInterfaceString = interface;
	strategy->m_dbusMethod = method;

	m_list += strategy;

	return strategy;
}

std::shared_ptr<Strategy> StrategyManager::sessionBus(const Cond &cond, QDBusInterface *dbus, const QString &method) {
	auto strategy = lazySessionBus(cond, dbus->service(), dbus->path(), dbus->interface(), method);
	strategy->m_dbus = dbus;

	return strategy;
}

std::shared_ptr<Strategy> StrategyManager::sessionBus(const Cond &cond, const QString &service, const QString &path, const QString &interface, const QString &method) {
	auto strategy = lazySessionBus(cond, service, path, interface, method);
	strategy->initDBusInterface();

	return strategy;
}
#endif // QT_DBUS_LIB

void StrategyManager::terminate(const Cond &cond, const qint64 pid) {
	auto strategy = std::make_shared<Strategy>(cond);
	strategy->m_pid = pid;

	m_list += strategy;
}

// private:

void StrategyManager::printInfo(const QString &pluginID) {
	if (m_list.isEmpty())
		return;

	qCInfo(log, "Strategies for \"%s\":", KS_S(pluginID));

	for (auto &i : m_list) {
		qCInfo(log, "%s%s", (i->m_cond ? "  * " : "    "), KS_S(i->toString()));
	}
}

// Base

// public

Base::Base(const QString &id) :
	m_id(id),
	m_canBookmark(false) {
}

QWidget *Base::getContainerWidget() {
	if (m_containerWidget == nullptr) {
		m_containerWidget = new QWidget();
		//qDebug() << "Base::initContainerWidget()" << id();
		initContainerWidget();
	}

	return m_containerWidget;
}

QFormLayout *Base::makeFormLayout() {
	auto *layout = Utils::newFormLayout(getContainerWidget());
	Utils::setMargin(layout, 0_px);

	return layout;
}

QHBoxLayout *Base::makeHBoxLayout() {
	return UWidgets::newHBoxLayout(getContainerWidget(), { }, 10_px);
}

QVBoxLayout *Base::makeVBoxLayout() {
	return UWidgets::newVBoxLayout(getContainerWidget(), { }, 10_px);
}

void Base::setState([[maybe_unused]] const State state) { }

// protected

void Base::setEnabled([[maybe_unused]] const bool enabled, const QString &status, const InfoWidget::Type statusType) {
	setStatus(status, statusType);
}

#ifdef Q_OS_WIN32
void Base::setLastError() {
	DWORD lastError = ::GetLastError();

	wchar_t *buffer = nullptr;
	DWORD result = ::FormatMessageW(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		0,
		lastError,
		0,
		(wchar_t *)&buffer,
		0,
		0
	);

	if ((result > 0) && (buffer != nullptr)) {
		m_error = QString::fromWCharArray(buffer);
		LocalFree(buffer);
	}
	else {
		m_error = i18n("Unknown error") + "\n";
	}

	m_error +=
		" (error code: " + QString::number(lastError) +
		" | 0x" + QString::number(lastError, 16).toUpper() + ')';
}
#endif // Q_OS_WIN32

// private:

void Base::setStatus(const QString &status, const InfoWidget::Type statusType) {
	m_status = status;
	m_statusType = statusType;

	if (MainWindow::isInstance())
		updateStatusWidget(MainWindow::self());
}

// Action

// public

Action::Action(const QString &text, const QString &iconName, const QString &id) :
	Base(id),
	m_shouldStopTimer(true) {
	m_originalText = text;

	m_uiAction = new QAction();
	if (!iconName.isNull())
		m_uiAction->setIcon(QIcon::fromTheme(iconName));
	m_uiAction->setIconVisibleInMenu(true);
	m_uiAction->setText(text);
	connect(m_uiAction, &QAction::triggered, [this] { onFire(); });

	if (Utils::isRestricted("kshutdown/action/" + id))
		setEnabled(false, i18n("Disabled by Administrator"));
}

void Action::activate() {
	m_uiAction->trigger();
}

bool Action::authorize(QWidget *parent) {
	return PasswordDialog::authorize(
		parent,
		m_originalText,
		"kshutdown/action/" + m_id
	);
}

QAction *Action::createConfirmAction(const bool alwaysShowConfirmationMessage) {
	auto *result = new QAction(this);

	// clone basic properties
	result->setEnabled(m_uiAction->isEnabled());
	result->setIcon(m_uiAction->icon());
	result->setIconVisibleInMenu(m_uiAction->isIconVisibleInMenu());
	result->setMenu(m_uiAction->menu());
	result->setShortcut(m_uiAction->shortcut());
	result->setText(m_uiAction->text());
	result->setToolTip(m_uiAction->toolTip());

	connect(result, &QAction::triggered, [this, /*FU &*/alwaysShowConfirmationMessage] {
		if (shouldStopTimer())
			MainWindow::self()->setActive(false);

		bool wantConfirmation =
			(alwaysShowConfirmationMessage || Config::confirmAction) &&
			(this != LockAction::self()); // lock action - no confirmation

		if (!wantConfirmation || showConfirmationMessage()) {
			if (authorize(MainWindow::self()))
				activate();
		}
	});

	return result;
}

int Action::getUIGroup() const {
	if ((m_id == "shutdown") || (m_id == "reboot"))
		return 0;

	if ((m_id == "hibernate") || (m_id == "suspend"))
		return 1;

	if ((m_id == "lock") || (m_id == "logout"))
		return 2;

	return 3;
}

bool Action::isCommandLineOptionSet() const {
	return m_commandLineOption && CLI::getArgs()->isSet(m_commandLineOption.value());
}

bool Action::showConfirmationMessage() {
	auto *mainWindow = MainWindow::self();

	UMessageBuilder message(UMessageBuilder::Type::Question);

	QString okText = originalText();
	#ifdef Q_OS_WIN32
	// HACK: add left/right margins
	okText = ' ' + okText + ' ';
	#endif // Q_OS_WIN32
	message.okText(okText);

	message.cancelDefault(Config::cancelDefault.getBool());
	message.icon(icon());
	message.plainText(i18n("Are you sure?"));
	message.title(i18n("Confirm Action"));

	return message.exec(mainWindow->isVisible() ? mainWindow : nullptr);
}

void Action::updateStatusWidget(MainWindow *mainWindow) {
	//qDebug() << "updateStatusWidget:" << id();

	if ((mainWindow->getSelectedAction() == this) && !mainWindow->active()) {
		mainWindow->actionInfoWidget()
			->setText(status(), statusType());
	}
}

// protected

#ifdef QT_DBUS_LIB
// DOC: http://www.freedesktop.org/wiki/Software/systemd/logind/
QDBusInterface *Action::getLoginInterface() {
	if (m_loginInterface == nullptr) {
		m_loginInterface = new QDBusInterface(
			"org.freedesktop.login1",
			"/org/freedesktop/login1",
			"org.freedesktop.login1.Manager",
			QDBusConnection::systemBus()
		);
		if (m_loginInterface->isValid())
			qDebug() << "systemd/logind backend found...";
		else
			qDebug() << "systemd/logind backend NOT found...";
	}

	return m_loginInterface;
}
#endif // QT_DBUS_LIB

void Action::setCommandLineOption(const QStringList &names, const QString &description) {
	setCommandLineOptionValue({
		names,
		description.isEmpty() ? originalText() : description
	});
}

void Action::setCommandLineOptionValue(const QCommandLineOption &option) {
	//qDebug() << "Action::setCommandLineOptionValue: " << option.names() << option.description();
	m_commandLineOption = option;
	CLI::getArgs()->addOption(m_commandLineOption.value());
}

void Action::setEnabled(const bool enabled, const QString &status, const InfoWidget::Type statusType) {
	m_uiAction->setEnabled(enabled);

	Base::setEnabled(enabled, status, statusType);
}

bool Action::unsupportedAction() {
	m_error = i18n("Unsupported action: %0")
		.arg(originalText());

	return false;
}

// private:

void Action::readProperties() {
	QString group = configGroup();
	m_visibleInMainMenu = Config::readBool(group, "Visible In Main Menu", m_visibleInMainMenu);
	m_visibleInSystemTrayMenu = Config::readBool(group, "Visible In System Tray Menu", m_visibleInSystemTrayMenu);
	m_visibleInWindow = Config::readBool(group, "Visible In Window", m_visibleInWindow);
}

// event handlers:

void Action::onFire() {
	Log::info("Execute action: " + m_id + " (" + m_originalText + ')');

	qDebug() << "Action::onFire() [ id=" << m_id << " ]";

	auto showErrorMessage = [this](const QString &actionText, const QString &errorText) {
		QString result = actionText
			.arg(m_uiAction->text());

		result += "\n\n";
		result += errorText.isEmpty() ? i18n("Unknown error") : errorText;

// TODO: use UDialog::warning to match InfoWidget::Type
		UDialog::error(nullptr, result);
	};

	if (!isEnabled()) {
// TODO: show solution dialog
// TODO: "Action not available:", etc. text should be set by an action itself
		showErrorMessage(i18n("Action not available: %0"), status());

		return;
	}

	m_error = QString(); // reset

	// ensure the config is always written
	MainWindow::self()->writeConfig();

	if (!onAction()) {
		m_totalExit = false;
		if (!m_error.isNull()) {
			showErrorMessage(i18n("Action failed: %0"), m_error);
		}
	}
}

// Trigger

// public

Trigger::Trigger(const QString &text, const QString &iconName, const QString &id) :
	Base(id),
	m_icon(QIcon::fromTheme(iconName)),
	m_text(text) {
}

QString Trigger::createDisplayStatus(Action *action, const unsigned int options) {
	bool html = (options & DISPLAY_STATUS_HTML) != 0;
	QString result = "";

	if ((options & DISPLAY_STATUS_APP_NAME) != 0)
		result += QApplication::applicationDisplayName() + "\n\n";

	if ((options & DISPLAY_STATUS_HTML_NO_ACTION) == 0) {
		QString actionText = action->originalText();

		if (html) {
			actionText =
				"<b>" +
				actionText.toHtmlEscaped() +
				"</b>";
		}

		result += i18n("Action: %0").arg(actionText);
	}

	QString triggerStatus = status();
	if (!triggerStatus.isEmpty()) {
		if (html)
			triggerStatus = triggerStatus.toHtmlEscaped();

		if (!result.isEmpty())
			result += "\n\n";

		triggerStatus.replace("${BEGIN:b}", html ? "<b>"  : "");
		triggerStatus.replace("${END:b}"  , html ? "</b>" : "");

		result += triggerStatus;
	}

	if (html) {
		result.replace("\n", "<br>");
		result = "<qt>" + result + "</qt>";
	}

	//qDebug() << result;

	return result;
}

void Trigger::updateStatusWidget(MainWindow *mainWindow) {
	//qDebug() << "updateStatusWidget:" << id();

	if ((mainWindow->getSelectedTrigger() == this) && !mainWindow->active()) {
		auto *action = mainWindow->getSelectedAction();

		mainWindow->triggerInfoWidget()
			->setText(createDisplayStatus(action, DISPLAY_STATUS_HTML | DISPLAY_STATUS_HTML_NO_ACTION), statusType());
	}
}

// PluginManager

// public:

Action *PluginManager::actionAlias(const QString &id) {
	if (id == "halt")
		return action("shutdown");

	if (id == "logoff")
		return action("logout");

	if (id == "restart")
		return action("reboot");

	if (id == "sleep")
		return action("suspend");

	return action(id);
}

void PluginManager::add(Action *action) {
	//qDebug() << "Add Action:" << action->id();

	m_actionMap[action->id()] = action;
	m_actionList.append(action);
}

void PluginManager::add(Trigger *trigger) {
	//qDebug() << "Add Trigger:" << trigger->id();

	m_triggerMap[trigger->id()] = trigger;
	m_triggerList.append(trigger);
}

void PluginManager::initActionsAndTriggers() {
	qDebug() << "Init Actions";

	add(new ShutDownAction());
	add(new RebootAction());
	add(new HibernateAction());
	add(new SuspendAction());
	add(LockAction::self());
	add(new LogoutAction());
	add(Extras::self());
	add(new TestAction());

	if (Utils::linuxOS)
		add(new UnlockAction());

	qDebug() << "Init Triggers";

	add(new NoDelayTrigger());
	add(new TimeFromNowTrigger());
	add(new DateTimeTrigger());
	add(new ProcessMonitor());
	add(new FileMonitor());

	auto *idleMonitor = new IdleMonitor();
	if (idleMonitor->isSupported())
		add(idleMonitor);
	else
		delete idleMonitor;

	for (auto *i : actionList())
		i->strategyManager()->printInfo(i->id());

	for (auto *i : triggerList())
		i->strategyManager()->printInfo(i->id());
}

void PluginManager::readConfig() {
	//qDebug() << "PluginManager::readConfig";

	for (Action *i : actionList()) {
		i->readProperties();
		i->readConfig();
	}

	for (Trigger *i : triggerList())
		i->readConfig();
}

void PluginManager::shutDown() {
	//qDebug() << "PluginManager::shutDown (Action)";

	qDeleteAll(m_actionList);
	m_actionList.clear();
	m_actionMap.clear();

	//qDebug() << "PluginManager::shutDown (Trigger)";

	qDeleteAll(m_triggerList);
	m_triggerList.clear();
	m_triggerMap.clear();
}

void PluginManager::writeConfig() {
	for (Action *i : actionList())
		i->writeConfig();

	for (Trigger *i : triggerList())
		i->writeConfig();
}
