/*
 * Copyright (C) 2022-2025 Seagull Developers
 *
 * Seagull is the legal property of its developers, whose names are too
 * numerous to list here. Please refer to the AUTHORS file distributed
 * with this source distribution
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
 */

#include <gio/gio.h>

#include <sqlite3.h>

#include "seagullmigrations.h"

G_DEFINE_QUARK(seagull-migrations-error, seagull_migrations_error)

/******************************************************************************
 * Helpers
 *****************************************************************************/
static gboolean
seagull_migrations_run(SeagullSqlite3 *db,
                       int version,
                       const char *migration,
                       GError **error)
{
	char *errmsg = NULL;
	char *str = NULL;
	gboolean success = TRUE;

	str = g_strdup_printf("BEGIN;%s;PRAGMA user_version=%d;COMMIT;", migration,
	                      version);

	sqlite3_exec(db, str, NULL, NULL, &errmsg);
	if(errmsg != NULL) {
		g_set_error(error,
		            SEAGULL_MIGRATIONS_ERROR,
		            SEAGULL_MIGRATIONS_ERROR_EXECUTION_FAILED,
		            "failed to run migration: %s",
		            errmsg);

		sqlite3_free(errmsg);

		sqlite3_exec(db, "ROLLBACK", NULL, NULL, &errmsg);
		if(errmsg != NULL) {
			g_error("failed to rollback transaction: %s", errmsg);

			sqlite3_free(errmsg);
		}

		success = FALSE;
	}

	g_free(str);

	return success;
}

/******************************************************************************
 * Public API
 *****************************************************************************/
int
seagull_migrations_get_schema_version(SeagullSqlite3 *db, GError **error) {
	sqlite3_stmt *stmt = NULL;
	int version = -1;

	g_return_val_if_fail(db != NULL, -1);

	sqlite3_prepare_v2(db, "PRAGMA user_version", -1, &stmt, NULL);

	if(stmt == NULL) {
		g_set_error(error,
		            SEAGULL_MIGRATIONS_ERROR,
		            SEAGULL_MIGRATIONS_ERROR_PREPARED_STATEMENT_FAILED,
		            "error while creating prepared statement: %s",
		            sqlite3_errmsg(db));

		return -1;
	}

	if(sqlite3_step(stmt) == SQLITE_ROW) {
		version = sqlite3_column_int(stmt, 0);
	} else {
		g_set_error_literal(error,
		                    SEAGULL_MIGRATIONS_ERROR,
		                    SEAGULL_MIGRATIONS_ERROR_PRAGMA_VERSION_FAILED,
		                    "'PRAGMA user_version' didn't return a row");

		sqlite3_finalize(stmt);

		return -1;
	}

	sqlite3_finalize(stmt);

	return version;
}

gboolean
seagull_migrations_run_from_strings(SeagullSqlite3 *db,
                                    const char *migrations[],
                                    GError **error)
{
	int current_version = 0;
	guint n_migrations = 0;

	g_return_val_if_fail(db != NULL, FALSE);
	g_return_val_if_fail(migrations != NULL, FALSE);

	/* Get the current version or bail if it failed. */
	current_version = seagull_migrations_get_schema_version(db, error);
	if(current_version == -1) {
		return FALSE;
	}

	n_migrations = g_strv_length((char **)migrations);
	if((guint)current_version > n_migrations) {
		g_set_error(error,
		            SEAGULL_MIGRATIONS_ERROR,
		            SEAGULL_MIGRATIONS_ERROR_SCHEMA_TOO_NEW,
		            "schema version %u is higher than known migrations %u",
		            (guint)current_version, n_migrations);

		return FALSE;
	}

	for(int i = current_version; migrations[i] != NULL; i++) {
		int version = i + 1;

		if(!seagull_migrations_run(db, version, migrations[i], error)) {
			return FALSE;
		}
	}

	return TRUE;
}

gboolean
seagull_migrations_run_from_resources(SeagullSqlite3 *db,
                                      const char *path,
                                      const char *migrations[],
                                      GError **error)
{
	GError *local_error = NULL;
	int current_version = 0;
	guint n_migrations = 0;

	g_return_val_if_fail(db != NULL, FALSE);
	g_return_val_if_fail(path != NULL, FALSE);
	g_return_val_if_fail(migrations != NULL, FALSE);

	/* Get the current version or bail if it failed. */
	current_version = seagull_migrations_get_schema_version(db, error);
	if(current_version == -1) {
		return FALSE;
	}

	n_migrations = g_strv_length((char **)migrations);
	if((guint)current_version > n_migrations) {
		g_set_error(error,
		            SEAGULL_MIGRATIONS_ERROR,
		            SEAGULL_MIGRATIONS_ERROR_SCHEMA_TOO_NEW,
		            "schema version %u is higher than known migrations %u",
		            (guint)current_version, n_migrations);

		return FALSE;
	}

	/* `PRAGMA user_version` starts at 0, so write our version as i + 1. We
	 * start iterating the list of migrations at the current version of the
	 * database. If the database is already up to date, then current_version
	 * will point us at the null terminator in the list of migrations, which
	 * will short circuit the for loop.
	 */
	for(int i = current_version; migrations[i] != NULL; i++) {
		GBytes *data = NULL;
		char *full_path = NULL;
		const gchar *migration = NULL;
		int version = i + 1;

		/* Get the data from the resource */
		full_path = g_build_path("/", path, migrations[i], NULL);

		data = g_resources_lookup_data(full_path, G_RESOURCE_LOOKUP_FLAGS_NONE,
		                               &local_error);
		if(data == NULL || local_error != NULL) {
			if(local_error == NULL) {
				local_error = g_error_new(SEAGULL_MIGRATIONS_ERROR,
				                          SEAGULL_MIGRATIONS_ERROR_FAILED_TO_LOAD_RESOURCE,
				                          "failed to load resource %s",
				                          full_path);
			}

			g_propagate_error(error, local_error);

			g_clear_pointer(&data, g_bytes_unref);
			g_free(full_path);

			return FALSE;
		}

		g_free(full_path);

		migration = (const char *)g_bytes_get_data(data, NULL);
		if(!seagull_migrations_run(db, version, migration, error)) {
			g_bytes_unref(data);

			return FALSE;
		}

		g_bytes_unref(data);
	}

	return TRUE;
}

