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

#include <yandex/maps/wiki/common/extended_xml_doc.h>
#include <yandex/maps/wiki/common/default_config.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/common/geom.h>
#include <yandex/maps/wiki/common/robot.h>

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

#include <yandex/maps/wiki/groupedit/session.h>
#include <yandex/maps/wiki/groupedit/actions/delete.h>

#include <boost/algorithm/string/split.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/program_options.hpp>

#include <fstream>
#include <iostream>

namespace po = boost::program_options;
namespace mwc = maps::wiki::common;

namespace maps {
namespace wiki {
namespace {

const std::string OPT_RESULT_IDS_FILE = "result-ids-file";
const std::string OPT_HELP = "help";
const std::string OPT_CONFIG = "config";
const std::string OPT_ACTION = "action";
const std::string OPT_USER_ID = "user-id";
const std::string OPT_IDS = "ids";
const std::string OPT_DRY_RUN = "dry-run";
const std::string OPT_WKT_FILE = "wkt-file";
const std::string OPT_CATEGORIES = "categories";

const std::string CORE_DATABASE_NAME = "long-read";
const std::string CORE_POOL_NAME = "revisionapi";

const std::string VIEW_TRUNK_DATABASE_NAME = "view-trunk";
const std::string VIEW_TRUNK_POOL_NAME = "editor-tool";

const std::string ACTION_SET_GEOLOCKS = "set-geolocks";
const std::string ACTION_REMOVE_GEOLOCKS = "remove-geolocks";
const std::string ACTION_INFO = "info";
const std::string ACTION_SHOW_WKT = "show-wkt";
const std::string ACTION_DELETE_OBJECTS = "delete-objects";
const std::string OPT_INTERSECTS = "intersects";

const std::string STDIN_FILE = "-";
const std::string CATEGORY_PREFIX = "cat:";

const int EXITCODE_OK = 0;
const int EXITCODE_LOGIC_ERROR = 1;
const int EXITCODE_RUNTIME_ERROR = 2;

const std::set<std::string> ALL_ACTIONS {
    ACTION_INFO,
    ACTION_SHOW_WKT,
    ACTION_SET_GEOLOCKS,
    ACTION_REMOVE_GEOLOCKS,
    ACTION_DELETE_OBJECTS,
};


std::vector<std::string>
splitString(const std::string& str)
{
    if (str.empty()) {
        return {};
    }

    std::vector<std::string> parts;
    auto delimiter = [] (char c)
    {
        return c == ',';
    };
    boost::split(parts, str, delimiter, boost::algorithm::token_compress_on);
    return parts;
}

template <typename Type>
Type
getValue(const po::variables_map& vm, const std::string& option)
{
    if (!vm.count(option)) {
        return {};
    }
    try {
        return vm[option].as<Type>();
    } catch (...) {
        ERROR() << "invalid type of option: " << option;
        throw;
    }
}

struct Params
{
    explicit Params(const po::variables_map& vm)
        : vm(vm)
        , uid(getValue<revision::UserID>(vm, OPT_USER_ID))
        , action(getValue<std::string>(vm, OPT_ACTION))
        , resultIdsFileName(getValue<std::string>(vm, OPT_RESULT_IDS_FILE))
    {
        REQUIRE(uid, OPT_USER_ID << " zero");
        REQUIRE(ALL_ACTIONS.count(action), "unallowed action: " << action);

        loadIds();
        loadWkts();
        loadCategories();
    }

    void loadIds()
    {
        for (const auto& idStr : splitString(getValue<std::string>(vm, OPT_IDS))) {
            auto id = boost::lexical_cast<revision::DBID>(idStr);
            REQUIRE(id, "invalid input id");
            ids.insert(id);
        }
    }

    void loadCategories()
    {
        for (const auto& catStr : splitString(getValue<std::string>(vm, OPT_CATEGORIES))) {
            auto pos = catStr.find(CATEGORY_PREFIX);
            if (pos == std::string::npos) {
                categories.insert(CATEGORY_PREFIX + catStr);
            } else {
                REQUIRE(pos == 0, "invalid category: " << catStr);
                categories.insert(catStr);
            }
        }
    }

