/**
 * Copyright (c) 2014-2026 Governikus GmbH & Co. KG, Germany
 */


#include "TcToken.h"

#include "AppSettings.h"

#include <QBuffer>
#include <QLoggingCategory>
#include <QRegularExpression>
#include <QXmlStreamReader>

using namespace governikus;

Q_DECLARE_LOGGING_CATEGORY(developermode)
Q_DECLARE_LOGGING_CATEGORY(secure)


TcToken::TcToken(const QSharedPointer<QIODevice>& pStream)
	: mSchemaConform(false)
	, mBinding()
	, mPathSecurityProtocol()
	, mPsk()
	, mSessionIdentifier()
	, mServerAddress()
	, mCommunicationErrorAddress()
	, mRefreshAddress()
{
	if (pStream && (pStream->isOpen() || pStream->open(QIODeviceBase::ReadOnly)))
	{
		parse(pStream);
	}
}


TcToken::TcToken(QByteArray pData) // clazy:exclude=function-args-by-ref
	: TcToken(QSharedPointer<QBuffer>::create(&pData))
{
}


void TcToken::parse(const QSharedPointer<QIODevice>& pStream)
{
	QXmlStreamReader reader(pStream.data());

	QString binding;
	QString pathSecurityProtocol;
	QString serverAddress;
	QString refreshAddress;
	QString communicationErrorAddress;
	QByteArray psk;
	QByteArray sessionIdentifier;

	for (reader.readNext(); !reader.isEndDocument() && !reader.hasError(); reader.readNext())
	{
		if (reader.isStartElement())
		{
			const QStringView currentName = reader.name();
			if (currentName == QLatin1String("Binding"))
			{
				binding = readElementValue(reader);
			}
			else if (currentName == QLatin1String("PathSecurity-Protocol"))
			{
				pathSecurityProtocol = readElementValue(reader);
			}
			else if (currentName == QLatin1String("PSK"))
			{
				psk = readElementValue(reader, false).toUtf8();
			}
			else if (currentName == QLatin1String("ServerAddress"))
			{
				serverAddress = readElementValue(reader);
			}
			else if (currentName == QLatin1String("SessionIdentifier"))
			{
				sessionIdentifier = readElementValue(reader).toUtf8();
			}
			else if (currentName == QLatin1String("CommunicationErrorAddress"))
			{
				communicationErrorAddress = readElementValue(reader);
			}
			else if (currentName == QLatin1String("RefreshAddress"))
			{
				refreshAddress = readElementValue(reader);
			}
		}
	}

	if (reader.hasError())
	{
		qDebug() << "XML error:" << reader.errorString();
	}
	else if (valuesAreSchemaConform(binding, pathSecurityProtocol, psk, sessionIdentifier, serverAddress, communicationErrorAddress, refreshAddress))
	{
		mSchemaConform = true;
		mBinding = binding;
		mPathSecurityProtocol = pathSecurityProtocol;
		mPsk = psk;
		mServerAddress.setUrl(serverAddress);
		mSessionIdentifier = sessionIdentifier;
		mCommunicationErrorAddress.setUrl(communicationErrorAddress);
		mRefreshAddress.setUrl(refreshAddress);
	}

	reader.clear();
}


QString TcToken::readElementValue(QXmlStreamReader& pReader, bool pLogValue) const
{
	// helper method to distinguish between the cases,
	// 1) where the element was present but empty (so the value is not null but empty) and
	// 2) where the element was absent (so the value is null).
	const auto& value = pReader.readElementText().trimmed();
	qDebug() << pReader.name()
			 << '='
			 << (pLogValue || secure().isDebugEnabled() ? value : QStringLiteral("(...)"));
	return value.isNull() ? QLatin1String("") : value;
}


