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

#include "src/datovka_shared/identifiers/account_id.h"
#include "src/datovka_shared/utility/date_time.h"
#include "src/models/helper.h"
#include "src/models/message_timestamp_listing_model.h"

MsgTstListingModel::MsgTstListingModel(QObject *parent)
    : QAbstractTableModel(parent),
    m_entries(),
    m_restampNeededDays(RESTAMPING_INTERVAL_NEEDED),
    m_currentTime(),
    m_timeSuggestesRestamping(),
    m_timeNeededRestamping()
{
	updateCurrentTime();
}

int MsgTstListingModel::rowCount(const QModelIndex &parent) const
{
	if (parent.isValid()) {
		return 0;
	} else {
		return m_entries.size();
	}
}

int MsgTstListingModel::columnCount(const QModelIndex &parent) const
{
	if (parent.isValid()) {
		return 0;
	} else {
		return MAX_COLNUM;
	}
}

QVariant MsgTstListingModel::data(const QModelIndex &index, int role) const
{
	const int row = index.row();
	const int col = index.column();
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return QVariant();
	}
	if (Q_UNLIKELY((col < 0) || (col >= columnCount()))) {
		Q_ASSERT(0);
		return QVariant();
	}

	switch (role) {
	case Qt::DisplayRole:
		switch (col) {
		case COL_ACCOUNT_ID:
			return m_entries.at(row).tstEntry.username;
			break;
		case COL_MESSAGE_ID:
			return m_entries.at(row).tstEntry.msgId.dmId();
			break;
		case COL_ANNOTATION:
			return m_entries.at(row).msgEntry.dmAnnotation;
			break;
		case COL_EXPIRATION_TIME:
			{
				const QDateTime &expirationTime =
				    m_entries.at(row).tstEntry.tstExpiration;
				return expirationTime.isValid()
				    ? expirationTime.toString(Utility::dateTimeDisplayFormat)
				    : QString();
			}
			break;
		case COL_ERROR:
			return m_entries.at(row).error;
			break;
		default:
			Q_ASSERT(0);
			return QVariant();
			break;
		}
		break;

	case Qt::DecorationRole:
		return QVariant();
		break;

	case Qt::ToolTipRole:
		switch (col) {
		case COL_EXPIRATION_TIME:
			if (m_currentTime.isValid()) {
				const QDateTime &expirationTime =
				    m_entries.at(row).tstEntry.tstExpiration;
				if (expirationTime.isValid()) {
					if (expirationTime < m_currentTime) {
						return tr("Time stamp has expired and cannot be restamped.");
					} else if (expirationTime < m_timeNeededRestamping) {
						return tr("You should restamp the message soon.");
					} else if (expirationTime < m_timeSuggestesRestamping) {
						return tr("You can restamp the message.");
					} else {
						return tr("Time stamp is still valid.");
					}
				}
			}
			break;
		case COL_ERROR:
			return m_entries.at(row).error;
			break;
		default:
			break;
		}
		return QVariant();
		break;

	case Qt::ForegroundRole:
		switch (col) {
		case COL_EXPIRATION_TIME:
			if (m_currentTime.isValid()) {
				const QDateTime &expirationTime =
				    m_entries.at(row).tstEntry.tstExpiration;
				if (expirationTime.isValid()) {
					if (expirationTime < m_currentTime) {
						return notificationColour(RS_EXPIRED);
					} else if (expirationTime < m_timeNeededRestamping) {
						return notificationColour(RS_NEEDED);
					} else if (expirationTime < m_timeSuggestesRestamping) {
						return notificationColour(RS_SUGGESTED);
					}
				}
			}
			break;
		default:
			break;
		}
		return QVariant();
		break;

	case Qt::AccessibleTextRole:
		switch (col) {
		case COL_ACCOUNT_ID:
			return tr("Data-box username") + QLatin1String(" ")
			    + m_entries.at(row).tstEntry.username;
			break;
		case COL_MESSAGE_ID:
		case COL_ANNOTATION:
		case COL_EXPIRATION_TIME:
		case COL_ERROR:
			return headerData(index.column(), Qt::Horizontal).toString()
			    + QLatin1String(" ") + data(index).toString();
			break;
		default:
			Q_ASSERT(0);
			return QVariant();
			break;
		}
		break;

	case ROLE_PLAIN_DISPLAY:
		switch (col) {
		case COL_ACCOUNT_ID:
			return m_entries.at(row).tstEntry.username;
			break;
		case COL_MESSAGE_ID:
			return m_entries.at(row).tstEntry.msgId.dmId();
			break;
		case COL_ANNOTATION:
			return m_entries.at(row).msgEntry.dmAnnotation;
			break;
		case COL_EXPIRATION_TIME:
			return m_entries.at(row).tstEntry.tstExpiration;
			break;
		case COL_ERROR:
			return m_entries.at(row).error;
			break;
		default:
			Q_ASSERT(0);
			return QVariant();
			break;
		}
		break;

	case ROLE_PROXYSORT:
		switch (col) {
		case COL_ACCOUNT_ID:
			return ModelHelper::sortRank(
			    m_entries.at(row).tstEntry.username,
			    m_entries.at(row).tstEntry.msgId.dmId());
			break;
		case COL_MESSAGE_ID:
			return m_entries.at(row).tstEntry.msgId.dmId();
			break;
		case COL_ANNOTATION:
			return m_entries.at(row).msgEntry.dmAnnotation;
			break;
		case COL_EXPIRATION_TIME:
			return ModelHelper::sortRank(
			    m_entries.at(row).tstEntry.tstExpiration,
			    m_entries.at(row).tstEntry.msgId.dmId());
			break;
		case COL_ERROR:
			return m_entries.at(row).error;
			break;
		default:
			Q_ASSERT(0);
			return QVariant();
			break;
		}
		break;

	default:
		return QVariant();
		break;
	}
}

