/*
 * 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 <QSettings>

#include "src/datovka_shared/crypto/crypto_pwd.h"
#include "src/datovka_shared/crypto/crypto_wrapped.h"
#include "src/datovka_shared/log/log.h"
#include "src/settings/tag_container_settings.h"

#define SETTINGS_TAG_STORAGE_GROUP "tag_storage"

#define SETTINGS_BACKEND "backend"
#define SETTINGS_CLIENT_HOSTNAME "hostname"
#define SETTINGS_CLIENT_PORT "port_number"
#define SETTINGS_CLIENT_CERT_PATH "cert_path"
#define SETTINGS_CLIENT_USERNAME "username"
namespace CredNames {
	static const QString pwd("password");
	static const QString pwdAlg("password_alg");
	static const QString pwdSalt("password_salt");
	static const QString pwdIv("password_iv");
	static const QString pwdCode("password_code");
}
#define SETTINGS_CLIENT_PROFILE_ID "profile_id"
#define SETTINGS_CLIENT_PROFILE_NAME "profile_name"

static const QString settingsBackendDbStr("local_db");
static const QString settingsBackendClientStr("server_client");

#define DFLT_BACKEND TagContainer::BACK_DB
const QString nullString;
#define DFLT_PORT -1
#define DFLT_PROFILE_ID -1
const QByteArray nullByteArray;

class TagContainerSettingsPrivate {
public:
	TagContainerSettingsPrivate(void)
	    : m_backend(DFLT_BACKEND), m_clientHostname(),
	    m_clientPort(DFLT_PORT), m_certPath(), m_username(),
	    m_password(), m_pwdAlg(), m_pwdSalt(), m_pwdIv(), m_pwdCode(),
	    m_profileId(DFLT_PROFILE_ID), m_profileName()
	{ }

	TagContainerSettingsPrivate &operator=(const TagContainerSettingsPrivate &other) Q_DECL_NOTHROW
	{
		m_backend = other.m_backend;
		m_clientHostname = other.m_clientHostname;
		m_clientPort = other.m_clientPort;
		m_certPath = other.m_certPath;
		m_username = other.m_username;
		m_password = other.m_password;
		m_pwdAlg = other.m_pwdAlg;
		m_pwdSalt = other.m_pwdSalt;
		m_pwdIv = other.m_pwdIv;
		m_pwdCode = other.m_pwdCode;
		m_profileId = other.m_profileId;
		m_profileName = other.m_profileName;

		return *this;
	}

	bool operator==(const TagContainerSettingsPrivate &other) const
	{
		return (m_backend == other.m_backend) &&
		    (m_clientHostname == other.m_clientHostname) &&
		    (m_clientPort == other.m_clientPort) &&
		    (m_certPath == other.m_certPath) &&
		    (m_username == other.m_username) &&
		    (m_password == other.m_password) &&
		    (m_pwdAlg == other.m_pwdAlg) &&
		    (m_pwdSalt == other.m_pwdSalt) &&
		    (m_pwdIv == other.m_pwdIv) &&
		    (m_pwdCode == other.m_pwdCode) &&
		    (m_profileId == other.m_profileId) &&
		    (m_profileName == other.m_profileName);
	}

	enum TagContainer::Backend m_backend;
	QString m_clientHostname;
	int m_clientPort;
	QString m_certPath;
	QString m_username;

	QString m_password;
	QString m_pwdAlg;
	QByteArray m_pwdSalt;
	QByteArray m_pwdIv;
	QByteArray m_pwdCode;

	qint64 m_profileId;
	QString m_profileName;
};

TagContainerSettings::TagContainerSettings(void)
    : d_ptr(Q_NULLPTR)
{
}

TagContainerSettings::TagContainerSettings(const TagContainerSettings &other)
    : d_ptr((other.d_func() != Q_NULLPTR) ? (new (::std::nothrow) TagContainerSettingsPrivate) : Q_NULLPTR)
{
	Q_D(TagContainerSettings);
	if (d == Q_NULLPTR) {
		return;
	}

	*d = *other.d_func();
}

#ifdef Q_COMPILER_RVALUE_REFS
TagContainerSettings::TagContainerSettings(TagContainerSettings &&other) Q_DECL_NOEXCEPT
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
    : d_ptr(other.d_ptr.release()) //d_ptr(::std::move(other.d_ptr))
#else /* < Qt-5.12 */
    : d_ptr(other.d_ptr.take())
