/*
 * Copyright (C) 2014-2026 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 <QDateTime>
#include <QThread>

#include "src/common.h"
#include "src/datovka_shared/identifiers/account_id.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/worker/pool.h"
#include "src/global.h"
#include "src/io/isds_sessions.h"
#include "src/io/session_keeper.h"
#include "src/settings/accounts.h"
#include "src/worker/task_keep_alive.h"

/*!
 * @brief Keep-alive thread.
 *
 * @note There is no need for multiple keep-alive threads as the worker pool
 *     currently holds only one thread.
 */
class KeepAliveThread : public QThread {
public:
	/*!
	 * @brief Constructor.
	 *
	 * @param[in] regularAccounts Account container holding regular accounts.
	 * @param[in] sessions Session container.
	 * @param[in] parent Parent object.
	 */
	explicit KeepAliveThread(AccountsMap *regularAccounts,
	    IsdsSessions *sessions, bool &quitInactive, int &processedTypeFlags,
	    QObject *parent = Q_NULLPTR)
	    : QThread(parent),
	    m_regularAccounts(regularAccounts),
	    m_sessions(sessions),
	    m_quitInactive(quitInactive),
	    m_processedTypeFlags(processedTypeFlags)
	{ }

	/*!
	 * @brief Starting point of the thread.
	 */
	virtual
	void run(void) Q_DECL_OVERRIDE;

private:
	AccountsMap *m_regularAccounts; /*!< Account container holding regular accounts. */
	IsdsSessions *m_sessions; /*!< Session container. */

	volatile bool &m_quitInactive; /*!< Whether to quit inactive sessions. */
	volatile int &m_processedTypeFlags; /*!< Account types that should be kept alive. */
};

/*!
 * @brief Ping the ISDS server.
 *
 * @param[in] acntId Account identifier.
 */
static
bool pingIsdsServer(const AcntId &acntId)
{
	if (Q_UNLIKELY(!acntId.isValid())) {
		Q_ASSERT(0);
		return false;
	}

	logInfoNL("Pinging '%s'.", acntId.username().toUtf8().constData());

	TaskKeepAlive *task = new (::std::nothrow) TaskKeepAlive(acntId);
	if (Q_UNLIKELY(task == Q_NULLPTR)) {
		return false;
	}
	task->setAutoDelete(false);
	/* This will block the thread and all workers. */
	if (Q_UNLIKELY(!GlobInstcs::workPoolPtr->runSingle(task))) {
		delete task; task = Q_NULLPTR;
		logWarningNL("%s", "Cannot execute task.");
		return false;
	}

	const bool isAlive = task->m_isAlive;

	delete task; task = Q_NULLPTR;

	return isAlive;
}

void KeepAliveThread::run(void)
{
	if (Q_UNLIKELY((Q_NULLPTR == m_regularAccounts)
	        || (Q_NULLPTR == m_sessions))) {
		return;
	}

	const QList<AcntId> acntIds = m_regularAccounts->acntIds();
	for (const AcntId &acntId : acntIds) {
		if (!m_sessions->holdsSession(acntId.username())) {
			/* Ignore missing sessions. */
			continue;
		}
		if (IsdsSessions::SESSION_LOGGED_IN != m_sessions->sessionState(acntId.username())) {
			/* Ignore those sessions whose login procedure isn't completed. */
			continue;
		}

		const AcntData acntData = m_regularAccounts->acntData(acntId);

		/*
		 * For LIM_UNAME_PWD, LIM_UNAME_CRT and LIM_UNAME_PWD_CRT
		 * complete login credentials are sent in every request.
		 * There's no negotiated sever-client secret held in a session
		 * cookie that might expire.
		 */

		switch (acntData.loginMethod()) {
		case AcntSettings::LIM_UNKNOWN:
			continue; /* Skip pinging the server. */
			break;
		case AcntSettings::LIM_UNAME_PWD:
			if (!(m_processedTypeFlags & SessionKeeper::LT_UNAME_PWD)) {
				continue;
			}
			break;
		case AcntSettings::LIM_UNAME_CRT:
			if (!(m_processedTypeFlags & SessionKeeper::LT_UNAME_CRT)) {
				continue;
			}
			break;
		case AcntSettings::LIM_UNAME_PWD_CRT:
			if (!(m_processedTypeFlags & SessionKeeper::LT_UNAME_PWD_CRT)) {
				continue;
			}
			break;
		case AcntSettings::LIM_UNAME_PWD_HOTP:
			if (!(m_processedTypeFlags & SessionKeeper::LT_UNAME_PWD_HOTP)) {
				continue;
			}
			break;
		case AcntSettings::LIM_UNAME_PWD_TOTP:
			if (!(m_processedTypeFlags & SessionKeeper::LT_UNAME_PWD_TOTP)) {
				continue;
			}
			break;
		case AcntSettings::LIM_UNAME_MEP:
			if (!(m_processedTypeFlags & SessionKeeper::LT_UNAME_MEP)) {
				continue;
			}
			break;
		default:
			/* Unsupported login method type. */
			continue;
			break;
		}

		const bool isAlive = pingIsdsServer(acntId);
		if (Q_UNLIKELY(!isAlive && m_quitInactive)) {
			logInfoNL("Quitting inactive session for '%s'.",
			    acntId.username().toUtf8().constData());
			m_sessions->quitSession(acntId.username());
		}
	}
}

