#include "configuration.h"
#include "firmware_signature.h"
#include "yacare_params.h"

#include <maps/infra/yacare/include/yacare.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/libs/db/include/device_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/rollout_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/libs/db/include/rollout_history_gateway.h>

#include <functional>
#include <unordered_map>

namespace maps::fw_updater {

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

namespace {

std::string loadDeviceBranch(
    pqxx::transaction_base& txn,
    const std::string& hardware,
    const std::string& deviceId)
{
    sql_chemistry::FiltersCollection filter(sql_chemistry::op::Logical::And);
    filter.add(DeviceBranchTbl::hardwareId.equals(hardware));
    filter.add(DeviceBranchTbl::deviceId.equals(deviceId));

    auto deviceBranch = db::DeviceBranchGateway{txn}.tryLoadOne(filter);

    return deviceBranch ? deviceBranch->branch() : "stable";
}

sql_chemistry::FiltersCollection makeRolloutHistoryFilter(
    const std::string& hardware, const std::string& branch, db::Slot slot)
{
    sql_chemistry::FiltersCollection filter(sql_chemistry::op::Logical::And);
    filter.add(RolloutHistoryTbl::rolloutId.equals(RolloutTbl::id));
    filter.add(RolloutTbl::firmwareId.equals(FirmwareTbl::id));
    filter.add(RolloutTbl::branch.equals(branch));
    filter.add(FirmwareTbl::hardwareId.equals(hardware));
    filter.add(FirmwareTbl::slot.equals(slot));

    return filter;
}

bool isDeviceInExperiment(
    uint16_t experimentPercent,
    const std::string& experimentSeed,
    const std::string& deviceid
    )
{
    auto hash = std::hash<std::string>{}(deviceid + experimentSeed);
    return hash % 100 < experimentPercent;
}

void toTargetStateResponse(
    const std::unordered_map<db::Slot, db::Firmware>& slotToFirmware,
    json::ObjectBuilder result
    )
{
    result["slots"] << [&](json::ArrayBuilder b) {
        for (const auto& p : slotToFirmware) {
            const auto& slot = p.first;
            const auto& firmware = p.second;
            b << [&](json::ObjectBuilder b) {
                b["name"] = db::toString(slot);
                b["version"] = firmware.version();
                b["url"] = firmware.url();
                b["md5"] = firmware.md5();
                b["size"] = firmware.size();
            };
        };
   };
}

void logRequestUserAgent(const yacare::Request& request)
{
    const auto& requestUserAgent = request.env("HTTP_USER_AGENT");
    INFO() << "User-Agent: " << requestUserAgent;
}

void sortAndUnique(std::vector<db::TId>& rolloutIds)
{
    std::sort(rolloutIds.begin(), rolloutIds.end());
    rolloutIds.erase(std::unique(rolloutIds.begin(), rolloutIds.end()), rolloutIds.end());
}

bool isSignatureValid(const yacare::Request& request, const std::string& signatureKey)
{
    const auto& firmwareSignature = request.env("HTTP_X_YFIRMWARE_SIGNATURE");
    const auto& userAgent = request.env("HTTP_USER_AGENT");
    const auto& host = request.env("HTTP_HOST");
    const auto& requestMethod = request.env("REQUEST_METHOD");

    // endpoint name stored in different env depending on current mode
    // SCRIPT_NAME for fastcgi mode, PATH_INFO for http mode
    // use `request.env("SCRIPT_NAME") + request.env("PATH_INFO")` for both
    std::string pathAndQuery = request.env("PATH_INFO") + request.env("SCRIPT_NAME")
                                                        + request.env("QUERY_STRING");
    const auto& requestBody = request.body();

    return fw_signature::verifyFirmwareSignatureImpl(firmwareSignature,
                                                     userAgent,
                                                     host,
                                                     requestMethod,
                                                     pathAndQuery,
                                                     requestBody,
                                                     signatureKey);
}

} // namespace

YCR_RESPOND_TO("GET /firmware/1.x/target_state", hardware, deviceid, slots)
{
    logRequestUserAgent(request);
    if (!isSignatureValid(request, configuration()->signatureKey())) {
        throw yacare::errors::Forbidden() << "Invalid X-YFirmware-Signature";
    }

    auto txn = configuration()->pool().slaveTransaction();

    std::unordered_map<db::Slot, db::Firmware> slotToFirmware;
    std::string branch = loadDeviceBranch(*txn, hardware, deviceid);

    bool wasInactiveRollout = false;

    auto rolloutGtw = db::RolloutGateway{*txn};

    for (const auto& slot: slots) {
        auto rolloutHistories = db::RolloutHistoryGateway{*txn}.load(
            makeRolloutHistoryFilter(hardware, branch, slot),
            sql_chemistry::orderBy(RolloutHistoryTbl::id).desc()
        );

        db::TIds rolloutIds;
        for (const auto& rolloutHistory : rolloutHistories)
            rolloutIds.push_back(rolloutHistory.rolloutId());

        sortAndUnique(rolloutIds);
        auto rollouts = db::RolloutGateway{*txn}.loadByIds(rolloutIds);

        std::unordered_map<db::TId, db::Rollout> idToRollout;
        for (auto& rollout : rollouts) {
            db::TId rolloutId = rollout.id();
            idToRollout.insert({rolloutId, std::move(rollout)});
        }

        for (const auto& rolloutHistory : rolloutHistories) {
            const auto& rollout = idToRollout.at(rolloutHistory.rolloutId());
            if (isDeviceInExperiment(rolloutHistory.percent(), rollout.seed(), deviceid)) {
                if (rolloutHistory.status() == db::RolloutStatus::Active) {
                    slotToFirmware.insert({slot, db::FirmwareGateway{*txn}.loadById(rollout.firmwareId())});
                } else {
                    wasInactiveRollout = true;
                }
                break;
            }
        }
    }

   if (!slotToFirmware.empty()) {
        response << YCR_JSON(obj) {
            toTargetStateResponse(slotToFirmware, obj);
        };
    } else if (wasInactiveRollout) {
        response.setStatus(yacare::HTTPStatus::NoContent);
    } else {
        response.setStatus(yacare::HTTPStatus::NotFound);
    }
}

} // namespace maps::fw_updater