    void loadWkts()
    {
        auto wktFile = getValue<std::string>(vm, OPT_WKT_FILE);
        if (wktFile.empty()) {
            return;
        }

        auto loader = [&](std::istream& infile)
        {
            std::string str;
            while (std::getline(infile, str)) {
                if (!str.empty()) {
                    wkts.push_back(str);
                }
            }
        };

        if (wktFile == STDIN_FILE) {
            loader(std::cin);
        } else {
            std::ifstream infile(wktFile);
            REQUIRE(infile, "input wkt file not exists: " << wktFile);
            loader(infile);
        }
    }

    void checkIds() const
    {
        REQUIRE(!ids.empty(), "empty input ids list");
    }

    void checkWkts() const
    {
        REQUIRE(!wkts.empty(), "empty wkts list");
    }

    void checkCategories() const
    {
        REQUIRE(!categories.empty(), "empty categories list");
    }


    const po::variables_map& vm;
    const revision::UserID uid;
    const std::string action;
    const std::string resultIdsFileName;

    revision::DBIDSet ids;
    std::vector<std::string> wkts;
    std::set<std::string> categories;
};

template <typename Container>
void
dumpContainer(const std::string& filename, const Container& container)
{
    if (filename.empty()) {
        return;
    }

    std::ofstream outfile(filename);
    for (const auto& value : container) {
        outfile << value << std::endl;
    }
}

class Runner : private Params
{
public:
    explicit Runner(const po::variables_map& vm)
        : Params(vm)
    {
        if (!vm.count(OPT_CONFIG)) {
            xmlConfig = common::loadDefaultConfig();
        } else {
            xmlConfig.reset(new common::ExtendedXmlDoc(
                getValue<std::string>(vm, OPT_CONFIG)));
        }

        runners[ACTION_INFO] = [this]{ runInfo(); };
        runners[ACTION_SHOW_WKT] = [this]{ runShowWkt(); };
        runners[ACTION_SET_GEOLOCKS] = [this]{ runSetGeolocks(); };
        runners[ACTION_REMOVE_GEOLOCKS] = [this]{ runRemoveGeolocks(); };
        runners[ACTION_DELETE_OBJECTS] = [this]{ runDeleteObjects(); };
    }

    int run() const
    {
        auto it = runners.find(action);
        if (it == runners.end()) {
            ERROR() << "Unsupported action: " << action;
            return EXITCODE_LOGIC_ERROR;
        }
        it->second();
        return EXITCODE_OK;
    }

private:
    std::unique_ptr<common::ExtendedXmlDoc> xmlConfig;
    std::map<std::string, std::function<void()>> runners;

    static pgpool3::PoolConstants overrideConstants(pgpool3::PoolConstants p)
    {
        p.timeoutEarlyOnMasterUnavailable = false;
        return p;
    };

    std::unique_ptr<common::PoolHolder> createPoolHolder(
        const std::string& section,
        const std::string& poolName) const
    {
        try {
            return std::unique_ptr<common::PoolHolder>(new common::PoolHolder(
                overrideConstants, *xmlConfig, section, poolName));
        } catch (const maps::Exception& e) {
            WARN() << "section/pool " << section << "/" << poolName << " not found";
        }
        return std::unique_ptr<common::PoolHolder>(new common::PoolHolder(
            overrideConstants, *xmlConfig, CORE_DATABASE_NAME, CORE_POOL_NAME));
    };

    std::unique_ptr<common::PoolHolder> createViewTrunkHolder() const
    {
        return createPoolHolder(VIEW_TRUNK_DATABASE_NAME, VIEW_TRUNK_POOL_NAME);
    }

    std::unique_ptr<common::PoolHolder> createCoreHolder() const
    {
        return createPoolHolder(CORE_DATABASE_NAME, CORE_POOL_NAME);
    }

    std::string sqlWithIdGeom() const
    {
        checkIds();
        return
"WITH"
"    ids AS (SELECT id, (SELECT key FROM EACH(domain_attrs) WHERE key like \'cat:%\') AS category"
"        FROM vrevisions_trunk.objects WHERE id in (" + common::join(ids, ',') + ")),"
"    id_geom AS (SELECT a.id, a.the_geom FROM vrevisions_trunk.objects_a a JOIN ids USING (id)"
"        UNION"
"        (SELECT master_id, ST_GeometryN(ST_Polygonize(l.the_geom),1)"
"            FROM vrevisions_trunk.objects_l l, vrevisions_trunk.objects_r r, ids"
"            WHERE slave_id=l.id and master_id=ids.id GROUP BY 1))";
    }