QVariant MsgTstListingModel::headerData(int section, Qt::Orientation orientation,
    int role) const
{
	Q_UNUSED(orientation);

	switch (role) {
	case Qt::DisplayRole:
		switch (section) {
		case COL_ACCOUNT_ID:
			return tr("Data Box");
			break;
		case COL_MESSAGE_ID:
			return tr("Message ID");
			break;
		case COL_ANNOTATION:
			return tr("Subject");
			break;
		case COL_EXPIRATION_TIME:
			return tr("Expiration Time");
			break;
		case COL_ERROR:
			return tr("Processing Error");
			break;
		default:
			Q_ASSERT(0);
			return QVariant();
			break;
		}
		break;

	case Qt::DecorationRole:
		return QVariant();
		break;

	case Qt::ToolTipRole:
		return QVariant();
		break;

	default:
		return QVariant();
		break;
	}
}

void MsgTstListingModel::appendData(const QList<TstEntry> &tstEntries,
    const QHash <AcntId, QHash<MsgId, MessageDb::SoughtMsg> > &msgEntries)
{
	if (Q_UNLIKELY(tstEntries.isEmpty())) {
		return;
	}

	beginInsertRows(QModelIndex(), rowCount(),
	    rowCount() + tstEntries.size() - 1);

	for (const TstEntry &tstEntry : tstEntries) {
		const AcntId acntId(tstEntry.username, (tstEntry.msgId.testEnv() == Isds::Type::BOOL_TRUE));

		QHash <AcntId, QHash<MsgId, MessageDb::SoughtMsg> >::const_iterator it1 = msgEntries.constFind(acntId);
		if (it1 != msgEntries.constEnd()) {
			const MsgId msgId(tstEntry.msgId.dmId(), tstEntry.msgId.deliveryTime());

			QHash<MsgId, MessageDb::SoughtMsg>::const_iterator it2 = it1.value().constFind(msgId);
			if (it2 != it1.value().constEnd()) {

				m_entries.append(Entry(tstEntry, it2.value(), QString()));
				continue;

			}
		}

		m_entries.append(Entry(tstEntry, MessageDb::SoughtMsg(), QString()));
	}

	endInsertRows();
}

void MsgTstListingModel::remove(const Json::MsgId2List &msgIds)
{
	QList<int> rowsDescending;

	/* COllect uniquer rows in descending order. */
	{
		QSet<int> rowSet;

		for (const Json::MsgId2 &msgId : msgIds) {
			int row = findRow(msgId);
			if (row >= 0) {
				rowSet.insert(row);
			}
		}

#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
		rowsDescending = QList<int>(rowSet.constBegin(), rowSet.constEnd());
#else /* < Qt-5.14.0 */
		rowsDescending = rowSet.toList();
#endif /* >= Qt-5.14.0 */

		::std::sort(rowsDescending.begin(), rowsDescending.end(), ::std::greater<int>());
	}

	for (int foundRow : rowsDescending) {
		beginRemoveRows(QModelIndex(), foundRow, foundRow);

		m_entries.removeAt(foundRow);

		endRemoveRows();
	}
}

void MsgTstListingModel::removeAllRows(void)
{
	beginResetModel();
	m_entries.clear();

	endResetModel();
}

void MsgTstListingModel::updateError(const AcntId &anctId,
    const MsgId &msgId, const QString &error)
{
	for (int row = 0; row < m_entries.size(); ++row) {
		const Entry &entry = m_entries.at(row);

		if ((entry.tstEntry.username == anctId.username())
		    && (entry.tstEntry.msgId.dmId() == msgId.dmId())) {
			m_entries[row].error = error;

			emit dataChanged(index(row, COL_ERROR), index(row, COL_ERROR));
			return;
		}
	}
}

