BareGit
#include "app.h"

#include <chrono>
#include <exception>
#include <functional>
#include <string>
#include <string_view>

#include "embedded_assets.h"

#include <inja/inja.hpp>
#include <nlohmann/json.hpp>

namespace
{

enum class UiStatus
{
    GOOD,
    BAD,
    OTHER,
    NA
};

struct StatusPresentation
{
    std::string class_name;
    std::string label;
};

StatusPresentation present(UiStatus status)
{
    switch(status)
    {
    case UiStatus::GOOD:
        return {"good", "Good"};
    case UiStatus::BAD:
        return {"bad", "Bad"};
    case UiStatus::OTHER:
        return {"other", "Other"};
    case UiStatus::NA:
        return {"na", "N/A"};
    }
    std::terminate();
}

std::int64_t nowUnixSeconds()
{
    return std::chrono::duration_cast<std::chrono::seconds>(
        std::chrono::system_clock::now().time_since_epoch()).count();
}

bool isStale(const ServiceConfig& config, std::int64_t timestamp,
            std::int64_t now)
{
    if(timestamp >= now)
    {
        return false;
    }
    auto interval = std::chrono::duration_cast<std::chrono::seconds>(
        config.interval).count();
    if(interval <= 0)
    {
        interval = 1;
    }
    const auto age = now - timestamp;
    return age > interval && age - interval > interval;
}

UiStatus deriveStatus(const ServiceConfig& config,
                      const std::optional<StatusRecord>& record,
                      std::int64_t now)
{
    if(!record || isStale(config, record->timestamp, now))
    {
        return UiStatus::NA;
    }
    switch(record->status)
    {
    case ProbeStatus::GOOD:
        return UiStatus::GOOD;
    case ProbeStatus::BAD:
        return UiStatus::BAD;
    case ProbeStatus::OTHER:
        return UiStatus::OTHER;
    }
    return UiStatus::OTHER;
}

const EmbeddedAsset* findAsset(std::string_view path)
{
    for(const auto& asset : embeddedAssets())
    {
        if(asset.path == path)
        {
            return &asset;
        }
    }
    return nullptr;
}

mw::E<nlohmann::json> makePageData(const Configuration& configuration,
                                   DataSourceInterface& data_source)
{
    const auto now = nowUnixSeconds();
    nlohmann::json data = {
        {"groups", nlohmann::json::array()},
        {"no_groups", configuration.groups.empty()}
    };
    for(const auto& group : configuration.groups)
    {
        nlohmann::json group_data = {
            {"name", group.name},
            {"services", nlohmann::json::array()},
            {"no_services", group.services.empty()}
        };
        for(const auto& config : group.services)
        {
            auto latest = data_source.latest(config.id);
            if(!latest)
            {
                return std::unexpected(latest.error());
            }
            const auto& record = *latest;
            const auto status = deriveStatus(config, record, now);
            const auto presentation = present(status);
            nlohmann::json service_data = {
                {"id", config.id},
                {"name", config.name},
                {"description", config.description},
                {"status_class", presentation.class_name},
                {"status_label", presentation.label},
                {"has_url", config.url.has_value() && !config.url->empty()},
                {"url", config.url.value_or("")},
                {"has_result", record.has_value()},
                {"timestamp", record ? record->timestamp : 0},
                {"duration_microsecond", record
                    ? record->duration_microsecond : 0}
            };
            group_data["services"].push_back(std::move(service_data));
        }
        data["groups"].push_back(std::move(group_data));
    }
    return data;
}

}

App::App(const ListenAddress& listen, Configuration configuration,
         DataSourceInterface& data_source)
    : mw::HTTPServer(listen), configuration(std::move(configuration)),
      data_source(data_source)
{}

void App::setup()
{
    server.set_exception_handler(unexpectedException);
    server.Get("/", std::bind_front(&App::serveIndex, this));
    server.Get("/static/.*", serveStatic);
}

void App::serveIndex([[maybe_unused]] const Request& request,
                     Response& response) const
{
    auto data = makePageData(configuration, data_source);
    if(!data)
    {
        response.status = 500;
        response.set_content("Failed to read service status: " +
                                 data.error().msg() + "\n",
                             "text/plain; charset=utf-8");
        return;
    }
    const auto* index = findAsset("/");
    if(index == nullptr)
    {
        std::terminate();
    }
    inja::Environment environment;
    environment.set_html_autoescape(true);
    const auto html = environment.render(std::string(index->content), *data);
    response.set_content(html, "text/html; charset=utf-8");
}

void App::serveStatic(const Request& request, Response& response)
{
    for(const auto& asset : embeddedAssets())
    {
        if(request.path == asset.path)
        {
            response.set_content(asset.content.data(), asset.content.size(),
                                 std::string(asset.content_type));
            return;
        }
    }
    response.status = 404;
    response.set_content("Not found\n", "text/plain; charset=utf-8");
}

void App::unexpectedException([[maybe_unused]] const Request& request,
                             [[maybe_unused]] Response& response,
                             [[maybe_unused]] std::exception_ptr exception)
    noexcept
{
    std::terminate();
}