#include <yandex/maps/wiki/social/feed.h>

#include "factory.h"
#include "feed_helpers.h"
#include "helpers.h"

#include <maps/libs/common/include/exception.h>
#include <yandex/maps/wiki/common/pg_utils.h>
#include <yandex/maps/wiki/social/common.h>

#include <boost/lexical_cast.hpp>

#include <sstream>
#include <vector>

namespace maps::wiki::social {

namespace {

// Pre-approved feed must be sorted in ascending order.
PushOrder
getPushOrder(DataOrder dataOrder, FeedType feedType)
{
    if (dataOrder == DataOrder::Asc  && feedType != FeedType::PreApproved ||
        dataOrder == DataOrder::Desc && feedType == FeedType::PreApproved)
    {
        return PushOrder::Front;
    }

    return PushOrder::Back;
}

} // namespace

void Feed::whereClause(std::ostream& os) const
{
    switch (feedType_) {
        case FeedType::Region:
            ASSERT(false /* Use FeedType::Region is deprecated, use RegionFeed instead */);
            break;
        case FeedType::User:
            os << " WHERE ce." << sql::col::BRANCH_ID << " = " << branchId_
               << " AND ce." << sql::col::CREATED_BY << " = " << subscriberId_;
            break;
        case FeedType::Aoi:
            os << " JOIN ("
               << "SELECT " << sql::col::EVENT_ID
               << " FROM " << (branchId_ ? sql::table::AOI_FEED_STABLE : sql::table::AOI_FEED_TRUNK)
               << " WHERE " << sql::col::AOI_ID << " = " << subscriberId_
               << (branchId_ ? " AND " + sql::col::BRANCH_ID + " = " + std::to_string(branchId_) : "")
               << ") tmp USING(" << sql::col::EVENT_ID << ")"
               << " WHERE ce." << sql::col::BRANCH_ID << " = " << branchId_;
            break;
        case FeedType::Suspicious:
            os << " WHERE (ce." << sql::col::BRANCH_ID << " = " << branchId_ << ")";
            if (!filter_.actions_.empty()) {
                os << " AND (" << sql::col::ACTION << " IN ("
                   << common::join(
                          filter_.actions_.begin(), filter_.actions_.end(),
                          [this](const FeedAction& action) {
                              return work_.quote(boost::lexical_cast<std::string>(action));
                          },
                          ",")
                   << "))";
            }
            if (!filter_.creatorIds_.empty()) {
                os << " AND (" << sql::col::CREATED_BY << " IN ("
                   << common::join(
                           filter_.creatorIds_.begin(), filter_.creatorIds_.end(),
                           [](const TId& uid) {
                               return boost::lexical_cast<std::string>(uid);
                           },
                           ",")
                   << "))";
            }
            if (filter_.feedRegionGeomWkb_) {
                os << " AND ST_CoveredBy(" << sql::col::BOUNDS_GEOM << ","
                   << "ST_GeomFromWKB('"
                   << work_.esc_raw(*filter_.feedRegionGeomWkb_) << "',"
                   << sql::value::MERCATOR_SRID << "))";
            }
            break;
        case FeedType::PreApproved:
            os << " WHERE ce." << sql::col::BRANCH_ID << " = " << branchId_
               << " AND ce." << sql::col::TYPE << " = '" << sql::value::EVENT_TYPE_EDIT << "'";
            break;
    }
    if (filter_.createdAfter_) {
        os << " AND (ce." << sql::col::CREATED_AT << " >= "
           << work_.quote(chrono::formatSqlDateTime(*filter_.createdAfter_)) << ")";
    }
    if (filter_.createdBefore_) {
        os << " AND (ce." << sql::col::CREATED_AT << " <= "
           << work_.quote(chrono::formatSqlDateTime(*filter_.createdBefore_)) << ")";
    }
    os << " AND " << categoriesWhereCondition(work_, filter_.categoryIds_);

    if (!filter_.commitIds_.empty()) {
        os << " AND " << common::whereClause(sql::col::COMMIT_ID, filter_.commitIds_);
    }

    os << " AND ce." << sql::col::TYPE << "='" << sql::value::EVENT_TYPE_EDIT << "'";
}

Feed::Feed(
    pqxx::transaction_base& work,
    TId branchId,
    TId subscriberId,
    FeedType feedType,
    FeedFilter filter
)
    : work_(work)
    , branchId_(branchId)
    , subscriberId_(subscriberId)
    , feedType_(feedType)
    , filter_(std::move(filter))
{
    switch (feedType) {
        case FeedType::User:
            checkUid(subscriberId);
            break;
        case FeedType::Aoi:
            checkAoi(subscriberId);
            break;
        case FeedType::Suspicious:
            ASSERT(subscriberId == 0);
            break;
        case FeedType::PreApproved:
            ASSERT(subscriberId == 0);
            ASSERT(branchId == 0);
            break;
        case FeedType::Region:
            ASSERT(false /* Use FeedType::Region is deprecated, use RegionFeed instead */);
            break;
    }
}

Feed::Feed(
    pqxx::transaction_base& work,
    TId branchId,
    TId subscriberId,
    FeedType feedType
)
    : Feed(work, branchId, subscriberId, feedType, FeedFilter())
{}

Feed::Feed(
    pqxx::transaction_base& work,
    TId branchId,
    FeedType feedType,
    FeedFilter filter
)
    : Feed(work, branchId, 0, feedType, std::move(filter))
{}

size_t
Feed::count() const
{
    if (feedType_ == FeedType::Aoi && branchId_ == 0) {
        return limitedCount(AOI_TRUNK_COUNT_LIMIT);
    }

    std::ostringstream os;
    os << "SELECT COUNT(1) FROM " << sql::table::COMMIT_EVENT << " ce";
    whereClause(os);
    auto r = work_.exec(os.str());
    return r[0][0].as<size_t>();
}

size_t
Feed::limitedCount(size_t maximum) const
{
    std::ostringstream os;
    os << "SELECT COUNT(1) FROM (SELECT 1 FROM " << sql::table::COMMIT_EVENT << " ce";
    whereClause(os);
    os << " LIMIT " << maximum << ") tmp";
    auto r = work_.exec(os.str());
    return r[0][0].as<size_t>();
}

Events
Feed::events(size_t offset, size_t limit) const
{
    checkLimit(limit);

    std::ostringstream os;
    os << SELECT_COMMIT_EVENT_FIELDS;
    whereClause(os);
    os << " ORDER BY " << sql::col::EVENT_ID << " DESC"
       << " OFFSET " << offset
       << " LIMIT " << limit;

    return createEvents(
        work_.exec(os.str()), std::nullopt, getPushOrder(DataOrder::Desc, feedType_)
    ).first;
}

std::pair<Events, HasMore>
Feed::eventsHead(size_t limit) const
{
    checkLimit(limit);

    std::ostringstream os;
    os << SELECT_COMMIT_EVENT_FIELDS;
    whereClause(os);
    os << " ORDER BY " << sql::col::EVENT_ID << (feedType_ != FeedType::PreApproved ? " DESC" : " ASC")
       << " LIMIT " << (limit + 1);

    return createEvents(work_.exec(os.str()), limit, PushOrder::Back);
}

std::pair<Events, HasMore>
Feed::eventsAfter(TId eventId, size_t limit) const
{
    checkLimit(limit);

    std::ostringstream os;
    os << SELECT_COMMIT_EVENT_FIELDS;
    whereClause(os);
    os << " AND " << sql::col::EVENT_ID << ">"  << eventId
       << " ORDER BY " << sql::col::EVENT_ID << " ASC"
       << " LIMIT " << (limit + 1);

    return createEvents(work_.exec(os.str()), limit, getPushOrder(DataOrder::Asc, feedType_));
}

std::pair<Events, HasMore>
Feed::eventsBefore(TId eventId, size_t limit) const
{
    checkLimit(limit);
    std::ostringstream os;
    os << SELECT_COMMIT_EVENT_FIELDS;
    whereClause(os);
    os << " AND " << sql::col::EVENT_ID << "<" << eventId
       << " ORDER BY " << sql::col::EVENT_ID << " DESC"
       << " LIMIT " << (limit + 1);

    return createEvents(work_.exec(os.str()), limit, getPushOrder(DataOrder::Desc, feedType_));
}

} // namespace maps::wiki::social
