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

#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/program_options/variables_map.hpp>
#include <pqxx>

#include <iostream>
#include <sstream>
#include <set>


namespace po = boost::program_options;

typedef uint64_t DBID;
typedef std::set<DBID> DBIDSet;

const std::string OPT_HELP = "help";
const std::string OPT_CONN = "conn";
const std::string OPT_MASTER = "master";
const std::string OPT_SLAVE = "slave";
const std::string OPT_ROLE = "role";

const std::string USAGE = "Usage: ftfixer <options>";

template <class T>
void
loadParam(const po::variables_map& vm, const std::string& name, T& param)
{
    if(vm.count(name)) {
        param = vm[name].as<T>();
    }
}

struct Params
{
    Params(const po::variables_map& vm)
    {
        loadParam(vm, OPT_CONN, conn);
        loadParam(vm, OPT_MASTER, master);
        loadParam(vm, OPT_SLAVE, slave);
        loadParam(vm, OPT_ROLE, role);

        ASSERT(!conn.empty());
        ASSERT(!master.empty());
        ASSERT(!slave.empty());
        ASSERT(!role.empty());
    }

    std::string conn;
    std::string master;
    std::string slave;
    std::string role;
};

template <typename Iter>
std::string
valuesToString(Iter begin, Iter end)
{
    ASSERT(begin != end);

    std::ostringstream os;
    os << *begin;
    while (++begin != end) {
        os << ',' << *begin;
    }
    return os.str();
}

template <typename Cont>
std::string
valuesToString(const Cont& cont)
{
    return valuesToString(cont.begin(), cont.end());
}


void
tryToWriteBacktrace(std::ostream& os)
{
    try {
        throw;
    } catch (const maps::Exception &mex) {
        mex.backtrace(os);
    } catch(...) {
    }
}

DBIDSet
toIds(const pqxx::result& r)
{
    DBIDSet ids;
    for (size_t i = 0; i < r.size(); ++i) {
        ids.insert(r[i][0].as<DBID>());
    }
    return ids;
}


int
run(const Params& params)
{
    std::cout << std::endl << "Fixing cat:" << params.master << " -> cat:" << params.slave
        << " with role:" << params.role << std::endl;
    pqxx::connection conn(params.conn);
    pqxx::work work(conn);
    work.exec("SET search_path=revision,public;");

    auto r = work.exec(
        "SELECT id FROM attributes WHERE contents->'rel:role'='" + params.role + "'"
        " AND contents->'rel:master' in ('ft_el', 'ft_fc')");
    if (r.empty()) {
	std::cout << "Bad attrs with role=" << params.role << " not found. Nothing to do." << std::endl;
	return 1;
    }
    DBIDSet badAttrIds = toIds(r);
    auto badAttrIdsStr = valuesToString(badAttrIds);
    std::cout << "Found bad attr ids: " << badAttrIdsStr << std::endl;

    r = work.exec("SELECT id FROM attributes WHERE contents ? 'cat:" + params.master + "'");
    if (r.empty()) {
        std::cout << "Master attrs with cat:" << params.master << " not found. Nothing to do." << std::endl;
        return 1;
    }
    DBIDSet masterAttrIds = toIds(r);
    auto masterAttrIdsStr = valuesToString(masterAttrIds);
    std::cout << "Found master attr ids: " << masterAttrIdsStr << std::endl;

    r = work.exec(
        "SELECT object_id FROM object_revision"
        " WHERE attributes_id IN (" + masterAttrIdsStr + ")");
    REQUIRE(!r.empty(), "No master objects with found attributes");
    DBIDSet masterObjectIds = toIds(r);
    auto masterObjectIdsStr = valuesToString(masterObjectIds);
    std::cout << "Found master object ids: " << masterObjectIdsStr << std::endl;

    r = work.exec(
        "SELECT id FROM attributes WHERE contents->'rel:role'='" + params.role + "'"
        " AND contents->'rel:master' = '" + params.master + "'"
        " AND contents->'rel:slave' = '" + params.slave + "' LIMIT 1");
    DBID goodAttrId = 0;
    if (!r.empty()) {
        goodAttrId = r[0][0].as<DBID>();
        std::cout << "Found good attr id: " << goodAttrId << std::endl;
    } else {
        r = work.exec(
            "INSERT INTO attributes (contents) VALUES "
            "(hstore(ARRAY["
                "['rel:role','" + params.role + "']," +
                "['rel:master','" + params.master + "']," +
                "['rel:slave','" + params.slave + "']" +
            "])) RETURNING id");
        ASSERT(!r.empty());
        goodAttrId = r[0][0].as<DBID>();
        std::cout << "Insert good attr id: " << goodAttrId << std::endl;
    }
    ASSERT(goodAttrId);

    std::stringstream ss;
    ss <<
        "UPDATE object_revision"
        " SET attributes_id = " << goodAttrId <<
        " WHERE master_object_id IN (" << masterObjectIdsStr << ")"
        " AND attributes_id";
    if (badAttrIds.size() == 1) {
        ss << " = " << badAttrIdsStr;
    } else {
        ss << " IN (" << badAttrIdsStr << ")";
    }

    auto query = ss.str();
    std::cout << "Exec:" << std::endl << query << std::endl;

    r = work.exec(query);
    std::cout << "Fixed " << r.affected_rows() << " rows." << std::endl;

    work.commit();
    return 0;
}

int
main(int argc, char* argv[])
{
    po::options_description desc(USAGE + "\n" + "Allowed options");
    desc.add_options()
        (OPT_HELP.c_str(),
            "produce help message")
        (OPT_CONN.c_str(), po::value<std::string>(),
            "connection string")
        (OPT_MASTER.c_str(), po::value<std::string>(),
            "master category")
        (OPT_SLAVE.c_str(), po::value<std::string>(),
            "slave category")
        (OPT_ROLE.c_str(), po::value<std::string>(),
            "relation role");

    po::variables_map vm;
    po::store(po::parse_command_line(argc, argv, desc), vm);
    po::notify(vm);

    if (!vm.size() || vm.count(OPT_HELP)) {
        std::cerr << desc << std::endl;
        return 1;
    }

    try {
        Params params(vm);
        return run(params);

    } catch (const std::exception &ex) {
        std::cerr << "FAIL: " << ex.what() << std::endl;
        tryToWriteBacktrace(std::cerr);
        return 1;
    }
    return 0;
}
