#include <maps/wikimap/mapspro/services/mrc/libs/yt/include/config.h>
#include <maps/wikimap/mapspro/services/mrc/libs/yt/include/operation.h>
#include <maps/wikimap/mapspro/services/mrc/libs/yt/include/serialization.h>

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>

#include <maps/libs/common/include/retry.h>
#include <maps/libs/log8/include/log8.h>

#include <algorithm>
#include <memory>
#include <vector>

namespace maps::mrc::yt {
namespace impl {

class UploadFeatureImage: public Configured, public Mapper  {

public:
    UploadFeatureImage(size_t timeoutSeconds=10, size_t tryN=3);

    Y_SAVELOAD_JOB(timeoutSeconds_, tryN_);

    void Do(Reader* reader, Writer* writer) override;

private:
    mds::Mds& Mds();
    http::Client& Client();

    Bytes Load(const std::string& url);

    size_t timeoutSeconds_;
    size_t tryN_;

    std::unique_ptr<mds::Mds> mds_;
    std::unique_ptr<http::Client> client_;
};

UploadFeatureImage::UploadFeatureImage(size_t timeoutSeconds, size_t tryN)
    : timeoutSeconds_(timeoutSeconds)
    , tryN_(tryN)
{}

http::Client& UploadFeatureImage::Client()
{
    if (!client_) {
        http::Client client;
        client.setTimeout(std::chrono::seconds(timeoutSeconds_));

        client_ = std::make_unique<http::Client>(std::move(client));
    }

    return *client_;
}

mds::Mds& UploadFeatureImage::Mds()
{
    if (!mds_) {
        mds_ = std::make_unique<mds::Mds>(config().makeMdsClient());
    }

    return *mds_;
}

void UploadFeatureImage::Do(Reader* reader, Writer* writer)
{
    for (; reader->IsValid(); reader->Next()) {
        auto row = reader->GetRow();

        const auto entity = yt::deserialize<db::Feature>(row);
        const std::string url = Mds().makeReadUrl(entity.mdsKey());

        auto image = NYT::TNode::CreateEntity();
        try {
            image = serialize(Load(url));
        } catch (const ::maps::common::RetryNumberExceeded&) {
            ERROR() << "Impossible load url '" << url << "'";
        }

        row("image", image);
        writer->AddRow(row);
    }
}

Bytes UploadFeatureImage::Load(const std::string& url)
{
    static constexpr int HTTP_STATUS_OK = 200;

    return retry(
        [&]() -> Bytes {
            http::Request request(Client(), http::GET, http::URL(url));
            auto response = request.perform();

            if (response.status() != HTTP_STATUS_OK) {
                throw RuntimeError() << "Http status " << response.status();
            }

            return response.readBodyToVector();
        },
        ::maps::common::RetryPolicy().setTryNumber(tryN_)
    );
}

REGISTER_MAPPER(UploadFeatureImage);

} // namespace impl

size_t getRowCount(NYT::ICypressClient& client, TString path)
{
    return client.Get(path + "/@row_count").IntCast<size_t>();
}

Mapper* makeUploadFeatureImageMapper(size_t timeoutSeconds, size_t tryN)
{
    return new impl::UploadFeatureImage(timeoutSeconds, tryN);
}

void uploadFeatureImage(
        const common::Config& config,
        NYT::IClientBase& client,
        const TString& fromTable,
        const TString& toTable,
        std::optional<size_t> hintOnJobN,
        const NYT::TNode& operationOptions)
{
    static constexpr size_t UPLOAD_TIMEOUT_SECONDS = 10;
    static constexpr size_t MAX_UPLOAD_JOBS = 300;
    static constexpr size_t MIN_FEATURES_PER_JOB = 30;

    const size_t jobN = std::clamp(
        hintOnJobN ? *hintOnJobN : getRowCount(client, fromTable) / MIN_FEATURES_PER_JOB,
        1ul,
        MAX_UPLOAD_JOBS
    );

    client.Map(
        NYT::TMapOperationSpec()
            .AddInput<NYT::TNode>(fromTable)
            .AddOutput<NYT::TNode>(toTable)
            .JobCount(jobN),
        makeUploadFeatureImageMapper(UPLOAD_TIMEOUT_SECONDS),
        NYT::TOperationOptions()
            .Spec(operationOptions)
            .SecureVault(createSecureVault(config))
    );
}

} // namespace namespace maps::mrc::yt
