BareGit
#include "telegram_client.hpp"

#include <algorithm>
#include <chrono>
#include <cctype>
#include <format>
#include <optional>
#include <string_view>

#include <mw/http_client.hpp>
#include <mw/url.hpp>

#include "service_error.hpp"

namespace telegrammer
{

namespace
{

std::string lower(std::string value)
{
    std::transform(value.begin(), value.end(), value.begin(),
                   [](unsigned char character)
                   {
                       return static_cast<char>(std::tolower(character));
                   });
    return value;
}

std::optional<int> retryAfter(const json& body)
{
    if(!body.is_object() || !body.contains("parameters") ||
       !body["parameters"].is_object() ||
       !body["parameters"].contains("retry_after") ||
       !body["parameters"]["retry_after"].is_number_integer())
    {
        return std::nullopt;
    }

    int64_t value = 0;
    try
    {
        value = body["parameters"]["retry_after"].get<int64_t>();
    }
    catch(const std::exception&)
    {
        return std::nullopt;
    }
    if(value <= 0 || value > 3600)
    {
        return std::nullopt;
    }
    return static_cast<int>(value);
}

mw::Error telegramFailure(int status, const json& body)
{
    int error_code = 0;
    if(body.is_object() && body.contains("error_code") &&
       body["error_code"].is_number_integer())
    {
        try
        {
            error_code = body["error_code"].get<int>();
        }
        catch(const std::exception&)
        {
            error_code = 0;
        }
    }

    std::string description = "Telegram rejected the request";
    if(body.is_object() && body.contains("description") &&
       body["description"].is_string())
    {
        description = body["description"].get<std::string>();
    }

    std::optional<int> retry_after = retryAfter(body);
    if(status == 429 || error_code == 429 || retry_after.has_value())
    {
        return serviceError(429, "TELEGRAM_RATE_LIMITED", description,
                             retry_after, true);
    }
    if(status == 400 || status == 403 || error_code == 400 ||
       error_code == 403)
    {
        return serviceError(422, "TELEGRAM_REJECTED", description);
    }
    if(status == 401 || error_code == 401)
    {
        return serviceError(502, "UPSTREAM_ERROR", description);
    }
    return serviceError(502, "UPSTREAM_ERROR", description,
                        std::nullopt, status >= 500);
}

bool looksLikeTimeout(const std::string& message)
{
    std::string value = lower(message);
    return value.find("timeout") != std::string::npos ||
           value.find("timed out") != std::string::npos;
}

} // namespace

TelegramClient::TelegramClient(std::string token)
    : token_(std::move(token)),
      base_url_(std::format("https://api.telegram.org/bot{}/", token_))
{}

mw::E<json> TelegramClient::request(const std::string& method,
                                     const std::string& payload,
                                     bool post) const
{
    mw::HTTPSession session;
    auto connection_result = session.connectionTimeout(
        std::chrono::seconds(5));
    auto transfer_result = session.transferTimeout(
        std::chrono::seconds(post ? 10 : 40));
    auto size_result = session.maxSize(16 * 1024 * 1024);
    auto protocol_result = session.allowedProtocols("https");
    session.followRedirects(false);
    if(!connection_result.has_value() || !transfer_result.has_value() ||
       !size_result.has_value() || !protocol_result.has_value())
    {
        return std::unexpected(serviceError(
            500, "HTTP_CLIENT_CONFIGURATION", "Unable to configure HTTP"));
    }

    mw::HTTPResponse const* response = nullptr;
    if(post)
    {
        mw::HTTPRequest request(base_url_ + method);
        request.setContentType("application/json");
        request.setPayload(payload);
        auto result = session.post(request);
        if(!result.has_value())
        {
            std::string message = mw::errorMsg(result.error());
            if(looksLikeTimeout(message))
            {
                return std::unexpected(serviceError(
                    504, "UPSTREAM_TIMEOUT", "Telegram request timed out",
                    std::nullopt, true));
            }
            return std::unexpected(serviceError(
                502, "UPSTREAM_UNAVAILABLE", "Telegram is unavailable",
                std::nullopt, true));
        }
        response = *result;
    }
    else
    {
        auto result = session.get(base_url_ + method + payload);
        if(!result.has_value())
        {
            std::string message = mw::errorMsg(result.error());
            if(looksLikeTimeout(message))
            {
                return std::unexpected(serviceError(
                    504, "UPSTREAM_TIMEOUT", "Telegram request timed out",
                    std::nullopt, true));
            }
            return std::unexpected(serviceError(
                502, "UPSTREAM_UNAVAILABLE", "Telegram is unavailable",
                std::nullopt, true));
        }
        response = *result;
    }

    json body;
    try
    {
        body = json::parse(response->payloadAsStr());
    }
    catch(const std::exception&)
    {
        return std::unexpected(serviceError(
            502, "UPSTREAM_ERROR", "Telegram returned invalid JSON"));
    }

    bool successful = response->status >= 200 && response->status < 300 &&
                      body.is_object() && body.contains("ok") &&
                      body["ok"].is_boolean() && body["ok"].get<bool>();
    if(!successful)
    {
        return std::unexpected(telegramFailure(response->status, body));
    }
    if(!body.contains("result"))
    {
        return std::unexpected(serviceError(
            502, "UPSTREAM_ERROR", "Telegram returned an incomplete response"));
    }
    return body;
}

mw::E<json> TelegramClient::getMe() const
{
    auto result = request("getMe", "", false);
    if(!result.has_value())
    {
        return std::unexpected(result.error());
    }
    if(!(*result)["result"].is_object() ||
       !(*result)["result"].contains("id") ||
       !(*result)["result"]["id"].is_number_integer())
    {
        return std::unexpected(serviceError(
            502, "UPSTREAM_ERROR", "Telegram returned an invalid bot identity"));
    }
    try
    {
        [[maybe_unused]] auto bot_id =
            (*result)["result"]["id"].get<int64_t>();
    }
    catch(const std::exception&)
    {
        return std::unexpected(serviceError(
            502, "UPSTREAM_ERROR", "Telegram returned an invalid bot identity"));
    }
    return *result;
}

mw::E<json> TelegramClient::sendMessage(int64_t chat_id,
                                        const std::string& text) const
{
    auto result = request("sendMessage", json{{"chat_id", chat_id},
                                                {"text", text}}
                                              .dump(),
                          true);
    if(!result.has_value())
    {
        return std::unexpected(result.error());
    }
    if(!(*result)["result"].is_object())
    {
        return std::unexpected(serviceError(
            502, "UPSTREAM_ERROR", "Telegram returned an invalid message"));
    }
    return *result;
}

mw::E<json> TelegramClient::getUpdates(int64_t offset, int timeout) const
{
    std::string query = std::format(
        "?offset={}&timeout={}&allowed_updates={}", offset, timeout,
        mw::URL::encode("[\"message\"]"));
    auto result = request("getUpdates", query, false);
    if(!result.has_value())
    {
        return std::unexpected(result.error());
    }
    if(!(*result)["result"].is_array())
    {
        return std::unexpected(serviceError(
            502, "UPSTREAM_ERROR", "Telegram returned invalid updates"));
    }
    return *result;
}

} // namespace telegrammer