#include "common.h"
#include "configuration.h"
#include "response.h"
#include "yacare_params.h"

#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/libs/db/include/branch_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/libs/db/include/firmware_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/libs/db/include/hardware_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/libs/db/include/rollout_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/libs/db/include/rollout_history_gateway.h>
#include <maps/infra/yacare/include/yacare.h>
#include <maps/infra/yacare/include/params/tvm.h>

#include <cstdlib>
#include <map>

namespace maps::fw_updater::storage {

namespace {

using FirmwareTbl = db::table::Firmware;
using RolloutTbl = db::table::Rollout;
using RolloutHistoryTbl = db::table::RolloutHistory;

void checkHardware(pqxx::transaction_base& txn,
                   const auth::UserInfo& userInfo,
                   const std::string& hardware)
{
    auto dbHardware = db::HardwareGateway{txn}.tryLoadById(hardware);
    if (!dbHardware) {
        throw yacare::errors::BadRequest() << "Unknown hardware: " << hardware;
    }
    checkAcl(txn, userInfo, *dbHardware);
}

void checkBranch(pqxx::transaction_base& txn, const std::string& branch)
{
    if (!db::BranchGateway{txn}.existsById(branch)) {
        throw yacare::errors::BadRequest() << "Unknown branch: " << branch;
    }
}

std::string generateRandomSeed()
{
    static constexpr size_t SEED_LENGTH = 12;
    static const std::string CHAR_LIST =
        "abcdefghijklmnopqrstuvwxyz"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "0123456789";

    std::string result;
    result.reserve(SEED_LENGTH);
    for (size_t i = 0; i < SEED_LENGTH; ++i) {
       result.push_back(CHAR_LIST[std::rand() % CHAR_LIST.length()]);
    }
    return result;
}

db::TIds collectRolloutIds(const db::RolloutHistories& rolloutHistories)
{
    db::TIds rolloutIds;
    for (const auto& rolloutHistory : rolloutHistories) {
        rolloutIds.push_back(rolloutHistory.rolloutId());
    }
    sortUnique(rolloutIds);
    return rolloutIds;
}

db::TIds collectFirmwareIds(const db::Rollouts& rollouts)
{
    db::TIds firmwareIds;
    for (const auto& rollout : rollouts) {
        firmwareIds.push_back(rollout.firmwareId());
    }
    sortUnique(firmwareIds);
    return firmwareIds;
}


/// rolloutHistories must be in reverse chronological order
void collectStableAndExperimentalTargetStates(
    const db::RolloutHistories& rolloutHistories,
    db::RolloutHistories& output)
{
    bool shouldAddExperimentalRollout = true;

    for (const auto& rolloutHistory : rolloutHistories) {
        if (rolloutHistory.percent() == 100) {
            output.push_back(rolloutHistory);
            return;
        }

        if (rolloutHistory.status() == db::RolloutStatus::Active
            && shouldAddExperimentalRollout) {
            output.push_back(rolloutHistory);
        }
        shouldAddExperimentalRollout = false;
    }
}


void handleRolloutStatusChangeRequest(
    const auth::UserInfo& userInfo,
    db::TId rolloutId,
    db::RolloutStatus status,
    yacare::Response& response)
{
    auto txn = configuration()->pool().masterWriteableTransaction();
    auto rollout = db::RolloutGateway{*txn}.loadById(rolloutId);
    auto firmware = db::FirmwareGateway{*txn}.loadById(rollout.firmwareId());
    checkHardware(*txn, userInfo, firmware.hardwareId());

    db::RolloutHistoryGateway gtw{*txn};

    auto latestRolloutHistory = gtw.loadOne(
        RolloutHistoryTbl::rolloutId == rolloutId,
        sql_chemistry::orderBy(RolloutHistoryTbl::id).desc());

    if (latestRolloutHistory.status() == status) {
        makeRolloutResponse(response, latestRolloutHistory, rollout, firmware);
        return;
    }

    db::RolloutHistory rolloutHistory(
        rolloutId,
        latestRolloutHistory.percent(),
        status,
        userInfo.login());

    gtw.insert(rolloutHistory);
    txn->commit();

    makeRolloutResponse(response, rolloutHistory, rollout, firmware);
}

sql_chemistry::FiltersCollection makeRolloutsFilter(
    const std::string& hardware,
    const std::optional<db::Slot>& slot,
    const std::optional<std::string>& branch)
{
    sql_chemistry::FiltersCollection filter(sql_chemistry::op::Logical::And);
    filter.add(RolloutHistoryTbl::rolloutId == RolloutTbl::id);
    filter.add(RolloutTbl::firmwareId == FirmwareTbl::id);
    filter.add(FirmwareTbl::hardwareId == hardware);
    if (slot) {
        filter.add(FirmwareTbl::slot == *slot);
    }
    if (branch) {
        filter.add(RolloutTbl::branch == *branch);
    }
    return filter;
}

} // namespace


YCR_RESPOND_TO("POST /v2/rollout/create", userInfo, hardware, slot, branch, version)
{
    auto txn = configuration()->pool().masterWriteableTransaction();
    checkHardware(*txn, userInfo, hardware);

    auto firmware = db::FirmwareGateway{*txn}.tryLoadOne(
        FirmwareTbl::hardwareId == hardware &&
        FirmwareTbl::slot == slot &&
        FirmwareTbl::version == version);
    if (!firmware) {
        throw yacare::errors::BadRequest() << "Firmware does not exist";
    }

    checkBranch(*txn, branch);

    auto latestRollout = db::RolloutGateway{*txn}.tryLoadOne(
        RolloutTbl::firmwareId == FirmwareTbl::id &&
            RolloutTbl::branch == branch &&
            FirmwareTbl::hardwareId == hardware &&
            FirmwareTbl::slot == slot,
        sql_chemistry::orderBy(RolloutTbl::id).desc());

    std::string seed;
    if (latestRollout) {
        if (latestRollout->firmwareId() == firmware->id()) {
            throw yacare::errors::Conflict() << "Rollout exists";
        }

        auto latestRolloutHistory = db::RolloutHistoryGateway{*txn}.tryLoadOne(
            RolloutHistoryTbl::rolloutId == latestRollout->id(),
            sql_chemistry::orderBy(RolloutHistoryTbl::id).desc());

        // Use the same seed if last experiment did not reach 100%, and new seed otherwise
        seed = latestRolloutHistory && latestRolloutHistory->isExperimental()
             ? latestRollout->seed()
             : generateRandomSeed();
    } else {
        seed = generateRandomSeed();
    }

    db::Rollout rollout(branch, firmware->id(), seed);
    db::RolloutGateway{*txn}.insert(rollout);
    db::RolloutHistory rolloutHistory(rollout.id(), 0, db::RolloutStatus::Inactive, userInfo.login());
    db::RolloutHistoryGateway{*txn}.insert(rolloutHistory);
    txn->commit();

    makeRolloutResponse(response, rolloutHistory, rollout, *firmware);
    response.setStatus(yacare::HTTPStatus::Created);
}

YCR_RESPOND_TO("POST /v2/rollout/expand", userInfo, rollout_id, percent)
{
    auto txn = configuration()->pool().masterWriteableTransaction();
    auto rollout = db::RolloutGateway{*txn}.loadById(rollout_id);
    auto firmware = db::FirmwareGateway{*txn}.loadById(rollout.firmwareId());
    checkHardware(*txn, userInfo, firmware.hardwareId());

    db::RolloutHistory rolloutHistory(
        rollout_id,
        percent,
        db::RolloutStatus::Active,
        userInfo.login());
    db::RolloutHistoryGateway{*txn}.insert(rolloutHistory);
    txn->commit();

    makeRolloutResponse(response, rolloutHistory, rollout, firmware);
}

YCR_RESPOND_TO("POST /v2/rollout/pause", userInfo, rollout_id)
{
    handleRolloutStatusChangeRequest(
        userInfo,
        rollout_id,
        db::RolloutStatus::Inactive,
        response);
}

YCR_RESPOND_TO("POST /v2/rollout/resume", userInfo, rollout_id)
{
    handleRolloutStatusChangeRequest(
        userInfo,
        rollout_id,
        db::RolloutStatus::Active,
        response);
}


struct Scope {
    db::Slot slot;
    std::string branch;
    auto introspect() const { return std::tie(slot, branch); }
};
using introspection::operator<;

YCR_RESPOND_TO("GET /v2/rollout/target",
               userInfo, hardware, slot = db::Slot::Rootfs, branch = "")
{
    auto txn = Configuration::instance()->pool().slaveTransaction();
    auto dbHardware = db::HardwareGateway{*txn}.loadById(hardware);
    checkAcl(*txn, userInfo, dbHardware);

    std::optional<db::Slot> optSlot;
    if (has(slot)) {
        optSlot = slot;
    }
    std::optional<std::string> optBranch;
    if (has(branch)) {
        checkBranch(*txn, branch);
        optBranch = branch;
    }
    auto filter = makeRolloutsFilter(hardware, optSlot, optBranch);

    auto rolloutHistories = db::RolloutHistoryGateway{*txn}.load(
        filter, sql_chemistry::orderBy(RolloutHistoryTbl::id).desc());

    db::TIds rolloutIds = collectRolloutIds(rolloutHistories);
    auto rollouts = db::RolloutGateway{*txn}.loadByIds(rolloutIds);
    db::TIds firmwareIds = collectFirmwareIds(rollouts);
    auto firmwares = db::FirmwareGateway{*txn}.loadByIds(firmwareIds);
    auto idToRollout = moveToMap(std::move(rollouts));
    auto idToFirmware = moveToMap(std::move(firmwares));

    // Group all loaded rollout histories by scopes
    std::map<Scope, db::RolloutHistories> scopeToRolloutHistories;
    for (auto& rolloutHistory : rolloutHistories) {
        const auto& rollout = idToRollout.at(rolloutHistory.rolloutId());
        const auto& firmware = idToFirmware.at(rollout.firmwareId());
        Scope scope{firmware.slot(), rollout.branch()};
        scopeToRolloutHistories[scope].push_back(std::move(rolloutHistory));
    }

    // For each scope, pick the most recent stable and experimental rollouts (if exist)
    db::RolloutHistories result;
    for (const auto& [scope, histories] : scopeToRolloutHistories) {
        collectStableAndExperimentalTargetStates(histories, result);
    }

    if (result.empty()) {
        response.setStatus(yacare::HTTPStatus::NoContent);
    } else {
        makeRolloutsListResponse(response, result, idToRollout, idToFirmware);
    }
}

YCR_RESPOND_TO("GET /v2/rollout/history",
               userInfo, hardware, slot = db::Slot::Rootfs, branch = "",
               results = 0, skip = 0)
{
    auto txn = Configuration::instance()->pool().slaveTransaction();
    auto dbHardware = db::HardwareGateway{*txn}.loadById(hardware);
    checkAcl(*txn, userInfo, dbHardware);

    std::optional<db::Slot> optSlot;
    if (has(slot)) {
        optSlot = slot;
    }
    std::optional<std::string> optBranch;
    if (has(branch)) {
        checkBranch(*txn, branch);
        optBranch = branch;
    }
    auto filter = makeRolloutsFilter(hardware, optSlot, optBranch);
    auto orderBy = pageOrderBy(RolloutHistoryTbl::id, results, skip, /*asc=*/false);

    auto rolloutHistories = db::RolloutHistoryGateway{*txn}.load(filter, orderBy);

    db::TIds rolloutIds = collectRolloutIds(rolloutHistories);
    auto rollouts = db::RolloutGateway{*txn}.loadByIds(rolloutIds);
    db::TIds firmwareIds = collectFirmwareIds(rollouts);
    auto firmwares = db::FirmwareGateway{*txn}.loadByIds(firmwareIds);
    auto idToRollout = moveToMap(std::move(rollouts));
    auto idToFirmware = moveToMap(std::move(firmwares));

    if (rolloutHistories.empty()) {
        response.setStatus(yacare::HTTPStatus::NoContent);
    } else {
        makeRolloutsListResponse(response, rolloutHistories, idToRollout, idToFirmware);
    }
}

} //namespace maps::fw_updater::storage
