#include "branch_data.h"
#include "helpers.h"
#include "sql_strings.h"
#include <boost/format.hpp>
#include <boost/lexical_cast.hpp>
#include <memory>
#include <utility>
#include <yandex/maps/wiki/revision/branch.h>
#include <yandex/maps/wiki/revision/exception.h>

namespace maps::wiki::revision {

using namespace helpers;

namespace {

const boost::format FORMAT_CONCATENATE_ATTRIBUTES(
    "UPDATE " + sql::table::BRANCH +
        " SET " + sql::col::ATTRIBUTES + "=" +
        " COALESCE(" + sql::col::ATTRIBUTES + ",'')" + " || %1%"
        " WHERE " + sql::col::ID + "=%2%"
        " RETURNING " + BRANCH_COLUMNS);

const boost::format FORMAT_SET_TYPE(
    "UPDATE " + sql::table::BRANCH +
        " SET " + sql::col::TYPE + "='%1%'"
        " WHERE " + sql::col::TYPE + "<>'%1%' AND " + sql::col::ID + "=%2%"
            " AND " + sql::col::STATE + "<>'" + sql::val::BRANCH_STATE_PROGRESS + "'"
        " RETURNING " + BRANCH_COLUMNS);

const boost::format FORMAT_SET_STATE(
    "UPDATE " + sql::table::BRANCH +
        " SET " + sql::col::STATE + "='%1%'"
        " WHERE " + sql::col::STATE + "<>'%1%' AND " + sql::col::ID + "=%2%"
            " AND " + sql::col::TYPE + "<>'" + sql::val::BRANCH_TYPE_DELETED + "'"
        " RETURNING " + BRANCH_COLUMNS);

const boost::format FORMAT_TOUCH(
    "UPDATE " + sql::table::BRANCH +
        " SET " + sql::col::CREATED_AT + "=NOW(), " + sql::col::CREATED_BY + "='%1%'"
        " WHERE (" + sql::col::TYPE + "='" + sql::val::BRANCH_TYPE_APPROVED + "' OR " +
                     sql::col::TYPE + "='" + sql::val::BRANCH_TYPE_STABLE + "')"
            " AND " + sql::col::ID + "=%2%"
        " RETURNING " + BRANCH_COLUMNS);

const boost::format FORMAT_CLOSE_STABLE(
    "UPDATE " + sql::table::BRANCH +
        " SET " + sql::col::FINISHED_BY + "=%1%, " + sql::col::FINISHED_AT + "=now(), "
                + sql::col::TYPE + "='" + sql::val::BRANCH_TYPE_ARCHIVE + "'"
        " WHERE " + sql::col::FINISHED_BY + "=0 AND " + sql::col::ID + "=%2%"
            " AND " + sql::col::STATE + "<>'" + sql::val::BRANCH_STATE_PROGRESS + "'"
        " RETURNING " + BRANCH_COLUMNS);

} // namespace

const Branch::LockId Branch::MAX_LOCK_ID = 15;

class Branch::Impl {
public:
    explicit Impl(BranchData data)
        : data(std::move(data))
    {}

    template <typename T>
    std::optional<BranchData> setValue(
        pqxx::transaction_base& work,
        const boost::format& sql,
        const T& value) const
    {
        auto r = work.exec((boost::format(sql) % value % data.id).str());
        if (r.empty()) {
            return std::nullopt;
        }
        ASSERT(r.size() == 1);
        return BranchData(r[0]);
    }

    std::string dump() const
    {
        std::ostringstream os;
        os << data.type << " branch, id: " << data.id << " (" << data.state << ")";
        return os.str();
    }