bool TcToken::valuesAreSchemaConform(const QString& pBinding,
		const QString& pPathSecurityProtocol,
		const QByteArray& pPsk,
		const QByteArray& pSessionIdentifier,
		const QString& pServerAddress,
		const QString& pCommunicationErrorAddress,
		const QString& pRefreshAddress) const
{
	if (pBinding.isNull() || !isAnyUri(pBinding))
	{
		qCritical() << "Binding is no valid anyUri:" << pBinding;
		return false;
	}

	if (!pPathSecurityProtocol.isNull() && !QUrl(pPathSecurityProtocol).isValid())
	{
		qCritical() << "PathSecurity-Protocol is no valid URI:" << pPathSecurityProtocol;
	}

	if (pPsk.isNull())
	{
		// PSK is not needed for attached eID-Server model
		qWarning() << "PSK is null";
	}

	if (pSessionIdentifier.isNull())
	{
		qWarning() << "SessionIdentifier is null";
	}

	if (pServerAddress.isNull() || !isAnyUri(pServerAddress))
	{
		qCritical() << "ServerAddress no valid anyUri:" << pServerAddress;
		return false;
	}

	if (pRefreshAddress.isNull() || !isAnyUri(pRefreshAddress))
	{
		qCritical() << "RefreshAddress no valid anyUri:" << pRefreshAddress;
		return false;
	}

	if (!isAnyUri(pCommunicationErrorAddress))
	{
		qCritical() << "CommunicationErrorAddress no valid anyUri:" << pCommunicationErrorAddress;
		return false;
	}

	return true;
}


bool TcToken::isAnyUri(const QString& pCandidate) const
{
	return pCandidate.isEmpty() || QUrl(pCandidate).isValid();
}


bool TcToken::isValid() const
{
	if (!mSchemaConform)
	{
		return false;
	}

	if (mBinding != QLatin1String("urn:liberty:paos:2006-08"))
	{
		qCritical() << "Wrong binding:" << mBinding;
		return false;
	}

	if (!mPathSecurityProtocol.isNull() && mPathSecurityProtocol != QLatin1String("urn:ietf:rfc:4279"))
	{
		qCritical() << "Wrong PathSecurity-Protocol:" << mPathSecurityProtocol;
		return false;
	}

	if (mServerAddress.scheme() != QLatin1String("https"))
	{
		qCritical() << "ServerAddress no valid https url:" << mServerAddress;
		return false;
	}

	if (mRefreshAddress.scheme() != QLatin1String("https"))
	{
		const QString errorInvalidUrl = QStringLiteral("RefreshAddress no valid https url: %1").arg(mRefreshAddress.toString());
		if (Env::getSingleton<AppSettings>()->getGeneralSettings().isDeveloperMode())
		{
			qCCritical(developermode) << errorInvalidUrl;
		}
		else
		{
			qCritical() << errorInvalidUrl;
			return false;
		}
	}

	if (!mPsk.isNull() && !isHexBinary(QString::fromLatin1(mPsk)))
	{
		qCritical() << "PSK no valid hexBinary:" << mPsk;
		return false;
	}

	return true;
}


bool TcToken::isHexBinary(const QString& pCandidate) const
{
	static const QRegularExpression hexMatcher(QStringLiteral("^[0-9A-F]*$"), QRegularExpression::CaseInsensitiveOption);
	return pCandidate.isEmpty() || (hexMatcher.match(pCandidate).hasMatch() && pCandidate.size() % 2 == 0);
}


const QByteArray& TcToken::getSessionIdentifier() const
{
	return mSessionIdentifier;
}


const QUrl& TcToken::getServerAddress() const
{
	return mServerAddress;
}


const QUrl& TcToken::getRefreshAddress() const
{
	return mRefreshAddress;
}


const QUrl& TcToken::getCommunicationErrorAddress() const
{
	return mCommunicationErrorAddress;
}


bool TcToken::usePsk() const
{
	return !mPsk.isNull();
}


const QByteArray& TcToken::getPsk() const
{
	return mPsk;
}


bool TcToken::isSchemaConform() const
{
	return mSchemaConform;
}


const QString& TcToken::getBinding() const
{
	return mBinding;
}
