/*
 * 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 <QMap>
#include <QSet>
#include <utility> /* ::std::move */

#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/records_management/json/upload_hierarchy.h"
#include "src/records_management/content/automatic_upload_target.h"

/*!
 * @brief PIMPL AutomaticUploadTarget class.
 */
class RecMgmt::AutomaticUploadTargetPrivate {
public:
	AutomaticUploadTargetPrivate(void)
	    : m_caseIdTgts(), m_courtCaseIdTgts(),
	    m_ambiguousCaseIds(), m_ambiguousCourtCaseIds()
	{ }

	AutomaticUploadTargetPrivate &operator=(
	    const AutomaticUploadTargetPrivate &other) Q_DECL_NOTHROW
	{
		m_caseIdTgts = other.m_caseIdTgts;
		m_courtCaseIdTgts = other.m_courtCaseIdTgts;
		m_ambiguousCaseIds = other.m_ambiguousCaseIds;
		m_ambiguousCourtCaseIds = other.m_ambiguousCourtCaseIds;

		return *this;
	}

	QMap<QString, QString> m_caseIdTgts; /*< Maps case identifiers to hierarchy identifiers. */
	QMap<QString, QString> m_courtCaseIdTgts; /*!< Maps court case identifiers to hierarchy identifiers. */
	QSet<QString> m_ambiguousCaseIds; /*!< Ambiguous case identifiers. */
	QSet<QString> m_ambiguousCourtCaseIds; /*!< Ambiguous court case identifiers. */
};

RecMgmt::AutomaticUploadTarget::AutomaticUploadTarget(void)
    : d_ptr(Q_NULLPTR)
{
}

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

	*d = *other.d_func();
}

#ifdef Q_COMPILER_RVALUE_REFS
RecMgmt::AutomaticUploadTarget::AutomaticUploadTarget(AutomaticUploadTarget &&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 */


RecMgmt::AutomaticUploadTarget::~AutomaticUploadTarget(void)
{
}

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

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

	*d = *other.d_func();

	return *this;
}

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

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

bool RecMgmt::AutomaticUploadTarget::haveCaseId(const QString &cId) const
{
	if (Q_UNLIKELY(cId.isEmpty())) {
		/* No Q_ASSERT here. */
		return false;
	}

	Q_D(const AutomaticUploadTarget);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return false;
	}

	return d->m_caseIdTgts.contains(cId);
}

QString RecMgmt::AutomaticUploadTarget::hierarchyForCaseId(const QString &cId) const
{
	if (Q_UNLIKELY(cId.isEmpty())) {
		/* No Q_ASSERT here. */
		return QString();
	}

	Q_D(const AutomaticUploadTarget);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return QString();
	}

	return d->m_caseIdTgts.value(cId, QString());
}

void RecMgmt::AutomaticUploadTarget::setHierarchyForCaseId(const QString &cId,
    const QString &hId)
{
	if (Q_UNLIKELY(cId.isEmpty() || hId.isEmpty())) {
		Q_ASSERT(0);
		return;
	}

	ensureAutomaticUploadTargetPrivate();
	Q_D(AutomaticUploadTarget);
	d->m_caseIdTgts.insert(cId, hId);
}

bool RecMgmt::AutomaticUploadTarget::haveCourtCaseId(const QString &ccId) const
{
	if (Q_UNLIKELY(ccId.isEmpty())) {
		/* No Q_ASSERT here. */
		return false;
	}

	Q_D(const AutomaticUploadTarget);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return false;
	}

	return d->m_courtCaseIdTgts.contains(ccId);
}

QString RecMgmt::AutomaticUploadTarget::hierarchyForCourtCaseId(
    const QString &ccId) const
{
	if (Q_UNLIKELY(ccId.isEmpty())) {
		/* No Q_ASSERT here. */
		return QString();
	}

	Q_D(const AutomaticUploadTarget);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return QString();
	}

	return d->m_courtCaseIdTgts.value(ccId, QString());
}

void RecMgmt::AutomaticUploadTarget::setHierarchyForCourtCaseId(
    const QString &ccId, const QString &hId)
{
	if (Q_UNLIKELY(ccId.isEmpty() || hId.isEmpty())) {
		Q_ASSERT(0);
		return;
	}

	ensureAutomaticUploadTargetPrivate();
	Q_D(AutomaticUploadTarget);
	d->m_courtCaseIdTgts.insert(ccId, hId);
}

bool RecMgmt::AutomaticUploadTarget::haveAmbiguousCaseId(
    const QString &cId) const
{
	if (Q_UNLIKELY(cId.isEmpty())) {
		/* No Q_ASSERT here. */
		return false;
	}

	Q_D(const AutomaticUploadTarget);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return false;
	}

	return d->m_ambiguousCaseIds.contains(cId);
}

void RecMgmt::AutomaticUploadTarget::insertAmbiguousCaseId(const QString &cId)
{
	if (Q_UNLIKELY(cId.isEmpty())) {
		Q_ASSERT(0);
		return;
	}

	ensureAutomaticUploadTargetPrivate();
	Q_D(AutomaticUploadTarget);
	d->m_ambiguousCaseIds.insert(cId);
}