void MsgTstListingModel::updateExpiration(const TstValidityHash &values)
{
	for (const Json::MsgId1 &key : values.keys()) {
		int foundRow = findRow(key);
		if (foundRow >= 0) {
			m_entries[foundRow].tstEntry.tstExpiration = values.value(key).tstExpiration;

			const QModelIndex changedIdx = index(foundRow, COL_EXPIRATION_TIME);
			emit dataChanged(changedIdx, changedIdx);
		}
	}
}

int MsgTstListingModel::rowOf(const AcntId &anctId, const MsgId &msgId) const
{
	int foundRow = -1;
	for (int row = 0; row < rowCount(); ++row) {
		const Entry &entry = m_entries.at(row);

		if ((entry.tstEntry.username == anctId.username())
		    && (entry.tstEntry.msgId.dmId() == msgId.dmId())) {
			foundRow = row;
			break;
		}
	}
	return foundRow;
}

const TstEntry &MsgTstListingModel::tstEntry(int row) const
{
	static const TstEntry invalidEntry;

	if (Q_UNLIKELY(row > rowCount())) {
		Q_ASSERT(0);
		return invalidEntry;
	}

	return m_entries.at(row).tstEntry;
}

const MessageDb::SoughtMsg &MsgTstListingModel::msgEntry(int row) const
{
	static const MessageDb::SoughtMsg invalidEntry;

	if (Q_UNLIKELY(row > rowCount())) {
		Q_ASSERT(0);
		return invalidEntry;
	}

	return m_entries.at(row).msgEntry;
}

const QColor &MsgTstListingModel::notificationColour(int restampingStatus)
{
	static const QColor colourInvalid;
	static const QColor colourSuggested("green");
	static const QColor colourNeeded("orange");
	static const QColor colourExpired("red");

	if (restampingStatus & RS_EXPIRED) {
		return colourExpired;
	} else if (restampingStatus & RS_NEEDED) {
		return colourNeeded;
	} else if (restampingStatus & RS_SUGGESTED) {
		return colourSuggested;
	} else {
		return colourInvalid;
	}
}

qint64 MsgTstListingModel::normaliseRestampingNeeded(qint64 restampNeededDays)
{
	if ((0 < restampNeededDays) && (restampNeededDays <= RESTAMPING_INTERVAL_SUGGESTED)) {
		return restampNeededDays;
	} else {
		return RESTAMPING_INTERVAL_NEEDED; /* Default value. */
	}
}

void MsgTstListingModel::setRestampingNeededDays(qint64 restampNeededDays)
{
	restampNeededDays = normaliseRestampingNeeded(restampNeededDays);

	if (m_restampNeededDays != restampNeededDays) {
		m_restampNeededDays = restampNeededDays;

		/* Don't update current time, just the intervals. */
		m_timeSuggestesRestamping = m_currentTime.addDays(RESTAMPING_INTERVAL_SUGGESTED);
		m_timeNeededRestamping = m_currentTime.addDays(m_restampNeededDays);

		QVector<int> roles(1);
		roles[0] = Qt::ForegroundRole;
		emit dataChanged(index(0, COL_EXPIRATION_TIME),
		    index(rowCount() - 1, COL_EXPIRATION_TIME), roles);
	}
}

void MsgTstListingModel::updateCurrentTime(void)
{
	m_currentTime = QDateTime::currentDateTime();
	m_timeSuggestesRestamping = m_currentTime.addDays(RESTAMPING_INTERVAL_SUGGESTED);
	m_timeNeededRestamping = m_currentTime.addDays(m_restampNeededDays);

	QVector<int> roles(1);
	roles[0] = Qt::ForegroundRole;
	emit dataChanged(index(0, COL_EXPIRATION_TIME),
	    index(rowCount() - 1, COL_EXPIRATION_TIME), roles);
}

int MsgTstListingModel::findRow(const Json::MsgId1 &msgId) const
{
	int foundRow = -1;
	for (int row = 0; row < rowCount(); ++row) {
		const Json::MsgId2 &msgId2 = m_entries.at(row).tstEntry.msgId;

		if ((msgId2.testEnv() == msgId.testEnv())
		        && (msgId2.dmId() == msgId.dmId())) {
			foundRow = row;
			break;
		}
	}
	return foundRow;
}

int MsgTstListingModel::findRow(const Json::MsgId2 &msgId) const
{
	int foundRow = -1;
	for (int row = 0; row < rowCount(); ++row) {
		const TstEntry &tstEntry = m_entries.at(row).tstEntry;

		if (tstEntry.msgId == msgId) {
			foundRow = row;
			break;
		}
	}
	return foundRow;
}
