/*
 * 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 <algorithm> /* ::std::sort */
#include <QRegularExpression>
#include <QSettings>

#include "src/datovka_shared/settings/account_container.h"

template <class T>
AcntSettingsMap<T>::AcntSettingsMap(enum AcntContainer::CredentialType credType)
    : AcntContainer(),
    QMap<AcntId, T>(),
    m_credType(credType)
{
}

template <class T>
void AcntSettingsMap<T>::loadFromSettings(const QString &confDir,
    const QSettings &settings)
{
	/* Clear present rows. */
	this->clear();

	T itemSettings;

	/* For all credentials. */
	foreach (const QString &group,
	    (m_credType == CT_REGULAR) ? sortedCredentialGroups(settings) : sortedShadowGroups(settings)) {
		itemSettings.clear();

		itemSettings.loadFromSettings(confDir, settings, group);

		/* Associate map with item node. */
		Q_ASSERT(!itemSettings.userName().isEmpty());
		AcntId acntId(itemSettings.userName(), itemSettings.isTestAccount());
		Q_ASSERT(acntId.isValid());
		this->operator[](acntId) = itemSettings;
	}
}

template <class T>
void AcntSettingsMap<T>::decryptAllPwds(const QString &pinVal)
{
	for (typename AcntSettingsMap<T>::iterator it = this->begin();
	     it != this->end(); ++it) {
		it->decryptPassword(pinVal);
	}
}

template <class T>
AcntId AcntSettingsMap<T>::acntIdFromUsername(const QString &username) const
{
	foreach (const AcntId &acntId, AcntSettingsMap<T>::keys()) {
		if (acntId.username() == username) {
			return acntId;
		}
	}

	return AcntId();
}

/*!
 * @brief Used for sorting credentials.
 *
 * @param[in] s1  credentials[0-9]*
 * @param[in] s2  credentials[0-9]*
 * @return True if s1 comes before s2.
 *
 * @note The number is taken by its value rather like a string of characters.
 * cred < cred1 < cred2 < ... < cred10 < ... < cred100 < ...
 */
static
bool credentialsLessThan(const QString &s1, const QString &s2)
{
	const QRegularExpression trailingNumRe("(.*[^0-9]+)*([0-9]+)");
	QString a1, a2;
	int n1, n2;

	QRegularExpressionMatch match = trailingNumRe.match(s1);
	if (match.hasMatch()) {
		a1 = match.captured(1);
		n1 = match.captured(2).toInt();
	} else {
		a1 = s1;
		n1 = -1;
	}

	match = trailingNumRe.match(s2);
	if (match.hasMatch()) {
		a2 = match.captured(1);
		n2 = match.captured(2).toInt();
	} else {
		a2 = s2;
		n2 = -1;
	}

	return (a1 != a2) ? (a1 < a2) : (n1 < n2);
}

/*!
 * @brief @brief Return a (numerically) sorted list of groups.
 *
 * @param[in] settings Setting to read from.
 * @param[in] groupBaseName Group base name.
 * @return Numerically sorted groups.
 */
static
QStringList sortedGroups(const QSettings &settings, const QString &groupBaseName)
{
	/* Exact match. */
	const QRegularExpression credRe("^" + groupBaseName + ".*$");

	QStringList credentialList;
	/* Get list of credentials. */
	foreach (const QString &group, settings.childGroups()) {
		/* Matches regular expression. */
		if ((group.length() > 0) &&
		    (credRe.match(group).capturedLength() == group.length())) {
			credentialList.append(group);
		}
	}

	/* Sort the credentials list. */
	::std::sort(credentialList.begin(), credentialList.end(), credentialsLessThan);

	return credentialList;
}

template <class T>
QStringList AcntSettingsMap<T>::sortedCredentialGroups(const QSettings &settings)
{
	return sortedGroups(settings, CredNames::regularCreds);
}

template <class T>
QStringList AcntSettingsMap<T>::sortedShadowGroups(const QSettings &settings)
{
	return sortedGroups(settings, CredNames::shadowCreds);
}

/*!
 * @brief Create a group name.
 *
 * @param[in] num Group number (greater than 0).
 * @param[in] groupBaseName Group base name.
 * @return Group name.
 */
static
QString groupName(int num, const QString &groupBaseName)
{
	if (Q_UNLIKELY(num < 1)) {
		Q_ASSERT(0);
	}

	if (num != 1) {
		return groupBaseName + QString::number(num);
	} else {
		return groupBaseName;
	}
}

template <class T>
QString AcntSettingsMap<T>::credentialGroupName(int num)
{
	return groupName(num, CredNames::regularCreds);
}

template <class T>
QString AcntSettingsMap<T>::shadowGroupName(int num)
{
	return groupName(num, CredNames::shadowCreds);
}

/*!
 * @brief Get the maximal number of the credentials group.
 *
 * @param[in] credentialGroups Group list.
 * @return Number of the last credentials group.
 */
static
int maxCredentialNum(const QStringList &credentialGroups,
    const QString &groupBaseName)
{
	/* Exact match. */
	const QRegularExpression credRe("^" + groupBaseName + ".*$");

	int num = 0;
	foreach (const QString &group, credentialGroups) {
		/* Must match regular expression. */
		if (Q_UNLIKELY(credRe.match(group).capturedLength() != group.length())) {
			Q_ASSERT(0);
			continue;
		}
		QString numStr(group);
		numStr.replace(groupBaseName, QString());
		if (numStr.isEmpty()) {
			if (1 > num) {
				num = 1; /* Group 1 has no number. */
			}
		} else {
			bool iOk = false;
			int readNum = numStr.toInt(&iOk);
			if (Q_UNLIKELY(!iOk)) {
				Q_ASSERT(0);
				continue;
			}
			if (readNum > num) {
				num = readNum;
			}
		}
	}
	return num;
}

template <class T>
void AcntSettingsMap<T>::addCredentialsTorso(QSettings &settings,
    const QString &accountName, const AcntId &acntId)
{
	if (Q_UNLIKELY(accountName.isEmpty() || (!acntId.isValid()))) {
		Q_ASSERT(0);
		return;
	}

	int credMax = maxCredentialNum(sortedCredentialGroups(settings),
	    CredNames::regularCreds);

	T acntSettings;

	acntSettings.setAccountName(accountName);
	acntSettings.setUserName(acntId.username());
	acntSettings.setTestAccount(acntId.testing());
	/* Use password login method without stored password. */
	acntSettings.setLoginMethod(T::LIM_UNAME_PWD);
	acntSettings.setRememberPwd(false);

	acntSettings.saveToSettings(QString(), QString(), settings,
	    credentialGroupName(credMax + 1));
}
