/*
 * Copyright (C) 2014-2024 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 <QInputDialog>
#include <QMessageBox>
#include <QTime>

#include "src/common.h"
#include "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/datovka_shared/log/log.h"
#include "src/gui/dlg_create_account.h"
#include "src/gui/isds_login.h"
#include "src/io/isds_login.h"
#include "src/io/isds_sessions.h"
#include "src/settings/account.h"

Gui::LogInCycle::LogInCycle(const QString &syncAllActionName, QWidget *parent)
    : QObject(parent),
    m_syncAllActionName(syncAllActionName),
    m_cancel(false),
    m_waitMepLogin(Q_NULLPTR)
{
}

Gui::LogInCycle::~LogInCycle(void)
{
	/* The dialogue pointer should be null. */
	if (Q_UNLIKELY(m_waitMepLogin != Q_NULLPTR)) {
		Q_ASSERT(0);
		m_waitMepLogin->disconnect(SIGNAL(finished(int)),
		    this, SLOT(cancelLoop()));
		delete m_waitMepLogin;
	}
}

bool Gui::LogInCycle::logIn(IsdsSessions &isdsSessions, AcntData &acntSettings)
{
	if (Q_UNLIKELY(!acntSettings.isValid())) {
		return false;
	}

	const QString userName(acntSettings.userName());
	if (Q_UNLIKELY(userName.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}
	/* Create clean session if session doesn't exist. */
	if (!isdsSessions.holdsSession(userName)) {
		isdsSessions.createCleanSession(userName,
		    ISDS_CONNECT_TIMEOUT_MS);
	}

	QWidget *pw = qobject_cast<QWidget *>(parent()); /* Parent widget. */

	enum IsdsLogin::ErrorCode errCode = IsdsLogin::EC_ERR;
	IsdsLogin loginCtx(isdsSessions, acntSettings);

	do {
		if (Q_UNLIKELY(m_cancel)) {
			m_waitMepLogin->disconnect(SIGNAL(finished(int)),
			    this, SLOT(cancelLoop()));
			m_waitMepLogin->hide();
			delete m_waitMepLogin; m_waitMepLogin = Q_NULLPTR;

			QMessageBox::information(pw, tr("Login Cancelled"),
			    tr("The login procedure for data box '%1' has been cancelled.")
			        .arg(acntSettings.accountName()),
			QMessageBox::Ok, QMessageBox::Ok);
			emit logInStatusMessage(
			    tr("Login cancelled for data box '%1'.")
			        .arg(acntSettings.accountName()));
			return false;
		}

		errCode = loginCtx.logIn();

		acntSettings._setOtp(QString()); /* Erase OTP. */

		switch (errCode) {
		case IsdsLogin::IsdsLogin::EC_OK:
			/* Do nothing. */
			break;
		case IsdsLogin::EC_NO_CRT_AGAIN:
			QMessageBox::critical(pw,
			    tr("Invalid Certificate Data"),
			    tr("The certificate or the supplied pass-phrase are invalid.") +
			    "<br/><br/>" +
			    tr("Please enter a path to a valid certificate and/or provide a correct key to unlock the certificate."),
			    QMessageBox::Ok);
			emit logInStatusMessage(
			    tr("Bad certificate data for data box '%1'.")
			        .arg(acntSettings.accountName()));
			break;
		case IsdsLogin::EC_NOT_LOGGED_IN:
		case IsdsLogin::EC_PARTIAL_SUCCESS_AGAIN:
		case IsdsLogin::EC_ISDS_ERR:
			{
				const QPair<QString, QString> pair(
				    loginCtx.dialogueErrMsg());
				QMessageBox::critical(pw, pair.first,
				    pair.second, QMessageBox::Ok);
			}
			emit logInStatusMessage(
			    tr("It was not possible to connect to the data box '%1'.")
			        .arg(acntSettings.accountName()));
			break;
		case IsdsLogin::EC_NOT_IMPL:
			emit logInStatusMessage(
			    tr("The log-in method used in data box '%1' is not implemented.")
			        .arg(acntSettings.accountName()));
			return false;
			break;
		default:
			break;
		}

		if (errCode == IsdsLogin::EC_MEP_PARTIAL_SUCCESS) {
			if (m_waitMepLogin == Q_NULLPTR) {
				m_waitMepLogin = new (::std::nothrow) QMessageBox(
				    QMessageBox::NoIcon, tr("Mobile Key Login"),
				    tr("Waiting for acknowledgement from the Mobile key application for the data box '%1'.")
				        .arg(acntSettings.accountName()),
				    QMessageBox::Cancel, pw);
				if (Q_UNLIKELY(m_waitMepLogin == Q_NULLPTR)) {
					Q_ASSERT(0);
					return false;
				}
				connect(m_waitMepLogin, SIGNAL(finished(int)),
				    this, SLOT(cancelLoop()));
				/*
				 * The window must be non-modal because a modal
				 * window will block the event loop.
				 * TODO -- Move the notification into a dedicated
				 * widget accessible from within the main window.
				 */
				m_waitMepLogin->setModal(false);
				Qt::WindowFlags flags = m_waitMepLogin->windowFlags();
				flags = (flags | Qt::CustomizeWindowHint)
				    & ~(Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint);
				m_waitMepLogin->setWindowFlags(flags);
				m_waitMepLogin->show();
			} else {
				/* Keep window raised. */
				m_waitMepLogin->raise();
			}
		} else {
			if (m_waitMepLogin != Q_NULLPTR) {
				m_waitMepLogin->disconnect(SIGNAL(finished(int)),
				    this, SLOT(cancelLoop()));
				m_waitMepLogin->hide();
				delete m_waitMepLogin; m_waitMepLogin = Q_NULLPTR;
			}
		}

		switch (errCode) {
		case IsdsLogin::EC_OK:
			/* Do nothing. */
			break;
		case IsdsLogin::EC_NO_PWD:
			if (!DlgCreateAccount::modify(acntSettings,
			        DlgCreateAccount::ACT_PWD,
			        m_syncAllActionName, pw)) {
				acntSettings.setRememberPwd(false);
				acntSettings.setPassword(QString());
				emit logInStatusMessage(
				    tr("It was not possible to connect to the data box '%1'.")
				        .arg(acntSettings.accountName()));
				return false;
			}
			break;
		case IsdsLogin::EC_NO_CRT:
		case IsdsLogin::EC_NO_CRT_AGAIN:
			/* Erase pass-phrase. */
			acntSettings._setPassphrase(QString());

			if (!DlgCreateAccount::modify(acntSettings,
			        DlgCreateAccount::ACT_CERT,
			        m_syncAllActionName, pw)) {
				emit logInStatusMessage(
				    tr("It was not possible to connect to the data box '%1'.")
				        .arg(acntSettings.accountName()));
				return false;
			}
			break;
		case IsdsLogin::EC_NO_CRT_PWD:
			/* Erase pass-phrase. */
			acntSettings._setPassphrase(QString());

			if (!DlgCreateAccount::modify(acntSettings,
			        DlgCreateAccount::ACT_CERTPWD,
			        m_syncAllActionName, pw)) {
				emit logInStatusMessage(
				    tr("It was not possible to connect to the data box '%1'.")
				        .arg(acntSettings.accountName()));
				return false;
			}
			break;
		case IsdsLogin::EC_NO_CRT_PPHR:
			{
				/* Ask the user for password. */
				bool ok;
				QString enteredText = QInputDialog::getText(pw,
				    tr("Password Required"),
				    tr("Data box: %1\n"
				        "Username: %2\n"
				        "Certificate file: %3\n"
				        "Enter password to unlock certificate file:")
				        .arg(acntSettings.accountName())
				        .arg(userName)
				        .arg(acntSettings.p12File()),
				    QLineEdit::Password, QString(), &ok);
				if (ok) {
					/*
					 * We cannot pass null string into the
					 * login operation a it will return
					 * immediately. But the dialogue can
					 * return null string.
					 */
					if (enteredText.isNull()) {
						enteredText = "";
					}
					acntSettings._setPassphrase(
					    macroStdMove(enteredText));
				} else {
					/* Aborted. */
					return false;
				}
			}
			break;
		case IsdsLogin::EC_NO_OTP:
		case IsdsLogin::EC_PARTIAL_SUCCESS:
		case IsdsLogin::EC_PARTIAL_SUCCESS_AGAIN:
			{
				QString msgTitle(tr("Enter OTP Security Code"));
				QString msgBody(
				    tr("Data box '%1' requires an OTP security code authentication<br/>in order to connect to the data box.")
				        .arg(acntSettings.accountName()) +
				    "<br/><br/>" +
				    tr("Enter OTP security code for data box") +
				    "<br/><b>" +
				    acntSettings.accountName() +
				    " </b>(" + userName + ").");
				QString otpCode;
				do {
					bool ok;
					otpCode = QInputDialog::getText(pw,
					    msgTitle, msgBody,
					    QLineEdit::Normal, otpCode, &ok,
					    Qt::WindowStaysOnTopHint);
					if (!ok) {
						emit logInStatusMessage(
						    tr("It was not possible to connect to the data box '%1'.")
						        .arg(acntSettings.accountName()));
						return false;
					}
				} while (otpCode.isEmpty());

				acntSettings._setOtp(macroStdMove(otpCode));
			}
			break;
		case IsdsLogin::EC_NO_MEP:
			if (!DlgCreateAccount::modify(acntSettings,
			        DlgCreateAccount::ACT_MEP,
			        m_syncAllActionName, pw)) {
				acntSettings.setMepToken(QString());
				emit logInStatusMessage(
				    tr("It was not possible to connect to the data box '%1'.")
				        .arg(acntSettings.accountName()));
				return false;
			}
			break;
		case IsdsLogin::EC_MEP_PARTIAL_SUCCESS:
			{
				QTime dieTime = QTime::currentTime().addSecs(1);
				while (QTime::currentTime() < dieTime) {
					/*
					 * A modal window may block method.
					 * This is especially the case on macOS.
					 */
					QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
				}
			}
			break;
		case IsdsLogin::EC_NEED_TOTP_ACK:
			{
				QMessageBox::StandardButton reply =
				    QMessageBox::question(pw,
					tr("SMS Code for Data Box '%1'").arg(acntSettings.accountName()),
				        tr("Data box '%1' requires a security code authentication in order to connect.")
				            .arg(acntSettings.accountName()) +
				        "<br/>" +
					tr("The security code will be sent to you via a premium SMS.") +
				        "<br/><br/>" +
					tr("Do you want to send a premium SMS containing a security code into your mobile phone?"),
				        QMessageBox::Yes | QMessageBox::No,
				        QMessageBox::Yes);

				if (reply == QMessageBox::No) {
					emit logInStatusMessage(
					    tr("It was not possible to connect to the data box '%1'.")
					        .arg(acntSettings.accountName()));
					return false;
				}

				acntSettings._setOtp(""); /* Must be empty. */
			}
			break;
		case IsdsLogin::EC_NOT_LOGGED_IN:
			if (!DlgCreateAccount::modify(acntSettings,
			        DlgCreateAccount::ACT_EDIT,
			        m_syncAllActionName, pw)) {
				/* Don't clear password here. */
				emit logInStatusMessage(
				    tr("It was not possible to connect to the data box '%1'.")
				        .arg(acntSettings.accountName()));
				return false;
			}
			break;
		default:
			logErrorNL(
			    "Received log-in error code %d for account '%s'.",
			    errCode,
			    acntSettings.accountName().toUtf8().constData());
			return false;
			break;
		}
	} while (errCode != IsdsLogin::EC_OK);

	if (Q_UNLIKELY(errCode != IsdsLogin::EC_OK)) {
		Q_ASSERT(0);
		return false;
	}

	return true;
}

void Gui::LogInCycle::cancelLoop(void)
{
	m_cancel = true;
}
