/*
 * 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 <cinttypes> /* PRId64 */
#include <QAction>
#include <QPushButton>
#include <QTextEdit>
#include <QVBoxLayout>

#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/settings/prefs.h"
#include "src/io/timestamp_db.h"
#include "src/models/message_timestamp_listing_model.h"
#include "src/widgets/timestamp_tracking_widget.h"

/*!
 * @brief Set status label.
 *
 * @param[out] label Label whose text has to be set.
 * @param[in] timestampDb Timestamp database.
 */
static
void setStatusLabel(QLabel &label, const TimestampDb &timestampDb)
{
	QString labelText;
	QString toolTiptext;

	qint64 identCnt = 0;
	if (Q_UNLIKELY(!timestampDb.getIdentificationCnt(identCnt))) {
		/** Error reading value. */
		label.setText(QString());
		label.setToolTip(QString());
		return;
	}

	if (identCnt == 0) {
		labelText = TimestampTrackingControl::tr("Timestamps: not tracking");
		toolTiptext = TimestampTrackingControl::tr("There are no entries whose timestamps are being tracked.");
	} else {
		labelText = TimestampTrackingControl::tr("Timestamps: tracking");
		toolTiptext = TimestampTrackingControl::tr("Timestamps of %n entrie(s) are being tracked.", "", identCnt);
	}

	label.setText(labelText + "   ");
	label.setToolTip(toolTiptext);
}

/*!
 * @brief Set content of text edit.
 *
 * @param[out] label Label whose stylesheet has to be set.
 * @param[out] textEdit Text edit whose content has to be set.
 * @param[in] timestampDb Timestamp database.
 * @param[in] restampNeededDays Interval of days to notify about imminent
 *                              restamping.
 * @return True if there is a warning (expired or needed).
 */
static
bool setOverviewText(QLabel &label, QTextEdit &textEdit,
    const TimestampDb &timestampDb, qint64 restampNeededDays)
{
	int flags = MsgTstListingModel::RS_NORMAL;

	const QDateTime nullTime;
	const QDateTime timeNow = QDateTime::currentDateTime();

	qint64 totalMsgCnt = 0;
	qint64 suggestedMsgCnt = 0; /* Number of messages where restamping is suggested. */
	qint64 neededMsgCnt = 0; /* Number of messages where restamping is needed. */
	qint64 expiredMsgCnt = 0; /* Number of messages with expired timestamps. */

	const QDateTime timeSuggestesRestamping = timeNow.addDays(RESTAMPING_INTERVAL_SUGGESTED);
	const QDateTime timeNeededRestamping = timeNow.addDays(restampNeededDays);

	timestampDb.getMsgTimestampCnt(expiredMsgCnt, nullTime, timeNow);
	timestampDb.getMsgTimestampCnt(neededMsgCnt, timeNow, timeNeededRestamping);
	timestampDb.getMsgTimestampCnt(suggestedMsgCnt, timeNeededRestamping, timeSuggestesRestamping);
	timestampDb.getMsgTimestampCnt(totalMsgCnt, nullTime, nullTime);

	QString html;

	QString totalStr;
	QString suggestedStr;
	QString neededStr;
	QString expiredStr;

	if (expiredMsgCnt > 0) {
		expiredStr = TimestampTrackingControl::tr("Number of messages which cannot be restamped because of already expired timestamp: %1").arg(expiredMsgCnt);
		html += QString("<font color=\"%1\">%2</font><br/><br/>")
		    .arg(MsgTstListingModel::notificationColour(MsgTstListingModel::RS_EXPIRED).name(QColor::HexRgb))
		    .arg(expiredStr);
		flags |= MsgTstListingModel::RS_EXPIRED;
	}
	if (neededMsgCnt > 0) {
		neededStr = TimestampTrackingControl::tr("Number of messages which need restamping: %1").arg(neededMsgCnt);
		html += QString("<font color=\"%1\">%2</font><br/><br/>")
		    .arg(MsgTstListingModel::notificationColour(MsgTstListingModel::RS_NEEDED).name(QColor::HexRgb))
		    .arg(neededStr);
		flags |= MsgTstListingModel::RS_NEEDED;
	}
	if (suggestedMsgCnt > 0) {
		suggestedStr = TimestampTrackingControl::tr("Number of messages whose timestamps could be restamped: %1").arg(suggestedMsgCnt);
		html += QString("<font color=\"%1\">%2</font><br/><br/>")
		    .arg(MsgTstListingModel::notificationColour(MsgTstListingModel::RS_SUGGESTED).name(QColor::HexRgb))
		    .arg(suggestedStr);
		flags |= MsgTstListingModel::RS_SUGGESTED;
	}
	if (totalMsgCnt >= 0) {
		totalStr = TimestampTrackingControl::tr("Total number of tracked messages: %1").arg(totalMsgCnt) + "<br/>";
		html += QString("%1<br/><br/>").arg(totalStr);
	}

	textEdit.clear();

	if (!html.isEmpty()) {
		textEdit.setHtml(html);
	}

	if (flags & MsgTstListingModel::RS_EXPIRED) {
		label.setStyleSheet(QString("QLabel { color: %1 }")
		    .arg(MsgTstListingModel::notificationColour(MsgTstListingModel::RS_EXPIRED).name(QColor::HexRgb)));
	} else if (flags & MsgTstListingModel::RS_NEEDED) {
		label.setStyleSheet(QString("QLabel { color: %1 }")
		    .arg(MsgTstListingModel::notificationColour(MsgTstListingModel::RS_NEEDED).name(QColor::HexRgb)));
	} else if (flags & MsgTstListingModel::RS_SUGGESTED) {
		label.setStyleSheet(QString("QLabel { color: %1 }")
		    .arg(MsgTstListingModel::notificationColour(MsgTstListingModel::RS_SUGGESTED).name(QColor::HexRgb)));
	} else {
		label.setStyleSheet(QString()); /* Default. */
	}

	return flags & (MsgTstListingModel::RS_EXPIRED | MsgTstListingModel::RS_NEEDED);
}

