#include "commit_worker.h"
#include "result_interpreter.h"
#include "commit_data.h"
#include "conflicts_checker.h"
#include "revision_data.h"
#include "sql_strings.h"

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

#include <boost/format.hpp>
#include <sstream>

namespace maps::wiki::revision {

using namespace helpers;

namespace {

const size_t BULK_INSERT_OBJECTS_SIZE = 100;

const std::string IS_DRAFT = "is_draft";

const boost::format LOCK_OBJECTS_FOR_TRUNK (
    "SELECT *"
    " FROM " + sql::table::OBJECT_REVISION +
    " WHERE %1%"
    " ORDER BY " + sql::col::OBJECT_ID + "," + sql::col::COMMIT_ID +
    " FOR UPDATE NOWAIT");

const boost::format LOCK_OBJECTS_FOR_STABLE_OR_ARCHIVE (
    "SELECT r.*, "
        "(" + sql::col::STATE + " = '" + sql::val::COMMIT_STATE_DRAFT + "') AS " + IS_DRAFT +
    " FROM " + sql::table::OBJECT_REVISION + " r" +
    " JOIN " + sql::table::COMMIT + " ON " + sql::col::COMMIT_ID + "=" + sql::col::ID +
    " WHERE %1%"
    " ORDER BY " + sql::col::OBJECT_ID + "," + sql::col::COMMIT_ID +
    " FOR UPDATE OF r NOWAIT");

const std::string CHECK_NEW_OBJECTS_PREFIX =
    "SELECT DISTINCT " + sql::col::OBJECT_ID +
    " FROM " + sql::table::OBJECT_REVISION +
    " JOIN " + sql::table::COMMIT + " ON " + sql::col::COMMIT_ID + "=" + sql::col::ID +
    " WHERE ";

const std::string OBJECT_REVISION_FIELDS =
        sql::col::OBJECT_ID + "," + sql::col::COMMIT_ID + "," +
        sql::col::PREV_COMMIT_ID + "," + sql::col::IS_DELETED + "," +
        sql::col::GEOMETRY_ID + "," + sql::col::ATTRIBUTES_ID + "," + sql::col::DESCRIPTION_ID + "," +
        sql::col::MASTER_OBJECT_ID + "," + sql::col::SLAVE_OBJECT_ID;

const std::string STATEMENT_CREATE_GEOMETRY =
    "INSERT INTO " + sql::table::GEOMETRY + " (" + sql::col::CONTENTS + ")"
    " VALUES (ST_GeomFromWkb($1," + sql::val::GEOMETRY_SRID + "))"
    " RETURNING " + sql::col::ID;

} // namespace


CommitWorker::CommitWorker(
        Transaction& work,
        const ConstRange<NewRevisionData>& newData,
        const BranchType branchType,
        const DBID branchId)
    : work_(work)
    , branchType_(branchType)
    , branchId_(branchId)
    , newData_(newData)
{
    prepare();
}

CommitData
CommitWorker::createCommit(UserID createdBy, const PreparedCommitAttributes& attributes)
{
    TransactionGuard guard(work_);
    checkUpdates();

    auto commitData = CommitData::create(
        work_, branchId_, createdBy, attributes);

    // commitData.id already checked
    writePreviousUpdates(commitData.id);
    insertNewData(commitData.id);

    guard.ok();
    return commitData;
}

void
CommitWorker::prepare()
{
    DBIDSet inserts; // set of object_id

    auto it = newData_.iterator();
    for (auto pdata = it->next(); pdata != 0; pdata = it->next()) {
        const ObjectRevision::ID& id = pdata->first;
        DBID objectId = id.objectId();
        REQUIRE(objectId, "empty object id");

        DBID commitId = id.commitId();
        if (!commitId) { // insert
            REQUIRE(!pdata->second.deleted,
                "new object can not be deleted, object id " << objectId);

            REQUIRE(updates_.find(objectId) == updates_.end(),
                "inserting object id already exists in update list");
            bool success = inserts.insert(objectId).second;
            REQUIRE(success, "inserting object id already exists in insert list");
        }
        else { // update
            REQUIRE(inserts.find(objectId) == inserts.end(),
                "updating object id already exists in insert list");
            bool success = updates_.insert(std::make_pair(objectId, commitId)).second;
            REQUIRE(success, "updating object id already exists in update list");
            previousRevisionsMap_[commitId].insert(objectId);
            if (pdata->second.deleted) {
                deletedUpdateObjectIds_.insert(objectId);
            }
        }
    }
    if (!inserts.empty()) {
        // simple check duplicates
        // not protect for the same data from another transactions
        auto r = work_.exec(
            CHECK_NEW_OBJECTS_PREFIX +
            QueryGenerator::buildFilterByAttrValues(sql::col::OBJECT_ID, inserts));
        if (!r.empty()) {
            throw AlreadyExistsException() <<
                "new object ids already exists, ids: " <<
                valuesToString(loadIds(r, sql::col::OBJECT_ID));
        }
    }
}

void
CommitWorker::checkUpdates()
{
    if (updates_.empty()) {
        return;
    }

    boost::format query = isBranchTrunk()
        ? LOCK_OBJECTS_FOR_TRUNK
        : LOCK_OBJECTS_FOR_STABLE_OR_ARCHIVE;
    query % QueryGenerator::buildSpecialFilter(previousRevisionsMap_, sql::col::COMMIT_ID);

    const auto rows = lockData(work_, query.str(), "objects data");
    REQUIRE(rows.size() ==  updates_.size(),
        "can not fetch previous data for update: " << rows.size() << "!=" << updates_.size() <<
        " query: " << query); // int error

    for (const auto& row: rows) {
        DBID objectId = row[sql::col::OBJECT_ID].as<DBID>();
        auto it = updates_.find(objectId);

        DBID prevCommitId = it->second;
        RevisionDataWithCommitId prevData{loadRevisionDataFromPqxx(row), prevCommitId};
        if (isBranchTrunk()) {
            updatePreviousRevisionsMap_[prevCommitId].insert(objectId);
            if (prevData.data.nextCommitId) {
                throw ConflictsFoundException() <<
                    "previous updated data already modified, object id " <<
                    objectId << ", commit id " << prevCommitId;
            }
        } else { // stable or archive
            REQUIRE(!row[IS_DRAFT].as<bool>(),
                "found previous draft data for stable branch"
                ", object id " << objectId << ", commit id " << prevCommitId); // strange error
        }
        if (prevData.data.deleted && deletedUpdateObjectIds_.count(objectId) > 0) {
            throw AlreadyDeletedException() <<
                "previous revision already deleted, object id " <<
                objectId << ", commit id " << prevCommitId;
        }
        REQUIRE(updatePreviousData_.insert(std::make_pair(objectId, prevData)).second,
            "duplicated updating object id while locking previous revision"); // int error
    }

    if (!isBranchTrunk()) {
        ConflictsChecker checker(work_);
        checker.checkConflicts(branchType_, branchId_, previousRevisionsMap_);
    }

    REQUIRE(updates_.size() == updatePreviousData_.size(),
            "previous objects data not fetched"); // int error
}

void
CommitWorker::writePreviousUpdates(DBID commitId) const
{
    if (updatePreviousRevisionsMap_.empty() || !isBranchTrunk()) {
        return;
    }

    auto query = "UPDATE " + sql::table::OBJECT_REVISION + " SET " +
        sql::col::NEXT_COMMIT_ID + "=" + std::to_string(commitId) +
        " WHERE " +
        QueryGenerator::buildSpecialFilter(
            updatePreviousRevisionsMap_, sql::col::COMMIT_ID);

    auto result = work_.exec(query);
    REQUIRE(result.affected_rows() == QueryGenerator::countRows(updatePreviousRevisionsMap_),
        "update previous revisions failed");
}

void CommitWorker::setNewRevisionData(const ObjectRevision::Data& data,
    RevisionData& rdata)
{
    if (data.relationData) {
        rdata.masterObjectId = data.relationData->masterObjectId();
        rdata.slaveObjectId = data.relationData->slaveObjectId();
    }

    if (data.attributes) {
        rdata.attributesId = insertAttributes(
            data.relationData
                ? sql::table::ATTRIBUTES_RELATIONS
                : sql::table::ATTRIBUTES,
            *data.attributes);
    }

    if (data.description) {
        rdata.descriptionId = insertDescription(*data.description);
    }

    if (data.geometry) {
        rdata.geometryId = insertGeometry(*data.geometry);
    }
}

void
CommitWorker::insertNewData(DBID commitId)
{
    std::map<std::string, std::vector<std::string>> tableNameToValues;

    auto query = [](const std::string& tableName,  const std::vector<std::string>& values) {
        if (values.empty()) {
            return std::string();
        }
        return
            "INSERT INTO " + tableName + " (" + OBJECT_REVISION_FIELDS + ")"
            " VALUES " + valuesToString(values) + ";";
    };

    auto it = newData_.iterator();
    for (auto pdata = it->next(); pdata != 0; pdata = it->next()) {
        DBID objectId = pdata->first.objectId();

        const ObjectRevision::Data& data = pdata->second;

        DBID prevCommitId = 0;
        RevisionData rdata;
        auto it = updatePreviousData_.find(objectId);
        if (it != updatePreviousData_.end()) {
            const auto& prevData = it->second;
            prevCommitId = prevData.commitId;
            rdata = prevData.data;
        }

        if (data.deleted) {
            REQUIRE(prevCommitId,
                "set deleted revision for update only, object id " << objectId);
        } else {
            setNewRevisionData(data, rdata);
        }

        std::stringstream valuesQuery;
        valuesQuery
            << '(' << objectId << ',' << commitId
            << ',' << prevCommitId << ',' << (data.deleted ? sql::val::TRUE : sql::val::FALSE)
            << ',' << rdata.geometryId
            << ',' << rdata.attributesId << ',' << rdata.descriptionId
            << ',' << rdata.masterObjectId << ',' << rdata.slaveObjectId << ')';

        const auto& tableName = rdata.slaveObjectId
            ? sql::table::OBJECT_REVISION_RELATION
            : rdata.geometryId
                ? sql::table::OBJECT_REVISION_WITH_GEOMETRY
                : sql::table::OBJECT_REVISION_WITHOUT_GEOMETRY;

        auto& values = tableNameToValues[tableName];
        values.push_back(valuesQuery.str());
        if (values.size() >= BULK_INSERT_OBJECTS_SIZE) {
            work_.exec(query(tableName, values));
            values.clear();
        }
    }

    std::string queryAll;
    for (const auto& pair : tableNameToValues) {
        queryAll += query(pair.first, pair.second);
    }
    if (!queryAll.empty()) {
        work_.exec(queryAll);
    }
}

DBID
CommitWorker::insertAttributes(const std::string& tableName, const Attributes& data)
{
    if (data.empty()) {
        return 0;
    }

    auto hdata = attributesToHstore(work_, data);
    if (hdata.empty()) {
        return 0;
    }

    auto it = attrsCache_.find(hdata);
    if (it != attrsCache_.end()) {
        return it->second;
    }

    auto r = work_.exec(
        "SELECT " + sql::col::ID + " FROM " + tableName +
        " WHERE " + sql::col::CONTENTS + "=" + hdata + " LIMIT 1"
    );
    auto needInsert = r.empty();
    if (needInsert) {
        r = work_.exec(
            "INSERT INTO " + sql::table::ATTRIBUTES +
            " (" + sql::col::CONTENTS + ")"
            " VALUES (" + hdata + ") RETURNING " + sql::col::ID);
        REQUIRE(!r.empty(), "can not create attributes data");
    }

    auto id = r[0][0].as<DBID>();
    REQUIRE(id, "invalid attributes id");

    if (needInsert && tableName != sql::table::ATTRIBUTES) {
        r = work_.exec(
            "INSERT INTO " + tableName +
            " (" + sql::col::ID + "," + sql::col::CONTENTS + ")"
            " VALUES (" + std::to_string(id) + "," + hdata + ")");
    }

    attrsCache_[hdata] = id;
    return id;
}

DBID
CommitWorker::insertDescription(const Description& data) const
{
    if (data.empty()) {
        return 0;
    }

    auto r = work_.exec(
        "INSERT INTO " + sql::table::DESCRIPTION + " (" + sql::col::CONTENTS + ")"
        " VALUES (" + work_.quote(data) + ") RETURNING " + sql::col::ID
    );
    REQUIRE(!r.empty(), "can not create description data");

    auto id = r[0][0].as<DBID>();
    REQUIRE(id, "invalid description id");
    return id;
}

DBID
CommitWorker::insertGeometry(const Wkb& data) const
{
    if (data.empty()) {
        return 0;
    }

    auto r = work_.exec_params(
        STATEMENT_CREATE_GEOMETRY, pqxx::binarystring(data));
    REQUIRE(!r.empty(), "can not create geometry data");

    auto id = r[0][0].as<DBID>();
    REQUIRE(id, "invalid geometry id");
    return id;
}

} // namespace maps::wiki::revision
