/*
 * Copyright (C) 2014-2022 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <QMessageBox>

#include "src/datovka_shared/isds/account_interface.h"
#include "src/datovka_shared/isds/types.h"
#include "src/datovka_shared/utility/strings.h"
#include "src/datovka_shared/worker/pool.h"
#include "src/global.h"
#include "src/gui/dlg_change_pwd.h"
#include "src/gui/helper.h"
#include "src/io/isds_sessions.h"
#include "src/settings/accounts.h"
#include "src/worker/task_change_pwd.h"
#include "ui_dlg_change_pwd.h"

#define PWD_MIN_LENGTH 8 /* Minimal password length is 8 characters. */

DlgChangePwd::DlgChangePwd(const QString &boxId, const AcntId &acntId,
    QWidget *parent)
    : QDialog(parent),
    m_ui(new (::std::nothrow) Ui::DlgChangePwd),
    m_keepAliveTimer(this),
    m_acntId(acntId)
{
	m_ui->setupUi(this);
	/* Tab order is defined in UI file. */

	m_ui->userNameLine->setText(m_acntId.username());
	m_ui->accountLine->setText(boxId);
	connect(m_ui->togglePwdVisibilityButton, SIGNAL(clicked()), this,
	    SLOT(togglePwdVisibility()));
	connect(m_ui->generateButton, SIGNAL(clicked()), this,
	    SLOT(generatePassword()));

	connect(m_ui->currentPwdLine, SIGNAL(textChanged(QString)),
	    this, SLOT(checkInputFields()));
	connect(m_ui->newPwdLine, SIGNAL(textChanged(QString)),
	    this, SLOT(checkInputFields()));
	connect(m_ui->newPwdLine2, SIGNAL(textChanged(QString)),
	    this, SLOT(checkInputFields()));
	connect(m_ui->secCodeLine, SIGNAL(textChanged(QString)),
	    this, SLOT(checkInputFields()));

	m_ui->otpLabel->setEnabled(false);
	m_ui->secCodeLine->setEnabled(false);
	m_ui->smsPushButton->setEnabled(false);

	if (GlobInstcs::acntMapPtr->acntData(m_acntId).loginMethod() ==
	    AcntSettings::LIM_UNAME_PWD_HOTP) {
		m_ui->otpLabel->setText(tr("Enter security code:"));
		m_ui->otpLabel->setEnabled(true);
		m_ui->secCodeLine->setEnabled(true);
	}

	if (GlobInstcs::acntMapPtr->acntData(m_acntId).loginMethod() ==
	    AcntSettings::LIM_UNAME_PWD_TOTP) {
		m_ui->otpLabel->setText(tr("Enter SMS code:"));
		m_ui->otpLabel->setEnabled(true);
		m_ui->secCodeLine->setEnabled(true);
		m_ui->smsPushButton->setEnabled(true);
		connect(m_ui->smsPushButton, SIGNAL(clicked()), this,
		    SLOT(sendSmsCode()));
	}

	m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);

	m_keepAliveTimer.start(DLG_ISDS_KEEPALIVE_MS);
	connect(&m_keepAliveTimer, SIGNAL(timeout()), this,
	    SLOT(pingIsdsServer()));
}

DlgChangePwd::~DlgChangePwd(void)
{
	delete m_ui;
}

/*!
 * @brief Send new password request to ISDS.
 *
 * @param[in] acntId Account identifier.
 * @param[in] currentPwd Current password.
 * @param[in] newPwd New password.
 * @param[in] secCode Security code.
 * @param[in] parent Parent widget.
 * @return True on success.
 */
