#include "sync_routine.h"

#include "common_routines.h"
#include "fbapi_update_extra_data.h"

#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/feedback/task.h>
#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/feedback/gateway_ro.h>

#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/feedback/enums.h>
#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/feedback/history.h>
#include <maps/wikimap/mapspro/libs/http/include/yandex/maps/wiki/http/fbapi/models/common.h>

#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/exception.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/social/feedback/agent.h>
#include <yandex/maps/wiki/social/fbapi_issue.h>
#include <yandex/maps/wiki/social/gateway.h>

#include <string>
#include <optional>

namespace maps::wiki::sync_fbapi_feedback {

namespace sf = social::feedback;

namespace {

std::optional<std::string> getParam(
    const sf::HistoryItemParams& params,
    const std::string& paramName)
{
    const auto paramIt = params.find(paramName);
    if (paramIt != params.end()) {
        return paramIt->second;
    }
    return std::nullopt;
}

std::optional<sf::RejectReason> rejectReasonFromTask(
    const sf::Task& task)
{
    auto resolved = task.resolved();
    if (resolved) {
        return resolved->resolution.rejectReason();
    }

    return std::nullopt;
}

std::string loadMandatoryCommentText(
    pqxx::transaction_base& socialTxn,
    social::TId commentId)
{
    social::Gateway gw(socialTxn);
    const auto comments = gw.loadComments({commentId});
    REQUIRE(!comments.empty() && !comments.front().data().empty(),
        "Comment not found for id: " << commentId);
    return comments.front().data();
}

FbapiUpdateExtraData loadNeedInfoExtraData(
    const sf::History& taskHistory,
    pqxx::transaction_base& socialTxn)
{
    auto needInfoOperation = taskHistory.lastItemWith(sf::TaskOperation::NeedInfo);

    REQUIRE(
        needInfoOperation.has_value(),
        "No needInfo-historyItem for needInfo status");

    auto requestTemplate = getParam(needInfoOperation->params(), sf::HISTORY_PARAM_TEMPLATE);

    ASSERT(needInfoOperation->commentId());
    auto commentMessage =
        loadMandatoryCommentText(socialTxn, needInfoOperation->commentId().value());
    return FbapiUpdateExtraData::createForNeedInfo(
        std::move(commentMessage),
        requestTemplate
    );
}

FbapiUpdateExtraData loadRejectExtraData(
    const sf::Task& task,
    const sf::History& taskHistory,
    pqxx::transaction_base& socialTxn)
{
    auto rejectReason = rejectReasonFromTask(task);

    std::optional<std::string> commentMessage;
    auto rejectOperation = taskHistory.lastItemWith(sf::TaskOperation::Reject);
    if (rejectOperation) {
        if (rejectOperation->commentId()) {
            commentMessage = loadMandatoryCommentText(socialTxn, rejectOperation->commentId().value());
        }
    } else {
        REQUIRE(
            !rejectReason,
            "Reject was not found in history");
    }

    return FbapiUpdateExtraData::createForReject(
        commentMessage,
        rejectReason
    );
}

sf::TaskForUpdate getTaskForUpdate(sf::Agent& agent, social::TId taskId)
{
    auto socialTask = agent.taskForUpdateById(taskId);
    ASSERT(socialTask);
    return std::move(*socialTask);
}

struct SyncStat
{
    size_t patched = 0;
    size_t processed = 0;
    size_t errorCount = 0;
    size_t rejected = 0;
};

sf::TaskForUpdate rejectNeedInfoTaskAndUpdateIssueStatus(
    fbapi::IGateway& fbapiGateway,
    sf::Agent& agent,
    const sf::TaskForUpdate& socialTask,
    const fbapi::Task& originalIssue)
{
    auto openedTask = agent.openTask(socialTask);
    ASSERT(openedTask);
    auto rejectedTask = agent.resolveTaskCascade(
        *openedTask,
        sf::Resolution::createRejected(sf::RejectReason::NoInfo));
    ASSERT(rejectedTask);

    auto actualStatus = fbapiStatusFromSocialTask(*rejectedTask);
    if (actualStatus != originalIssue.status()) {
        fbapiGateway.changeTaskById(originalIssue.id(), makeUpdateStatusParams(actualStatus));

        INFO() << "fbapiIssue '" << originalIssue.id() << "' patched(rejected) successfully.";
    }
    return std::move(*rejectedTask);
}

enum class UpdateIssueResult
{
    Done,
    NeedReject,
};

UpdateIssueResult updateIssue(
    fbapi::IGateway& fbapiGateway,
    const std::string& issueId,
    fbapi::TaskStatus actualStatus,
    const std::optional<FbapiUpdateExtraData>& fbapiUpdateExtraData)
{
    try {
        auto params = makeUpdateStatusParams(actualStatus);
        if (fbapiUpdateExtraData) {
            params.update(fbapiUpdateExtraData->makeParams());
        }
        fbapiGateway.changeTaskById(issueId, params);

        INFO() << "fbapiIssue '" << issueId << "' patched successfully.";
    } catch (const fbapi::BadRequestError& ex) {
        INFO() << "fbapiIssue '" << issueId << "' patching failed: '" << ex.what() << "'";
        if (actualStatus == fbapi::TaskStatus::NeedInfo) {
            return UpdateIssueResult::NeedReject;
        }
        throw;
    }
    return UpdateIssueResult::Done;
}

} // unnamed namespace

void syncFbapiIssue(
    fbapi::IGateway& fbapiGateway,
    pgpool3::Pool& socialPool,
    const social::FbapiIssue& fbapiIssue,
    social::TId socialTaskId,
    SyncStat& stat)
try {
    auto originalIssue = fbapiGateway.taskById(fbapiIssue.issueId());
    if (originalIssue.status() == fbapi::TaskStatus::New) {
        return; // fbapi issues in New status processed in importRoutine()
    }

    auto txnSocialWrite = socialPool.masterWriteableTransaction();
    sf::Agent agent(txnSocialWrite.get(), common::ROBOT_UID);
    auto socialTask = getTaskForUpdate(agent, socialTaskId);
    const auto socialTaskHistory =
        sf::GatewayRO(txnSocialWrite.get()).history(socialTaskId);

    fbapi::TaskStatus actualStatus = fbapiStatusFromSocialTask(socialTask);
    std::optional<fbapi::TaskResolution> actualResolution =
        getTaskResolution(rejectReasonFromTask(socialTask));

    if (actualStatus != originalIssue.status()
        || actualResolution != originalIssue.nmapsIntegrationResolution())
    {
        std::optional<FbapiUpdateExtraData> fbapiUpdateExtraData;
        if (actualStatus == fbapi::TaskStatus::NeedInfo) {
            fbapiUpdateExtraData = loadNeedInfoExtraData(socialTaskHistory, txnSocialWrite.get());
        } else if (actualStatus == fbapi::TaskStatus::Rejected) {
            fbapiUpdateExtraData = loadRejectExtraData(socialTask, socialTaskHistory, txnSocialWrite.get());
        }
        switch(updateIssue(fbapiGateway, originalIssue.id(), actualStatus, fbapiUpdateExtraData)) {
            case UpdateIssueResult::Done:
                ++stat.patched;
                break;

            case UpdateIssueResult::NeedReject:
                socialTask = rejectNeedInfoTaskAndUpdateIssueStatus(
                    fbapiGateway, agent, socialTask, originalIssue);
                ++stat.rejected;
                break;
        }
    }

    social::updateFbapiIssueHashValue(
        txnSocialWrite.get(),
        fbapiIssue.id(),
        socialTask.hashValue());

    txnSocialWrite->commit();

    ++stat.processed;
} catch (const Exception& ex) {
    ++stat.errorCount;
    ERROR() << "Processing fbapiIssue '" << fbapiIssue.issueId() << "' failed" << ex;
}

void syncRoutine(
    fbapi::IGateway& fbapiGateway,
    pgpool3::Pool& socialPool,
    tasks::StatusWriter& statusWriter)
{
    social::FbapiIssues fbapiIssues;
    {
        // get social read txn
        auto txnSocialRead = socialPool.slaveTransaction();

        // get fbapiIssue list
        fbapiIssues = social::fbapiIssuesUnmatchedHash(txnSocialRead.get());
        INFO() << fbapiIssues.size() << " fbapi Issues with unmatched hash found.";
        if (fbapiIssues.empty()) {
            return;
        }
    }

    // process every fbapiIssue
    SyncStat stat;
    for (const auto& fbapiIssue : fbapiIssues) {
        syncFbapiIssue(fbapiGateway, socialPool, fbapiIssue, fbapiIssue.feedbackTaskId(), stat);
    }

    if (stat.errorCount) {
        statusWriter.warn(
            "Processing fbapi issues problem. " +
            std::string("Failed to process ") + std::to_string(stat.errorCount) + " issues"
        );
    }

    INFO() << "fbapiIssues: "
            << stat.processed << " processed ("
                << stat.patched << " patched, "
                << stat.rejected << "rejected)"
            << " + " << fbapiIssues.size() - stat.processed - stat.errorCount << " skipped"
            << " + " << stat.errorCount << " errors"
            << " = " << fbapiIssues.size() << " Total";
}

} // namespace maps::wiki::sync_fbapi_feedback
