#include "worker.h"
#include "yt_reader.h"
#include "yt_row.h"

#include <maps/libs/auth/include/tvm.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/http/include/http.h>
#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/log8/include/log8.h>
#include <unordered_map>
#include <unordered_set>

namespace maps::wiki::toloka_downloader {

namespace {

const auto TVM_ALIAS_MRC_SERVICE = "mrc-agent-proxy";
const auto CONFIG_MRC_URL = "/config/services/mrc-agent-proxy/url";

const auto RETRY_POLICY = maps::common::RetryPolicy()
    .setTryNumber(5)
    .setInitialCooldown(std::chrono::seconds(1))
    .setCooldownBackoff(2.0);

std::string loadAttachmentData(http::Client& httpClient, const std::string& dataUrl)
{
    http::URL url(dataUrl);
    auto [responseBody, status] = httpClient.get(url, RETRY_POLICY);
    if (status != static_cast<int>(maps::http::Status::OK)) {
        WARN() << "Failed to make http get: " << url;
        return {};
    }
    return responseBody;
}

using ProcessedIds = std::unordered_map<PoolId, std::unordered_set<AssignmentId>>;

ProcessedIds loadProcessedIds(Database& database)
{
    size_t size = 0;
    ProcessedIds result;
    for (auto poolId : database.loadPoolIds()) {
        auto assignmentIds = database.loadAssignmentIds(poolId);
        result[poolId].insert(assignmentIds.begin(), assignmentIds.end());
        size += assignmentIds.size();
    }
    INFO() << "Loaded processed ids from db: " << size;
    return result;
}

CreateObjectRequest makeMrcCreateObjectRequest(
    TaskType taskType,
    const Assignment& assignment,
    const Attachments& attachments)
{
    const auto& data = assignment.data();

    CreateObjectRequest request;
    request.taskType = taskType;
    request.comment = data.comment;
    request.objectCoordinate = data.objectPosition;
    request.nmapsObjectId = data.nmapsObjectId;

    const auto azimuth = geolib3::geoDirection(
        {data.workerPosition, data.objectPosition}
    ).heading().value();

    for (const auto& attachment : attachments) {
        auto& photo = request.photos[attachment.id];
        photo.image.original = attachment.data;
        photo.workerCoordinate = data.workerPosition;
        photo.azimuth = azimuth;
        photo.takenAt = data.submitTs;
    }

    return request;
}


class TableReader : public YtReader
{
public:
    TableReader(Database& database, MrcClient& mrcClient, RunOptions runOptions)
        : database_(database)
        , mrcClient_(mrcClient)
        , runOptions_(std::move(runOptions))
        , processedIds_(loadProcessedIds(database_))
    {}

protected:
    Result readRow(const NYT::TNode& row) override
    {
        YtRow ytRow(row);
        auto assignment = ytRow.createAssignment();
        if (!assignment) {
            return Result::Failed;
        }

        bool processed = isProcessed(*assignment);
        INFO()  << "poolId: " << assignment->poolId()
                << " id: " << assignment->id()
                << (processed ? " skipped" : "");

        try {
            if (processed) {
                if (runOptions_.updateAssignmentData && ytRow.loadAssignmentData(*assignment)) {
                    database_.updateAssignmentData(*assignment);
                }
                return Result::Skipped;
            }
            if (!ytRow.loadAssignmentData(*assignment)) {
                database_.saveAssignment(*assignment, TaskStatus::Failed);
                return Result::Failed;
            }

            if (!database_.saveAssignment(*assignment, TaskStatus::Processing)) {
                return Result::Duplicated;
            }

            if (process(*assignment)) {
                database_.updateStatus(*assignment, TaskStatus::Ok);
                return Result::Ok;
            }
        } catch (const maps::Exception& ex) {
            ERROR() << "Error on process assignment,"
                    << " poolId: " << assignment->poolId()
                    << " id: " << assignment->id() << " : " << ex;
        } catch (const std::exception& ex) {
            ERROR() << "Error on process assignment,"
                    << " poolId: " << assignment->poolId()
                    << " id: " << assignment->id() << " : " << ex.what();
        }
        database_.updateStatus(*assignment, TaskStatus::Failed);
        return Result::Failed;
    }

private:
    bool needPublishAttaachments(
        const Assignment& assignment,
        const AttachmentIds& attachmentIds) const
    {
        auto savedPhotoIds = database_.loadSavedPhotoIds(assignment);
        if (savedPhotoIds.empty()) {
            return true;
        }

        // TODO: compare attachmentId sets
        REQUIRE(savedPhotoIds.size() == attachmentIds.size(),
                "Attachments changed, saved: " << savedPhotoIds.size() <<
                " from assignment: " << attachmentIds.size());
        return false;
    }

    Attachments loadAttachments(
        const Assignment& assignment,
        const AttachmentIds& attachmentIds)
    {
        Attachments result;
        for (const auto& attachmentId : attachmentIds) {
            Attachment attachment;
            attachment.id = attachmentId;
            attachment.data = loadAttachmentData(httpClient_, attachmentId);
            INFO() << "Loaded attachment id: " << attachmentId << " size: " << attachment.data.size();
            REQUIRE(
                !attachment.data.empty(),
                "Empty attachment: " << attachmentId << " (" << assignment.id() << ")");
            result.emplace_back(std::move(attachment));
        }
        return result;
    }

    bool process(const Assignment& assignment)
    {
        const auto& photoUrls = assignment.data().photoUrls;

        AttachmentIds attachmentIds{photoUrls.begin(), photoUrls.end()};
        REQUIRE(!attachmentIds.empty(),
                "Assignment " << assignment.id() << " without attachments");

        if (!needPublishAttaachments(assignment, attachmentIds)) {
            WARN() << "Attachments for assignment " << assignment.id()
                   << " already published to MRC";
            return false;
        }

        auto attachments = loadAttachments(assignment, attachmentIds);
        auto request = makeMrcCreateObjectRequest(
            database_.taskType(), assignment, attachments);

        auto publishResult = mrcClient_.submit(request);
        if (!database_.saveAttachments(assignment, publishResult.attachmentToPhotoIds)) {
            return false;
        }
        INFO() << "Attachments for assignment " << assignment.id() << " saved to MRC";
        return true;
    }

    bool isProcessed(const Assignment& assignment) const
    {
        auto it = processedIds_.find(assignment.poolId());
        if (it == processedIds_.end()) {
            return false;
        }
        return it->second.contains(assignment.id());
    }

    Database& database_;
    MrcClient& mrcClient_;
    http::Client httpClient_;
    const RunOptions runOptions_;
    const ProcessedIds processedIds_;
};

} // namespace

Worker::Worker(
        const common::ExtendedXmlDoc& config,
        const std::string& tvmAlias,
        TaskType taskType)
    : database_(config, taskType)
    , mrcClient_(config.get<std::string>(CONFIG_MRC_URL))
{
    auto tvmToolSettings = maps::auth::TvmtoolSettings().selectClientAlias(tvmAlias);
    auto tvmClient = std::make_shared<NTvmAuth::TTvmClient>(tvmToolSettings.makeTvmClient());

    mrcClient_.setTvmTicketProvider(
        [tvmClient]() {
            return tvmClient->GetServiceTicketFor(TVM_ALIAS_MRC_SERVICE);
        }
    );
}

Stats Worker::run(const std::string& ytTable, const RunOptions& runOptions)
{
    TableReader reader(database_, mrcClient_, runOptions);
    return reader.readData(ytTable);
}

} // namespace maps::wiki::toloka_downloader