    void runInfo() const
    {
        auto query = sqlWithIdGeom() +
"SELECT id, SUBSTR(category,5) AS category, ST_Area(the_geom) AS area,"
"       ST_AsText(ST_Transform(st_centroid(the_geom),4326)) AS center"
"    FROM id_geom JOIN ids USING (id) ORDER BY id";

        auto poolHolder = createViewTrunkHolder();
        auto work = poolHolder->pool().masterReadOnlyTransaction();
        for (const auto& row : work->exec(query)) {
            std::vector<std::string> strings;
            for (const auto& value : row) {
                strings.push_back(value.c_str());
            }
            std::cout << common::join(strings, " : ") << std::endl;
        }
    }

    void runShowWkt() const
    {
        auto query = sqlWithIdGeom() +
            "SELECT ST_AsText(the_geom), id FROM id_geom ORDER BY id";

        auto poolHolder = createViewTrunkHolder();
        auto work = poolHolder->pool().masterReadOnlyTransaction();
        for (const auto& row : work->exec(query)) {
            std::cout << row[0].c_str() << std::endl;
        }
    }

    void runSetGeolocks() const
    {
        auto poolHolder = createCoreHolder();
        auto work = poolHolder->pool().masterWriteableTransaction();
        INFO() << "WKTS: " << wkts.size();
        checkWkts();
        std::vector<std::string> values;
        for (const auto& wkt : wkts) {
            values.push_back(
                "(0,0," + std::to_string(uid) + ",ST_GeometryFromText('" + wkt + "'))");
        }
        auto query =
            "INSERT INTO geolocks.geometry_lock"
            " (branch_id,commit_id,created_by,extent)"
            " VALUES " + common::join(values, ',') +
            " RETURNING id";
        auto resultIds = loadIds(work->exec(query));
        dumpContainer(resultIdsFileName, resultIds);
        auto commitInfoMessage = doCommit(*work);
        INFO() << "set geolock-ids: " << common::join(resultIds, ',') << " " << commitInfoMessage;
    }

    void runRemoveGeolocks() const
    {
        checkIds();
        auto poolHolder = createCoreHolder();
        auto work = poolHolder->pool().masterWriteableTransaction();
        auto query =
            "DELETE FROM geolocks.geometry_lock"
            " WHERE branch_id=0"
              " AND commit_id=0"
              " AND created_by=" + std::to_string(uid) +
              " AND id IN (" + common::join(ids, ',') + ")"
            " RETURNING id";
        auto resultIds = loadIds(work->exec(query));
        dumpContainer(resultIdsFileName, resultIds);
        auto commitInfoMessage = doCommit(*work);
        INFO() << "remove geolock-ids: " << common::join(resultIds, ',') << " " << commitInfoMessage;
    }

    void runDeleteObjects() const
    {
        checkWkts();
        checkCategories();
        INFO() << "WKTS: " << wkts.size();
        INFO() << "CATEGORIES: " << common::join(categories, ',');
        auto mode = groupedit::GeomPredicate::Within;
        if (vm.count(OPT_INTERSECTS)) {
            mode = groupedit::GeomPredicate::Intersects;
            INFO() << "INTERSECTS MODE";
        }

        std::vector<groupedit::RevGeomFilter> filters;
        for (const auto& wkt : wkts) {
            filters.emplace_back(groupedit::RevGeomFilter{
                revision::filters::Attr::definedAny(categories),
                mode,
                common::wkt2wkb(wkt)
            });
        }

        auto poolHolder = createCoreHolder();
        auto work = poolHolder->pool().masterWriteableTransaction();

        INFO() << "deleting objects";
        groupedit::Session session(*work, 0);
        auto commitIds = groupedit::actions::deleteObjects(
            session, filters, uid);

        INFO() << "calculating statistics";
        printDeletedStatistics(*work, commitIds);

        dumpContainer(resultIdsFileName, commitIds);
        auto commitInfoMessage = doCommit(*work);
        INFO() << "commit-ids: " << common::join(commitIds, ',') << " " << commitInfoMessage;
    }