    const BranchData data;
};


Branch::Branch(Branch&& other) noexcept
    : impl_(std::move(other.impl_))
{}

Branch::Branch(const Branch& other)
    : impl_(new Impl(other.impl_->data))
{}

Branch::Branch(const BranchData& data)
    : impl_(new Impl(data))
{}

Branch::~Branch() = default;

Branch& Branch::operator = (const Branch& other)
{
    if (&other != this) {
        impl_ = std::make_unique<Impl>(other.impl_->data);
    }
    return *this;
}

DBID
Branch::id() const
{
    return impl_->data.id;
}

BranchType
Branch::type() const
{
    return impl_->data.type;
}

BranchState
Branch::state() const
{
    return impl_->data.state;
}

UserID
Branch::createdBy() const
{
    return impl_->data.createdBy;
}

const std::string&
Branch::createdAt() const
{
    return impl_->data.createdAt;
}

UserID
Branch::finishedBy() const
{
    return impl_->data.finishedBy;
}

const std::string&
Branch::finishedAt() const
{
    return impl_->data.finishedAt;
}

const Attributes&
Branch::attributes() const
{
    return impl_->data.attributes;
}

bool
Branch::concatenateAttributes(
        pqxx::transaction_base& work, const Attributes& attributes)
{
    if (attributes.empty()) {
        return false;
    }

    auto hstore = attributesToHstore(work, attributes);
    TransactionGuard guard(work);

    auto newData = impl_->setValue(work, FORMAT_CONCATENATE_ATTRIBUTES, hstore);
    if (newData) {
        impl_ = std::make_unique<Impl>(*newData);
    }

    guard.ok();
    return static_cast<bool>(newData);
}

bool
Branch::isReadOnlyType(const BranchType& type)
{
    return (type == BranchType::Approved) ||
            (type == BranchType::Deleted);
}

bool
Branch::isWriteAllowedType(const BranchType& type)
{
    return (type == BranchType::Trunk) ||
        (type == BranchType::Stable) ||
        (type == BranchType::Archive);
}

bool
Branch::isReadingAllowed() const
{
    return state() != BranchState::Unavailable;
}

bool
Branch::isWritingAllowed() const
{
    return (state() == BranchState::Normal) &&
        Branch::isWriteAllowedType(type());
}


bool
Branch::setType(pqxx::transaction_base& work, BranchType type)
{
    if (impl_->data.type == type) {
        return false;
    }

    if (type == BranchType::Trunk    || impl_->data.type == BranchType::Trunk ||
        type == BranchType::Approved || impl_->data.type == BranchType::Approved)
    {
        throw BranchForbiddenOperationException()
            << "can not change type " << type << " for " << impl_->dump();
    }

    if (impl_->data.state == BranchState::Progress) {
        throw BranchInProgressException()
            << "can not change type " << type << " for " << impl_->dump();
    }

    TransactionGuard guard(work);

    auto newData = impl_->setValue(
        work, FORMAT_SET_TYPE, boost::lexical_cast<std::string>(type));
    if (newData) {
        impl_ = std::make_unique<Impl>(*newData);
    }

    guard.ok();
    return static_cast<bool>(newData);
}

bool
Branch::setState(pqxx::transaction_base& work, BranchState state)
{
    if (impl_->data.state == state) {
        return false;
    }

    if (state != BranchState::Unavailable && impl_->data.type == BranchType::Deleted) {
        throw BranchForbiddenOperationException()
            << "can not change state " << state << " for " << impl_->dump();
    }


    TransactionGuard guard(work);

    auto newData = impl_->setValue(
        work, FORMAT_SET_STATE, boost::lexical_cast<std::string>(state));
    if (newData) {
        impl_ = std::make_unique<Impl>(*newData);
    }

    guard.ok();
    return static_cast<bool>(newData);
}

bool
Branch::finish(pqxx::transaction_base& work, UserID uid)
{
    checkUserId(uid);
    if (impl_->data.finishedBy) {
        throw StableBranchAlreadyFinishedException()
            << "stable branch already finished"
            << ", id: " << impl_->data.id
            << ", uid: " << impl_->data.finishedBy;
    }
    if (impl_->data.type != BranchType::Stable) {
        throw BranchForbiddenOperationException()
            << "can not finish " << impl_->dump();
    }

    if (impl_->data.state == BranchState::Progress) {
        throw BranchInProgressException()
            << "can not finish " << impl_->dump();
    }

    TransactionGuard guard(work);

    auto newData = impl_->setValue(work, FORMAT_CLOSE_STABLE, uid);
    if (newData) {
        ASSERT(newData->type == BranchType::Archive);
        impl_ = std::make_unique<Impl>(*newData);
    }

    guard.ok();
    return static_cast<bool>(newData);
}

bool
Branch::touchCreated(pqxx::transaction_base& work, UserID uid)
{
    checkUserId(uid);
    if (impl_->data.type != BranchType::Approved &&
        impl_->data.type != BranchType::Stable) {

        throw BranchForbiddenOperationException()
            << "can not touch " << impl_->dump();
    }

    TransactionGuard guard(work);

    auto newData = impl_->setValue(work, FORMAT_TOUCH, uid);
    if (newData) {
        impl_ = std::make_unique<Impl>(*newData);
    }

    guard.ok();
    return static_cast<bool>(newData);
}


/// Lock branches

namespace {

class BranchLock
{
public:
    BranchLock(DBID branchId, Branch::LockId lockId)
        : branchId_(branchId)
    {
        if (lockId > Branch::MAX_LOCK_ID) {
            throw BranchLockIdOutOfRange()
                << "Branch " << branchId_
                << " lock id " << lockId << " is out of range"
                << ", must be [0.." << Branch::MAX_LOCK_ID << "]";
        }
        composeLockId(lockId);
    }

