#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/common/pyutils.hpp>
#include <yandex/maps/wiki/common/date_time.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <maps/libs/common/include/exception.h>
#include <yandex/maps/pylogger/helpers.h>
#include <maps/libs/introspection/include/hashing.h>

#include <boost/python.hpp>
#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
#include <boost/python/suite/indexing/map_indexing_suite.hpp>
#include <boost/python/raw_function.hpp>
#include <boost/python/copy_const_reference.hpp>
#include <boost/python/return_by_value.hpp>
#include <boost/lexical_cast.hpp>

#include <memory>
#include <optional>
#include <sstream>
#include <vector>


namespace bp = boost::python;

namespace maps {
namespace wiki {
namespace revision {
namespace python {

namespace {

const std::string BRANCH_ALREADY_LOCKED    = "";
const Branch::LockId GLOBAL_BRANCH_LOCK_ID = 0;

template <typename T>
std::optional<T> toStdOptional(boost::optional<T>&& v)
{
    if (!v) {
        return std::nullopt;
    }
    return std::optional<T>(std::move(*v));
}

} // namespace

std::shared_ptr<Attributes> createAttributes(bp::dict kwargs)
{
    std::shared_ptr<Attributes> res(new Attributes());
    bp::list keys = kwargs.keys();
    for(int i = 0; i < bp::len(keys); ++i) {
        bp::object curArg = kwargs[keys[i]];
        if(curArg) {
            (*res)[bp::extract<std::string>(keys[i])] = bp::extract<std::string>(kwargs[keys[i]]);
        }
    }
    return res;
}

class PyRevisionsGateway
{
public:
    PyRevisionsGateway(
            bp::object pgPoolPyObject,
            DBID branchId)
        : pgPoolPyObject_(pgPoolPyObject)
        , pgPool_(bp::extract<pgpool3::Pool&>(pgPoolPyObject_))
        , transaction_(pgPool_.masterWriteableTransaction())
        , branch_(BranchManager(*transaction_).load(branchId))
    {
        revisionsGateway_.reset(new RevisionsGateway(*transaction_, branch_));
    }

    std::vector<ObjectRevision::ID>
    revisionIdsByFilter(const filters::ProxyFilterExpr& expr,
                        DBID commitId) const
    {
        auto snapshot = revisionsGateway_->snapshot(commitId);
        return snapshot.revisionIdsByFilter(expr);
    }

    Commit
    createCommit(
            bp::object commitData,
            UserID createdBy,
            const Attributes& attrs)
    {
        std::vector<RevisionsGateway::NewRevisionData> data;
        data.reserve(bp::len(commitData));

        for (int i = 0; i < bp::len(commitData); ++i)
        {
            ObjectData objectData;
            bp::object pyObjectData = commitData[i];
            RevisionID id = bp::extract<RevisionID>(bp::getattr(pyObjectData, "id"));
            objectData.attributes = toStdOptional(
                common::toOptional<Attributes>(bp::getattr(pyObjectData, "attrs")));
            objectData.description = toStdOptional(
                common::toOptional<Description>(bp::getattr(pyObjectData, "desc")));
            objectData.geometry = toStdOptional(
                common::toOptional<Wkb>(bp::getattr(pyObjectData, "geom")));
            objectData.relationData = toStdOptional(
                common::toOptional<RelationData>(bp::getattr(pyObjectData, "relation_data")));

            bp::extract<bool> deletedExtractor(
                bp::getattr(pyObjectData, "deleted"));
            objectData.deleted = deletedExtractor();

            data.push_back(RevisionsGateway::NewRevisionData(id, objectData));
        }
        return revisionsGateway_->createCommit(begin(data), end(data), createdBy, attrs);
    }

    void commitTxn()
    {
        DBID branchId = branch_.id();
        transaction_->commit();

        transaction_.releaseConnection();
        transaction_ = pgPool_.masterWriteableTransaction();
        branch_ = BranchManager(*transaction_).load(branchId);
        revisionsGateway_.reset(new RevisionsGateway(*transaction_, branch_));
    }

    bp::object objectRevision(DBID objectId, DBID commitId)
    {
        auto snapshot = revisionsGateway_->snapshot(commitId);
        return common::toPyObject(snapshot.objectRevision(objectId));
    }

