/*
 * Copyright (c) 2010-2022 Belledonne Communications SARL.
 *
 * This file is part of Liblinphone
 * (see https://gitlab.linphone.org/BC/public/liblinphone).
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#ifndef _L_DB_TRANSACTION_H_
#define _L_DB_TRANSACTION_H_

#include <bctoolbox/defs.h>

#include "db/main-db-p.h"
#include "logger/logger.h"

// =============================================================================

#define L_DB_TRANSACTION_C(CONTEXT)                                                                                    \
	LinphonePrivate::DbTransactionInfo().set(__func__, CONTEXT) *[&](BCTBX_UNUSED(SmartTransaction & tr))

#define L_DB_TRANSACTION L_DB_TRANSACTION_C(this)

LINPHONE_BEGIN_NAMESPACE

class SmartTransaction {
public:
	SmartTransaction(soci::session *session, const char *name) : mSession(session), mName(name), mIsCommitted(false) {
		lDebug() << "Start transaction " << this << " in MainDb::" << mName << ".";
		mSession->begin();
	}

	~SmartTransaction() {
		if (!mIsCommitted) {
			lDebug() << "Rollback transaction " << this << " in MainDb::" << mName << ".";
			try {
				mSession->rollback();
			} catch (std::runtime_error &e) {
				lError() << "Error during rollback transaction " << this << " in MainDb::" << mName
				         << ". Error : " << e.what();
			}
		}
	}

	void commit() {
		if (mIsCommitted) {
			lError() << "Transaction " << this << " in MainDb::" << mName << " already committed!!!";
			return;
		}

		lDebug() << "Commit transaction " << this << " in MainDb::" << mName << ".";
		mIsCommitted = true;
		mSession->commit();
	}

private:
	soci::session *mSession;
	const char *mName;
	bool mIsCommitted;

	L_DISABLE_COPY(SmartTransaction);
};

struct DbTransactionInfo {
	DbTransactionInfo &set(const char *_name, const MainDb *_mainDb) {
		name = _name;
		mainDb = const_cast<MainDb *>(_mainDb);
		return *this;
	}

	const char *name = nullptr;
	MainDb *mainDb = nullptr;
};

template <typename Function>
class DbTransaction {
	using InternalReturnType =
	    typename std::remove_reference<decltype(std::declval<Function>()(std::declval<SmartTransaction &>()))>::type;

public:
	using ReturnType =
	    typename std::conditional<std::is_same<InternalReturnType, void>::value, bool, InternalReturnType>::type;

	DbTransaction(DbTransactionInfo &info, Function &&function) : mFunction(std::move(function)) {
		MainDb *mainDb = info.mainDb;
		const char *name = info.name;
		soci::session *session = mainDb->getPrivate()->dbSession.getBackendSession();

		try {
			SmartTransaction tr(session, name);
			mResult = exec<InternalReturnType>(tr);
		} catch (const soci::soci_error &e) {
			lWarning() << "Caught exception in MainDb::" << name << "(" << e.what() << ").";
			soci::soci_error::error_category category = e.get_error_category();
			if ((category == soci::soci_error::connection_error || category == soci::soci_error::unknown) &&
			    mainDb->forceReconnect()) {
				try {
					SmartTransaction tr(session, name);
					mResult = exec<InternalReturnType>(tr);
				} catch (const std::exception &e) {
					lError() << "Unable to execute query after reconnect in MainDb::" << name << "(" << e.what()
					         << ").";
				}
				return;
			}
			lError() << "Unhandled [" << getErrorCategoryAsString(category) << "] exception in MainDb::" << name
			         << ": `" << e.what() << "`.";
		} catch (const std::exception &e) {
			lError() << "Unhandled generic exception in MainDb::" << name << ": `" << e.what() << "`.";
		}
	}

	DbTransaction(DbTransaction &&DbTransaction) : mFunction(std::move(DbTransaction.mFunction)) {
	}

	operator ReturnType() const {
		return mResult;
	}

private:
	// Exec function with no return type.
	template <typename T>
	typename std::enable_if<std::is_same<T, void>::value, bool>::type exec(SmartTransaction &tr) const {
		mFunction(tr);
		return true;
	}

	// Exec function with return type.
	template <typename T>
	typename std::enable_if<!std::is_same<T, void>::value, T>::type exec(SmartTransaction &tr) const {
		return mFunction(tr);
	}

	static const char *getErrorCategoryAsString(soci::soci_error::error_category category) {
		switch (category) {
			case soci::soci_error::connection_error:
				return "CONNECTION ERROR";
			case soci::soci_error::invalid_statement:
				return "INVALID STATEMENT";
			case soci::soci_error::no_privilege:
				return "NO PRIVILEGE";
			case soci::soci_error::no_data:
				return "NO DATA";
			case soci::soci_error::constraint_violation:
				return "CONSTRAINT VIOLATION";
			case soci::soci_error::unknown_transaction_state:
				return "UNKNOWN TRANSACTION STATE";
			case soci::soci_error::system_error:
				return "SYSTEM ERROR";
			case soci::soci_error::unknown:
				return "UNKNOWN";
		}

		// Unreachable.
		L_ASSERT(false);
		return nullptr;
	}

	Function mFunction;
	ReturnType mResult{};

	L_DISABLE_COPY(DbTransaction);
};

template <typename Function>
typename DbTransaction<Function>::ReturnType operator*(DbTransactionInfo &info, Function &&function) {
	return DbTransaction<Function>(info, std::forward<Function>(function));
}

LINPHONE_END_NAMESPACE

#endif // ifndef _L_DB_TRANSACTION_H_
