#include "branch_manager_impl.h"
#include "sql_strings.h"
#include "helpers.h"

#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/revision/exception.h>

#include <boost/lexical_cast.hpp>

#include <sstream>

namespace maps::wiki::revision {

using namespace helpers;

namespace {

bool
isSingleton(BranchType type)
{
    return type == BranchType::Trunk ||
           type == BranchType::Approved ||
           type == BranchType::Stable;
}


// Request (without Deleted) :
//      branchLimits = {
//        {BranchType::Trunk, 1},
//        {BranchType::Stable, 1},
//        {BranchType::Archive, 1}}
// Result:
// SELECT * FROM revision.branch
//  WHERE type='trunk'
//     OR type='stable'
//     OR id IN (SELECT id FROM revision.branch
//                WHERE type='archive'
//                ORDER BY id DESC LIMIT 1)
//
class BulkBranchesQueryBuilder
{
public:
    std::string operator() (const BranchManager::BranchLimits& branchLimits) const
    {
        ASSERT(!branchLimits.empty());

        std::ostringstream query;
        query << "SELECT " << BRANCH_COLUMNS
              << " FROM " << sql::table::BRANCH << " WHERE ";
        bool first = true;
        for (const auto& p : branchLimits) {
            if (first) {
                first = false;
            } else {
                query << " OR ";
            }
            query << sqlByBranchType(p.first, p.second);
        }
        return query.str();
    }

private:
    static std::string sqlByBranchType(BranchType type, size_t limit)
    {
        std::ostringstream query;

        if (limit == BranchManager::UNLIMITED || isSingleton(type)) {
            query << sql::col::TYPE << "='" << type << "'";
        } else {
            query << sql::col::ID << " IN "
                  << "(SELECT " << sql::col::ID << " FROM " << sql::table::BRANCH
                  << " WHERE " << sql::col::TYPE << "='" << type << "'"
                  << " ORDER BY " << sql::col::ID << " DESC LIMIT " << limit
                  << ")";
        }
        return query.str();
    }
};

} // namespace


const size_t BranchManager::UNLIMITED = 0;

BranchManager::BranchManager(pqxx::transaction_base& work)
    : impl_(new BranchManagerImpl(work))
{}

BranchManager::~BranchManager() = default;

Branch
BranchManager::createApproved(UserID createdBy, const Attributes& attributes) const
{
    checkUserId(createdBy);
    try {
        return Branch(impl_->create(BranchType::Approved, createdBy, attributes));
    }
    catch (const pqxx::unique_violation&) {
        throw ApprovedBranchAlreadyExistsException() << "approved branch already exists";
    }
}

Branch
BranchManager::createStable(UserID createdBy, const Attributes& attributes) const
{
    checkUserId(createdBy);
    try {
        return Branch(impl_->create(BranchType::Stable, createdBy, attributes));
    }
    catch (const pqxx::unique_violation&) {
        throw StableBranchAlreadyExistsException() << "stable branch already exists";
    }
}

BranchManager::Branches
BranchManager::load(const BranchLimits& branchLimits) const
{
    Branches branches;
    if (branchLimits.empty()) {
        return branches;
    }

    auto r = impl_->work().exec(BulkBranchesQueryBuilder()(branchLimits));

    std::map<DBID, size_t, std::greater<DBID>> indicies;
    for (size_t i = 0; i < r.size(); ++i) {
        BranchData data(r[i]);
        if (data.id == TRUNK_BRANCH_ID) {
            branches.push_front(Branch(data)); // first
        } else if (data.type == BranchType::Approved) {
            branches.push_back(Branch(data)); // second
        } else {
            indicies.insert({data.id, i});
        }
    }

    for (const auto& pair : indicies) {
        BranchData data(r[pair.second]);
        branches.push_back(Branch(data)); // third, ordered by id desc
    }
    return branches;
}

BranchManager::Branches
BranchManager::load(const DBIDSet& branchIds) const
{
    if (branchIds.empty()) {
        return {};
    }

    std::ostringstream query;
    query << "SELECT " << BRANCH_COLUMNS
          << " FROM " << sql::table::BRANCH
          << " WHERE " << sql::col::ID << " IN (" << helpers::valuesToString(branchIds) << ")";

    auto result = impl_->work().exec(query.str());

    Branches branches;
    for (const auto& row: result) {
        BranchData data(row);
        branches.push_back(Branch(data));
    }

    return branches;
}

Branch
BranchManager::loadTrunk() const
{
    return Branch(impl_->loadByType(BranchType::Trunk));
}

Branch
BranchManager::loadApproved() const
{
    return Branch(impl_->loadByType(BranchType::Approved));
}

Branch
BranchManager::loadStable() const
{
    return Branch(impl_->loadByType(BranchType::Stable));
}

Branch
BranchManager::load(DBID id) const
{
    return Branch(impl_->loadById(id));
}

Branch
BranchManager::loadByString(const std::string& branchStr) const
{
    REQUIRE(!branchStr.empty(), "empty branch name");

    if (::isdigit(branchStr[0])) {
        DBID branchId = 0;
        try {
            branchId = boost::lexical_cast<DBID>(branchStr);
        }
        catch (const boost::bad_lexical_cast&) {
            throw maps::RuntimeError() << "invalid branch id: " << branchStr;
        }
        return load(branchId);
    }

    auto type = BranchType::Trunk;
    try {
        type = boost::lexical_cast<BranchType>(branchStr);
    }
    catch (const boost::bad_lexical_cast&) {
        throw maps::RuntimeError() << "invalid branch type: " << branchStr;
    }

    REQUIRE(isSingleton(type), "can not load " << type << " branch without id");

    return Branch(impl_->loadByType(type));
}

} // namespace maps::wiki::revision
