/*
 * 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 <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QString>

#include "src/datovka_shared/io/db_time.h"
#include "src/datovka_shared/io/message_db_tables.h"
#include "src/datovka_shared/isds/type_conversion.h"
#include "src/datovka_shared/log/log.h"
#include "src/db_repair/io/corrupt_message_db.h"

CorruptMessageDb::CorruptMessageDb(const QString &connectionName)
    : MessageDb(connectionName)
{
}

bool CorruptMessageDb::openDb(const QString &fileName,
    enum SQLiteDb::OpenFlag flag)
{
	bool ret = DelayedAccessSQLiteDb::openDb(fileName, flag);

	/* Directly force database opening. */
	ret = ret && SQLiteDb::openDb(fileName, flag);
	if (ret) {
		Q_EMIT opened(fileName);
	}

	return ret;
}

/*!
 * @brief Check whether a table with name \a tabName is present.
 *
 * @param[in] db Database.
 * @param[in] tabName Table name to search for.
 * @return True if table can be found.
 */
static
bool tablePresent(const QSqlDatabase &db, const QString &tabName)
{
	bool contains = false;

	QSqlQuery query(db);
	QString queryStr = "SELECT EXISTS(SELECT 1 FROM sqlite_master "
	    "WHERE (type = :type) AND (name = :name))";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":type", QString("table"));
	query.bindValue(":name", tabName);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		contains = query.value(0).toBool();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return contains;

fail:
	return false;
}

/*!
 * @brief Get the SQL code to create a table with the name \'a tabName.
 *
 * @param[in] db Database.
 * @param[in] tabName Table name to search for.
 * @return String of SQL code if table is present.
 */
static
QString createTableSql(const QSqlDatabase &db, const QString &tabName)
{
	QString createTableSql;

	QSqlQuery query(db);
	QString queryStr = "SELECT sql FROM sqlite_master "
	    "WHERE (type = :type) and (name = :name)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":type", QString("table"));
	query.bindValue(":name", tabName);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			createTableSql = query.value(0).toString();
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (Q_UNLIKELY(createTableSql.isEmpty())) {
		goto fail;
	}

fail:
	return createTableSql;
}

bool CorruptMessageDb::containsKnownTables(void)
{
	QMutexLocker locker(&m_lock);

	const QSqlDatabase &db = DelayedAccessSQLiteDb::accessDb();

	bool hasContent = tablePresent(db, MessageDbTables::msgsTbl.tabName);
	hasContent = hasContent || tablePresent(db, MessageDbTables::rwmsgdtTbl.tabName);
	hasContent = hasContent || tablePresent(db, MessageDbTables::rwdlvrinfdtTbl.tabName);

	return hasContent;
}

bool CorruptMessageDb::knowsDmVodz(void)
{
	const QSqlDatabase &db = DelayedAccessSQLiteDb::accessDb();
	const QString sqlStr = createTableSql(db, "supplementary_message_data");
	return sqlStr.contains("dmVODZ", Qt::CaseSensitive);
}