    bp::dict objectRevisions(bp::object objectIds, DBID commitId)
    {
        auto buffer = common::toVector<DBID>(objectIds);
        bp::dict result;
        auto snapshot = revisionsGateway_->snapshot(commitId);
        for (const auto& objectRevisionPair : snapshot.objectRevisions(buffer)) {
            result[objectRevisionPair.first] = objectRevisionPair.second;
        }

        return result;
    }

    bp::list objectRevisionsByFilter(
            const filters::ProxyFilterExpr& filter,
            DBID commitId)
    {
        return common::toPyList(
            revisionsGateway_->snapshot(commitId).objectRevisionsByFilter(filter));
    }

    bp::list commitRevisionIds(DBID commitId)
    {
        return common::toPyList(
            revisionsGateway_->reader().commitRevisionIds(commitId));
    }

    bp::list commitRevisions(DBID commitId)
    {
        return common::toPyList(
            revisionsGateway_->reader().commitRevisions(commitId));
    }

    std::string branchType() const
    {
        return boost::lexical_cast<std::string>(branch_.type());
    }

    std::string branchState() const
    {
        return boost::lexical_cast<std::string>(branch_.state());
    }

    DBID branchId() const { return revisionsGateway_->branchId(); }

    DBID headCommitId() const
    { return revisionsGateway_->headCommitId(); }

    ObjectRevision loadRevision(const ObjectRevision::ID& revisionId) const
    { return revisionsGateway_->reader().loadRevision(revisionId); }

    ObjectRevision::ID acquireObjectId() const
    { return revisionsGateway_->acquireObjectId(); }

private:
    bp::object pgPoolPyObject_;
    pgpool3::Pool& pgPool_;
    pgpool3::TransactionHandle transaction_;
    Branch branch_;
    std::unique_ptr<RevisionsGateway> revisionsGateway_;
};


class PyBranchManager
{
public:
    explicit PyBranchManager(bp::object pgPoolPyObject)
        : pgPoolPyObject_(pgPoolPyObject)
        , pgPool_(bp::extract<pgpool3::Pool&>(pgPoolPyObject_))
    {}

    // return branch state
    std::string branchState(DBID branchId)
    {
        auto transaction = pgPool_.masterWriteableTransaction();
        BranchManager branchManager(*transaction);

        auto branch = branchManager.load(branchId);
        if (!branch.tryLock(*transaction, GLOBAL_BRANCH_LOCK_ID,
                            revision::Branch::LockType::Shared)) {
            return BRANCH_ALREADY_LOCKED;
        }
        return boost::lexical_cast<std::string>(branch.state());
    }

    // return previous branch state
    std::string setBranchState(DBID branchId, const std::string& branchState)
    {
        return setBranchStateInternal(
            branchId, boost::lexical_cast<BranchState>(branchState));
    }

    // switch state from source branch state only
    // return previous branch state
    std::string setBranchStateRestricted(
        DBID branchId,
        const std::string& branchState,
        const std::string& srcBranchState = "")
    {
        const auto targetBranchState = boost::lexical_cast<BranchState>(branchState);
        if (srcBranchState.empty()) {
            return setBranchStateInternal(branchId, targetBranchState);
        }

        auto sourceBranchState = boost::lexical_cast<BranchState>(srcBranchState);
        ASSERT(sourceBranchState != targetBranchState);

        return setBranchStateInternal(branchId, targetBranchState, sourceBranchState);
    }

    bp::object branchIdByType(const std::string& branchType)
    {
        auto branchIds = branchIdsByTypeInternal(
            boost::lexical_cast<BranchType>(branchType), 1);

        boost::optional<DBID> result;
        if (!branchIds.empty()) {
            result = *branchIds.begin();
        }
        return common::toPyObject(result);
    }

    bp::list branchIdsByType(const std::string& branchType)
    {
        return common::toPyList(branchIdsByTypeInternal(
            boost::lexical_cast<BranchType>(branchType), BranchManager::UNLIMITED));
    }

    bp::list branchesByType(const std::string& branchType, bp::object limit)
    {
        auto limitValue = BranchManager::UNLIMITED;
        if (!limit.is_none()) {
            auto tempLimitValue = bp::extract<size_t>(limit)();
            if (tempLimitValue == 0) {
                return bp::list();
            }
            limitValue = tempLimitValue;
        }

        auto transaction = pgPool_.masterReadOnlyTransaction();
        auto branches = BranchManager(*transaction).load({{
            boost::lexical_cast<BranchType>(branchType),
            limitValue}});
        return common::toPyList(branches);
    }

