/* Copyright 2020, Michele Santullo
 * This file is part of orotool.
 *
 * Orotool 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.
 *
 * Orotool 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 Orotool.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "orotool_config.hpp"
#if defined(OROTOOL_WITH_SQLITE)

#include "originsdb_sqlite.hpp"
#include "SQLiteCpp/Database.h"
#include "SQLiteCpp/Statement.h"
#include "SQLiteCpp/Transaction.h"
#include "SQLiteCpp/SQLiteCpp.h"
#include "oro/items.hpp"
#include "oro/icons.hpp"
#include "oro/shops.hpp"
#include "oro/creators.hpp"
#include "oro/private/dateconv.hpp"
#include "oro/private/tiger.hpp"
#include "oro/private/sqlite_helpers.hpp"
#include "duckhandy/int_conv.hpp"
#include "duckhandy/string_bt.hpp"
#include "magic_enum.hpp"
#include <mutex>
#include <optional>
#include <type_traits>
#include <unordered_map>
#include <cstdint>
#include <utility>

namespace oro {

namespace {
	typedef decltype(std::declval<SQLite::Column>().getInt64()) SQLiteID;

	constexpr const char g_create_fame_list[] =
		"CREATE TABLE IF NOT EXISTS fame_list("
			"id INTEGER PRIMARY KEY NOT NULL"
			", type TINYINT"
			", master_id INTEGER UNIQUE"
			", name TEXT"
			", points INTEGER"
			", source_id INTEGER NOT NULL"
		")";
	constexpr const char g_create_slotted_cards[] =
		"CREATE TABLE IF NOT EXISTS slotted_cards("
			"id INTEGER PRIMARY KEY NOT NULL"
			", shop_item_id INTEGER NOT NULL"
			", card_id INTEGER NOT NULL"
			", FOREIGN KEY(shop_item_id) REFERENCES shop_items(id)"
			", FOREIGN KEY(card_id) REFERENCES items(id)"
		")";
	constexpr const char g_create_shop_items[] =
		"CREATE TABLE IF NOT EXISTS shop_items("
			"id INTEGER PRIMARY KEY NOT NULL"
			", snapshot_id INTEGER NOT NULL"
			", item_id INTEGER NOT NULL"
			", amount INTEGER NOT NULL"
			", price INTEGER NOT NULL"
			", refine INTEGER NOT NULL"
			", star_crumbs INTEGER NOT NULL"
			", element TEXT"
			", creator INTEGER"
			", beloved TINYINT"
			", FOREIGN KEY(snapshot_id) REFERENCES shop_snapshots(id)"
			", FOREIGN KEY(item_id) REFERENCES items(id)"
		")";
	constexpr const char g_create_shop_snapshots[] =
		"CREATE TABLE IF NOT EXISTS shop_snapshots("
			"id INTEGER PRIMARY KEY NOT NULL"
			", shop_id INTEGER NOT NULL"
			", hash TEXT NOT NULL"
			", discovery_date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
			", last_seen_date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
			", source_id INTEGER NOT NULL"
			", FOREIGN KEY(shop_id) REFERENCES shops(id)"
		")";
	constexpr const char g_create_shops[] =
		"CREATE TABLE IF NOT EXISTS shops("
			"id INTEGER PRIMARY KEY NOT NULL"
			", title TEXT"
			", owner TEXT"
			", creation_date TEXT"
			", loc_map TEXT"
			", loc_x INTEGER"
			", loc_y INTEGER"
			", type TINYINT"
		")";
	constexpr const char g_create_items_a[] = "CREATE TABLE IF NOT EXISTS ";
	constexpr const char g_create_items_b[] =
		" ("
			"id INTEGER PRIMARY KEY NOT NULL"
			", item_id INTEGER"
			", unique_name TEXT"
			", name TEXT"
			", type TINYINT"
			", subtype TINYINT"
			", npc_price INTEGER"
			", slots TINYINT"
			", removal_date TEXT"
			", source_id INTEGER NOT NULL"
		")";
	constexpr const char g_create_icons_a[] = "CREATE TABLE IF NOT EXISTS ";
	constexpr const char g_create_icons_b[] =
		" ("
			"id INTEGER PRIMARY KEY NOT NULL"
			", item_id INTEGER NOT NULL"
			", icon TEXT"
			", source_id INTEGER NOT NULL"
			", FOREIGN KEY(item_id) REFERENCES items(id)"
		")";
	constexpr const char g_create_access[] =
		"CREATE TABLE IF NOT EXISTS access("
			"last TEXT NOT NULL DEFAULT '1970-01-01 00:00:00'"
			", operation TINYINT PRIMARY KEY NOT NULL"
			", next_update INTEGER NOT NULL DEFAULT '1970-01-01 00:00:00'"
		")";
	constexpr const char g_create_source_store[] =
		"CREATE TABLE IF NOT EXISTS source_store("
			"id INTEGER PRIMARY KEY NOT NULL"
			", source TEXT"
			", format TEXT"
		")";

	std::string to_string (int num) {
		auto ary = dhandy::int_to_ary<int>(num);
		return std::string(ary.begin(), ary.begin() + ary.size() - 1);
	}

	class ItemIdToTableId {
	public:
		explicit ItemIdToTableId (SQLite::Database& db, bool with_caching=true) :
			m_db(db),
			m_query(m_db, "SELECT id FROM items WHERE item_id = ? AND removal_date IS NULL"),
			m_use_cache(with_caching)
		{
		}

		SQLiteID operator() (int item_id) {
			if (m_use_cache) {
				auto it_found = m_cache.find(item_id);
				if (m_cache.end() != it_found)
					return it_found->second;
			}

			m_query.bind(1, item_id);
			if (not m_query.executeStep()) {
				m_query.reset();
				throw std::runtime_error("No item_id " + to_string(item_id) + " found in table items");
			}

			const SQLiteID table_id = m_query.getColumn(0);
			m_query.reset();
			if (m_use_cache)
				m_cache[item_id] = table_id;

			return table_id;
		}

	private:
		std::unordered_map<int, SQLiteID> m_cache;
		SQLite::Database& m_db;
		SQLite::Statement m_query;
		bool m_use_cache;
	};

	class InsertShopIfn {
	public:
		explicit InsertShopIfn (SQLite::Database& db) :
			m_sel(db, "SELECT id FROM shops WHERE creation_date = ? AND owner = ?"),
			m_ins(db, "INSERT INTO shops(title, owner, creation_date, loc_map, loc_x, loc_y, type) VALUES(?, ?, ?, ?, ?, ?, ?)"),
			m_db(db)
		{
		}

		std::pair<SQLiteID, bool> operator() (const oro::Shop& shop) {
			std::string shop_creation_date = to_sqlite_string(shop.creation_date);

			auto row_id_opt = exec_first_res<SQLiteID>(m_sel, shop_creation_date, shop.owner);
			if (row_id_opt) {
				return {*row_id_opt, false};
			}
			else {
				bind_all(m_ins, shop.title, shop.owner,
					no_copy(shop_creation_date), shop.location.map,
					shop.location.x, shop.location.y,
					static_cast<int>(shop.type.value)
				);
				m_ins.exec();
				m_ins.reset();

				return {m_db.getLastInsertRowid(), true};
			}
		}

	private:
		SQLite::Statement m_sel;
		SQLite::Statement m_ins;
		const SQLite::Database& m_db;
	};

	std::optional<oro::Shop> fetch_shop (
		SQLite::Database& db,
		const std::string& owner,
		const oro::Timestamp& creation_timestamp
	) {
		SQLite::Statement query_shop(db, "SELECT title, owner, creation_date, loc_map, loc_x, loc_y, type, id FROM shops where owner = ? AND creation_date = ?");
		bind_all(query_shop, owner, to_sqlite_string(creation_timestamp));
		if (not query_shop.executeStep())
			return {};


		oro::Shop shop;
		shop.title = query_shop.getColumn(0).getString();
		shop.owner = query_shop.getColumn(1).getString();
		shop.creation_date = from_sqlite_timestamp(query_shop.getColumn(2));
		shop.location.map = query_shop.getColumn(3).getString();
		shop.location.x = query_shop.getColumn(4);
		shop.location.y = query_shop.getColumn(5);
		shop.type = static_cast<int>(query_shop.getColumn(6));

		SQLite::Statement query_items(db, "SELECT item_id, amount, price, refine, star_crumbs, element, creator, beloved FROM shop_items WHERE shop_id = ?");
		query_items.bind(1, query_shop.getColumn(7).getInt64());
		while (query_items.executeStep()) {
			oro::ShopItem item;
			item.item_id = query_items.getColumn(0);
			item.amount = query_items.getColumn(1);
			item.price = query_items.getColumn(2);
			item.refine = query_items.getColumn(3);
			item.star_crumbs = query_items.getColumn(4);
			if (not query_items.getColumn(5).isNull())
				item.element = query_items.getColumn(5).getString();
			if (not query_items.getColumn(6).isNull())
				item.creator = query_items.getColumn(6).getUInt();
			if (not query_items.getColumn(7).isNull())
				item.beloved = (query_items.getColumn(7).getInt() != 0);

			shop.items.push_back(item);
		}

		return shop;
	}

	std::string shop_hash (const oro::Shop& shop) {
		using dhandy::int_to_ary;

		mchlib::TigerHash th;
		mchlib::tiger_init_hash(th);

		std::string buff;
		for (const oro::ShopItem& itm : shop.items) {
			buff.clear();
			buff.append(int_to_ary(itm.item_id).to_string_view()).append("|");
			buff.append(int_to_ary(itm.amount).to_string_view()).append("|");
			buff.append(int_to_ary(itm.price).to_string_view()).append("|");
			buff.append(int_to_ary(itm.refine).to_string_view()).append("|");
			buff.append(int_to_ary(itm.star_crumbs).to_string_view()).append("|");

			for (unsigned int id : itm.cards) {
				buff.append(int_to_ary(id).to_string_view()).append("|");
			}

			if (itm.element)
				buff.append(*itm.element).append("|");
			if (itm.creator)
				buff.append(int_to_ary(*itm.creator).to_string_view()).append("|");
			if (itm.beloved)
				buff.append(int_to_ary(*itm.beloved).to_string_view()).append("|");
			tiger_data(buff, th);
		}
		return  mchlib::tiger_to_string(th);
	}

	void update_last_access (SQLite::Database& db, DBOperation op, const oro::Timestamp& next_update) {
		db.exec(
			std::string("INSERT INTO access (operation, last, next_update) VALUES(").append(
				dhandy::int_to_ary<int>(static_cast<int>(op)).to_string_view()
			) +
			", CURRENT_TIMESTAMP, " +
			"'" + to_sqlite_string(next_update) + "'" +
			") ON CONFLICT(operation) DO UPDATE "
			"SET last=CURRENT_TIMESTAMP"
			", next_update='" + to_sqlite_string(next_update) + "'"
		);
	}

	template <oro::DBOperation... Ops, std::size_t... N>
	constexpr auto make_sel_last_acces_b(std::index_sequence<N...>) {
		return (
			(
				dhandy::bt::string<(N==0 ? 0 : 3)+1, char>(N == 0 ? "" : "),(") +
				dhandy::bt::string<
					dhandy::int_to_ary(static_cast<int>(Ops)).size(),
					char
				>(dhandy::int_to_ary(static_cast<int>(Ops)).data())
			) + ... +
			dhandy::bt::make_string("")
		);
	}

	template <std::size_t... Indices>
	constexpr auto make_sel_last_acces_a (std::index_sequence<Indices...> seq) {
		return make_sel_last_acces_b<magic_enum::enum_values<oro::DBOperation>()[Indices]...>(seq);
	}

	constexpr auto make_select_last_acces_str() {
		using dhandy::bt::make_string;
		using std::make_index_sequence;
		using magic_enum::enum_count;
		using oro::DBOperation;
		constexpr auto prefix = make_string("WITH list(operation) AS (VALUES(");
		constexpr auto suffix = make_string("))"
			" SELECT IFNULL(next_update, '1970-01-01 00:00:00') AS next_update"
			" FROM list"
			" LEFT JOIN access USING(operation)"
			" WHERE operation="
		);
		constexpr auto values =
			make_sel_last_acces_a(make_index_sequence<enum_count<DBOperation>()>{});

		return (prefix + values + suffix);
	}

	SQLiteID insert_source_ifn (SQLite::Database& db, const Source& source) {
		if (not source.data)
			return 0;

		db.exec(g_create_source_store);
		SQLite::Statement query(db, "INSERT INTO source_store(source, format) VALUES(?, ?)");
		bind_all(query, no_copy(*source.data), static_cast<int>(source.format));
		query.exec();
		return db.getLastInsertRowid();
	}
} //unnamed namespace

OriginsDBSQLite::OriginsDBSQLite (std::string_view path) :
	m_path(path.data(), path.size()),
	m_db(std::make_unique<SQLite::Database>(m_path, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE))
{
	{
		std::unique_lock<std::mutex> lock(m_db_mutex);
		m_db->exec(g_create_access);
	}
}

OriginsDBSQLite::~OriginsDBSQLite() noexcept = default;

void OriginsDBSQLite::update (const Items& items, const oro::Timestamp& next_update, const Source& source) {
	auto& db = *m_db;
	std::unique_lock<std::mutex> lock(m_db_mutex);
	update_last_access(db, DBOperation::Items, next_update);

	//example:
	//{
	//	"item_id":501,
	//	"unique_name":"Red_Potion",
	//	"name":"Red Potion",
	//	"type":"IT_HEALING",
	//	"npc_price":50
	//}

	db.exec("DROP TABLE IF EXISTS items_staging");
	db.exec(std::string(g_create_items_a).append("items_staging") + g_create_items_b);

	db.exec(std::string(g_create_items_a).append("items") + g_create_items_b);
	db.exec("CREATE UNIQUE INDEX IF NOT EXISTS items_item_id_idx ON items(item_id, removal_date)");
	//see https://stackoverflow.com/questions/22699409/sqlite-null-and-unique
	db.exec("CREATE UNIQUE INDEX IF NOT EXISTS unique_active_item_constraint ON items(item_id, ifnull(removal_date, 0))");

	SQLite::Statement query(db,
		"INSERT INTO items_staging "
			"(item_id, unique_name, name, type, subtype, npc_price, slots, source_id) "
			"VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"
	);
	SQLite::Transaction transaction(db);
	const SQLiteID source_id = insert_source_ifn(db, source);
	for (const auto& item : items.items) {
		bind_all(query, item.item_id, no_copy(item.unique_name),
			no_copy(item.name), static_cast<int>(item.type.value),
			cast<int>(item.subtype), item.npc_price, item.slots,
			source_id
		);

		query.exec();
		query.reset();
	}

	db.exec(
		"UPDATE items SET removal_date = CURRENT_TIMESTAMP WHERE "
		"removal_date IS NULL AND item_id IN ("
			"SELECT item_id FROM ("
				"SELECT item_id FROM items WHERE removal_date IS NULL EXCEPT "
				"SELECT item_id FROM items_staging"
			")"
		")"
	);
	db.exec(
		"INSERT INTO items (item_id, unique_name, name, type, subtype, npc_price, slots, source_id) "
		"SELECT item_id, unique_name, name, type, subtype, npc_price, slots, source_id FROM items_staging WHERE true "
		"ON CONFLICT(item_id, ifnull(removal_date, 0)) DO UPDATE SET "
		"unique_name=excluded.unique_name, "
		"name=excluded.name, type=excluded.type, subtype=excluded.subtype, "
		"npc_price=excluded.npc_price, slots=excluded.slots, source_id=excluded.source_id"
	);
	db.exec("DROP TABLE items_staging");

	transaction.commit();
}

void OriginsDBSQLite::update (const Icons& icons, const oro::Timestamp& next_update, const Source& source) {
	//example:
	//{
	//	"item_id":501,
	//	"icon":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAP1BMVEX/AP/XqpHDkHqudmL////ox6X/9LxJSUm3t7eSkpLb29tubm7Kt6//8+u+QzDTjG7tZEfyhVmPIhi4o5v/8OVQ35yOAAAAAXRSTlMAQObYZgAAAAFiS0dEBI9o2VEAAAAHdElNRQfkAg4MBBc1u27AAAAAmElEQVQoz7WP2w6DIBAFsSyedavLRf//W7uQVmk0afvQeSIzOQSc+wfDcPOXnigE/1MYxxBw0szTYAMwv2kikunuPcBEc+8XknkCVNXOfHhADOYYVYG9mE92V2y08iksQBbRp+/CDM3CSRulKNb9E5o4p/QK6/Fe0WyhpVI6bxORZK7SD+pELn0t7ZqTd25raT35mrZL/TUPifEKODGf6AcAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDItMTRUMTI6MDQ6MjMrMDA6MDCsyg5gAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTAyLTE0VDEyOjA0OjIzKzAwOjAw3Ze23AAAAABJRU5ErkJggg=="
	//}

	std::unique_lock<std::mutex> lock(m_db_mutex);
	auto& db = *m_db;
	update_last_access(db, DBOperation::Icons, next_update);

	db.exec(std::string(g_create_icons_a).append("icons_staging") + g_create_icons_b);
	db.exec(std::string(g_create_icons_a).append("icons") + g_create_icons_b);
	db.exec("CREATE UNIQUE INDEX IF NOT EXISTS icons_item_id_idx ON icons(item_id)");
	SQLite::Statement ins_empty_item(db, "INSERT OR IGNORE INTO items(item_id) VALUES(?)");
	SQLite::Statement query(db, "INSERT INTO icons_staging(item_id, icon, source_id) VALUES(?, ?, ?)");

	ItemIdToTableId item_id_to_table_id(db, false);
	SQLite::Transaction transaction(db);
	const SQLiteID source_id = insert_source_ifn(db, source);
	for (const auto& ico : icons.icons) {
		bind_all(ins_empty_item, ico.item_id);
		ins_empty_item.exec();
		ins_empty_item.reset();

		bind_all(query, item_id_to_table_id(ico.item_id), no_copy(ico.icon), source_id);
		query.exec();
		query.reset();

		//base64_decode(std::string_view(ico.icon).substr(std::string_view("data:image/png;base64,").size()));
	}
	db.exec(
		"INSERT INTO icons (item_id, icon, source_id) "
		"SELECT item_id, icon, source_id FROM icons_staging WHERE true "
		"ON CONFLICT(item_id) DO UPDATE SET "
		"icon=excluded.icon, source_id=excluded.source_id"
	);
	db.exec("DROP TABLE icons_staging");

	transaction.commit();
}

void OriginsDBSQLite::update (const Shops& shops, const oro::Timestamp& next_update, const Source& source) {
	//example:
	//{
	//	"title":"• B\u003ePoison Bottle •",
	//	"owner":"Mr. Boom Bastic",
	//	"location":{
	//		"map":"prontera",
	//		"x":124,
	//		"y":170
	//	},
	//	"creation_date":"2020-06-22T13:12:31Z",
	//	"type":"B",
	//	"items":[{"item_id":678,"amount":200,"price":31000}]
	//}

	auto& db = *m_db;
	std::unique_lock<std::mutex> lock(m_db_mutex);
	update_last_access(db, DBOperation::Shops, next_update);

	db.exec(g_create_shops);
	db.exec("CREATE UNIQUE INDEX IF NOT EXISTS shops_owner_idx ON shops(owner, creation_date)");
	db.exec(g_create_shop_snapshots);
	db.exec("CREATE UNIQUE INDEX IF NOT EXISTS shop_snapshots_shop_hash_idx ON shop_snapshots(shop_id, hash)");
	db.exec(g_create_shop_items);
	db.exec(g_create_slotted_cards);

	SQLite::Statement ins_sshot(db, "INSERT OR IGNORE INTO shop_snapshots(shop_id, hash, source_id) VALUES(?, ?, ?)");
	SQLite::Statement ins_empty_item(db, "INSERT OR IGNORE INTO items(item_id, source_id) VALUES(?, 0)");
	SQLite::Statement sel_sshot(db, "SELECT id FROM shop_snapshots WHERE shop_id = ? AND hash = ?");
	SQLite::Statement ins_item(db, "INSERT INTO shop_items(snapshot_id, item_id, amount, price, refine, star_crumbs, element, creator, beloved) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)");
	SQLite::Statement ins_card(db, "INSERT INTO slotted_cards(shop_item_id, card_id) VALUES(?, ?)");
	SQLite::Statement upd_snapshot_date(db, "UPDATE shop_snapshots SET last_seen_date=CURRENT_TIMESTAMP WHERE id=?");

	InsertShopIfn insert_shop_ifn(db);
	ItemIdToTableId item_id_to_table_id(db, true);
	SQLite::Transaction transaction(db);
	const SQLiteID source_id = insert_source_ifn(db, source);
	for (const auto& shop : shops.shops) {
		{
			//insert new shop or get its id if already there
			const auto shop_id_ins = insert_shop_ifn(shop);
			std::string hash(shop_hash(shop));

			//get snapshot hash if already there
			const auto sshot_id_opt = exec_first_res<SQLiteID>(sel_sshot, shop_id_ins.first, no_copy(hash));
			if (sshot_id_opt) {
				//already there, update last seen date and move on
				bind(upd_snapshot_date, 1, *sshot_id_opt);
				upd_snapshot_date.exec();
				upd_snapshot_date.reset();
				continue;
			}

			//snapshot is new, insert it
			bind_all(ins_sshot, shop_id_ins.first, no_copy(hash), source_id);
			ins_sshot.exec();
			ins_sshot.reset();
		}

		//insert items relative to this snapshot and associated slotted cards
		const auto sshot_id = db.getLastInsertRowid();
		for (const auto& item : shop.items) {
			bind_all(ins_empty_item, item.item_id);
			ins_empty_item.exec();
			ins_empty_item.reset();

			bind_all(ins_item, sshot_id, item_id_to_table_id(item.item_id),
				item.amount, item.price, item.refine, item.star_crumbs,
				item.element, item.creator, cast<int>(item.beloved)
			);
			ins_item.exec();
			ins_item.reset();

			const auto shop_item_id = db.getLastInsertRowid();
			for (unsigned int card_id : item.cards) {
				bind_all(ins_card, shop_item_id, item_id_to_table_id(card_id));
				ins_card.exec();
				ins_card.reset();
			}
		}
	}
	transaction.commit();
}

void OriginsDBSQLite::update (const Creators& creat, const oro::Timestamp& next_update, const Source& source) {
	std::unique_lock<std::mutex> lock(m_db_mutex);
	auto& db = *m_db;
	update_last_access(db, DBOperation::Creators, next_update);

	db.exec(g_create_fame_list);

	auto insert_creators = [](SQLite::Statement& query, const std::vector<oro::Creator>& creats, int type, SQLiteID source_id) {
		for (const auto& creator : creats) {
			bind_all(query, type, creator.char_id, no_copy(creator.name), creator.points, source_id);
			query.exec();
			query.reset();
		}
	};

	SQLite::Statement query(
		db,
		"INSERT INTO fame_list(type, master_id, name, points, source_id) "
			"VALUES (?, ?, ?, ?, ?) "
			"ON CONFLICT(master_id) DO UPDATE SET "
			"points=excluded.points"
			", source_id=excluded.source_id"
	);

	SQLite::Transaction transaction(db);
	const SQLiteID source_id = insert_source_ifn(db, source);
	insert_creators(query, creat.brewers, 1, source_id);
	insert_creators(query, creat.forgers, 2, source_id);
	transaction.commit();
}

Timestamp OriginsDBSQLite::next_access_time (DBOperation op) const {
	constexpr auto select_last_acces_str = make_select_last_acces_str();
	SQLite::Statement sel_last(
		*m_db,
		std::string(select_last_acces_str.data()).append(
			dhandy::int_to_ary<int>(static_cast<int>(op)).to_string_view()
		)
	);

	auto info = exec_first_res<std::string>(sel_last);
	if (not info)
		throw std::runtime_error("Query returned no value for access.operation " + to_string(static_cast<int>(op)));

	return from_sqlite_timestamp(std::move(*info));
}

void OriginsDBSQLite::update_access_time (const oro::Timestamp& next_update, DBOperation op) {
	update_last_access(*m_db, op, next_update);
}

} //namespace oro

#endif