#endif /* >= Qt-5.12 */
{
}
#endif /* Q_COMPILER_RVALUE_REFS */

TagContainerSettings::~TagContainerSettings(void)
{
}

/*!
 * @brief Ensures private TagContainerSettings presence.
 *
 * @note Returns if private TagContainerSettings could not be allocated.
 */
#define ensureTagContainerSettingsPrivate(_x_) \
	do { \
		if (Q_UNLIKELY(d_ptr == Q_NULLPTR)) { \
			TagContainerSettingsPrivate *p = new (::std::nothrow) TagContainerSettingsPrivate; \
			if (Q_UNLIKELY(p == Q_NULLPTR)) { \
				Q_ASSERT(0); \
				return _x_; \
			} \
			d_ptr.reset(p); \
		} \
	} while (0)

TagContainerSettings &TagContainerSettings::operator=(const TagContainerSettings &other) Q_DECL_NOTHROW
{
	if (other.d_func() == Q_NULLPTR) {
		d_ptr.reset(Q_NULLPTR);
		return *this;
	}
	ensureTagContainerSettingsPrivate(*this);
	Q_D(TagContainerSettings);

	*d = *other.d_func();

	return *this;
}

#ifdef Q_COMPILER_RVALUE_REFS
TagContainerSettings &TagContainerSettings::operator=(TagContainerSettings &&other) Q_DECL_NOTHROW
{
	swap(*this, other);
	return *this;
}
#endif /* Q_COMPILER_RVALUE_REFS */

bool TagContainerSettings::operator==(const TagContainerSettings &other) const
{
	Q_D(const TagContainerSettings);
	if ((d == Q_NULLPTR) && ((other.d_func() == Q_NULLPTR))) {
		return true;
	} else if ((d == Q_NULLPTR) || ((other.d_func() == Q_NULLPTR))) {
		return false;
	}

	return *d == *other.d_func();
}

bool TagContainerSettings::operator!=(const TagContainerSettings &other) const
{
	return !operator==(other);
}

bool TagContainerSettings::isNull(void) const
{
	Q_D(const TagContainerSettings);
	return d == Q_NULLPTR;
}

enum TagContainer::Backend TagContainerSettings::backend(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return DFLT_BACKEND;
	}

	return d->m_backend;
}

void TagContainerSettings::setBackend(enum TagContainer::Backend b)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_backend = b;
}

const QString &TagContainerSettings::clientHostname(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_clientHostname;
}

void TagContainerSettings::setClientHostname(const QString &h)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_clientHostname = h;
}

#ifdef Q_COMPILER_RVALUE_REFS
void TagContainerSettings::setClientHostname(QString &&h)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_clientHostname = ::std::move(h);
}
#endif /* Q_COMPILER_RVALUE_REFS */

int TagContainerSettings::clientPort(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return DFLT_PORT;
	}

	return d->m_clientPort;
}

void TagContainerSettings::setClientPort(int p)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_clientPort = p;
}

const QString &TagContainerSettings::certPath(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_certPath;
}

void TagContainerSettings::setCertPath(const QString &p)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_certPath = p;
}

#ifdef Q_COMPILER_RVALUE_REFS
void TagContainerSettings::setCertPath(QString &&p)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_certPath = ::std::move(p);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QString &TagContainerSettings::username(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_username;
}

void TagContainerSettings::setUsername(const QString &n)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_username = n;
}

#ifdef Q_COMPILER_RVALUE_REFS
void TagContainerSettings::setUsername(QString &&n)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_username = ::std::move(n);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QString &TagContainerSettings::password(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_password;
}