bool RecMgmt::AutomaticUploadTarget::haveAmbiguousCourtCaseId(
    const QString &ccId) const
{
	if (Q_UNLIKELY(ccId.isEmpty())) {
		/* No Q_ASSERT here. */
		return false;
	}

	Q_D(const AutomaticUploadTarget);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return false;
	}

	return d->m_ambiguousCourtCaseIds.contains(ccId);
}

void RecMgmt::AutomaticUploadTarget::insertAmbiguousCourtCaseId(
    const QString &ccId)
{
	if (Q_UNLIKELY(ccId.isEmpty())) {
		Q_ASSERT(0);
		return;
	}

	ensureAutomaticUploadTargetPrivate();
	Q_D(AutomaticUploadTarget);
	d->m_ambiguousCourtCaseIds.insert(ccId);
}

/*!
 * @brief Collects all case and court-case hierarchy targets.
 *
 * @param[out] cTgts Map, key is the case id, value is a set of hierarchy targets.
 * @param[out] ccTgts Map, key is the court case id, value is a set of hierarchy targets.
 * @param[in]  node  Upload hierarchy node.
 */
static
void collectCaseTargetsRecursive(QMap<QString, QSet<QString> > &cTgts,
    QMap<QString, QSet<QString> > &ccTgts,
    const RecMgmt::UploadHierarchyResp::NodeEntry *node)
{
	if (Q_UNLIKELY(node == Q_NULLPTR)) {
		return;
	}

	if ((!node->id().isEmpty()) && (node->caseId().size() > 0)) {
		for (const QString &caseId : node->caseId()) {
			if (!caseId.isEmpty()) {
				cTgts[caseId].insert(node->id());
			} else {
				logWarningNL(
				    "Ignoring empty case_id for hierarchy entry with ID '%s'.",
				    node->id().toUtf8().constData());
			}
		}
	}
	if ((!node->id().isEmpty()) && (node->courtCaseId().size() > 0)) {
		for (const QString &courtCaseId : node->courtCaseId()) {
			if (!courtCaseId.isEmpty()) {
				ccTgts[courtCaseId].insert(node->id());
			} else {
				logWarningNL(
				    "Ignoring empty court_case_id for hierarchy entry with ID '%s'.",
				    node->id().toUtf8().constData());
			}
		}
	}

	for (const RecMgmt::UploadHierarchyResp::NodeEntry *n : node->sub()) {
		collectCaseTargetsRecursive(cTgts, ccTgts, n);
	}
}

/*!
 * @brief Constructs a printable mapping overview.
 *
 * @param[in] mapping Mapping of case IDs to upload hierarchy identifiers.
 * @return Printable string containing the mapping overview.
 */
static
QStringList printableMapping(const QMap<QString, QString> &mapping)
{
	QStringList strList;
	for (const QString &key : mapping.keys()) {
		strList.append(QStringLiteral("{['%1']->'%2'}").arg(key).arg(mapping[key]));
	}
	return strList;
}

QStringList RecMgmt::AutomaticUploadTarget::dumpCaseIdMapping(void) const
{
	Q_D(const AutomaticUploadTarget);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return QStringList();
	}

	return printableMapping(d->m_caseIdTgts);
}

QStringList RecMgmt::AutomaticUploadTarget::dumpCourtCaseIdMapping(void) const
{
	Q_D(const AutomaticUploadTarget);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return QStringList();
	}

	return printableMapping(d->m_courtCaseIdTgts);
}

RecMgmt::AutomaticUploadTarget RecMgmt::AutomaticUploadTarget::getUnambiguousTargets(
    const UploadHierarchyResp &resp)
{
	AutomaticUploadTarget targets;

	QMap<QString, QSet<QString> > caseTgts;
	QMap<QString, QSet<QString> > courtCaseTgts;

	collectCaseTargetsRecursive(caseTgts, courtCaseTgts, resp.root());

	QMap<QString, QSet<QString> >::const_iterator it;
	for (it = caseTgts.cbegin(); it != caseTgts.cend(); ++it) {
		const QString &key = it.key();
		if (it.value().size() == 1) {
			/* There is only one target tor this case id. */
			const QString val(*(it.value().cbegin()));
			if (Q_UNLIKELY(key.isEmpty() || val.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			targets.setHierarchyForCaseId(key, val);
		} else {
			targets.insertAmbiguousCaseId(key);
			logWarningNL(
			    "Ambiguous records management upload hierarchy target for case_id '%s'.",
			    key.toUtf8().constData());
		}
	}
	for (it = courtCaseTgts.cbegin(); it != courtCaseTgts.cend(); ++it) {
		const QString &key = it.key();
		if (it.value().size() == 1) {
			/* There is only one target for this court case id. */
			const QString val(*(it.value().cbegin()));
			if (Q_UNLIKELY(key.isEmpty() || val.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			targets.setHierarchyForCourtCaseId(key, val);
		} else {
			targets.insertAmbiguousCourtCaseId(key);
			logWarningNL(
			    "Ambiguous records management upload hierarchy target for court_case_id '%s'.",
			    key.toUtf8().constData());
		}
	}

	return targets;
}

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