    DBID createApprovedBranchIfNotExists(UserID uid)
    {
        auto transaction = pgPool_.masterWriteableTransaction();
        BranchManager branchManager(*transaction);
        auto branches = branchManager.load({{BranchType::Approved, 1}});
        if (!branches.empty()) {
            return branches.front().id();
        }
        auto branch = branchManager.createApproved(uid, {});
        transaction->commit();
        return branch.id();
    }

    void concatenateAttrs(DBID branchId, bp::dict kwargs)
    {
        auto transaction = pgPool_.masterWriteableTransaction();
        BranchManager branchManager(*transaction);
        auto branch = branchManager.load(branchId);

        auto attrs = createAttributes(kwargs);
        branch.concatenateAttributes(*transaction, *attrs);

        transaction->commit();
    }

private:
    static bool needChange(
        const Branch& branch,
        BranchState targetBranchState,
        const boost::optional<BranchState>& sourceBranchState)
    {
        auto branchState = branch.state();
        return branchState != targetBranchState &&
               (!sourceBranchState || branchState == *sourceBranchState);
    }

    std::string setBranchStateInternal(
        DBID branchId,
        BranchState targetBranchState,
        const boost::optional<BranchState>& sourceBranchState = boost::none)
    {
        auto transaction = pgPool_.masterWriteableTransaction();
        BranchManager branchManager(*transaction);

        auto branch = branchManager.load(branchId);
        auto prevBranchState = branch.state();
        if (needChange(branch, targetBranchState, sourceBranchState)) {
            if (!branch.tryLock(*transaction, GLOBAL_BRANCH_LOCK_ID,
                                revision::Branch::LockType::Shared)) {
                return BRANCH_ALREADY_LOCKED;
            }
            branch = branchManager.load(branchId);
            prevBranchState = branch.state();
            if (needChange(branch, targetBranchState, sourceBranchState)) {
                if (!branch.setState(*transaction, targetBranchState)) {
                    return BRANCH_ALREADY_LOCKED; // concurrent change without lock?
                }
                transaction->commit();
            }
        }
        return boost::lexical_cast<std::string>(prevBranchState);
    }