#define LAYOUT_MARGIN_PX 3

#define FIRST_START_CHECK_SECS 10
#define PERIODIC_CHECK_SECS 7200 /* 2 hours */

TimestampTrackingControl::TimestampTrackingControl(TimestampDb *timestampDb,
    DbContainer *msgDbs, QAction *actionTimeStampRenewal, Prefs *prefsPtr,
    QWidget *parent)
    : QLabel(parent),
    m_timestampDb(timestampDb),
    m_msgDbs(msgDbs),
    m_tstPopup(Q_NULLPTR),
    m_textEdit(Q_NULLPTR),
    m_timestampRenewalButton(Q_NULLPTR),
    m_actionTimeStampRenewal(actionTimeStampRenewal),
    m_restampNeededDays(RESTAMPING_INTERVAL_NEEDED),
    m_checkTimer(this),
    m_popupRaisedOnWarning(false)
{
	if (Q_NULLPTR != m_timestampDb) {
		connect(m_timestampDb, SIGNAL(identificationsInserted(Json::MsgId2List, AcntId)),
		    this, SLOT(watchIdentificationsInserted(Json::MsgId2List, AcntId)));
		connect(m_timestampDb, SIGNAL(identificationsDeleted(Json::MsgId2List)),
		    this, SLOT(watchIdentificationsDeleted(Json::MsgId2List)));
		connect(m_timestampDb, SIGNAL(msgTimestampDataUpdated(TstValidityHash)),
		    this, SLOT(watchMsgTimestampDataUpdated(TstValidityHash)),
		    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */

		setStatusLabel(*this, *m_timestampDb);

		connect(&m_checkTimer, SIGNAL(timeout()),
		    this, SLOT(periodicStatusCheck()));
	}

	QVBoxLayout *verticalLayout = Q_NULLPTR;

	m_tstPopup = new (::std::nothrow) QWidget(parentWidget());
	if (Q_NULLPTR != m_tstPopup) {
		m_tstPopup->setObjectName(QString::fromUtf8("m_tstPopup"));
		m_tstPopup->setWindowFlags(Qt::Widget | Qt::Popup);

		m_textEdit = new (::std::nothrow) QTextEdit(m_tstPopup);
		if (Q_NULLPTR != m_textEdit) {
			m_textEdit->setObjectName(QString::fromUtf8("m_textEdit"));

			m_textEdit->setReadOnly(true);
		}

		if (Q_NULLPTR != m_actionTimeStampRenewal) {
			m_timestampRenewalButton = new (::std::nothrow) QPushButton(m_tstPopup);
			if (Q_NULLPTR != m_timestampRenewalButton) {
				m_timestampRenewalButton->setObjectName(QString::fromUtf8("m_timestampRenewalButton"));

				m_timestampRenewalButton->setIcon(m_actionTimeStampRenewal->icon());
				m_timestampRenewalButton->setText(m_actionTimeStampRenewal->text());
				m_timestampRenewalButton->setToolTip(m_actionTimeStampRenewal->toolTip());

				connect(m_timestampRenewalButton, SIGNAL(clicked()),
				   this, SLOT(callActionTimeStampRenewal()));
			}
		}

		verticalLayout = new (::std::nothrow) QVBoxLayout(m_tstPopup);
		if (Q_NULLPTR != verticalLayout) {
			verticalLayout->setObjectName(QString::fromUtf8("verticalLayout"));

			verticalLayout->setContentsMargins(
			    LAYOUT_MARGIN_PX, LAYOUT_MARGIN_PX,
			    LAYOUT_MARGIN_PX, LAYOUT_MARGIN_PX);

			if (Q_NULLPTR != m_textEdit) {
				verticalLayout->addWidget(m_textEdit);
			}

			if (Q_NULLPTR != m_timestampRenewalButton) {
				verticalLayout->addWidget(m_timestampRenewalButton);
			}
		}
	}

	if (Q_NULLPTR != prefsPtr) {
		/* Load current notification value. */
		qint64 val = 0;
		if (prefsPtr->intVal("crypto.message.timestamp.expiration.notify_ahead.days", val)) {
			m_restampNeededDays = MsgTstListingModel::normaliseRestampingNeeded(val);
		}

		/* And watch changes. */
		connect(prefsPtr, SIGNAL(entrySet(int, QString, QVariant)),
		    this, SLOT(watchPrefsModification(int, QString, QVariant)));
	}

	if ((Q_NULLPTR != m_textEdit) && (Q_NULLPTR != m_timestampDb)) {
		setOverviewText(*this, *m_textEdit, *m_timestampDb,
		    m_restampNeededDays);
	}

	m_checkTimer.start(FIRST_START_CHECK_SECS * 1000);
}

TimestampTrackingControl::~TimestampTrackingControl(void)
{
	delete m_tstPopup;
}

void TimestampTrackingControl::mousePressEvent(QMouseEvent *event)
{
	Q_UNUSED(event);

	if (Q_NULLPTR != m_textEdit) {
		/* Only raise pop up if there is something to display. */
		raisePopup();
	}
}

void TimestampTrackingControl::periodicStatusCheck(void)
{
	generateOverviewAndPopupOnWarning();

	m_checkTimer.start(PERIODIC_CHECK_SECS * 1000);
}

void TimestampTrackingControl::callActionTimeStampRenewal(void)
{
	if (Q_NULLPTR != m_actionTimeStampRenewal) {
		m_actionTimeStampRenewal->trigger();
	}

	if (Q_NULLPTR != m_tstPopup) {
		m_tstPopup->close();
	}
}

void TimestampTrackingControl::watchIdentificationsInserted(
    const Json::MsgId2List &msgIds, const AcntId &acntId)
{
	Q_UNUSED(msgIds);
	Q_UNUSED(acntId);

	setStatusLabel(*this, *m_timestampDb);
	generateOverviewAndPopupOnWarning();
}

void TimestampTrackingControl::watchIdentificationsDeleted(
    const Json::MsgId2List &msgIds)
{
	Q_UNUSED(msgIds);

	setStatusLabel(*this, *m_timestampDb);
	generateOverviewAndPopupOnWarning();
}

void TimestampTrackingControl::watchMsgTimestampDataUpdated(
    const TstValidityHash &values)
{
	Q_UNUSED(values);

	generateOverviewAndPopupOnWarning();
}

void TimestampTrackingControl::watchPrefsModification(int valueType,
    const QString &name, const QVariant &value)
{
	switch (valueType) {
	case PrefsDb::VAL_INTEGER:
		if (name == "crypto.message.timestamp.expiration.notify_ahead.days") {
			bool ok = false;
			qint64 val = value.toLongLong(&ok);
			if (Q_UNLIKELY(!ok)) {
				logErrorNL("%s", "Cannot convert value.");
				return;
			}
			m_restampNeededDays = MsgTstListingModel::normaliseRestampingNeeded(val);
			generateOverviewAndPopupOnWarning();
		}
		break;
	default:
		/* Do nothing. */
		break;
	}
}

void TimestampTrackingControl::generateOverviewAndPopupOnWarning(void)
{
	if ((Q_NULLPTR != m_textEdit) && (Q_NULLPTR != m_timestampDb)) {
		const int haveWarning = setOverviewText(*this, *m_textEdit,
		    *m_timestampDb, m_restampNeededDays);

		const bool newWarning = (!m_popupRaisedOnWarning) && haveWarning;
		if (m_popupRaisedOnWarning != haveWarning) {
			m_popupRaisedOnWarning = haveWarning;
		}
		if (newWarning) {
			raisePopup();
		}
	}
}

void TimestampTrackingControl::raisePopup(void)
{
	static int viewItemWidth = -1;
	static int viewItemHeight = -1;
	if (Q_UNLIKELY(viewItemWidth < 0)) {
		QSize sizeHint = QLabel(
		    "Quite long string used for view size estimation and a little bit more")
		        .sizeHint();
		viewItemWidth = sizeHint.width() + 40; // 40 for borders etc.
		viewItemHeight = sizeHint.height() * 12; // 12 times the height of list entry
	}

	if (Q_NULLPTR != m_tstPopup) {
		const QPoint parentTopLeft = this->mapToGlobal(QPoint(0, 0));
		const int width = viewItemWidth;
		const int height = viewItemHeight;

		m_tstPopup->setGeometry(
		    parentTopLeft.x() - width + this->width(),
		    parentTopLeft.y() - height, width, height);
		m_tstPopup->show();
		m_tstPopup->raise();
	}
}
