#include "worker.h"
#include "assignment.h"
#include "common.h"

#include <maps/libs/auth/include/tvm.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/geom_utils.h>

#include <map>
#include <set>
#include <vector>

namespace maps::wiki::toloka {
namespace {

void dumpAssignments(const Assignments& assignments)
{
    INFO() << "Assignments, size: " << assignments.size();
    size_t index = 0;
    std::map<std::string, size_t> statuses;
    for (const auto& assignment : assignments) {
        ++index;
        INFO() << "--- ";
        for (const auto& [key, value] : assignment.data()) {
            INFO() << index << " " << key << " : " << value;
        }
        statuses[assignment.status()]++;
    }
    INFO() << "--- STATUSES ---";
    for (const auto& [status, count] : statuses) {
        INFO() << status << " : " << count;
    }
    INFO() << "---";
}

bool needPublishAttaachments(
    Database& db,
    const Assignment& assignment,
    const AttachmentIds& attachmentIds)
{
    auto savedPhotoIds = db.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;
}

CreateObjectRequest makeMrcCreateObjectRequest(
    const Assignment& assignment,
    const Attachments& attachments)
{
    auto strToDoubles = [&](const auto& assignmentKey) -> std::vector<double> {
        const auto& value = assignment.get(assignmentKey);
        if (value.empty()) {
            return {};
        }
        return wiki::common::convertToVectorOfDouble(value);
    };

    CreateObjectRequest request;
    request.comment = assignment.get("output_comment");
    auto coords = strToDoubles("input_coordinates");
    REQUIRE(coords.size() == 2, "Invalid input coordinate");
    request.objectCoordinate = geolib3::Point2(coords[1], coords[0]); // y,x

    coords = strToDoubles("output_photopoint");
    REQUIRE(coords.size() == 2, "Invalid output coordinate");

    const auto workerCoordinate = geolib3::Point2(coords[1], coords[0]); // y,x
    const auto azimuth = geolib3::geoDirection(
        {workerCoordinate, request.objectCoordinate}
    ).heading().value();

    const auto& startedDateTimeStr = assignment.get("assignment_started");
    REQUIRE(!startedDateTimeStr.empty(), "Empty assignment started date");
    const auto startedDateTime = chrono::parseIsoDateTime(startedDateTimeStr);

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

    return request;
}

} // namespace

Worker::Worker(
        const common::ExtendedXmlDoc& config,
        const std::string& tvmAlias,
        const Client& client)
    : db_(config, client.projectId())
    , mrcClient_(config.get<std::string>("/config/services/mrc-agent-proxy/url"))
    , client_(client)
{
    auto tvmToolSettings = maps::auth::TvmtoolSettings().selectClientAlias(tvmAlias);
    auto tvmClient = std::make_shared<NTvmAuth::TTvmClient>(tvmToolSettings.makeTvmClient());

    mrcClient_.setTvmTicketProvider(
        [tvmClient]() {
            return tvmClient->GetServiceTicketFor("mrc-agent-proxy");
        }
    );
}

bool Worker::run(
    const std::set<Action>& actions,
    PoolId poolId)
{
    client_.dumpPool(poolId);

    auto assignments = client_.loadAssignments(poolId);
    if (actions.contains(Action::Dump)) {
        dumpAssignments(assignments);
    }

    static const auto STATUS_ACCEPTED =
        (std::ostringstream() << maps::mrc::toloka::io::AssignmentStatus::Accepted).str();

    bool allAccepted = true;
    auto processAssignment = [&](const auto& assignment) {
        if (assignment.status() != STATUS_ACCEPTED) {
            allAccepted = false;
            return;
        }

        if (db_.saveAssignment(poolId, assignment)) {
            INFO() << "Assignment saved: " << assignment.id();
        }

        auto attachmentIds = assignment.attachmentIds();
        REQUIRE(!attachmentIds.empty(),
                "Assignment " << assignment.id() << " without attachments");

        if (!actions.contains(Action::Upload)) {
            return;
        }

        if (!needPublishAttaachments(db_, assignment, attachmentIds)) {
            INFO() << "Attachments for assignment " << assignment.id()
                   << " already published into MRC";
            return;
        }

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

        auto publishResult = mrcClient_.submit(request);
        if (db_.saveAttachments(assignment, publishResult.attachmentToPhotoIds)) {
            INFO() << "Attachments for assignment " << assignment.id()
                   << " saved into MRC";
        }
    };

    size_t errors = 0;
    for (const auto& assignment : assignments) {
        try {
            processAssignment(assignment);
        } catch (const maps::Exception& ex) {
            ERROR() << "Assignment: " << assignment.id() << " error: " << ex;
            ++errors;
        } catch (const std::exception& ex) {
            ERROR() << "Assignment: " << assignment.id() << " error: " << ex.what();
            ++errors;
        }
    }
    if (errors) {
        ERROR() << "Processing finished with " << errors << " errors";
        return false;
    }

    if (allAccepted && actions.contains(Action::Archive)) {
        REQUIRE(actions.contains(Action::Upload),
                "Pool archivation forbidden without upload assignments");
        try {
            //client_.archivePool(poolId);
        } catch (const std::exception& ex) {
            WARN() << "Pool archivation " << poolId << " failed: " << ex.what();
        }
    }
    INFO() << "Done";
    return true;
}

} // namespace maps::wiki::toloka