    DBIDSet branchIdsByTypeInternal(BranchType branchType, size_t limit)
    {
        if (branchType == BranchType::Trunk) {
            return {TRUNK_BRANCH_ID};
        }

        auto transaction = pgPool_.masterReadOnlyTransaction();
        auto branches = BranchManager(*transaction).load({{branchType, limit}});
        DBIDSet branchIds;
        for (const auto& branch : branches) {
            branchIds.insert(branch.id());
        }
        return branchIds;
    }

private:
    bp::object pgPoolPyObject_;
    pgpool3::Pool& pgPool_;
};


bool objectRevisionIsDeleted(const ObjectRevision& rev)
{ return rev.data().deleted; }


bp::object objectRevisionAttrs(const ObjectRevision& rev)
{ return common::toPyObject(rev.data().attributes); }


bp::object objectRevisionDesc(const ObjectRevision& rev)
{ return common::toPyObject(rev.data().description); }


bp::object objectRevisionRelationData(const ObjectRevision& rev)
{ return common::toPyObject(rev.data().relationData); }


RevisionID objectRevisionId(const ObjectRevision& rev)
{ return rev.id(); }


bp::object objectRevisionGeom(const ObjectRevision& rev)
{ return common::toPyObject(rev.data().geometry); }


std::string getCommitCreatedAt(const Commit& commit)
{ return commit.createdAt(); }


std::string revisionIdToString(const RevisionID& rid)
{
    std::stringstream sstr;
    sstr << rid;
    return sstr.str();
}

std::string getBranchType(const Branch& branch)
{
    return boost::lexical_cast<std::string>(branch.type());
}

std::string getBranchState(const Branch& branch)
{
    return boost::lexical_cast<std::string>(branch.state());
}

std::string getBranchCreatedAt(const Branch& branch)
{
    return common::canonicalDateTimeString(
        branch.createdAt(), common::WithTimeZone::Yes);
}

std::string getBranchFinishedAt(const Branch& branch)
{
    return common::canonicalDateTimeString(
        branch.finishedAt(), common::WithTimeZone::Yes);
}

BOOST_PYTHON_MODULE(revision) {

    PyEval_InitThreads();

    using namespace bp;

    enum_<CommitState>("CommitState")
        .value("DRAFT", CommitState::Draft)
        .value("APPROVED", CommitState::Approved);


    class_<std::vector<ObjectRevision::ID> >("ObjectRevisionIdVector")
        .def(vector_indexing_suite<std::vector<ObjectRevision::ID> >());


    class_<RevisionID>("RevisionId", init<uint64_t, uint64_t>())
        .def("__eq__", &maps::introspection::operator==<RevisionID>)
        .def("__hash__", &maps::introspection::computeHash<RevisionID>)
        .def("__str__", revisionIdToString)
        .add_property("object_id", &RevisionID::objectId)
        .add_property("commit_id", &RevisionID::commitId)
        .add_property("empty", &RevisionID::empty)
        .add_property("valid", &RevisionID::valid);

    class_<std::list<RevisionID>>("RevisionIdList")
        .def("__iter__", iterator<std::list<RevisionID>>());

    class_<Attributes>("Attributes")
        .def("__init__", make_constructor(createAttributes))
        .def(map_indexing_suite<Attributes>());


    class_<RelationData>("RelationData", init<DBID, DBID>())
        .add_property("master_object_id", &RelationData::masterObjectId)
        .add_property("slave_object_id", &RelationData::slaveObjectId);


    class_<ObjectRevision>("ObjectRevision", no_init)
        .add_property("id", objectRevisionId)
        .add_property("prev_id", &ObjectRevision::prevId)
        .add_property("next_trunk_id", &ObjectRevision::nextTrunkId)
        .add_property("deleted", &objectRevisionIsDeleted)
        .add_property("attrs", &objectRevisionAttrs)
        .add_property("desc", &objectRevisionDesc)
        .add_property("geom", &objectRevisionGeom)
        .add_property("relation_data", &objectRevisionRelationData);

    class_<Commit>("Commit", no_init)
        .add_property("id", &Commit::id)
        .add_property("state", &Commit::state)
        .add_property("created_by", &Commit::createdBy)
        .add_property("created_at", &getCommitCreatedAt)
        .add_property("in_trunk", &Commit::inTrunk)
        .add_property("in_stable", &Commit::inStable);

    class_<PyRevisionsGateway, boost::noncopyable>(
            "RevisionsGateway",
            init<bp::object, DBID>(args("pgpool", "branch_id")))
                .def("branch_type", &PyRevisionsGateway::branchType)
                .def("branch_state", &PyRevisionsGateway::branchState)
                .def("branch_id", &PyRevisionsGateway::branchId)
                .def("head_commit_id", &PyRevisionsGateway::headCommitId)
                .def("revision_ids", &PyRevisionsGateway::revisionIdsByFilter)
                .def("load_revision", &PyRevisionsGateway::loadRevision)
                .def("object_revision", &PyRevisionsGateway::objectRevision)
                .def("object_revisions", &PyRevisionsGateway::objectRevisions)
                .def("object_revisions_by_filter",
                    &PyRevisionsGateway::objectRevisionsByFilter)
                .def("commit_revision_ids", &PyRevisionsGateway::commitRevisionIds)
                .def("commit_revisions", &PyRevisionsGateway::commitRevisions)
                .def("acquire_object_id", &PyRevisionsGateway::acquireObjectId)
                .def("create_commit", &PyRevisionsGateway::createCommit)
                .def("commit_txn", &PyRevisionsGateway::commitTxn);

    class_<Branch>("Branch", no_init)
        .add_property("id", &Branch::id)
        .add_property("type", &getBranchType)
        .add_property("state", &getBranchState)
        .add_property("created_by", &Branch::createdBy)
        .add_property("created_at", &getBranchCreatedAt)
        .add_property("finished_by", &Branch::finishedBy)
        .add_property("finished_at", &getBranchFinishedAt);

    class_<PyBranchManager, boost::noncopyable>(
            "BranchManager",
            init<bp::object>(args("pgpool")))
                .def("branch_state", &PyBranchManager::branchState)
                .def("set_branch_state", &PyBranchManager::setBranchState)
                .def("set_branch_state_restricted", &PyBranchManager::setBranchStateRestricted)
                .def("create_approved_branch_if_not_exists", &PyBranchManager::createApprovedBranchIfNotExists)
                .def("branch_id_by_type", &PyBranchManager::branchIdByType)
                .def("branch_ids_by_type", &PyBranchManager::branchIdsByType)
                .def("branches_by_type", &PyBranchManager::branchesByType,
                    (bp::arg("limit") = bp::object()))
                .def("concatenate_attrs", &PyBranchManager::concatenateAttrs);
}

} // namespace python
} // namespace revision
} // namespace wiki
} // namespace maps