static
bool sendChangePwdRequest(const AcntId &acntId,
    const QString &currentPwd, const QString &newPwd,
    const QString &secCode, QWidget *parent = Q_NULLPTR)
{
	if (Q_UNLIKELY(!acntId.isValid())) {
		Q_ASSERT(0);
		return false;
	}

	TaskChangePwd *task = Q_NULLPTR;
	AcntData acntData(GlobInstcs::acntMapPtr->acntData(acntId));

	if (acntData.loginMethod() == AcntSettings::LIM_UNAME_PWD_HOTP ||
	    acntData.loginMethod() == AcntSettings::LIM_UNAME_PWD_TOTP) {
		Isds::Otp otp;
		otp.setMethod(
		    (acntData.loginMethod() == AcntSettings::LIM_UNAME_PWD_HOTP) ?
		        Isds::Type::OM_HMAC : Isds::Type::OM_TIME);
		otp.setOtpCode(secCode);

		task = new (::std::nothrow) TaskChangePwd(acntId.username(),
		    currentPwd, newPwd, otp);
	} else {
		task = new (::std::nothrow) TaskChangePwd(acntId.username(),
		    currentPwd, newPwd);
	}
	if (Q_UNLIKELY(task == Q_NULLPTR)) {
		Q_ASSERT(0);
		return false;
	}
	task->setAutoDelete(false);
	GlobInstcs::workPoolPtr->runSingle(task);

	enum Isds::Type::Error taskStatus = task->m_errorCode;
	QString errorStr(task->m_isdsError);
	QString longErrorStr(task->m_isdsLongError);
	delete task; task = Q_NULLPTR;

	if (taskStatus == Isds::Type::ERR_SUCCESS) {
		QMessageBox::information(parent,
		    DlgChangePwd::tr("Password has been changed"),
		    DlgChangePwd::tr("Password has been successfully changed on the ISDS server.") +
		    "\n\n" +
		    DlgChangePwd::tr("Restart the application. "
		        "Also don't forget to remember the new password so you will still be able to log into your data box via the web interface."),
		    QMessageBox::Ok);

		acntData.setPassword(newPwd);
		GlobInstcs::acntMapPtr->updateAccount(acntData);

		/*
		 * TODO - Delete and create new ISDS context with new settings.
		 */
	} else {
		QString error(DlgChangePwd::tr("Error: ") + errorStr);
		if (!longErrorStr.isEmpty()) {
			error += "\n" + DlgChangePwd::tr("ISDS returns") +
			    QLatin1String(": ") + longErrorStr;
		}

		QMessageBox::warning(parent, DlgChangePwd::tr("Password error"),
		    DlgChangePwd::tr("An error occurred during an attempt to change the password.") +
		    "\n\n" + error + "\n\n" +
		    DlgChangePwd::tr("Fix the problem and try it again."),
		    QMessageBox::Ok);
	}

	return taskStatus == Isds::Type::ERR_SUCCESS;
}

bool DlgChangePwd::changePassword(const QString &boxId, const AcntId &acntId,
    QWidget *parent)
{
	if (Q_UNLIKELY(boxId.isEmpty() || (!acntId.isValid()))) {
		Q_ASSERT(0);
		return false;
	}

	DlgChangePwd dlg(boxId, acntId, parent);
	if (QDialog::Accepted == dlg.exec()) {
		return sendChangePwdRequest(acntId,
		    dlg.m_ui->currentPwdLine->text(),
		    dlg.m_ui->newPwdLine->text(), dlg.m_ui->secCodeLine->text(),
		    parent);
	} else {
		return false;
	}
}

void DlgChangePwd::togglePwdVisibility(void)
{
	enum QLineEdit::EchoMode echoMode =
	    (m_ui->currentPwdLine->echoMode() == QLineEdit::Password) ?
	        QLineEdit::Normal : QLineEdit::Password;
	QString buttonText(
	    (m_ui->currentPwdLine->echoMode() == QLineEdit::Password) ?
	        tr("Hide") : tr("Show"));

	m_ui->currentPwdLine->setEchoMode(echoMode);
	m_ui->newPwdLine->setEchoMode(echoMode);
	m_ui->newPwdLine2->setEchoMode(echoMode);
	m_ui->togglePwdVisibilityButton->setText(buttonText);
}