    template <typename TIds>
    void printDeletedStatistics(pqxx::transaction_base& work, const TIds& commitIds) const
    {
        if (commitIds.empty()) {
            return;
        }

        auto filter =
            revision::filters::ObjRevAttr::commitId().in(commitIds) &&
            revision::filters::ObjRevAttr::isNotRelation();

        static const auto attrDecorator =  [](const std::pair<std::string,std::string>& pair)
        {
            return pair.first + ":" + pair.second;
        };

        std::map<std::string, size_t> deletedCategories;
        revision::RevisionsGateway gateway(work);
        for (const auto& rev : gateway.reader().loadRevisions(filter)) {
            REQUIRE(rev.data().deleted, "found not deleted revision: " << rev.id());
            const auto& attrs = rev.data().attributes;
            REQUIRE(attrs, "strange object without attribbutes: " << rev.id());
            for (const auto& pair : *attrs) {
                if (pair.first.find(CATEGORY_PREFIX) == 0) {
                    deletedCategories[pair.first]++;
                    INFO() << "DELETED " << rev.id().objectId() << " "
                           << common::join(*attrs, attrDecorator, " , ");
                }
            }
        }
        for (const auto& cat2count : deletedCategories) {
            const auto& category = cat2count.first;
            const auto count = cat2count.second;
            INFO() << "deleted category: " << category << " : " << count;
        }
    }

    revision::DBIDSet loadIds(const pqxx::result& rows) const
    {
        revision::DBIDSet resultIds;
        for (const auto& row : rows) {
            resultIds.insert(row["id"].as<revision::DBID>());
        }
        return resultIds;
    }

    std::string doCommit(pqxx::transaction_base& work) const
    {
        if (vm.count(OPT_DRY_RUN)) {
            return "commit into database is skipped\n";
        }
        work.commit();
        return {};
    }
};

} // namespace
} // namespace wiki
} // namespace maps

int main(int argc, char** argv)
{
    using namespace maps::wiki;

    po::options_description desc("Usage: <tool> <options>\nOptions");
    desc.add_options()
        (OPT_HELP.c_str(),
            "show help message")
        (OPT_CONFIG.c_str(), po::value<std::string>(),
            "config path (default: path to services.xml)")
        (OPT_USER_ID.c_str(), po::value<revision::UserID>()->default_value(mwc::ROBOT_UID),
            "TDS committer")
        (OPT_IDS.c_str(), po::value<std::string>(),
            "input ids (object-ids or geolock-ids)")
        (OPT_CATEGORIES.c_str(), po::value<std::string>(),
            "categories for deletion")
        (OPT_WKT_FILE.c_str(), po::value<std::string>(),
            ("input file with WKT ('" + STDIN_FILE + "' - read from stdin)").c_str())
        (OPT_ACTION.c_str(), po::value<std::string>()->default_value(ACTION_INFO),
            ("(" +  common::join(ALL_ACTIONS, ',') + ")").c_str())
        (OPT_INTERSECTS.c_str(),
            "delete objects in intersects mode (default: within)")
        (OPT_DRY_RUN.c_str(),
            "start process in simulation mode")
        (OPT_RESULT_IDS_FILE.c_str(), po::value<std::string>(),
            "result ids file name");

    po::variables_map vm;
    try {
        po::store(po::parse_command_line(argc, argv, desc), vm);
    } catch (po::error& e) {
        std::cerr << "error parsing command line: " << e.what() << std::endl;
        return EXITCODE_LOGIC_ERROR;
    }
    po::notify(vm);

    if (argc == 1 || vm.count(OPT_HELP)) {
        std::cerr << desc << std::endl;
        return EXITCODE_LOGIC_ERROR;
    }

    try {
        return Runner(vm).run();
    } catch(maps::Exception& ex) {
        ERROR() << ex;
    } catch(std::exception& ex) {
        ERROR() << ex.what();
    }
    return EXITCODE_RUNTIME_ERROR;
}