void TagContainerSettings::setPassword(const QString &pwd)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_password = pwd;
}

#ifdef Q_COMPILER_RVALUE_REFS
void TagContainerSettings::setPassword(QString &&pwd)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_password = ::std::move(pwd);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QString &TagContainerSettings::pwdAlg(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_pwdAlg;
}

void TagContainerSettings::setPwdAlg(const QString &pwdAlg)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_pwdAlg = pwdAlg;
}

#ifdef Q_COMPILER_RVALUE_REFS
void TagContainerSettings::setPwdAlg(QString &&pwdAlg)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_pwdAlg = ::std::move(pwdAlg);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QByteArray &TagContainerSettings::pwdSalt(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullByteArray;
	}

	return d->m_pwdSalt;
}

void TagContainerSettings::setPwdSalt(const QByteArray &pwdSalt)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_pwdSalt = pwdSalt;
}

#ifdef Q_COMPILER_RVALUE_REFS
void TagContainerSettings::setPwdSalt(QByteArray &&pwdSalt)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_pwdSalt = ::std::move(pwdSalt);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QByteArray &TagContainerSettings::pwdIv(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullByteArray;
	}

	return d->m_pwdIv;
}

void TagContainerSettings::setPwdIv(const QByteArray &pwdIv)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_pwdIv = pwdIv;
}

#ifdef Q_COMPILER_RVALUE_REFS
void TagContainerSettings::setPwdIv(QByteArray &&pwdIv)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_pwdIv = ::std::move(pwdIv);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QByteArray &TagContainerSettings::pwdCode(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullByteArray;
	}

	return d->m_pwdCode;
}

void TagContainerSettings::setPwdCode(const QByteArray &pwdCode)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_pwdCode = pwdCode;
}

#ifdef Q_COMPILER_RVALUE_REFS
void TagContainerSettings::setPwdCode(QByteArray &&pwdCode)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_pwdCode = ::std::move(pwdCode);
}
#endif /* Q_COMPILER_RVALUE_REFS */

qint64 TagContainerSettings::profileId(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return DFLT_PROFILE_ID;
	}

	return d->m_profileId;
}

void TagContainerSettings::setProfileId(qint64 i)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_profileId = i;
}

const QString &TagContainerSettings::profileName(void) const
{
	Q_D(const TagContainerSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_profileName;
}

void TagContainerSettings::setProfileName(const QString &n)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_profileName = n;
}

#ifdef Q_COMPILER_RVALUE_REFS
void TagContainerSettings::setProfileName(QString &&n)
{
	ensureTagContainerSettingsPrivate();
	Q_D(TagContainerSettings);
	d->m_profileName = ::std::move(n);
}
#endif /* Q_COMPILER_RVALUE_REFS */

void TagContainerSettings::decryptPassword(const QString &oldPin)
{
	if (!password().isEmpty()) {
		/* Password already stored in decrypted form. */
		logDebugLv0NL(
		    "Tag client password for username '%s' already held in decrypted form.",
		    username().toUtf8().constData());
		return;
	}

	if (oldPin.isEmpty()) {
		/*
		 * Old PIN not given, password should already be in plain
		 * format.
		 */
		logDebugLv0NL(
		    "No PIN supplied to decrypt tag client password for username '%s'.",
		    username().toUtf8().constData());
		return;
	}

	if (!pwdAlg().isEmpty() && !pwdSalt().isEmpty() &&
	    !pwdCode().isEmpty()) {
		logDebugLv0NL("Decrypting tag client password for username '%s'.",
		    username().toUtf8().constData());
		QString decrypted(decryptPwd(pwdCode(), oldPin, pwdAlg(),
		    pwdSalt(), pwdIv()));
		/* Store password. */
		if (!decrypted.isEmpty()) {
			setPassword(decrypted);
		} else {
			logWarningNL(
			    "Failed tag client decrypting password for username '%s'.",
			    username().toUtf8().constData());
		}
	}
}

/*!
 * @brief Converts string to enumeration value.
 */