void DlgChangePwd::generatePassword(void)
{
	/* Set one digit as last character. */
	const QString pwd(Utility::generateRandomString(PWD_MIN_LENGTH) + "0");
	m_ui->newPwdLine->setText(pwd);
	m_ui->newPwdLine2->setText(pwd);
}

void DlgChangePwd::checkInputFields(void)
{
	bool buttonEnabled = !m_ui->currentPwdLine->text().isEmpty() &&
	    !m_ui->newPwdLine->text().isEmpty() &&
	    m_ui->newPwdLine->text().length() >= PWD_MIN_LENGTH &&
	    !m_ui->newPwdLine2->text().isEmpty() &&
	    m_ui->newPwdLine2->text().length() >= PWD_MIN_LENGTH &&
	    m_ui->newPwdLine->text() == m_ui->newPwdLine2->text();

	if (Q_UNLIKELY(!m_acntId.isValid())) {
		Q_ASSERT(0);
		return;
	}

	const AcntData acntData(GlobInstcs::acntMapPtr->acntData(m_acntId));
	if (acntData.loginMethod() == AcntSettings::LIM_UNAME_PWD_HOTP ||
	    acntData.loginMethod() == AcntSettings::LIM_UNAME_PWD_TOTP) {
		buttonEnabled = buttonEnabled &&
		    !m_ui->secCodeLine->text().isEmpty();
	}

	m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(
	    buttonEnabled);
}

void DlgChangePwd::pingIsdsServer(void)
{
	GuiHelper::pingIsdsServer(m_acntId);
}

void DlgChangePwd::sendSmsCode(void)
{
	if (Q_UNLIKELY(!m_acntId.isValid())) {
		Q_ASSERT(0);
		return;
	}

	/* Show Premium SMS request dialogue. */
	const QString accountName = GlobInstcs::acntMapPtr->acntData(m_acntId).accountName();
	QMessageBox::StandardButton reply = QMessageBox::question(this,
	    tr("SMS code for account '%1'").arg(accountName),
	    tr("Account '%1' requires authentication via security code to connect to data box.")
	        .arg(accountName) +
	    "<br/>" +
	    tr("The security code will be sent to you via a Premium SMS.") +
	    "<br/><br/>" +
	    tr("Do you request a Premium SMS containing the security code?"),
	    QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);

	if (reply == QMessageBox::No) {
		return;
	}

	Isds::Otp otp;
	otp.setMethod(Isds::Type::OM_TIME);

	TaskChangePwd *task = new (::std::nothrow) TaskChangePwd(
	    m_acntId.username(), m_ui->currentPwdLine->text(),
	    m_ui->newPwdLine->text(), otp);
	if (Q_UNLIKELY(task == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}
	task->setAutoDelete(false);
	GlobInstcs::workPoolPtr->runSingle(task);

	enum Isds::Type::Error taskStatus = task->m_errorCode;
	delete task; task = Q_NULLPTR;

	if (Isds::Type::ERR_PARTIAL_SUCCESS == taskStatus) {
		QMessageBox::information(this, tr("Enter SMS security code"),
		    tr("SMS security code for account '%1'<br/>has been sent to your mobile phone...")
		        .arg(accountName) +
		    "<br/><br/>" +
		    tr("Enter SMS security code for account") +
		        "<br/><b>" + accountName +
		        "</b> (" + m_acntId.username() + ").",
		    QMessageBox::Ok);
		m_ui->otpLabel->setText(tr("Enter SMS code:"));
		m_ui->smsPushButton->setEnabled(false);
	} else {
		QMessageBox::critical(this, tr("Login error"),
		    tr("An error occurred while preparing request for SMS with OTP security code.") +
		    "<br/><br/>" +
		    tr("Please try again later or you have to use the official web interface of Datové schránky to access to your data box."),
		    QMessageBox::Ok);
	}
}