Isds::Envelope CorruptMessageDb::getMessageEnvelopeNoDmVodz(qint64 dmId)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	Isds::Envelope envelope;
	QList<Isds::Event> events;

	QString queryStr = "SELECT dmAnnotation, "
	    "dbIDSender, dmSender, dmSenderAddress, dmSenderType, "
	    "dmSenderOrgUnit, dmSenderOrgUnitNum, dmSenderRefNumber, "
	    "dmSenderIdent, dbIDRecipient, dmRecipient, dmRecipientAddress, "
	    "dmRecipientOrgUnit, dmRecipientOrgUnitNum, dmAmbiguousRecipient, "
	    "dmRecipientRefNumber, dmRecipientIdent, dmLegalTitleLaw, "
	    "dmLegalTitleYear, dmLegalTitleSect, dmLegalTitlePar, "
	    "dmLegalTitlePoint, dmToHands, dmPersonalDelivery, "
	    "dmAllowSubstDelivery, dmQTimestamp, dmDeliveryTime, "
	    "dmAcceptanceTime, dmMessageStatus, dmAttachmentSize, _dmType "
	    "FROM messages WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		envelope.setDmId(dmId);
		envelope.setDmID(QString::number(dmId));
		envelope.setDmAnnotation(query.value(0).toString());
		envelope.setDbIDSender(query.value(1).toString());
		envelope.setDmSender(query.value(2).toString());
		envelope.setDmSenderAddress(query.value(3).toString());
		envelope.setDmSenderType(
		    Isds::intVariant2DbType(query.value(4)));
		envelope.setDmSenderOrgUnit(query.value(5).toString());
		envelope.setDmSenderOrgUnitNumStr(query.value(6).toString());
		envelope.setDmSenderRefNumber(query.value(7).toString());
		envelope.setDmSenderIdent(query.value(8).toString());
		envelope.setDbIDRecipient(query.value(9).toString());
		envelope.setDmRecipient(query.value(10).toString());
		envelope.setDmRecipientAddress(query.value(11).toString());
		envelope.setDmRecipientOrgUnit(query.value(12).toString());
		envelope.setDmRecipientOrgUnitNumStr(
		    query.value(13).toString());
		envelope.setDmAmbiguousRecipient(
		    Isds::variant2NilBool(query.value(14)));
		envelope.setDmRecipientRefNumber(query.value(15).toString());
		envelope.setDmRecipientIdent(query.value(16).toString());
		envelope.setDmLegalTitleLawStr(query.value(17).toString());
		envelope.setDmLegalTitleYearStr(query.value(18).toString());
		envelope.setDmLegalTitleSect(query.value(19).toString());
		envelope.setDmLegalTitlePar(query.value(20).toString());
		envelope.setDmLegalTitlePoint(query.value(21).toString());
		envelope.setDmToHands(query.value(22).toString());
		envelope.setDmPersonalDelivery(
		     Isds::variant2NilBool(query.value(23)));
		envelope.setDmAllowSubstDelivery(
		    Isds::variant2NilBool(query.value(24)));
		envelope.setDmQTimestamp(query.value(25).toByteArray());
		envelope.setDmDeliveryTime(
		    dateTimeFromDbFormat(query.value(26).toString()));
		envelope.setDmAcceptanceTime(
		    dateTimeFromDbFormat(query.value(27).toString()));
		envelope.setDmMessageStatus(
		    Isds::variant2DmState(query.value(28)));
		envelope.setDmAttachmentSize(query.value(29).toLongLong());
		envelope.setDmType(Isds::variant2Char(query.value(30)));
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Don't read dmVODZ as it may be missing. */

	/* Events may be empty. */
	queryStr = "SELECT dmEventTime, dmEventDescr"
	    " FROM events WHERE message_id = :dmID"
	    " ORDER BY dmEventTime ASC";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
	}
	query.bindValue(":dmID", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			Isds::Event event;
			event.setTime(dateTimeFromDbFormat(query.value(0).toString()));
			event.setDescr(query.value(1).toString());
			events.append(event);
			query.next();
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
	}

	envelope.setDmEvents(events);
	return envelope;

fail:
	return Isds::Envelope();
}

void CorruptMessageDb::collectDmIdsFromMsgTable(QSet<qint64> &all)
{
	all.clear();

	QSet<qint64> dmIds;

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr = "SELECT dmID FROM messages";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			all.insert(query.value(0).toLongLong());
			query.next();
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	return;
}

/*!
 * @brief Read all message identifiers in table \a tabName.
 *
 * @param[in] db Database.
 * @param[in] tabName Table name to search for.
 * @return Set of identifiers.
 */