static
enum TagContainer::Backend strToBackend(const QString &str)
{
	if (str == settingsBackendDbStr) {
		return TagContainer::BACK_DB;
	} else if (str == settingsBackendClientStr) {
		return TagContainer::BACK_CLIENT;
	} else {
		/* Default to local database. */
		logErrorNL("Unknown backend value '%s'.", str.toUtf8().constData());
		return TagContainer::BACK_DB;
	}
}

/*!
 * @brief Converts enumeration value to string.
 */
static
const QString &backendToStr(enum TagContainer::Backend backend)
{
	switch (backend) {
	case TagContainer::BACK_DB:
		return settingsBackendDbStr;
		break;
	case TagContainer::BACK_CLIENT:
		return settingsBackendClientStr;
		break;
	default:
		/* Default to local database. */
		logErrorNL("Unknown backend value '%d'.", backend);
		return settingsBackendDbStr;
		break;
	}
}

/*!
 * @brief Read password data.
 *
 * @note The PIN value may not be known when the settings are read. Therefore
 *     password decryption is performed somewhere else.
 * @todo Modify the programme to ensure that PIN is known at the time when
 *     account data is read.
 *
 * @param[in,out] cData Container data to store password data into.
 * @param[in]     settings Settings structure.
 * @param[in]     groupName Settings group name.
 */
static
void readPwdData(TagContainerSettings &cData, const QSettings &settings,
    const QString &groupName)
{
	QString prefix;
	if (!groupName.isEmpty()) {
		prefix = groupName + QLatin1String("/");
	}

	{
		QString pwd(settings.value(prefix + CredNames::pwd,
		    QString()).toString());
		cData.setPassword(
		    QString::fromUtf8(QByteArray::fromBase64(pwd.toUtf8())));
	}

	cData.setPwdAlg(settings.value(prefix + CredNames::pwdAlg,
	    QString()).toString());

	cData.setPwdSalt(QByteArray::fromBase64(
	    settings.value(prefix + CredNames::pwdSalt,
	        QString()).toString().toUtf8()));

	cData.setPwdIv(QByteArray::fromBase64(
	    settings.value(prefix + CredNames::pwdIv,
	        QString()).toString().toUtf8()));

	cData.setPwdCode(QByteArray::fromBase64(
	    settings.value(prefix + CredNames::pwdCode,
	        QString()).toString().toUtf8()));

	if (!cData.password().isEmpty() && !cData.pwdCode().isEmpty()) {
		logWarningNL(
		    "Tag client with username '%s' has both encrypted and unencrypted password set.",
		    cData.username().toUtf8().constData());
	}
}

void TagContainerSettings::loadFromSettings(const QSettings &settings)
{
	setBackend(strToBackend(settings.value(
	    SETTINGS_TAG_STORAGE_GROUP "/" SETTINGS_BACKEND,
	    settingsBackendDbStr).toString()));
	setClientHostname(settings.value(
	    SETTINGS_TAG_STORAGE_GROUP "/" SETTINGS_CLIENT_HOSTNAME,
	    QString()).toString());
	setClientPort(settings.value(
	    SETTINGS_TAG_STORAGE_GROUP "/" SETTINGS_CLIENT_PORT,
	    DFLT_PORT).toInt());
	setCertPath(settings.value(
	    SETTINGS_TAG_STORAGE_GROUP "/" SETTINGS_CLIENT_CERT_PATH,
	    QString()).toString());
	setUsername(settings.value(
	    SETTINGS_TAG_STORAGE_GROUP "/" SETTINGS_CLIENT_USERNAME,
	    QString()).toString());

	readPwdData(*this, settings, SETTINGS_TAG_STORAGE_GROUP);

	setProfileId(settings.value(
	    SETTINGS_TAG_STORAGE_GROUP "/" SETTINGS_CLIENT_PROFILE_ID,
	    DFLT_PROFILE_ID).toLongLong());
	setProfileName(settings.value(
	    SETTINGS_TAG_STORAGE_GROUP "/" SETTINGS_CLIENT_PROFILE_NAME,
	    QString()).toString());
}

