#include "moderation_log.h"
#include "commit.h"

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/wiki/social/gateway.h>
#include <yandex/maps/wiki/revision/commit.h>

#include <chrono>
#include <iostream>
#include <unordered_map>
#include <unistd.h>

namespace maps {
namespace wiki {

namespace {

const std::string LOGGING_PREFIX = "ModerationLogger: ";
const unsigned REOPEN_FAIL_SLEEP_SECONDS = 5;
const unsigned LOG_FAIL_SLEEP_SECONDS = 2;
const size_t PRECISION = 12;

std::ostream& operator << (
        std::ostream& os,
        const std::optional<geolib3::BoundingBox>& bbox)
{
    if (!bbox) {
        return os;
    }

    std::ostream::sentry sentry(os);
    if (!sentry) {
        return os;
    }

    auto lowerCorner = geolib3::mercator2GeoPoint(bbox->lowerCorner());
    auto upperCorner = geolib3::mercator2GeoPoint(bbox->upperCorner());
    os << '['
        << '[' << lowerCorner.x() << ',' << lowerCorner.y() << ']' << ','
        << '[' << upperCorner.x() << ',' << upperCorner.y() << ']' << ']';
    return os;
}

template<typename Resolution>
void serializeEvent(
        std::ostream& logStream,
        TUid taskId,
        const std::string& sqlDatetime,
        TUid uid,
        ModerationLogger::Action action,
        Resolution resolution,
        const social::Event& commitEvent,
        const boost::optional<RevertReason>& revertReason)
{
    ASSERT(commitEvent.primaryObjectData());
    ASSERT(commitEvent.commitData());

    auto unixtime = std::chrono::system_clock::to_time_t(
        std::chrono::system_clock::time_point(
            std::chrono::duration_cast<std::chrono::system_clock::duration>(
                chrono::parseSqlDateTime(sqlDatetime).time_since_epoch())));

    logStream << "tskv"
        << "\ttskv_format=nmaps-moderation-log"
        << "\tunixtime=" << unixtime
        << "\tpuid=" << uid
        << "\ttask_action=" << action
        << "\tresolution=" << resolution
        << "\tcommit_id=" << commitEvent.commitData()->commitId()
        << "\tbranch_id=" << commitEvent.commitData()->branchId()
        << "\taction=" << commitEvent.commitData()->action()
        << "\tobject_id=" << commitEvent.primaryObjectData()->id()
        << "\tobject_category=" << commitEvent.primaryObjectData()->categoryId()
        << "\tgeom=" << commitEvent.commitData()->bbox()
        << "\tevent_type=" << commitEvent.type()
        << "\trevert_reason=";
    if (revertReason) {
        logStream << *revertReason;
    }
    logStream << "\ttask_id=" << taskId
        << "\n";
}

social::Tasks loadTasksByTaskIds(const social::TaskIds& taskIds)
{
    auto txn = cfg()->poolSocial().masterReadOnlyTransaction();
    return social::Gateway(*txn).loadTasksByTaskIds(taskIds);
}

void logEvents(
        std::ostream& logStream,
        const std::list<ModerationLogger::Event>& events)
{
    social::TaskIds taskIds;
    for (const auto& event : events) {
        taskIds.insert(event.taskId);
    }

    social::TIds revertedCommitIds;
    std::unordered_map<social::TId, social::Task> taskById;
    for (auto& task : loadTasksByTaskIds(taskIds)) {
        if (task.isResolved()
                && task.resolved().resolution() == social::ResolveResolution::Revert) {
            revertedCommitIds.insert(task.commitId());
        }
        taskById.insert(std::make_pair(task.id(), std::move(task)));
    }

    auto txn = cfg()->poolCore().masterReadOnlyTransaction();

    social::TIds revertingDirectlyCommitIds;
    std::unordered_map<social::TId, revision::Commit> revertedCommitById;
    if (!revertedCommitIds.empty()) {
        for (auto& commit
                : revision::Commit::load(*txn, revision::filters::CommitAttr::id().in(revertedCommitIds)))
        {
            if (auto rdCommitId = commit.revertingDirectlyCommitId()) {
                revertingDirectlyCommitIds.insert(*rdCommitId);
            }
            revertedCommitById.insert(std::make_pair(commit.id(), std::move(commit)));
        }
    }

    std::unordered_map<social::TId, boost::optional<RevertReason>> revertReasonByRDCommitId; // RD - RevertingDirectly
    if (!revertingDirectlyCommitIds.empty()) {
        for (auto& rdCommit
                : revision::Commit::load(*txn, revision::filters::CommitAttr::id().in(revertingDirectlyCommitIds)))
        {
            revertReasonByRDCommitId.insert(std::make_pair(rdCommit.id(), revertReason(rdCommit)));
        }
    }

    for (const auto& event : events) {
        auto taskIt = taskById.find(event.taskId);
        if (taskIt == std::end(taskById)) {
            ERROR() << LOGGING_PREFIX
                << "Task " << event.taskId << " not found";
            continue;
        }

        const auto& task = taskIt->second;
        const auto& commitEvent = task.event();
        if (!commitEvent.primaryObjectData() || !commitEvent.commitData()) {
            continue;
        }

        boost::optional<RevertReason> revertReason;
        if (task.isResolved()
                && task.resolved().resolution() == social::ResolveResolution::Revert) {
            auto commitIt = revertedCommitById.find(task.commitId());
            if (commitIt == std::end(revertedCommitById)) {
                WARN() << LOGGING_PREFIX
                       << "Reverted task " << task.id() << " has no commit";
            } else {
                const revision::Commit& commit = commitIt->second;
                if (auto rdCommitId = commit.revertingDirectlyCommitId()) {
                    auto rdCommitIt = revertReasonByRDCommitId.find(*rdCommitId);
                    if (rdCommitIt != std::end(revertReasonByRDCommitId)) {
                        revertReason = rdCommitIt->second;
                    }
                }
            }
        }

        switch (event.action) {
            case ModerationLogger::Action::Created:
                serializeEvent(
                    logStream,
                    task.id(),
                    commitEvent.createdAt(),
                    commitEvent.createdBy(),
                    ModerationLogger::Action::Created,
                    std::string(),
                    commitEvent,
                    revertReason);
                break;
            case ModerationLogger::Action::Resolved:
                if (!task.isResolved()) {
                    WARN() << LOGGING_PREFIX << "Task " << task.id()
                        << " status mismatch: expected resolved";
                    continue;
                }
                serializeEvent(
                    logStream,
                    task.id(),
                    task.resolved().date(),
                    task.resolved().uid(),
                    ModerationLogger::Action::Resolved,
                    task.resolved().resolution(),
                    commitEvent,
                    revertReason);
                break;
            case ModerationLogger::Action::Closed:
                if (!task.isClosed()) {
                    WARN() << LOGGING_PREFIX << "Task " << task.id()
                        << " status mismatch: expected closed";
                    continue;
                }
                if (!task.isResolved()) {
                    ERROR() << LOGGING_PREFIX
                        << "Task " << task.id() << " is closed but not resolved";
                    continue;
                }

                if (task.resolved().uid() == task.closed().uid()
                       && task.resolved().date() == task.closed().date()) {
                    // task was resolved automatically
                    serializeEvent(
                        logStream,
                        task.id(),
                        task.resolved().date(),
                        task.resolved().uid(),
                        ModerationLogger::Action::Resolved,
                        task.resolved().resolution(),
                        commitEvent,
                        revertReason);
                }

                serializeEvent(
                    logStream,
                    task.id(),
                    task.closed().date(),
                    task.closed().uid(),
                    ModerationLogger::Action::Closed,
                    task.closed().resolution(),
                    commitEvent,
                    revertReason);
                break;
        }
    }
}

} // namespace

ModerationLogger::ModerationLogger(const std::string& logPath)
    : logPath_(logPath)
    , reopenFlag_(true)
    , writerThread_([this] { writeLog(); })
{}

ModerationLogger::~ModerationLogger()
{
    eventsQueue_.finish();
    if (writerThread_.joinable()) {
        writerThread_.join();
    }
}

void ModerationLogger::log(Event event)
{ eventsQueue_.push(std::move(event)); }

void ModerationLogger::log(social::TId taskId, Action action)
{ log({taskId, action}); }

void ModerationLogger::log(const social::TaskIds& taskIds, Action action)
{
    for (auto taskId : taskIds) {
        log(taskId, action);
    }
}

void ModerationLogger::onLogrotate()
{ reopenFlag_ = true; }

void ModerationLogger::writeLog() noexcept
{
    try {
        std::list<Event> eventsBuffer;
        while (!eventsQueue_.finished() || eventsQueue_.pendingItemsCount()) {
            eventsQueue_.popAll(eventsBuffer);
            if (eventsBuffer.empty()) {
                continue;
            }

            while (reopenFlag_.exchange(false)) {
                INFO() << LOGGING_PREFIX
                    << "reopening log file `" << logPath_ << "'";
                logStream_.close();
                logStream_.open(logPath_, std::ios_base::app);
                logStream_.precision(PRECISION);

                if (!logStream_) {
                    WARN() << LOGGING_PREFIX
                        << "error reopening log file `" << logPath_ << "'";
                    if (eventsQueue_.finished()) {
                        ERROR() << LOGGING_PREFIX << "lost some records";
                        return;
                    }
                    reopenFlag_ = true;
                    ::sleep(REOPEN_FAIL_SLEEP_SECONDS);
                }
            }

            try {
                logEvents(logStream_, eventsBuffer);
                logStream_.flush();
            } catch (const std::exception& ex) {
                WARN() << LOGGING_PREFIX << "error logging events: " << ex.what();
                eventsQueue_.pushAll(std::move(eventsBuffer));
                ::sleep(LOG_FAIL_SLEEP_SECONDS);
            }
        }
    } catch (const maps::Exception& ex) {
        FATAL() << LOGGING_PREFIX << "failed: " << ex;
    } catch (const std::exception& ex) {
        FATAL() << LOGGING_PREFIX << "failed: " << ex.what();
    }
}

std::ostream& operator << (std::ostream& os, ModerationLogger::Action action)
{
    std::ostream::sentry sentry(os);
    if (!sentry) {
        return os;
    }

    switch (action) {
        case ModerationLogger::Action::Created:
            os << "created";
            break;
        case ModerationLogger::Action::Resolved:
            os << "resolved";
            break;
        case ModerationLogger::Action::Closed:
            os << "closed";
            break;
    }
    return os;
}

} // namespace wiki
} // namespace maps