static
QSet<qint64> collectAllMessageIdentifiers(const QSqlDatabase &db,
    const QString &tabName)
{
	QSet<qint64> dmIds;

	QSqlQuery query(db);

	/* Table name cannot be inserted using bindValue(). */
	QString queryStr = QString("SELECT message_id FROM %1").arg(tabName);
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			dmIds.insert(query.value(0).toLongLong());
			query.next();
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	return dmIds;
}

/*!
 * @brief Read all message identifiers in table \a tabName with specified
 *     direction.
 *
 * @param[in] db Database.
 * @param[in] tabName Table name to search for.
 * @param[in] type Message direction.
 * @return Set of identifiers.
 */
static
QSet<qint64> collectMessageIdentifiersOfType(const QSqlDatabase &db,
    const QString &tabName, enum MessageDb::MessageType type)
{
	QSet<qint64> dmIds;

	QSqlQuery query(db);

	/* Table name cannot be inserted using bindValue(). */
	QString queryStr = QString("SELECT message_id FROM %1 "
	    "WHERE (message_type = :message_type)").arg(tabName);
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", type);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			dmIds.insert(query.value(0).toLongLong());
			query.next();
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	return dmIds;
}

void CorruptMessageDb::collectDmIdsFromRawMsgTable(
    QSet<qint64> &received, QSet<qint64> &sent, QSet<qint64> &unknown)
{
	QMutexLocker locker(&m_lock);

	const QSqlDatabase &db = DelayedAccessSQLiteDb::accessDb();

	received = collectMessageIdentifiersOfType(db,
	    "raw_message_data", MessageDb::TYPE_RECEIVED);

	sent = collectMessageIdentifiersOfType(db,
	    "raw_message_data", MessageDb::TYPE_SENT);

	unknown = collectAllMessageIdentifiers(db,
	    "raw_message_data");

	/* Subtract send and received from all. */
	unknown.subtract(received);
	unknown.subtract(sent);
}

void CorruptMessageDb::collectDmIdsFromRawDelInfoTable(QSet<qint64> &all)
{
	QMutexLocker locker(&m_lock);

	const QSqlDatabase &db = DelayedAccessSQLiteDb::accessDb();

	all = collectAllMessageIdentifiers(db, "raw_delivery_info_data");
}

void CorruptMessageDb::collectDmIdsFromSupplementary(QSet<qint64> &received,
    QSet<qint64> &sent, QSet<qint64> &unknown)
{
	QMutexLocker locker(&m_lock);

	const QSqlDatabase &db = DelayedAccessSQLiteDb::accessDb();

	received = collectMessageIdentifiersOfType(db,
	    "supplementary_message_data", MessageDb::TYPE_RECEIVED);

	sent = collectMessageIdentifiersOfType(db,
	    "supplementary_message_data", MessageDb::TYPE_SENT);

	unknown = collectAllMessageIdentifiers(db,
	    "supplementary_message_data");

	/* Subtract send and received from all. */
	unknown.subtract(received);
	unknown.subtract(sent);
}

void CorruptMessageDb::collectDmIdsFromSupplementaryVODZ(
    QSet<qint64> &highVolume)
{
	highVolume.clear();

	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr = "SELECT message_id, dmVODZ "
	    "FROM supplementary_message_data";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			if (query.value(1).toBool()) {
				highVolume.insert(query.value(0).toLongLong());
			}
			query.next();
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	return;
}

QByteArray CorruptMessageDb::getCompleteMessageRawDirect(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr = "SELECT data FROM raw_message_data "
	    "WHERE (message_id = :dmId)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			return QByteArray::fromBase64(query.value(0).toByteArray());
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	return QByteArray();
}

QByteArray CorruptMessageDb::getDeliveryInfoRawDirect(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr = "SELECT data FROM raw_delivery_info_data "
	    "WHERE (message_id = :dmId)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s; %s.",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			return QByteArray::fromBase64(query.value(0).toByteArray());
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query: %s; %s",
		    query.lastQuery().toUtf8().constData(),
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	return QByteArray();
}