/*!
 * @brief Stores encrypted password into settings.
 *
 * @param[in]     pinVal PIN value to be used for encryption.
 * @param[in,out] settings Settings structure.
 * @param[in]     cData Container data to be stored.
 * @param[in]     password Password to be stored.
 */
static
bool storeEncryptedPwd(const QString &pinVal, QSettings &settings,
    const TagContainerSettings &cData, const QString &password)
{
	/* Currently only one cryptographic algorithm is supported. */
	const struct pwd_alg *pwdAlgDesc = &aes256_cbc;

	/* Ignore the algorithm settings. */
	const QString pwdAlg(aes256_cbc.name);
	QByteArray pwdSalt(cData.pwdSalt());
	QByteArray pwdIV(cData.pwdIv());

	if (pwdSalt.size() < pwdAlgDesc->key_len) {
		pwdSalt = randomSalt(pwdAlgDesc->key_len);
	}

	if (pwdIV.size() < pwdAlgDesc->iv_len) {
		pwdIV = randomSalt(pwdAlgDesc->iv_len);
	}

	QByteArray pwdCode = encryptPwd(password, pinVal, pwdAlgDesc->name,
	    pwdSalt, pwdIV);
	if (Q_UNLIKELY(pwdCode.isEmpty())) {
		return false;
	}

	settings.setValue(CredNames::pwdAlg, pwdAlg);
	settings.setValue(CredNames::pwdSalt,
	    QString::fromUtf8(pwdSalt.toBase64()));
	settings.setValue(CredNames::pwdIv,
	    QString::fromUtf8(pwdIV.toBase64()));
	settings.setValue(CredNames::pwdCode,
	    QString::fromUtf8(pwdCode.toBase64()));

	return true;
}

void TagContainerSettings::saveToSettings(const QString &pinVal,
    QSettings &settings) const
{
	if ((!isNull()) &&
	    ((backend() != TagContainer::BACK_DB) ||
	     (!clientHostname().isEmpty()) ||
	     (!certPath().isEmpty()) ||
	     (!username().isEmpty()) ||
	     (!password().isEmpty()) ||
	     (profileId() >= 0))) {
		settings.beginGroup(SETTINGS_TAG_STORAGE_GROUP);

		/* Don't store local database value if used. */
		if (backend() != TagContainer::BACK_DB) {
			settings.setValue(SETTINGS_BACKEND, backendToStr(backend()));
		}

		if (!clientHostname().isEmpty()) {
			settings.setValue(SETTINGS_CLIENT_HOSTNAME, clientHostname());

			if ((clientPort() >= 0) && (clientPort() <= 65535)) {
				settings.setValue(SETTINGS_CLIENT_PORT, clientPort());
			}
		}

		if ((!certPath().isEmpty())) {
			settings.setValue(SETTINGS_CLIENT_CERT_PATH, certPath());
		}

		if (!username().isEmpty()) {
			settings.setValue(SETTINGS_CLIENT_USERNAME, username());
		}

		if (!password().isEmpty()) {
			bool writePlainPwd = pinVal.isEmpty();
			if (!writePlainPwd) {
				writePlainPwd = !storeEncryptedPwd(pinVal, settings,
				    *this, password());
			}
			if (writePlainPwd) { /* Only when plain or encryption fails. */
				/* Store unencrypted password. */
				settings.setValue(CredNames::pwd,
				    QString::fromUtf8(password().toUtf8().toBase64()));
			}
		}

		if (profileId() >= 0) {
			settings.setValue(SETTINGS_CLIENT_PROFILE_ID, profileId());
			if (!profileName().isEmpty()) {
				settings.setValue(SETTINGS_CLIENT_PROFILE_NAME, profileName());
			}
		}

		settings.endGroup();
	}
}

void swap(TagContainerSettings &first, TagContainerSettings &second) Q_DECL_NOTHROW
{
	using ::std::swap;
	swap(first.d_ptr, second.d_ptr);
}
