// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest

#include "everest/logging.hpp"
#include "ocpp/v2/ocpp_enums.hpp"
#include "ocpp/v2/ocpp_types.hpp"
#include <boost/algorithm/string/join.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <everest/database/sqlite/statement.hpp>
#include <numeric>
#include <ocpp/common/message_queue.hpp>
#include <ocpp/v2/database_handler.hpp>
#include <ocpp/v2/types.hpp>
#include <ocpp/v2/utils.hpp>
#include <string>
#include <vector>

using namespace everest::db;
using namespace everest::db::sqlite;

namespace ocpp {

using namespace common;

namespace {
std::int64_t to_unix_milliseconds(const DateTime& dt) {
    return std::chrono::duration_cast<std::chrono::milliseconds>(dt.to_time_point().time_since_epoch()).count();
}

DateTime from_unix_milliseconds(std::int64_t ms_since_epoch) {
    return DateTime(date::utc_clock::time_point(std::chrono::milliseconds(ms_since_epoch)));
}
} // namespace

namespace v2 {

DatabaseHandler::DatabaseHandler(std::unique_ptr<ConnectionInterface> database,
                                 const fs::path& sql_migration_files_path) :
    DatabaseHandlerCommon(std::move(database), sql_migration_files_path, MIGRATION_FILE_VERSION_V2) {
}

void DatabaseHandler::init_sql() {
    if (sqlite3_threadsafe() != 1) {
        throw std::logic_error("SQLite must be in serialized thread mode");
    }

    auto get_stmt = this->database->new_statement("SELECT * FROM TRANSACTIONS");
    if (get_stmt->step() == SQLITE_ROW) {
        EVLOG_info << "Not clearing tables as there is an ongoing transaction";
    } else {
        this->inintialize_enum_tables();
    }
}

void DatabaseHandler::inintialize_enum_tables() {

    // TODO: Don't throw away all meter value items to allow resuming transactions
    // Also we should add functionality then to clean up old/unknown transactions from the database
    if (!this->database->clear_table("METER_VALUE_ITEMS") or !this->database->clear_table("METER_VALUES")) {
        EVLOG_error << "Could not clear tables METER_VALUE_ITEMS or METER_VALUES";
        throw QueryExecutionException(this->database->get_error_message());
    }

    init_enum_table<ReadingContextEnum>("READING_CONTEXT_ENUM", ReadingContextEnum::Interruption_Begin,
                                        ReadingContextEnum::Trigger, conversions::reading_context_enum_to_string);

    init_enum_table<MeasurandEnum>("MEASURAND_ENUM", MeasurandEnum::Current_Export, MeasurandEnum::Voltage,
                                   conversions::measurand_enum_to_string);

    init_enum_table<PhaseEnum>("PHASE_ENUM", PhaseEnum::L1, PhaseEnum::L3_L1, conversions::phase_enum_to_string);

    init_enum_table<LocationEnum>("LOCATION_ENUM", LocationEnum::Body, LocationEnum::Outlet,
                                  conversions::location_enum_to_string);
}

void DatabaseHandler::init_enum_table_inner(const std::string& table_name, const int begin, const int end,
                                            std::function<std::string(int)> conversion) {
    if (!this->database->clear_table(table_name)) {
        EVLOG_critical << "Table \"" + table_name + "\" does not exist";
        throw QueryExecutionException(this->database->get_error_message());
    }

    auto transaction = this->database->begin_transaction();

    const std::string sql = "INSERT INTO " + table_name + " VALUES (@id, @value);";
    auto insert_stmt = this->database->new_statement(sql);

    for (int i = begin; i <= end; i++) {
        auto string = conversion(i);

        insert_stmt->bind_int("@id", i);
        insert_stmt->bind_text("@value", string);

        if (insert_stmt->step() != SQLITE_DONE) {
            EVLOG_error << "Could not perform step.";
            throw QueryExecutionException(this->database->get_error_message());
        }

        (*insert_stmt).reset();
    }

    transaction->commit();
}

template <typename T>
void DatabaseHandler::init_enum_table(const std::string& table_name, T begin, T end,
                                      std::function<std::string(T)> conversion) {
    auto conversion_func = [conversion](int value) { return conversion(static_cast<T>(value)); };
    init_enum_table_inner(table_name, static_cast<int>(begin), static_cast<int>(end), conversion_func);
}

void DatabaseHandler::authorization_cache_insert_entry(const std::string& id_token_hash,
                                                       const IdTokenInfo& id_token_info) {
    const std::string sql =
        "INSERT OR REPLACE INTO AUTH_CACHE (ID_TOKEN_HASH, ID_TOKEN_INFO, LAST_USED, EXPIRY_DATE) VALUES "
        "(@id_token_hash, @id_token_info, @last_used, @expiry_date)";
    auto insert_stmt = this->database->new_statement(sql);

    insert_stmt->bind_text("@id_token_hash", id_token_hash);
    insert_stmt->bind_text("@id_token_info", json(id_token_info).dump(), SQLiteString::Transient);
    insert_stmt->bind_int64("@last_used", to_unix_milliseconds(DateTime()));
    if (id_token_info.cacheExpiryDateTime.has_value()) {
        insert_stmt->bind_int64("@expiry_date", to_unix_milliseconds(id_token_info.cacheExpiryDateTime.value()));
    } else {
        insert_stmt->bind_null("@expiry_date");
    }

    if (insert_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::authorization_cache_update_last_used(const std::string& id_token_hash) {
    const std::string sql = "UPDATE AUTH_CACHE SET LAST_USED = @last_used WHERE ID_TOKEN_HASH = @id_token_hash";
    auto insert_stmt = this->database->new_statement(sql);

    insert_stmt->bind_int64("@last_used", to_unix_milliseconds(DateTime()));
    insert_stmt->bind_text("@id_token_hash", id_token_hash);

    if (insert_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

std::optional<AuthorizationCacheEntry>
DatabaseHandler::authorization_cache_get_entry(const std::string& id_token_hash) {
    const std::string sql = "SELECT ID_TOKEN_INFO, LAST_USED FROM AUTH_CACHE WHERE ID_TOKEN_HASH = @id_token_hash";
    auto select_stmt = this->database->new_statement(sql);

    select_stmt->bind_text("@id_token_hash", id_token_hash);

    const auto status = select_stmt->step();

    if (status == SQLITE_DONE) {
        return std::nullopt;
    }

    if (status == SQLITE_ROW) {
        return AuthorizationCacheEntry{json::parse(select_stmt->column_text(0)),
                                       from_unix_milliseconds(select_stmt->column_int64(1))};
    }

    throw QueryExecutionException(this->database->get_error_message());
}

void DatabaseHandler::authorization_cache_delete_entry(const std::string& id_token_hash) {
    const std::string sql = "DELETE FROM AUTH_CACHE WHERE ID_TOKEN_HASH = @id_token_hash";
    auto delete_stmt = this->database->new_statement(sql);

    delete_stmt->bind_text("@id_token_hash", id_token_hash);

    if (delete_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::authorization_cache_delete_nr_of_oldest_entries(size_t nr_to_remove) {
    const std::string sql = "DELETE FROM AUTH_CACHE WHERE ID_TOKEN_HASH IN (SELECT ID_TOKEN_HASH FROM AUTH_CACHE ORDER "
                            "BY LAST_USED ASC LIMIT @nr_to_remove)";
    auto delete_stmt = this->database->new_statement(sql);

    delete_stmt->bind_int("@nr_to_remove", clamp_to<int>(nr_to_remove));

    if (delete_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::authorization_cache_delete_expired_entries(
    std::optional<std::chrono::seconds> auth_cache_lifetime) {

    const std::string sql = "DELETE FROM AUTH_CACHE WHERE ID_TOKEN_HASH IN (SELECT ID_TOKEN_HASH FROM AUTH_CACHE WHERE "
                            "EXPIRY_DATE < @before_date OR LAST_USED < @before_last_used)";
    auto delete_stmt = this->database->new_statement(sql);

    const DateTime now;
    delete_stmt->bind_int64("@before_date", to_unix_milliseconds(now));
    if (auth_cache_lifetime.has_value()) {
        delete_stmt->bind_int64("@before_last_used",
                                to_unix_milliseconds(DateTime(now.to_time_point() - auth_cache_lifetime.value())));
    } else {
        delete_stmt->bind_null("@before_last_used");
    }

    if (delete_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::authorization_cache_clear() {
    if (!this->database->clear_table("AUTH_CACHE")) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

size_t DatabaseHandler::authorization_cache_get_binary_size() {
    const std::string sql = "SELECT SUM(\"payload\") FROM \"dbstat\" WHERE name='AUTH_CACHE';";
    auto stmt = this->database->new_statement(sql);

    if (stmt->step() != SQLITE_ROW) {
        throw QueryExecutionException(this->database->get_error_message());
    }

    return stmt->column_int(0);
}

void DatabaseHandler::insert_availability(std::int32_t evse_id, std::int32_t connector_id,
                                          OperationalStatusEnum operational_status, bool replace) {
    std::string sql;

    if (replace) {
        sql = "INSERT OR REPLACE INTO AVAILABILITY (EVSE_ID, CONNECTOR_ID, OPERATIONAL_STATUS) VALUES "
              "(@evse_id, @connector_id, @operational_status)";
    } else {
        sql = "INSERT OR IGNORE INTO AVAILABILITY (EVSE_ID, CONNECTOR_ID, OPERATIONAL_STATUS) VALUES "
              "(@evse_id, @connector_id, @operational_status)";
    }

    auto insert_stmt = this->database->new_statement(sql);

    insert_stmt->bind_int("@evse_id", evse_id);
    insert_stmt->bind_int("@connector_id", connector_id);

    insert_stmt->bind_text("@operational_status", conversions::operational_status_enum_to_string(operational_status),
                           SQLiteString::Transient);

    if (insert_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

OperationalStatusEnum DatabaseHandler::get_availability(std::int32_t evse_id, std::int32_t connector_id) {
    const std::string sql =
        "SELECT OPERATIONAL_STATUS FROM AVAILABILITY WHERE EVSE_ID = @evse_id AND CONNECTOR_ID = @connector_id;";
    auto select_stmt = this->database->new_statement(sql);

    select_stmt->bind_int("@evse_id", evse_id);
    select_stmt->bind_int("@connector_id", connector_id);

    const int status = select_stmt->step();

    if (status == SQLITE_DONE) {
        throw everest::db::RequiredEntryNotFoundException("Could not find operational status for connector");
    }

    if (status != SQLITE_ROW) {
        throw QueryExecutionException(this->database->get_error_message());
    }
    return conversions::string_to_operational_status_enum(select_stmt->column_text(0));
}

void DatabaseHandler::insert_or_update_local_authorization_list_version(std::int32_t version) {
    const std::string sql = "INSERT OR REPLACE INTO AUTH_LIST_VERSION (ID, VERSION) VALUES (0, @version)";
    auto stmt = this->database->new_statement(sql);

    stmt->bind_int("@version", version);

    if (stmt->step() != SQLITE_DONE) {
        EVLOG_error << "Could not insert or replace into AUTH_LIST_VERSION table";
        throw QueryExecutionException(this->database->get_error_message());
    }
}

std::int32_t DatabaseHandler::get_local_authorization_list_version() {
    const std::string sql = "SELECT VERSION FROM AUTH_LIST_VERSION WHERE ID = 0";
    auto stmt = this->database->new_statement(sql);

    if (stmt->step() != SQLITE_ROW) {
        EVLOG_error << "Error selecting auth list version";
        throw QueryExecutionException(this->database->get_error_message());
    }

    return stmt->column_int(0);
}

void DatabaseHandler::insert_or_update_local_authorization_list_entry(const IdToken& id_token,
                                                                      const IdTokenInfo& id_token_info) {
    // add or replace
    const std::string sql = "INSERT OR REPLACE INTO AUTH_LIST (ID_TOKEN_HASH, ID_TOKEN_INFO) "
                            "VALUES (@id_token_hash, @id_token_info)";
    auto stmt = this->database->new_statement(sql);

    stmt->bind_text("@id_token_hash", utils::generate_token_hash(id_token), SQLiteString::Transient);
    stmt->bind_text("@id_token_info", json(id_token_info).dump(), SQLiteString::Transient);

    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::insert_or_update_local_authorization_list(
    const std::vector<AuthorizationData>& local_authorization_list) {
    bool success = true; // indicates if all database operations succeeded
    for (const auto& authorization_data : local_authorization_list) {
        try {
            if (authorization_data.idTokenInfo.has_value()) {
                this->insert_or_update_local_authorization_list_entry(authorization_data.idToken,
                                                                      authorization_data.idTokenInfo.value());
            } else {
                this->delete_local_authorization_list_entry(authorization_data.idToken);
            }
        } catch (const QueryExecutionException& e) {
            // catch but continue with remaining entries
            success = false;
        }
    }

    if (!success) {
        throw QueryExecutionException("At least one insertion or deletion of local authorization list entries failed");
    }
}

void DatabaseHandler::delete_local_authorization_list_entry(const IdToken& id_token) {
    const std::string sql = "DELETE FROM AUTH_LIST WHERE ID_TOKEN_HASH = @id_token_hash;";
    auto stmt = this->database->new_statement(sql);

    stmt->bind_text("@id_token_hash", utils::generate_token_hash(id_token), SQLiteString::Transient);

    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

std::optional<IdTokenInfo> DatabaseHandler::get_local_authorization_list_entry(const IdToken& id_token) {

    const std::string sql = "SELECT ID_TOKEN_INFO FROM AUTH_LIST WHERE ID_TOKEN_HASH = @id_token_hash;";
    auto stmt = this->database->new_statement(sql);

    stmt->bind_text("@id_token_hash", utils::generate_token_hash(id_token), SQLiteString::Transient);

    const int status = stmt->step();

    if (status == SQLITE_DONE) {
        return std::nullopt;
    }

    if (status == SQLITE_ROW) {
        return IdTokenInfo(json::parse(stmt->column_text(0)));
    }

    throw QueryExecutionException(this->database->get_error_message());
}

void DatabaseHandler::clear_local_authorization_list() {
    const auto retval = this->database->clear_table("AUTH_LIST");
    if (retval == false) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

std::int32_t DatabaseHandler::get_local_authorization_list_number_of_entries() {
    const std::string sql = "SELECT COUNT(*) FROM AUTH_LIST;";
    auto stmt = this->database->new_statement(sql);

    if (stmt->step() != SQLITE_ROW) {
        throw QueryExecutionException(this->database->get_error_message());
    }

    return stmt->column_int(0);
}

void DatabaseHandler::transaction_metervalues_insert(const std::string& transaction_id, const MeterValue& meter_value) {
    if (meter_value.sampledValue.empty()) {
        return;
    }

    auto sampled_value_context = meter_value.sampledValue.at(0).context;
    if (!sampled_value_context.has_value()) {
        return;
    }

    auto context = sampled_value_context.value();
    if (std::find_if(meter_value.sampledValue.begin(), meter_value.sampledValue.end(), [context](const auto& item) {
            return !item.context.has_value() or item.context.value() != context;
        }) != meter_value.sampledValue.end()) {
        throw std::invalid_argument("All metervalues must have the same context");
    }

    const std::string sql1 =
        "INSERT INTO METER_VALUES (TRANSACTION_ID, TIMESTAMP, READING_CONTEXT, CUSTOM_DATA) VALUES "
        "(@transaction_id, @timestamp, @context, @custom_data)";

    auto stmt = this->database->new_statement(sql1);

    stmt->bind_text("@transaction_id", transaction_id);
    stmt->bind_int64("@timestamp", to_unix_milliseconds(meter_value.timestamp));
    stmt->bind_int("@context", static_cast<int>(context));
    stmt->bind_null("@custom_data");

    if (stmt->step() != SQLITE_DONE) {
        EVLOG_warning << "Could not insert meter values into database";
        throw QueryExecutionException(this->database->get_error_message());
    }

    auto last_row_id = this->database->get_last_inserted_rowid();
    (*stmt).reset();

    const std::string sql2 =
        "INSERT INTO METER_VALUE_ITEMS (METER_VALUE_ID, VALUE, MEASURAND, PHASE, LOCATION, CUSTOM_DATA, "
        "UNIT_CUSTOM_DATA, UNIT_TEXT, UNIT_MULTIPLIER, SIGNED_METER_DATA, SIGNING_METHOD, "
        "ENCODING_METHOD, PUBLIC_KEY) VALUES (@meter_value_id, @value, @measurand, "
        "@phase, @location, @custom_data, @unit_custom_data, @unit_text, @unit_multiplier, "
        "@signed_meter_data, @signing_method, @encoding_method, @public_key);";

    auto transaction = this->database->begin_transaction();
    auto insert_stmt = this->database->new_statement(sql2);

    for (const auto& item : meter_value.sampledValue) {
        insert_stmt->bind_int("@meter_value_id", clamp_to<int>(last_row_id));
        insert_stmt->bind_double("@value", item.value);

        if (item.measurand.has_value()) {
            insert_stmt->bind_int("@measurand", static_cast<int>(item.measurand.value()));
        } else {
            insert_stmt->bind_null("@measurand");
        }

        if (item.phase.has_value()) {
            insert_stmt->bind_int("@phase", static_cast<int>(item.phase.value()));
        } else {
            insert_stmt->bind_null("@phase");
        }

        if (item.location.has_value()) {
            insert_stmt->bind_int("@location", static_cast<int>(item.location.value()));
        }

        if (item.customData.has_value()) {
            insert_stmt->bind_text("@custom_data", item.customData.value().at("vendorId").get<std::string>(),
                                   SQLiteString::Transient);
        }

        if (item.unitOfMeasure.has_value()) {
            const auto& unitOfMeasure = item.unitOfMeasure.value();

            if (unitOfMeasure.customData.has_value()) {
                insert_stmt->bind_text("@unit_custom_data",
                                       unitOfMeasure.customData.value().at("vendorId").get<std::string>(),
                                       SQLiteString::Transient);
            }
            if (unitOfMeasure.unit.has_value()) {
                insert_stmt->bind_text("@unit_text", unitOfMeasure.unit.value().get(), SQLiteString::Transient);
            }
            if (unitOfMeasure.multiplier.has_value()) {
                insert_stmt->bind_int("@unit_multiplier", unitOfMeasure.multiplier.value());
            }
        }

        if (item.signedMeterValue.has_value()) {
            const auto& signedMeterValue = item.signedMeterValue.value();

            insert_stmt->bind_text("@signed_meter_data", signedMeterValue.signedMeterData.get(),
                                   SQLiteString::Transient);
            if (signedMeterValue.signingMethod.has_value()) {
                insert_stmt->bind_text("@signing_method", signedMeterValue.signingMethod.value().get(),
                                       SQLiteString::Transient);
            } else {
                insert_stmt->bind_null("@signing_method");
            }
            insert_stmt->bind_text("@encoding_method", signedMeterValue.encodingMethod.get(), SQLiteString::Transient);
            if (signedMeterValue.publicKey.has_value()) {
                insert_stmt->bind_text("@public_key", signedMeterValue.publicKey.value().get(),
                                       SQLiteString::Transient);
            } else {
                insert_stmt->bind_null("@public_key");
            }
        } else {
            insert_stmt->bind_null("@signed_meter_data");
            insert_stmt->bind_null("@signing_method");
            insert_stmt->bind_null("@encoding_method");
            insert_stmt->bind_null("@public_key");
        }

        if (insert_stmt->step() != SQLITE_DONE) {
            throw QueryExecutionException(this->database->get_error_message());
        }

        (*insert_stmt).reset();
    }

    transaction->commit();
}

std::vector<MeterValue> DatabaseHandler::transaction_metervalues_get_all(const std::string& transaction_id) {

    const std::string sql1 = "SELECT * FROM METER_VALUES WHERE TRANSACTION_ID = @transaction_id;";
    const std::string sql2 = "SELECT * FROM METER_VALUE_ITEMS WHERE METER_VALUE_ID = @row_id;";
    auto select_stmt = this->database->new_statement(sql1);
    auto select_stmt2 = this->database->new_statement(sql2);

    select_stmt->bind_text("@transaction_id", transaction_id);

    std::vector<MeterValue> result;

    int status = SQLITE_ERROR;
    while ((status = select_stmt->step()) == SQLITE_ROW) {
        MeterValue value;
        value.timestamp = from_unix_milliseconds(select_stmt->column_int64(2));

        if (select_stmt->column_type(4) == SQLITE_TEXT) {
            value.customData = CustomData{select_stmt->column_text(4)};
        }

        auto row_id = select_stmt->column_int(0);
        auto context = static_cast<ReadingContextEnum>(select_stmt->column_int(3));

        select_stmt2->bind_int("@row_id", row_id);

        while ((status = select_stmt2->step()) == SQLITE_ROW) {
            SampledValue sampled_value;

            sampled_value.value = clamp_to<float>(select_stmt2->column_double(1));
            sampled_value.context = context;

            if (select_stmt2->column_type(2) == SQLITE_INTEGER) {
                sampled_value.measurand = static_cast<MeasurandEnum>(select_stmt2->column_int(2));
            }

            if (select_stmt2->column_type(3) == SQLITE_INTEGER) {
                sampled_value.phase = static_cast<PhaseEnum>(select_stmt2->column_int(3));
            }

            if (select_stmt2->column_type(4) == SQLITE_INTEGER) {
                sampled_value.location = static_cast<LocationEnum>(select_stmt2->column_int(4));
            }

            if (select_stmt2->column_type(5) == SQLITE_TEXT) {
                sampled_value.customData = CustomData{select_stmt->column_text(5)};
            }

            if (select_stmt2->column_type(6) == SQLITE_TEXT or select_stmt2->column_type(7) == SQLITE_TEXT or
                select_stmt2->column_type(8) == SQLITE_INTEGER) {
                UnitOfMeasure unit;
                if (select_stmt2->column_type(6) == SQLITE_TEXT) {
                    unit.customData = CustomData{select_stmt2->column_text(6)};
                }
                if (select_stmt2->column_type(7) == SQLITE_TEXT) {
                    unit.unit = select_stmt2->column_text(7);
                }
                if (select_stmt2->column_type(8) == SQLITE_INTEGER) {
                    unit.multiplier = select_stmt2->column_int(8);
                }
                sampled_value.unitOfMeasure.emplace(unit);
            }

            if (select_stmt2->column_type(9) == SQLITE_TEXT and select_stmt2->column_type(10) == SQLITE_TEXT and
                select_stmt2->column_type(11) == SQLITE_TEXT and select_stmt2->column_type(12) == SQLITE_TEXT) {
                SignedMeterValue signed_meter_value;
                signed_meter_value.signedMeterData = select_stmt2->column_text(9);
                signed_meter_value.signingMethod = select_stmt2->column_text(10);
                signed_meter_value.encodingMethod = select_stmt2->column_text(11);
                signed_meter_value.publicKey = select_stmt2->column_text(12);

                sampled_value.signedMeterValue.emplace(signed_meter_value);
            }

            value.sampledValue.push_back(std::move(sampled_value));
        }

        if (status != SQLITE_DONE) {
            throw QueryExecutionException(this->database->get_error_message());
        }

        result.push_back(std::move(value));

        (*select_stmt2).reset();
    }

    if (status != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }

    return result;
}

void DatabaseHandler::transaction_metervalues_clear(const std::string& transaction_id) {

    const std::string sql1 = "SELECT ROWID FROM METER_VALUES WHERE TRANSACTION_ID = @transaction_id;";

    auto select_stmt = this->database->new_statement(sql1);

    select_stmt->bind_text("@transaction_id", transaction_id);

    const std::string sql2 = "DELETE FROM METER_VALUE_ITEMS WHERE METER_VALUE_ID = @row_id";
    auto delete_stmt = this->database->new_statement(sql2);
    int status = SQLITE_ERROR;
    while ((status = select_stmt->step()) == SQLITE_ROW) {
        auto row_id = select_stmt->column_int(0);
        delete_stmt->bind_int("@row_id", row_id);

        if (delete_stmt->step() != SQLITE_DONE) {
            throw QueryExecutionException(this->database->get_error_message());
        }
        (*delete_stmt).reset();
    }

    if (status != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }

    const std::string sql3 = "DELETE FROM METER_VALUES WHERE TRANSACTION_ID = @transaction_id";
    auto delete_stmt2 = this->database->new_statement(sql3);
    delete_stmt2->bind_text("@transaction_id", transaction_id);
    if (delete_stmt2->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::insert_cs_availability(OperationalStatusEnum operational_status, bool replace) {
    this->insert_availability(0, 0, operational_status, replace);
}

OperationalStatusEnum DatabaseHandler::get_cs_availability() {
    return this->get_availability(0, 0);
}

void DatabaseHandler::insert_evse_availability(std::int32_t evse_id, OperationalStatusEnum operational_status,
                                               bool replace) {
    assert(evse_id > 0);
    this->insert_availability(evse_id, 0, operational_status, replace);
}

OperationalStatusEnum DatabaseHandler::get_evse_availability(std::int32_t evse_id) {
    assert(evse_id > 0);
    return this->get_availability(evse_id, 0);
}

void DatabaseHandler::insert_connector_availability(std::int32_t evse_id, std::int32_t connector_id,
                                                    OperationalStatusEnum operational_status, bool replace) {
    assert(evse_id > 0);
    assert(connector_id > 0);
    this->insert_availability(evse_id, connector_id, operational_status, replace);
}

OperationalStatusEnum DatabaseHandler::get_connector_availability(std::int32_t evse_id, std::int32_t connector_id) {
    assert(evse_id > 0);
    assert(connector_id > 0);
    return this->get_availability(evse_id, connector_id);
}

// transactions
void DatabaseHandler::transaction_insert(const EnhancedTransaction& transaction, std::int32_t evse_id) {
    const std::string sql =
        "INSERT INTO TRANSACTIONS "
        "(TRANSACTION_ID, EVSE_ID, CONNECTOR_ID, TIME_START, SEQ_NO, CHARGING_STATE, ID_TAG_SENT) VALUES"
        "(@transaction_id, @evse_id, @connector_id, @time_start, @seq_no, @charging_state, @id_token_sent)";
    auto insert_stmt = this->database->new_statement(sql);

    insert_stmt->bind_text("@transaction_id", transaction.transactionId.get(), SQLiteString::Transient);
    insert_stmt->bind_int("@evse_id", evse_id);
    insert_stmt->bind_int("@connector_id", transaction.connector_id);
    insert_stmt->bind_int64("@time_start", to_unix_milliseconds(transaction.start_time));
    insert_stmt->bind_int("@seq_no", transaction.seq_no);
    insert_stmt->bind_text(
        "@charging_state",
        conversions::charging_state_enum_to_string(transaction.chargingState.value_or(ChargingStateEnum::Idle)),
        SQLiteString::Transient);
    insert_stmt->bind_int("@id_token_sent", transaction.id_token_sent ? 1 : 0);

    if (insert_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

std::unique_ptr<EnhancedTransaction> DatabaseHandler::transaction_get(const std::int32_t evse_id) {
    const std::string sql = "SELECT TRANSACTION_ID, CONNECTOR_ID, TIME_START, SEQ_NO, CHARGING_STATE, ID_TAG_SENT FROM "
                            "TRANSACTIONS WHERE EVSE_ID = @evse_id";
    auto get_stmt = this->database->new_statement(sql);
    get_stmt->bind_int("@evse_id", evse_id);

    if (get_stmt->step() != SQLITE_ROW) {
        return nullptr;
    }

    // Hardcode database_enabled to true because we would not be here otherwise
    auto transaction = std::make_unique<EnhancedTransaction>(*this, true);

    // Fill transaction
    transaction->transactionId = get_stmt->column_text(0);
    transaction->connector_id = get_stmt->column_int(1);
    transaction->start_time = from_unix_milliseconds(get_stmt->column_int64(2));
    transaction->seq_no = get_stmt->column_int(3);
    transaction->chargingState = conversions::string_to_charging_state_enum(get_stmt->column_text(4));
    transaction->id_token_sent = get_stmt->column_int(5) != 0;

    if (get_stmt->step() == SQLITE_ROW) {
        // We should never have more than 1 transaction per evse_id in the database as per the UNIQUE constraint
        EVLOG_error << "There are more than 1 transactions for evse_id " << evse_id << " in the database";
    }

    return transaction;
}

void DatabaseHandler::transaction_update_seq_no(const std::string& transaction_id, std::int32_t seq_no) {
    const std::string sql = "UPDATE TRANSACTIONS SET SEQ_NO = @seq_no WHERE TRANSACTION_ID = @transaction_id";
    auto update_stmt = this->database->new_statement(sql);

    update_stmt->bind_int("@seq_no", seq_no);
    update_stmt->bind_text("@transaction_id", transaction_id);

    if (update_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::transaction_update_charging_state(const std::string& transaction_id,
                                                        const ChargingStateEnum charging_state) {
    const std::string sql =
        "UPDATE TRANSACTIONS SET CHARGING_STATE = @charging_state WHERE TRANSACTION_ID = @transaction_id";
    auto update_stmt = this->database->new_statement(sql);

    update_stmt->bind_text("@charging_state", conversions::charging_state_enum_to_string(charging_state));
    update_stmt->bind_text("@transaction_id", transaction_id);

    if (update_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::transaction_update_id_token_sent(const std::string& transaction_id, bool id_token_sent) {
    const std::string sql =
        "UPDATE TRANSACTIONS SET ID_TAG_SENT = @id_token_sent WHERE TRANSACTION_ID = @transaction_id";
    auto update_stmt = this->database->new_statement(sql);

    update_stmt->bind_int("@id_token_sent", id_token_sent ? 1 : 0);
    update_stmt->bind_text("@transaction_id", transaction_id);

    if (update_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::transaction_delete(const std::string& transaction_id) {
    const std::string sql = "DELETE FROM TRANSACTIONS WHERE TRANSACTION_ID = @transaction_id";
    auto delete_stmt = this->database->new_statement(sql);
    delete_stmt->bind_text("@transaction_id", transaction_id);
    if (delete_stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::insert_or_update_charging_profile(const int evse_id, const v2::ChargingProfile& profile,
                                                        const CiString<20> charging_limit_source) {
    // add or replace
    const std::string sql =
        "INSERT OR REPLACE INTO CHARGING_PROFILES (ID, EVSE_ID, STACK_LEVEL, CHARGING_PROFILE_PURPOSE, "
        "TRANSACTION_ID, PROFILE, CHARGING_LIMIT_SOURCE) VALUES "
        "(@id, @evse_id, @stack_level, @charging_profile_purpose, @transaction_id, @profile, @charging_limit_source)";
    auto stmt = this->database->new_statement(sql);

    const json json_profile(profile);

    stmt->bind_int("@id", profile.id);
    stmt->bind_int("@evse_id", evse_id);
    stmt->bind_int("@stack_level", profile.stackLevel);
    stmt->bind_text("@charging_profile_purpose",
                    conversions::charging_profile_purpose_enum_to_string(profile.chargingProfilePurpose),
                    SQLiteString::Transient);
    if (profile.transactionId.has_value()) {
        stmt->bind_text("@transaction_id", profile.transactionId.value().get(), SQLiteString::Transient);
    } else {
        stmt->bind_null("@transaction_id");
    }

    stmt->bind_text("@profile", json_profile.dump(), SQLiteString::Transient);
    stmt->bind_text("@charging_limit_source", charging_limit_source.get());

    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

bool DatabaseHandler::delete_charging_profile(const int profile_id) {
    const std::string sql = "DELETE FROM CHARGING_PROFILES WHERE ID = @profile_id;";
    auto stmt = this->database->new_statement(sql);

    stmt->bind_int("@profile_id", profile_id);
    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }

    return stmt->changes() > 0;
}

void DatabaseHandler::delete_charging_profile_by_transaction_id(const std::string& transaction_id) {
    const std::string sql = "DELETE FROM CHARGING_PROFILES WHERE TRANSACTION_ID = @transaction_id";
    auto stmt = this->database->new_statement(sql);

    stmt->bind_text("@transaction_id", transaction_id);
    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

bool DatabaseHandler::clear_charging_profiles() {
    return this->database->clear_table("CHARGING_PROFILES");
}

std::vector<std::int32_t>
DatabaseHandler::clear_charging_profiles_matching_criteria(const std::optional<std::int32_t> profile_id,
                                                           const std::optional<ClearChargingProfile>& criteria) {
    // K10.FR.03, K10.FR.09: explicit id.
    if (profile_id.has_value()) {
        if (this->delete_charging_profile(profile_id.value())) {
            return {profile_id.value()};
        }
        return {};
    }

    auto transaction = this->database->begin_transaction();

    // No criteria means clear all. Snapshot the ids first so the caller can drop their deadline
    // tracking for exactly the rows that were removed.
    if (!criteria.has_value()) {
        std::vector<std::int32_t> ids;
        auto select_all = this->database->new_statement("SELECT ID FROM CHARGING_PROFILES");
        int status;
        while ((status = select_all->step()) == SQLITE_ROW) {
            ids.push_back(select_all->column_int(0));
        }
        if (status != SQLITE_DONE) {
            throw QueryExecutionException(this->database->get_error_message());
        }
        if (!this->clear_charging_profiles()) {
            return {};
        }
        transaction->commit();
        return ids;
    }

    if (criteria->chargingProfilePurpose.has_value() || criteria->evseId.has_value() ||
        criteria->stackLevel.has_value()) {
        // Shared criteria WHERE: the K10.FR.04 guard (never delete ChargingStationExternalConstraints)
        // plus the optional purpose/stack/evse filters. The SAME clause and binds drive both the id
        // SELECT and the DELETE, so the returned ids are exactly the deleted rows; callers never
        // mirror this filter.
        std::vector<std::string> filters = {"CHARGING_PROFILE_PURPOSE != 'ChargingStationExternalConstraints'"};
        if (criteria->chargingProfilePurpose.has_value()) {
            filters.emplace_back("CHARGING_PROFILE_PURPOSE = @charging_profile_purpose");
        }
        if (criteria->stackLevel.has_value()) {
            filters.emplace_back("STACK_LEVEL = @stack_level");
        }
        if (criteria->evseId.has_value()) {
            filters.emplace_back("EVSE_ID = @evse_id");
        }
        const std::string where = " WHERE " + boost::algorithm::join(filters, " AND ");

        const auto bind_criteria = [&criteria](everest::db::sqlite::StatementInterface& stmt) {
            if (criteria->chargingProfilePurpose.has_value()) {
                stmt.bind_text(
                    "@charging_profile_purpose",
                    conversions::charging_profile_purpose_enum_to_string(criteria->chargingProfilePurpose.value()),
                    SQLiteString::Transient);
            }
            if (criteria->stackLevel.has_value()) {
                stmt.bind_int("@stack_level", criteria->stackLevel.value());
            }
            if (criteria->evseId.has_value()) {
                stmt.bind_int("@evse_id", criteria->evseId.value());
            }
        };

        std::vector<std::int32_t> ids;
        auto select_stmt = this->database->new_statement("SELECT ID FROM CHARGING_PROFILES" + where);
        bind_criteria(*select_stmt);
        int status;
        while ((status = select_stmt->step()) == SQLITE_ROW) {
            ids.push_back(select_stmt->column_int(0));
        }
        if (status != SQLITE_DONE) {
            throw QueryExecutionException(this->database->get_error_message());
        }

        auto delete_stmt = this->database->new_statement("DELETE FROM CHARGING_PROFILES" + where);
        bind_criteria(*delete_stmt);
        if (delete_stmt->step() != SQLITE_DONE) {
            throw QueryExecutionException(this->database->get_error_message());
        }

        transaction->commit();
        return ids;
    }

    return {};
}

std::vector<ReportedChargingProfile>
DatabaseHandler::get_charging_profiles_matching_criteria(const std::optional<std::int32_t> evse_id,
                                                         const ChargingProfileCriterion& criteria) {
    auto results = std::vector<ReportedChargingProfile>();

    std::string select_stmt = "SELECT EVSE_ID, PROFILE, CHARGING_LIMIT_SOURCE FROM CHARGING_PROFILES";
    std::vector<std::string> where_clauses;

    if (evse_id.has_value()) {
        where_clauses.emplace_back("EVSE_ID = @evse_id");
    }

    if (criteria.chargingProfileId.has_value() && !criteria.chargingProfileId->empty()) {
        const std::string profile_ids =
            boost::algorithm::join(criteria.chargingProfileId.value() |
                                       boost::adaptors::transformed([](std::int32_t id) { return std::to_string(id); }),
                                   ", ");

        where_clauses.push_back("ID IN (" + profile_ids + ")");

        select_stmt += " WHERE " + boost::algorithm::join(where_clauses, " AND ");

        auto stmt = this->database->new_statement(select_stmt);

        if (evse_id.has_value()) {
            stmt->bind_int("@evse_id", evse_id.value());
        }

        int status = SQLITE_ERROR;
        while ((status = stmt->step()) == SQLITE_ROW) {
            results.emplace_back(json::parse(stmt->column_text(1)), // profile
                                 stmt->column_int(0),               // EVSE ID
                                 CiString<20>(stmt->column_text(2)) // source
            );
        }
        if (status != SQLITE_DONE) {
            throw QueryExecutionException(this->database->get_error_message());
        }
        return results;
    }

    if (criteria.chargingProfilePurpose.has_value()) {
        where_clauses.emplace_back("CHARGING_PROFILE_PURPOSE = @charging_profile_purpose");
    }

    if (criteria.stackLevel.has_value()) {
        where_clauses.emplace_back("STACK_LEVEL = @stack_level");
    }

    if (criteria.chargingLimitSource.has_value() && !criteria.chargingLimitSource->empty()) {
        const std::string sources = boost::algorithm::join(
            criteria.chargingLimitSource.value() |
                boost::adaptors::transformed([](CiString<20> source) { return "'" + source.get() + "'"; }),
            ", ");

        where_clauses.push_back("CHARGING_LIMIT_SOURCE IN (" + sources + ")");
    }

    if (!where_clauses.empty()) {
        select_stmt += " WHERE " + boost::algorithm::join(where_clauses, " AND ");
    }

    auto stmt = this->database->new_statement(select_stmt);

    if (criteria.chargingProfilePurpose.has_value()) {
        stmt->bind_text("@charging_profile_purpose",
                        conversions::charging_profile_purpose_enum_to_string(criteria.chargingProfilePurpose.value()),
                        SQLiteString::Transient);
    }

    if (criteria.stackLevel.has_value()) {
        stmt->bind_int("@stack_level", criteria.stackLevel.value());
    }

    if (evse_id.has_value()) {
        stmt->bind_int("@evse_id", evse_id.value());
    }

    int status = SQLITE_ERROR;
    while ((status = stmt->step()) == SQLITE_ROW) {
        results.emplace_back(json::parse(stmt->column_text(1)), // profile
                             stmt->column_int(0),               // EVSE ID
                             CiString<20>(stmt->column_text(2)) // source
        );
    }
    if (status != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }

    return results;
}

std::vector<v2::ChargingProfile> DatabaseHandler::get_charging_profiles_for_evse(const int evse_id) {
    std::vector<v2::ChargingProfile> profiles;

    const std::string sql = "SELECT PROFILE FROM CHARGING_PROFILES WHERE EVSE_ID = @evse_id";

    auto stmt = this->database->new_statement(sql);

    stmt->bind_int("@evse_id", evse_id);

    int status = SQLITE_ERROR;
    while ((status = stmt->step()) == SQLITE_ROW) {
        auto profile = json::parse(stmt->column_text(0));
        profiles.push_back(profile);
    }
    if (status != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }

    return profiles;
}

std::vector<v2::ChargingProfile> DatabaseHandler::get_all_charging_profiles() {
    std::vector<v2::ChargingProfile> profiles;

    const std::string sql = "SELECT PROFILE FROM CHARGING_PROFILES";

    auto stmt = this->database->new_statement(sql);

    int status = SQLITE_ERROR;
    while ((status = stmt->step()) == SQLITE_ROW) {
        auto profile = json::parse(stmt->column_text(0));
        profiles.push_back(profile);
    }
    if (status != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }

    return profiles;
}

std::map<std::int32_t, std::vector<v2::ChargingProfile>> DatabaseHandler::get_all_charging_profiles_group_by_evse() {
    std::map<std::int32_t, std::vector<v2::ChargingProfile>> map;

    const std::string sql = "SELECT EVSE_ID, PROFILE FROM CHARGING_PROFILES";

    auto stmt = this->database->new_statement(sql);

    int status = SQLITE_ERROR;
    while ((status = stmt->step()) == SQLITE_ROW) {
        auto evse_id = stmt->column_int(0);
        auto profile = json::parse(stmt->column_text(1));

        auto profiles = map[evse_id];
        profiles.emplace_back(profile);

        map[evse_id] = profiles;
    }
    if (status != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }

    return map;
}

CiString<20> DatabaseHandler::get_charging_limit_source_for_profile(const int profile_id) {
    const std::string sql = "SELECT CHARGING_LIMIT_SOURCE FROM CHARGING_PROFILES WHERE ID = @profile_id;";

    auto stmnt = this->database->new_statement(sql);

    stmnt->bind_int("@profile_id", profile_id);

    if (stmnt->step() != SQLITE_ROW) {
        EVLOG_warning << "No record found for " << profile_id;
    }

    auto res = CiString<20>(stmnt->column_text(0));

    if (stmnt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }

    return res;
}

// DER Control persistence

void DatabaseHandler::insert_or_update_der_control(const std::string& control_id, bool is_default,
                                                   const std::string& control_type, bool is_superseded,
                                                   int32_t priority, const std::optional<std::string>& start_time,
                                                   const std::optional<float>& duration,
                                                   const std::string& control_json) {
    // UPSERT (not INSERT OR REPLACE) preserves STARTED_NOTIFIED and PENDING_SUPERSEDE_ID
    // across re-sets of the same controlId; otherwise duplicate NotifyDERStartStop fires.
    const std::string sql = "INSERT INTO DER_CONTROLS "
                            "(CONTROL_ID, IS_DEFAULT, CONTROL_TYPE, IS_SUPERSEDED, PRIORITY, START_TIME, DURATION, "
                            "CONTROL_JSON) VALUES "
                            "(@control_id, @is_default, @control_type, @is_superseded, @priority, @start_time, "
                            "@duration, @control_json) "
                            "ON CONFLICT(CONTROL_ID) DO UPDATE SET "
                            "IS_DEFAULT = excluded.IS_DEFAULT, "
                            "CONTROL_TYPE = excluded.CONTROL_TYPE, "
                            "IS_SUPERSEDED = excluded.IS_SUPERSEDED, "
                            "PRIORITY = excluded.PRIORITY, "
                            "START_TIME = excluded.START_TIME, "
                            "DURATION = excluded.DURATION, "
                            "CONTROL_JSON = excluded.CONTROL_JSON";
    auto stmt = this->database->new_statement(sql);

    stmt->bind_text("@control_id", control_id, SQLiteString::Transient);
    stmt->bind_int("@is_default", is_default ? 1 : 0);
    stmt->bind_text("@control_type", control_type, SQLiteString::Transient);
    stmt->bind_int("@is_superseded", is_superseded ? 1 : 0);
    stmt->bind_int("@priority", priority);
    if (start_time.has_value()) {
        stmt->bind_text("@start_time", start_time.value(), SQLiteString::Transient);
    } else {
        stmt->bind_null("@start_time");
    }
    if (duration.has_value()) {
        stmt->bind_double("@duration", static_cast<double>(duration.value()));
    } else {
        stmt->bind_null("@duration");
    }
    stmt->bind_text("@control_json", control_json, SQLiteString::Transient);

    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

std::optional<std::string> DatabaseHandler::get_der_control(const std::string& control_id) {
    const std::string sql = "SELECT CONTROL_JSON FROM DER_CONTROLS WHERE CONTROL_ID = @control_id";
    auto stmt = this->database->new_statement(sql);
    stmt->bind_text("@control_id", control_id, SQLiteString::Transient);

    if (stmt->step() == SQLITE_ROW) {
        return std::string(stmt->column_text(0));
    }
    return std::nullopt;
}

std::size_t DatabaseHandler::count_der_controls() {
    const std::string sql = "SELECT COUNT(*) FROM DER_CONTROLS";
    auto stmt = this->database->new_statement(sql);
    if (stmt->step() == SQLITE_ROW) {
        const auto count = stmt->column_int(0);
        return count < 0 ? 0U : static_cast<std::size_t>(count);
    }
    throw QueryExecutionException(this->database->get_error_message());
}

std::vector<std::string>
DatabaseHandler::get_der_controls_matching_criteria(const std::optional<bool>& is_default,
                                                    const std::optional<std::string>& control_type,
                                                    const std::optional<std::string>& control_id) {
    std::vector<std::string> results;
    std::string sql = "SELECT CONTROL_JSON FROM DER_CONTROLS";
    std::vector<std::string> where_clauses;

    if (is_default.has_value()) {
        where_clauses.push_back("IS_DEFAULT = @is_default");
    }
    if (control_type.has_value()) {
        where_clauses.push_back("CONTROL_TYPE = @control_type");
    }
    if (control_id.has_value()) {
        where_clauses.push_back("CONTROL_ID = @control_id");
    }

    if (!where_clauses.empty()) {
        sql += " WHERE ";
        for (size_t i = 0; i < where_clauses.size(); ++i) {
            if (i > 0) {
                sql += " AND ";
            }
            sql += where_clauses[i];
        }
    }

    auto stmt = this->database->new_statement(sql);

    if (is_default.has_value()) {
        stmt->bind_int("@is_default", is_default.value() ? 1 : 0);
    }
    if (control_type.has_value()) {
        stmt->bind_text("@control_type", control_type.value(), SQLiteString::Transient);
    }
    if (control_id.has_value()) {
        stmt->bind_text("@control_id", control_id.value(), SQLiteString::Transient);
    }

    int status = SQLITE_ERROR;
    while ((status = stmt->step()) == SQLITE_ROW) {
        results.emplace_back(stmt->column_text(0));
    }
    if (status != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
    return results;
}

bool DatabaseHandler::delete_der_control(const std::string& control_id) {
    const std::string sql = "DELETE FROM DER_CONTROLS WHERE CONTROL_ID = @control_id";
    auto stmt = this->database->new_statement(sql);
    stmt->bind_text("@control_id", control_id, SQLiteString::Transient);

    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
    return stmt->changes() > 0;
}

bool DatabaseHandler::delete_der_control_by_id_and_default(const std::string& control_id, bool is_default) {
    const std::string sql = "DELETE FROM DER_CONTROLS WHERE CONTROL_ID = @control_id AND IS_DEFAULT = @is_default";
    auto stmt = this->database->new_statement(sql);
    stmt->bind_text("@control_id", control_id, SQLiteString::Transient);
    stmt->bind_int("@is_default", is_default ? 1 : 0);
    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
    return stmt->changes() > 0;
}

int DatabaseHandler::delete_der_controls_matching_criteria(bool is_default,
                                                           const std::optional<std::string>& control_type) {
    std::string sql = "DELETE FROM DER_CONTROLS WHERE IS_DEFAULT = @is_default";
    if (control_type.has_value()) {
        sql += " AND CONTROL_TYPE = @control_type";
    }

    auto stmt = this->database->new_statement(sql);
    stmt->bind_int("@is_default", is_default ? 1 : 0);
    if (control_type.has_value()) {
        stmt->bind_text("@control_type", control_type.value(), SQLiteString::Transient);
    }

    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
    return stmt->changes();
}

void DatabaseHandler::update_der_control_superseded(const std::string& control_id, bool is_superseded) {
    // Keep CONTROL_JSON.isSuperseded in sync with the IS_SUPERSEDED column; readers go through the JSON.
    const std::string sql = "UPDATE DER_CONTROLS SET "
                            "IS_SUPERSEDED = @is_superseded, "
                            "CONTROL_JSON = json_set(CONTROL_JSON, '$.isSuperseded', json(@is_superseded_text)) "
                            "WHERE CONTROL_ID = @control_id";
    auto stmt = this->database->new_statement(sql);
    stmt->bind_int("@is_superseded", is_superseded ? 1 : 0);
    stmt->bind_text("@is_superseded_text", is_superseded ? "true" : "false", SQLiteString::Transient);
    stmt->bind_text("@control_id", control_id, SQLiteString::Transient);

    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::append_der_control_displaced_id(const std::string& control_id, const std::string& displaced_id) {
    // COALESCE seeds an empty array on first append.
    const std::string sql =
        "UPDATE DER_CONTROLS SET CONTROL_JSON = json_set("
        "CONTROL_JSON, '$.displacedIds', "
        "json_insert(COALESCE(json_extract(CONTROL_JSON, '$.displacedIds'), json_array()), '$[#]', @displaced_id)) "
        "WHERE CONTROL_ID = @control_id";
    auto stmt = this->database->new_statement(sql);
    stmt->bind_text("@displaced_id", displaced_id, SQLiteString::Transient);
    stmt->bind_text("@control_id", control_id, SQLiteString::Transient);

    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

std::unique_ptr<StatementInterface> DatabaseHandler::new_statement(const std::string& sql) {
    return this->database->new_statement(sql);
}

void DatabaseHandler::set_der_control_pending_supersede(const std::string& new_control_id,
                                                        const std::string& existing_control_id) {
    const std::string sql = "UPDATE DER_CONTROLS SET PENDING_SUPERSEDE_ID = @pending WHERE CONTROL_ID = @control_id";
    auto stmt = this->database->new_statement(sql);
    stmt->bind_text("@pending", existing_control_id, SQLiteString::Transient);
    stmt->bind_text("@control_id", new_control_id, SQLiteString::Transient);
    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

void DatabaseHandler::clear_der_control_pending_supersede(const std::string& control_id) {
    const std::string sql = "UPDATE DER_CONTROLS SET PENDING_SUPERSEDE_ID = NULL WHERE CONTROL_ID = @control_id";
    auto stmt = this->database->new_statement(sql);
    stmt->bind_text("@control_id", control_id, SQLiteString::Transient);
    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

std::vector<std::string> DatabaseHandler::get_der_controls_needing_start_notify(const DateTime& now) {
    const std::string sql = "SELECT CONTROL_ID FROM DER_CONTROLS WHERE IS_DEFAULT = 0 AND IS_SUPERSEDED = 0 AND "
                            "STARTED_NOTIFIED = 0 AND START_TIME IS NOT NULL AND START_TIME <= @now";
    auto stmt = this->database->new_statement(sql);
    stmt->bind_text("@now", now.to_rfc3339(), SQLiteString::Transient);

    std::vector<std::string> out;
    while (true) {
        const auto status = stmt->step();
        if (status == SQLITE_DONE) {
            break;
        }
        if (status != SQLITE_ROW) {
            throw QueryExecutionException(this->database->get_error_message());
        }
        out.emplace_back(stmt->column_text(0));
    }
    return out;
}

void DatabaseHandler::mark_der_control_started_notified(const std::string& control_id) {
    const std::string sql = "UPDATE DER_CONTROLS SET STARTED_NOTIFIED = 1 WHERE CONTROL_ID = @control_id";
    auto stmt = this->database->new_statement(sql);
    stmt->bind_text("@control_id", control_id, SQLiteString::Transient);
    if (stmt->step() != SQLITE_DONE) {
        throw QueryExecutionException(this->database->get_error_message());
    }
}

std::vector<DatabaseHandlerInterface::PendingSupersedeActivation>
DatabaseHandler::get_der_control_pending_supersede_activations(const DateTime& now) {
    // Defensive: a row already superseded must not activate its own deferred target.
    const std::string sql = "SELECT CONTROL_ID, PENDING_SUPERSEDE_ID FROM DER_CONTROLS WHERE "
                            "PENDING_SUPERSEDE_ID IS NOT NULL AND IS_SUPERSEDED = 0 AND "
                            "START_TIME IS NOT NULL AND START_TIME <= @now";
    auto stmt = this->database->new_statement(sql);
    stmt->bind_text("@now", now.to_rfc3339(), SQLiteString::Transient);

    std::vector<PendingSupersedeActivation> out;
    while (true) {
        const auto status = stmt->step();
        if (status == SQLITE_DONE) {
            break;
        }
        if (status != SQLITE_ROW) {
            throw QueryExecutionException(this->database->get_error_message());
        }
        PendingSupersedeActivation a;
        a.new_id = stmt->column_text(0);
        a.existing_id = stmt->column_text(1);
        out.push_back(std::move(a));
    }
    return out;
}

std::unique_ptr<TransactionInterface> DatabaseHandler::begin_transaction() {
    return this->database->begin_transaction();
}

} // namespace v2
} // namespace ocpp
