BareGit
#include "migrations.hpp"

#include <cstdint>
#include <format>
#include <string>
#include <vector>

#include <mw/database.hpp>
#include <mw/error.hpp>
#include <mw/utils.hpp>
#include <spdlog/spdlog.h>

#include "backup.hpp"

namespace overseer::db
{

namespace
{

struct MigrationStep
{
    int64_t from;
    int64_t to;
    mw::E<void> (*fn)(mw::SQLite&);
};

const std::vector<MigrationStep>& registry()
{
    static const std::vector<MigrationStep> r = {
        {0, 1, &migrateDB0To1},
    };
    return r;
}

mw::E<int64_t> getUserVersion(mw::SQLite& db)
{
    auto rt = db.evalToValue<int64_t>("PRAGMA user_version;");
    if(!rt.has_value())
    {
        return std::unexpected(rt.error());
    }
    return *rt;
}

mw::E<void> setUserVersion(mw::SQLite& db, int64_t v)
{
    // PRAGMA does not accept bound parameters, so format directly.
    // `v` is an internal integer; safe to format.
    return db.execute(std::format("PRAGMA user_version = {};", v));
}

} // namespace

mw::E<void> migrateIfNeeded(mw::SQLite& db, const std::string& db_path)
{
    auto current_r = getUserVersion(db);
    if(!current_r.has_value())
    {
        return std::unexpected(current_r.error());
    }
    const int64_t current = *current_r;
    const int64_t target = HIGHEST_KNOWN_VERSION;

    if(current == target)
    {
        spdlog::info("Database schema is at version {} (current).", current);
        return {};
    }
    if(current > target)
    {
        return std::unexpected(mw::runtimeError(std::format(
            "Database schema version {} is newer than this binary "
            "knows about (target {}). Refusing to start.",
            current, target)));
    }

    spdlog::info("Migrating database schema from version {} to {}.",
                 current, target);

    // Take ONE backup before any migration runs.
    if(!db_path.empty())
    {
        if(auto bk = backupDatabaseFile(db, db_path); !bk.has_value())
        {
            return std::unexpected(bk.error());
        }
    }

    for(const auto& step : registry())
    {
        if(step.from < current)
        {
            continue;
        }
        if(step.from >= target)
        {
            break;
        }

        spdlog::info("Applying migration {} -> {}.", step.from, step.to);

        if(auto rt = db.execute("BEGIN IMMEDIATE;"); !rt.has_value())
        {
            return std::unexpected(rt.error());
        }
        if(auto rt = step.fn(db); !rt.has_value())
        {
            // best-effort rollback; ignore its error
            (void)db.execute("ROLLBACK;");
            return std::unexpected(rt.error());
        }
        if(auto rt = setUserVersion(db, step.to); !rt.has_value())
        {
            (void)db.execute("ROLLBACK;");
            return std::unexpected(rt.error());
        }
        if(auto rt = db.execute("COMMIT;"); !rt.has_value())
        {
            (void)db.execute("ROLLBACK;");
            return std::unexpected(rt.error());
        }
    }

    spdlog::info("Database migration complete.");
    return {};
}

} // namespace overseer::db