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

#include "password.h"

#include "config.h"
#include "log.h"
#include "plugins.h"
#include "preferences.h"
#include "utils.h"
#include "uwidgets.h"

#include <QCryptographicHash>
#include <QDebug>
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>

#ifdef KS_PURE_QT
	#include <QInputDialog>
#endif // KS_PURE_QT

#ifdef KS_KF5
	#include <KPasswordDialog>
#endif // KS_KF5

// PasswordDialog

// public:

PasswordDialog::PasswordDialog(QWidget *parent) :
#ifdef KS_KF5
// TODO: review and maybe replace KNewPasswordDialog
	KNewPasswordDialog(parent) {
#else
	UDialog(parent, i18n("Enter New Password"), false) {
#endif // KS_KF5
	//qDebug() << "PasswordDialog::PasswordDialog()";

// TODO: show warning if Caps Lock is turned on (no reliable solution in Qt)
// TODO: https://api.kde.org/kmodifierkeyinfo.html

#ifdef KS_KF5
	setMinimumPasswordLength(MIN_LENGTH);
	setPrompt(i18n("Enter New Password"));
#else
	m_password = new QLineEdit();
	m_password->setClearButtonEnabled(true);
	m_password->setEchoMode(QLineEdit::Password);
	connect(m_password, &QLineEdit::textChanged, [this]() {
		updateStatus();
	});

	m_confirmPassword = new QLineEdit();
	m_confirmPassword->setClearButtonEnabled(true);
	m_confirmPassword->setEchoMode(QLineEdit::Password);
	connect(m_confirmPassword, &QLineEdit::textChanged, [this]() {
		updateStatus();
	});

	auto *layout = Utils::newFormLayout();
	layout->addRow(i18n("Password:"), m_password);
	layout->addRow(i18n("Confirm Password:"), m_confirmPassword);

	mainLayout()->addLayout(layout);
	Utils::setMargin(mainLayout(), 20_px);

	m_password->setFocus();

	updateStatus();

	setWindowSize(WindowSize::FIXED);
#endif // KS_KF5
}

PasswordDialog::~PasswordDialog() {
	//qDebug() << "PasswordDialog::~PasswordDialog()";
	#ifdef KS_PURE_QT
	m_password->setText("");
	m_confirmPassword->setText("");
	#endif // KS_PURE_QT
}

void PasswordDialog::apply() {
	Log::warning("Password changed/set by user");

	#ifdef KS_KF5
	QString password = KNewPasswordDialog::password();
	#else
	QString password = m_password->text();
	#endif // KS_KF5
	Config::writeString("Password Protection", "Hash", toHash(password));
	clearPassword(password);

	Config::sync();
}

bool PasswordDialog::authorize(QWidget *parent, const QString &caption, const QString &userAction) {
	if (!Config::readBool("Password Protection", userAction, false))
		return true;

	QString hash = Config::readString("Password Protection", "Hash", "");

	if (hash.isEmpty())
		return true;

	QString prompt = i18n("Enter password to perform action: %0").arg(caption);

retry:

	#ifdef KS_KF5
	auto dialog = std::make_unique<KPasswordDialog>(parent);
	dialog->setPrompt(prompt);
	bool ok = dialog->exec() == KPasswordDialog::Accepted;
	QString password = ok ? dialog->password() : QString();

	if (!ok)
		return false;
	#else
	bool ok;
	QString password = QInputDialog::getText(
		parent,
		QApplication::applicationDisplayName(), // title
		prompt,
		QLineEdit::Password,
		"",
		&ok
	);

	if (!ok)
		return false;
	#endif // KS_KF5

	QString enteredHash = toHash(password);
	//qDebug() << enteredHash;
	clearPassword(password);

	if (hash != enteredHash) {
		Log::warning("Invalid password for action: " + userAction);

		UDialog::error(parent, i18n("Invalid password"));

		goto retry; // goto considered useful
	}

	Log::warning("Action successfully authenticated (using password): " + userAction);

	return true;
}

bool PasswordDialog::authorizeQuit(QWidget *parent) {
	return authorize(parent, i18n("Quit KShutdown"), "action/file_quit");
}

bool PasswordDialog::authorizeSettings(QWidget *parent) {
	return authorize(parent, i18n("Preferences"), "action/settings");
}

void PasswordDialog::clearPassword(QString &password) {
	password.fill('\0');
	password.clear();
}

QString PasswordDialog::toHash(const QString &password) {
	if (password.isEmpty())
		return "";

// TODO: consider other algorithms introduced in Qt 5.x <http://doc.qt.io/qt-5/qcryptographichash.html#Algorithm-enum>
	QString saltedString = "kshutdown-" + password;
	QByteArray saltedArray = saltedString.toUtf8();

	QByteArray hash = QCryptographicHash::hash(saltedArray, QCryptographicHash::Sha1);

	clearPassword(saltedString);
	saltedArray.fill('\0');
	saltedArray.clear();

	return { hash.toHex() };
}

// private:

#ifdef KS_PURE_QT
void PasswordDialog::updateStatus() {
	QString password = m_password->text();
	QString confirmPassword = m_confirmPassword->text();

	bool ok = password.length() >= MIN_LENGTH;
	if (!ok) {
		infoWidget()->setText(i18n("Password is too short (need %0 characters or more)").arg(MIN_LENGTH), InfoWidget::Type::Error);
	}
	else {
		ok = (password == confirmPassword);
		if (!ok)
			infoWidget()->setText(i18n("Confirmation password is different"), InfoWidget::Type::Error);
		else
			infoWidget()->setText(QString(), InfoWidget::Type::Error);
	}

	acceptButton()->setEnabled(ok);

	clearPassword(password);
	clearPassword(confirmPassword);
}
#endif // KS_PURE_QT

// PasswordPreferences

// public:

PasswordPreferences::PasswordPreferences(QWidget *parent) :
	QWidget(parent),
	m_configKeyRole(Qt::UserRole)
{
	//qDebug() << "PasswordPreferences::PasswordPreferences()";

	m_enablePassword = new QCheckBox(i18n("Enable Password Protection"));
	m_enablePassword->setChecked(!Config::readString("Password Protection", "Hash", "").isEmpty());

	connect(
		m_enablePassword, &QCheckBox::clicked,
		[this](const bool checked) { onEnablePassword(checked); }
	);
	
	m_userActionList = new QListWidget();
	m_userActionList->setAlternatingRowColors(true);

	m_userActionList->setIconSize(Utils::getSmallIconSize());
	QIcon alignFallbackIcon = UWidgets::isIconThemeSupported() ? UWidgets::emptyIcon(m_userActionList->iconSize()) : QIcon();

	addItem("action/settings", i18n("Settings (recommended)"), QIcon::fromTheme("configure", alignFallbackIcon));

	for (const Action *action : PluginManager::actionList()) {
		QIcon icon = action->icon();
		addItem(
			"kshutdown/action/" + action->id(),
			action->originalText(),
			icon.isNull() ? alignFallbackIcon : icon
		);
	}

	addItem("kshutdown/action/cancel", i18n("Cancel"), QIcon::fromTheme("dialog-cancel", alignFallbackIcon));
	addItem("action/file_quit", i18n("Quit KShutdown"), QIcon::fromTheme("application-exit", alignFallbackIcon));

	auto *infoWidget = new InfoWidget();
	infoWidget->setText(Utils::makeHTML(
		i18n("See Also: %0")
			.arg(Utils::makeLink(infoWidget, "https://sourceforge.net/p/kshutdown/wiki/Kiosk/", "Kiosk"))
	), InfoWidget::Type::Info);


	auto *l = UWidgets::newVBoxLayout(this, { }, UWidgets::STYLE_SPACING, Preferences::PAGE_MARGIN);
	l->addWidget(m_enablePassword);
	l->addSpacing(10_px);
	l->addWidget(Utils::newLabel(i18n("Password Protected Actions:"), m_userActionList));
	l->addWidget(m_userActionList);
	l->addWidget(infoWidget);

	updateWidgets(m_enablePassword->isChecked());
}

void PasswordPreferences::apply() {
	//qDebug() << "PasswordPreferences::apply()";

	QString group = "Password Protection";

	if (!m_enablePassword->isChecked()) {
		Log::warning("Password protection disabled by user");

		Config::writeString(group, "Hash", "");
	}

	int count = m_userActionList->count();
	for (int i = 0; i < count; i++) {
		auto *item = static_cast<QListWidgetItem *>(m_userActionList->item(i));
		QString key = item->data(m_configKeyRole).toString();
		Config::writeBool(group, key, item->checkState() == Qt::Checked);
	}
}

// private:

QListWidgetItem *PasswordPreferences::addItem(const QString &key, const QString &text, const QIcon &icon) {
	auto *item = new QListWidgetItem(text, m_userActionList);
	item->setCheckState(
		Config::readBool("Password Protection", key, false)
		? Qt::Checked
		: Qt::Unchecked
	);
	item->setData(m_configKeyRole, key);
	item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
	item->setIcon(icon);
	
	return item;
}

void PasswordPreferences::updateWidgets(const bool passwordEnabled) {
	m_userActionList->setEnabled(passwordEnabled);
}

// event handlers:

void PasswordPreferences::onEnablePassword(bool checked) {
	//qDebug() << "PasswordPreferences::onEnablePassword: " << checked;
	
	if (checked) {
		auto dialog = std::make_unique<PasswordDialog>(this);
		if (dialog->exec() == PasswordDialog::Accepted)
			dialog->apply();
		else
			m_enablePassword->setChecked(false);
	}
	updateWidgets(m_enablePassword->isChecked());
}