    DBID branchId() const { return branchId_; }
    Branch::LockId dbLockId() const { return lockId_; }

private:
    /// Postgres advisory locks do not support 2x64bit key type
    void composeLockId(Branch::LockId lockId)
    {
        lockId_ =
            (Branch::LockId)branchId_ * (Branch::MAX_LOCK_ID + 1) + lockId;
    }

    DBID branchId_;
    Branch::LockId lockId_{};
};

std::string
buildLockBranchQuery(
        Branch::LockId dbLockId, Branch::LockMode mode, Branch::LockType type)
{
    std::ostringstream query;

    std::string advisoryLockFunctionName = mode == Branch::LockMode::Wait
        ? "pg_advisory_xact_lock"
        : "pg_try_advisory_xact_lock";

    if (type == Branch::LockType::Shared) {
        advisoryLockFunctionName += "_shared";
    }

    query << "SELECT " << advisoryLockFunctionName
        << "(CAST(" << dbLockId << " AS bigint))"
        << " WHERE pg_is_in_recovery()=false";

    return query.str();
}

bool
tryLockBranch(pqxx::transaction_base& work, BranchLock lock, Branch::LockType type)
{
    const auto lockId = lock.dbLockId();
    auto query = buildLockBranchQuery(lockId, Branch::LockMode::Nowait, type);
    auto r = work.exec(query);
    if (r.empty()) {
        return false;
    }
    REQUIRE(r.size() == 1,
        "Branch " << lock.branchId()
        << " try " << lockId << " lock invalid return data");

    return r[0][0].as<bool>();
}

void
lockBranch(pqxx::transaction_base& work, BranchLock lock, Branch::LockType type)
{
    const auto lockId = lock.dbLockId();
    auto query = buildLockBranchQuery(lockId, Branch::LockMode::Wait, type);
    work.exec(query);
}

} // namespace

bool
Branch::tryLock(pqxx::transaction_base& work, LockId lockId) const
{
    return tryLockBranch(
            work, BranchLock(impl_->data.id, lockId), LockType::Exclusive);
}

bool
Branch::tryLock(pqxx::transaction_base& work, LockId lockId, LockType type) const
{
    return tryLockBranch(work, BranchLock(impl_->data.id, lockId), type);
}

void
Branch::lock(pqxx::transaction_base& work, LockId lockId, LockMode mode) const
{
    lock(work, lockId, mode, Branch::LockType::Exclusive);
}

void
Branch::lock(
        pqxx::transaction_base& work,
        LockId lockId, LockMode mode, LockType type) const
{
    if (mode == Branch::LockMode::Wait) {
        lockBranch(work, BranchLock(impl_->data.id, lockId), type);
        return;
    }
    bool locked = tryLockBranch(work, BranchLock(impl_->data.id, lockId), type);
    if (!locked) {
        throw BranchAlreadyLockedException()
            << "Branch " << impl_->data.id
            << " is already locked, lock id " << lockId;
    }
}

} // namespace maps::wiki::revision
