#include "commit_data.h"
#include "sql_strings.h"
#include "helpers.h"

#include <maps/libs/common/include/exception.h>

namespace maps::wiki::revision {

using namespace helpers;

namespace {

const std::string CREATE_COMMIT_PREFIX =
    "INSERT INTO " + sql::table::COMMIT +
    " (" +
        sql::col::STATE + ", " +
        sql::col::IS_TRUNK + ", " +
        sql::col::STABLE_BRANCH_ID + ", " +
        sql::col::CREATED_BY + ", " +
        sql::col::ATTRIBUTES +
    ") VALUES (";

const std::string CREATE_COMMIT_POSTFIX =
    ") RETURNING " + sql::col::ID + "," + sql::col::CREATED_AT;

} // namespace

PreparedCommitAttributes::PreparedCommitAttributes(
        pqxx::transaction_base& work,
        const Attributes& attrs)
    : hstoreData_(attributesToHstore(work, attrs, &attributes_))
{
    REQUIRE(!hstoreData_.empty(), "invalid commit attributes");
}


CommitData::CommitData(
        DBID commitId,
        DBID approveOrder,
        CommitState state,
        UserID uid,
        Attributes attributes,
        std::string createdAt,
        DBID stableBranchId,
        bool isTrunk)
    : createdAt(std::move(createdAt))
    , state(state)
    , id(commitId)
    , approveOrder(approveOrder)
    , createdBy(uid)
    , stableBranchId(stableBranchId)
    , trunk(isTrunk)
    , loaded_(true)
    , attributes_(std::move(attributes))
{
    checkCommitId(commitId);
    checkUserId(uid);
    if (stableBranchId == TRUNK_BRANCH_ID) {
        REQUIRE(trunk, "invalid data in commit id: " << commitId);
    }
}

CommitData::CommitData(
        DBID commitId,
        UserID uid,
        Attributes attributes,
        std::string createdAt,
        DBID branchId) // pure Trunk or Stable, not merged
    : createdAt(std::move(createdAt))
    , state(branchId  == TRUNK_BRANCH_ID ? CommitState::Draft : CommitState::Approved)
    , id(commitId)
    , approveOrder(0)
    , createdBy(uid)
    , stableBranchId(branchId)
    , trunk(branchId == TRUNK_BRANCH_ID)
    , loaded_(false)
    , attributes_(std::move(attributes))
{
    checkCommitId(commitId);
    checkUserId(uid);
}

CommitData
CommitData::create(
    pqxx::transaction_base& work,
    DBID branchId,
    UserID uid,
    const PreparedCommitAttributes& attributes)
{
    std::ostringstream os;
    if (branchId == TRUNK_BRANCH_ID) {
        os <<
            "'" << sql::val::COMMIT_STATE_DRAFT << "'" << ", " <<
            sql::val::TRUE << ", " <<
            sql::val::UNDEFINED << ", " <<
            uid << ", " <<
            attributes.hstoreData()
        ;
    } else {
        os <<
            "'" << sql::val::COMMIT_STATE_APPROVED << "'" << ", " <<
            sql::val::FALSE << ", " <<
            branchId << ", " <<
            uid << ", " <<
            attributes.hstoreData()
        ;
    }

    auto r = work.exec(CREATE_COMMIT_PREFIX + os.str() + CREATE_COMMIT_POSTFIX);
    REQUIRE(!r.empty(), "can not create commit data");

    return CommitData(
        r[0][sql::col::ID].as<DBID>(),
        uid,
        attributes.attributes(),
        r[0][sql::col::CREATED_AT].as<std::string>(),
        branchId
    );
}

CommitData
CommitData::load(const pqxx::row& row)
{
    const auto& fieldStableBranchId = row[sql::col::STABLE_BRANCH_ID];
    DBID stableBranchId = fieldStableBranchId.is_null()
        ? TRUNK_BRANCH_ID
        : fieldStableBranchId.as<DBID>();

    return CommitData(
        row[sql::col::ID].as<DBID>(),
        row[sql::col::APPROVE_ORDER].as<DBID>(),
        stringToCommitState(row[sql::col::STATE].as<std::string>()),
        row[sql::col::CREATED_BY].as<UserID>(),
        stringToAttributes(row[sql::col::ATTRIBUTES].as<std::string>()),
        row[sql::col::CREATED_AT].as<std::string>(),
        stableBranchId,
        row[sql::col::IS_TRUNK].as<bool>());
}

void
CommitData::setAttributes(
    pqxx::transaction_base& work, PreparedCommitAttributes attributes)
{
    REQUIRE(!loaded_, "updating commit loaded from db is forbidden");
    work.exec(
        "UPDATE " + sql::table::COMMIT +
        " SET " + sql::col::ATTRIBUTES + "=" + attributes.hstoreData() +
        " WHERE " + sql::col::ID + "=" + std::to_string(id));
    attributes_.swap(attributes.attributes());
}

void
CommitData::addAttribute(
    pqxx::transaction_base& work, const std::string& key, const std::string& value)
{
    REQUIRE(loaded_, "adding attribute to not stored commit is forbidden");
    REQUIRE(!key.empty() && !value.empty(),
        "empty key/value contents is forbidden");
    REQUIRE(!attributes_.count(key),
        "overwriting attribute value is forbidden");

    work.exec(
        "UPDATE " + sql::table::COMMIT +
        " SET " + sql::col::ATTRIBUTES + "=" + sql::col::ATTRIBUTES +
        "|| hstore(" + work.quote(key) + ", " + work.quote(value) + ")"
        " WHERE " + sql::col::ID + "=" + std::to_string(id));
    attributes_.emplace(key, value);
}

namespace {

DBIDSet
fetchDBIDSet(const Attributes& attrs, const std::string& key)
{
    auto it = attrs.find(key);
    if (it == attrs.end()) {
        return {};
    }
    return stringToDBIDSet(it->second);

}

} // namespace

DBIDSet
CommitData::revertedCommitIds() const
{
    return fetchDBIDSet(attributes_, sql::col::ATTR_REVERTED_COMMIT_IDS);
}

DBIDSet
CommitData::revertedDirectlyCommitIds() const
{
    return fetchDBIDSet(attributes_, sql::col::ATTR_REVERTED_DIRECTLY_COMMIT_IDS);
}

DBIDSet
CommitData::revertingCommitIds() const
{
    return fetchDBIDSet(attributes_, sql::col::ATTR_REVERTING_COMMIT_IDS);
}

OptionalDBID
CommitData::revertingDirectlyCommitId() const
{
    auto it = attributes_.find(sql::col::ATTR_REVERTING_DIRECTLY_COMMIT_ID);
    if (it == attributes_.end()) {
        return std::nullopt;
    }
    return stringToDBID(it->second);
}

} // namespace maps::wiki::revision