SessionKeeper::SessionKeeper(AccountsMap *regularAccounts,
    IsdsSessions *sessions, QObject *parent)
    : QObject(parent),
    m_keepAliveThread(new (::std::nothrow) KeepAliveThread(regularAccounts, sessions, m_quitInactive, m_processedTypeFlags)),
    m_keepAliveTimer(this),
    m_quitInactive(false),
    m_processedTypeFlags(LT_UNAME_PWD_HOTP | LT_UNAME_PWD_TOTP | LT_UNAME_MEP),
    m_coolDown(0)
{
	if (Q_UNLIKELY((Q_NULLPTR == regularAccounts) || (Q_NULLPTR == sessions))) {
		/*
		 * Don't prepare anything as no accounts and/or sessions are
		 * being watched.
		 */
		return;
	}

	connect(&m_keepAliveTimer, SIGNAL(timeout()),
	    this, SLOT(runKeepAlive()));
}

SessionKeeper::~SessionKeeper(void)
{
	stopKeepAlive();

	m_keepAliveTimer.disconnect(SIGNAL(timeout()),
	    this, SLOT(runKeepAlive()));

	if (m_keepAliveThread != Q_NULLPTR) {
		if (Q_UNLIKELY(!m_keepAliveThread->wait(500))) {
			m_keepAliveThread->terminate();
			m_keepAliveThread->wait();
		}
		delete m_keepAliveThread; m_keepAliveThread = Q_NULLPTR;
	}
}

void SessionKeeper::setQuitInactive(bool quitInactive)
{
	m_quitInactive = quitInactive;
}

bool SessionKeeper::quitInactive(void) const
{
	return m_quitInactive;
}

void SessionKeeper::setProcessedTypeFlags(int processedTypeFlags)
{
	m_processedTypeFlags = processedTypeFlags;
}

int SessionKeeper::processedTypeFlags(void) const
{
	return m_processedTypeFlags;
}

void SessionKeeper::setCoolDown(int msec)
{
	m_coolDown = (msec >= 0) ? msec : 0;
}

void SessionKeeper::startKeepAlive(int msec)
{
	if (msec >= 0) {
		m_keepAliveTimer.start(msec);
	}
}

void SessionKeeper::stopKeepAlive(void)
{
	m_keepAliveTimer.stop();
}

void SessionKeeper::runKeepAlive(void)
{
	static QDateTime lastAttempt;
	QDateTime currentTime = QDateTime::currentDateTime().toUTC();

	if (Q_UNLIKELY(Q_NULLPTR == m_keepAliveThread)) {
		return;
	}

	if (lastAttempt.isValid()
	    && (lastAttempt.addMSecs(m_coolDown) > currentTime)) {
		/* Wait cool down period. */
		return;
	}

	lastAttempt = currentTime;

	m_keepAliveThread->start();
}
