#include <fstream>
#include <functional>

#include <Poco/Data/RecordSet.h>

#include <mapreduce/yt/interface/client.h>

#include <library/cpp/json/json_reader.h>
#include <library/cpp/uri/http_url.h>
#include <util/generic/ymath.h>
#include <util/generic/yexception.h>
#include <util/string/cast.h>

#include <wmconsole/legacy/util/database/database_holder.h>
#include <wmconsole/legacy/util/database/queries.h>
#include <wmconsole/legacy/util/session.h>
#include <wmconsole/legacy/util/safe_func.h>
#include <wmconsole/legacy/util/logger.h>
#include <wmconsole/legacy/util/database/session_func.h>
#include <wmconsole/legacy/util/thread.h>
#include <wmconsole/version3/wmcutil/http_client.h>

#include "host_processor.h"
#include "xml_holder.h"
#include "user_info_map.h"

using Poco::Data::Keywords::use;

namespace NWebmaster {

THostProcessor::THostProcessor(const std::string &cluster, const std::string &xml_limits_table, TMirrorResolver::Ptr mirrorResolver)
    : xml_l_min(0)
    , xml_l_max(0)
    , limit1_rank(0.0)
    , first_load(true)
    , yt_cluster(cluster)
    , input_table_name(xml_limits_table)
    , MirrorResolver(mirrorResolver)
{
    log_info << "Getting XML users";
    TUserInfoMap user_info_map;
    define_users(user_info_map);

    log_info << "Getting hosts info";
    THostInfoMap hosts_info_map;
    define_input_params(hosts_info_map, user_info_map);

    log_info << "Getting ranks";
    load_rank_owners_mirrors();
    double norm_sum_limit = load_ranks(hosts_info_map, user_info_map);

    log_info << "Calculating limits";
    ui32 ui32_sum_limit = define_limits(hosts_info_map, user_info_map, norm_sum_limit);
    log_info << "Current sum_limit = " << ui32_sum_limit << " norm_limit " << norm_sum_limit;

    if (ui32_sum_limit >= xml_l_max) {
        if (first_load) {
            ythrow yexception() << "Current sum_limit is more than xml_l_max. " << ui32_sum_limit << ">=" << xml_l_max;
        } else {
            log_error << "Current sum_limit is more than xml_l_max. " << ui32_sum_limit << ">=" << xml_l_max;
        }
    }

    if (first_load) {
        if (ui32_sum_limit >= xml_l_min) {
            log_error << "Current sum_limit is more than xml_l_min. " << ui32_sum_limit << ">=" << xml_l_min;
        }
    }

    log_info << "Going to update values in DBs";
    limits_to_wmc_db(hosts_info_map);
    limits_to_xml_db(user_info_map);
}

bool THostProcessor::endsWith(const std::string &str, const std::string &ending) {
    if (str.length() >= ending.length()) {
        return (0 == str.compare(str.length() - ending.length(), ending.length(), ending));
    }

    return false;
}

bool THostProcessor::isSubdomain(const std::string &_subdomain, const std::string &_domain) {
    std::string subdomain(_subdomain);
    std::string domain(_domain);

    bool https_subdomain = isHTTPS(subdomain);
    bool https_domain = isHTTPS(domain);

    if (https_subdomain != https_domain) {
        return false;
    }

    if (https_domain) {
        subdomain = subdomain.substr(8);
        domain = domain.substr(8);
    }

    if (!endsWith(subdomain, domain)) {
        return false;
    }

    if (subdomain == domain) {
        return true;
    }

    int supposed_dot = subdomain.length() - domain.length() - 1;

    return supposed_dot >= 0 && (subdomain[supposed_dot] == '.' || subdomain[supposed_dot] == '/');
}

bool THostProcessor::isHTTPS(const std::string &hostname) {
    return hostname.find("https://") == 0;
}

bool THostProcessor::isWWW(const std::string &hostname, bool https) {
    return hostname.find("www.") == (https ? 8 : 0);
}

std::string THostProcessor::alterWWW(const std::string &hostname) {
    std::string altHostname(hostname);

    bool https = isHTTPS(hostname);
    bool www = isWWW(hostname, https);

    if (https) {
        altHostname = hostname.substr(8);
    }

    if (www) {
        altHostname = altHostname.substr(4);
    } else {
        altHostname = std::string("www.") + altHostname;
    }

    if (https) {
        altHostname = std::string("https://") + altHostname;
    }

    return altHostname;
}

std::string THostProcessor::unWWW(const std::string &hostname) {
    bool https = isHTTPS(hostname);

    if (isWWW(hostname, https)) {
        return alterWWW(hostname);
    }

    return hostname;
}

ui32 THostProcessor::round_limit_trunc(double limit) {
    ui32 ln = log10(limit);
    if (ln < 1) {
        return ln;
    }
    ui32 pw = pow((double)10, (double)(ln - 1));
    ui32 res = limit;
    res = res - (res % pw);
    return res;
}

ui32 THostProcessor::round_to_radix(ui32 n, ui32 radix) {
    ui32 res = n;
    res = (res / radix) * radix;
    return res + radix / 2 >= n ? res : res + radix;
}

ui32 THostProcessor::round_limit(double limit) {
    ui32 res = limit;

    if (res <= 100) {
        return round_to_radix(res, 10);
    } else if (res < 5000) {
        return round_to_radix(res, 100);
    } else if (res < 10000) {
        return round_to_radix(res, 500);
    } else if (res < 50000) {
        return round_to_radix(res, 1000);
    }

    return round_limit_trunc(limit);
}

Poco::Nullable<double> THostProcessor::get_config_params(Poco::Data::Session &sess, const std::string &name) {
    Poco::Nullable<double> res;
    Poco::Data::Statement stmt(sess);
    stmt << "SELECT value FROM tbl_config_alt WHERE name=? LIMIT 1", use(name);
    stmt.execute();
    Poco::Data::RecordSet rs(stmt);
    if (0 == rs.rowCount()) {
        return res;
    }
    res = rs.row(0).get(0).extract<double>();
    return res;
}

void THostProcessor::define_input_params(THostInfoMap &hosts_info_map, const TUserInfoMap &user_info_map) {
    WMCSession::Ptr wmc_sess = TDatabaseHolder::instance().session();
    get_host_info_map(wmc_sess->getUserSession(), hosts_info_map, user_info_map, MirrorResolver);
    Poco::Nullable<double> temp = get_config_params(wmc_sess->getUserSession(), "xml_l_min");
    if (temp.isNull()) {
        ythrow yexception() << "xml_l_min not defined ";
    }
    xml_l_min = temp.value();
    temp = get_config_params(wmc_sess->getUserSession(), "xml_l_max");
    if (temp.isNull()) {
        ythrow yexception() << "xml_l_max not defined ";
    }
    xml_l_max = temp.value();
    temp = get_config_params(wmc_sess->getUserSession(), "xml_l1_rank");
    if (temp.isNull()) {
        limit1_rank = 0.0;
    } else {
        limit1_rank = temp;
    }

    //build grid in "rank_to_norm" map and load weights to it
    for (ui32 cur_rank = min_rank; cur_rank <= max_rank; cur_rank += rank_step) {
        ui32 round_rank = round_limit(cur_rank);
        if (rank_to_norm.find(round_rank) == rank_to_norm.end()) {
            std::string rank_name = "xml_l" + ToString(round_rank) + "_norm";
            Poco::Nullable<double> norm_limit = get_config_params(wmc_sess->getUserSession(), rank_name);
            if (norm_limit.isNull()) {
                ythrow yexception() << rank_name << " not defined ";
            }
            rank_to_norm[round_rank] = norm_limit;
            log_info << rank_name << " = " << norm_limit;
        }
    }
}

void THostProcessor::define_users(TUserInfoMap &user_info_map) {
    TUserInfoMap res;
    WMCSession::Ptr wmc_sess = TXmlHolder::instance().session();
    for (size_t i = 0; i < TXmlHolder::instance().getHostDbCount(); ++i) {
        Poco::Data::Session sess = wmc_sess->getHostSession(i);

        std::deque<TUserInfoDB> users_db_info;
        queries::select(sess, "SELECT id,calculated_limit from tbl_users", users_db_info);
        for (const TUserInfoDB &user_db_info : users_db_info) {
            user_info_map.add_user(i, user_db_info);
        }
    }
}

double THostProcessor::load_ranks(THostInfoMap &hosts_info_map, const TUserInfoMap &user_info_map) {
    const static char *F_OWNER = "owner";
    const static char *F_XML_LIMIT = "xml_limit";
    double sum_limit = 0;

    NYT::IClientPtr client = NYT::CreateClient(yt_cluster.c_str());
    auto reader = client->CreateTableReader<NYT::TNode>(input_table_name.c_str());

    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode &row = reader->GetRow();
        TString owner = row[F_OWNER].AsString();
        const ui32 rank = FromString(row[F_XML_LIMIT].AsString());
        owner.to_lower();

        try {
            process_rank(owner, rank, hosts_info_map, user_info_map, sum_limit);
        } catch(Poco::Exception &e) {
            log_error << "Poco::Exception " << e.message();
        } catch(std::exception& e) {
            log_error << "std::exception " << e.what();
        } catch(...) {
            log_error << "Unknown processing error!";
        }
    }

    return sum_limit;
}

void THostProcessor::load_rank_owners_mirrors() {
    std::deque<std::string> owners;
    const static char *F_OWNER = "owner";
    NYT::IClientPtr client = NYT::CreateClient(yt_cluster.c_str());
    auto reader = client->CreateTableReader<NYT::TNode>(input_table_name.c_str());

    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode &row = reader->GetRow();
        auto owner = row[F_OWNER].AsString();
        owner.to_lower();
        owners.push_back(owner);
    }

    MirrorResolver->ResolveMirrors(owners);
}


bool THostProcessor::find_host(THostInfoMap &hosts_info_map, const std::string &owner, THostInfoMap::iterator &iter, bool force_find_main_user) {
    bool logging = force_find_main_user;

    if (hosts_info_map.find_host(owner, iter, force_find_main_user)) {
        return true;
    } else {
        if (logging) {
            log_warn << "Owner " << owner << " not found. Altering 'www'";
        }

        std::string ownerAltWww(alterWWW(owner));

        if (hosts_info_map.find_host(ownerAltWww, iter, force_find_main_user)) {
            return true;
        } else {
            if (logging) {
                log_warn << "Owner (alt_www) " << ownerAltWww << " not found. Trying by mirror";
            }

            std::string mirror = MirrorResolver->GetMirror(owner);

            if (isSubdomain(mirror, unWWW(owner))) {
                if (!hosts_info_map.find_host(mirror, iter, force_find_main_user)) {
                    if (logging) {
                        log_warn << "Mirror " << mirror << " (owner: " << owner << ") not found";
                    }

                    return false;
                }
                return true;
            } else {
                if (logging) {
                    log_warn << "Mirror " << mirror << " is not subdomain of " << owner;
                }
                return false;
            }
        }
    }
}

void THostProcessor::process_rank(const std::string &owner,
                       ui32 rank,
                       THostInfoMap &hosts_info_map,
                       const TUserInfoMap &user_info_map,
                       double &sum_limit) {
    if (owner.empty()) {
        return;
    }

    if (0 == rank) {
        return;
    }

    THostInfoMap::iterator iter;

    if (!find_host(hosts_info_map, owner, iter, true)) { //forcing to find host with main user
        if (!find_host(hosts_info_map, owner, iter, false)) { //trying to find any host
            log_info << "Owner: " << owner << " not found in DB";
            return;
        }
    }

    THostInfoContainer &container = iter->second;

    // Тут немного упячка: мы сохранили ранк и поэтому далее посчитаем по нему какой то лимит для хоста,
    // но не факт что этот ранк войдет в общую сумму sum_limit (а лимит уйдет в копилку какого то пользователя).
    // Дело в том, что к сумме ранк прибавляется (и лимит уходит в копилку пользователя) только если получатель
    // лимита есть среди пользователе XML поиска.
    // В результате таблица tbl_hosts_limits содержит в себе мусорные лимиты: посчитанные, поскольку есть ранк,
    // но ничего не значащие, и поэтому сумма всех лимитов из tbl_hosts_limits больше чем общий раздаваемый лимит
    container.rank = rank;

    for (THostInfo &hostInfo : container.array) {
        if (hostInfo.main_user().isNull()) {
            log_info << "Host: " << container.array.begin()->name() <<
                     " rank: " << container.rank <<
                     "  norm_limit: " << rank_to_norm[rank] <<
                     " main_user is NULL. Skipping";
            continue;
        }

        Poco::Int64 main_user = hostInfo.main_user();
        const TUserInfo *user = user_info_map.find_user_id(main_user);

        // Получатель лимита не зареган в XML поиске
        if (nullptr == user) {
            log_info << "Host: " << container.array.begin()->name() <<
                     " rank: " << container.rank <<
                     "  norm_limit: " << rank_to_norm[rank] <<
                     " main_user no present. Skipping";

            continue;
        }

        hostInfo.dump_to_migration_tbl = true;
        sum_limit += rank_to_norm[rank];

        log_info << "Host: " << container.array.begin()->name() <<
                 " rank: " << container.rank <<
                 "  norm_limit: " << rank_to_norm[rank] <<
                 " main_user: " << main_user;
    }
}

void THostProcessor::define_limit1_rank(double sum_limit) {
    if (limit1_rank < 0.00001) {
        first_load = true;
        limit1_rank = xml_l_min / sum_limit;
        WMCSession::Ptr wmc_sess = TDatabaseHolder::instance().session();
        wmc_sess->getUserSession() <<
                                   "INSERT INTO tbl_config_alt(name,value) VALUES('xml_l1_rank', ?) ON DUPLICATE KEY UPDATE value=VALUES(value)",
                                   Poco::Data::Keywords::use(limit1_rank),
                                   Poco::Data::Keywords::now;
    } else {
        first_load = false;
    }
}

ui32 THostProcessor::define_limits(THostInfoMap &host_info_map, TUserInfoMap &user_info_map, double sum_limit) {
    define_limit1_rank(sum_limit);
    ui32 res = 0;

    for (auto & p : host_info_map) {
        THostInfoContainer &container = p.second;
        ui32 rank = container.rank;
        if (rank > 0) {
            //divide limit between all hosts inside a group: (http://)site.com, (ftp://)site.com
            ui32 limit = round_limit((limit1_rank * rank_to_norm[rank]) / container.array.size());

            if (limit < 10) {
                limit = 10;
            }

            for (auto & val : container.array) { //Iterate hosts inside group; charge limit to users in XMLSearch
                val.limit = limit;
                if (val.main_user().isNull()) {
                    continue;
                }

                TUserInfo *user = user_info_map.find_user_id(val.main_user());

                if (nullptr == user) {
                    log_info << "Granted limit " << ToString(limit) << " from host " << val.name() << " to nobody";
                    continue;
                }

                log_info << "Granted limit " << ToString(limit) << " from host " << val.name() << " to user " << ToString(user->get<0>());

                user->limit += limit;

                res += limit;
            }
        }
    }

    return res;
}

void THostProcessor::construct_wmc_query(std::string_view table_name, std::stringstream &query, ui32 &cur_insert_num, const THostInfo &hi) {
    if (hi.db_limit().isNull()) {
        if (hi.limit == 0) {
            return;
        }
    } else if (hi.db_limit() == hi.limit) {
        return;
    }

    construct_wmc_query_no_check(table_name, query, cur_insert_num, hi);
}

void THostProcessor::construct_wmc_query_no_check(std::string_view table_name, std::stringstream &query, ui32 &cur_insert_num, const THostInfo &hi) {
    if (0 == cur_insert_num) {
        query << "INSERT INTO " << table_name << "(host_id,`limit`) VALUES";
    } else {
        query << ",";
    }
    query << "(" << hi.id() << "," << hi.limit << ")";
    ++cur_insert_num;
}


void THostProcessor::construct_xml_query(std::stringstream &query, ui32 &cur_insert_num, const TUserIdMap::value_type &user) {
    if (user.second.limit == user.second.db_limit) {
        return;
    }

    if (0 == cur_insert_num) {
        query << "INSERT INTO tbl_users(id,calculated_limit) VALUES";
    } else {
        query << ",";
    }

    query << "(" << user.first << "," << user.second.limit << ")";
    ++cur_insert_num;
}

void THostProcessor::limits_to_wmc_db(const THostInfoMap &hosts_info_map) {
    log_info << "Updating " + ToString(hosts_info_map.size()) + " host limits";
    std::stringstream query;
    ui32 cur_insert_num = 0;

    for (const auto &p : hosts_info_map) {
        const std::vector<THostInfo> &arr = p.second.array;
        for (const THostInfo &hostInfo : arr) {
            construct_wmc_query("tbl_hosts_limits", query, cur_insert_num, hostInfo);
            if (cur_insert_num >= insert_pack) {
                query << " ON DUPLICATE KEY UPDATE tbl_hosts_limits.limit=VALUES(tbl_hosts_limits.limit)";
                log_exec_wmc_query(query);
                query.str(std::string());
                cur_insert_num = 0;
            }
        }
    }

    if (cur_insert_num > 0) {
        query << " ON DUPLICATE KEY UPDATE tbl_hosts_limits.limit=VALUES(tbl_hosts_limits.limit)";
        log_exec_wmc_query(query);
    }

    // Специальная таблица для миграции XML лимитов на новую схему
    query.str("DELETE FROM tbl_hosts_limits_migration");
    log_exec_wmc_query(query);
    cur_insert_num = 0;
    for (const auto &p : hosts_info_map) {
        const std::vector<THostInfo> &arr = p.second.array;
        for (const THostInfo &hostInfo : arr) {
            if (!hostInfo.dump_to_migration_tbl) {
                continue;
            }
            construct_wmc_query_no_check("tbl_hosts_limits_migration", query, cur_insert_num, hostInfo);
            if (cur_insert_num >= insert_pack) {
                log_exec_wmc_query(query);
                query.str(std::string());
                cur_insert_num = 0;
            }
        }
    }

    if (cur_insert_num > 0) {
        log_exec_wmc_query(query);
    }

    log_info << "Done updating host limits";
}

void THostProcessor::limits_to_xml_db(const TUserInfoMap &user_info_map) {
    log_info << "Updating " + ToString(user_info_map.users_count()) << " user limits";
    std::vector< TSimpleSharedPtr<threads::thread> > thrs;

    for (size_t i = 0; i < TXmlHolder::instance().getHostDbCount(); ++i) {
        TUserInfoMap::const_iterator iter = user_info_map.find(i);

        if (iter == user_info_map.end()) {
            continue;
        }

        const TUserIdMap &xml_users = iter->second;
        std::function<void()> func = std::bind(&THostProcessor::thr_xml_db,
                                       i,
                                       std::cref(xml_users));
        std::function<void()> safe_func = std::bind(t_only_log_safe_func(), func);
        thrs.push_back(TSimpleSharedPtr<threads::thread>(new threads::thread(safe_func)));
    }

    for (size_t i = 0; i < TXmlHolder::instance().getHostDbCount(); ++i) {
        thrs[i]->join();
    }
    log_info << "Done updating user limits";
}

void THostProcessor::exec_query(Poco::Data::Session &sess, const std::stringstream &query) {
    sess << query.str(), Poco::Data::Keywords::now;
}

void THostProcessor::log_exec_query(Poco::Data::Session &sess, const std::stringstream &query) {
    std::function<void(Poco::Data::Session &)> exec_func = std::bind(
                &THostProcessor::exec_query,
                std::placeholders::_1,
                std::cref(query)
            );

    std::function<void()> sess_exec_func = std::bind(
                session_func<std::function<void(Poco::Data::Session &)> >,
                std::cref(exec_func),
                std::ref(sess)
            );

    only_log_safe_func(sess_exec_func);
}

void THostProcessor::log_exec_wmc_query(const std::stringstream &query) {
    WMCSession::Ptr wmc_sess = TDatabaseHolder::instance().session();
    Poco::Data::Session &sess = wmc_sess->getUserSession();

    log_exec_query(sess, query);
}

void THostProcessor::log_exec_xml_query(size_t db_num, const std::stringstream &query) {
    WMCSession::Ptr wmc_sess = TXmlHolder::instance().session();
    Poco::Data::Session sess = wmc_sess->getHostSession(db_num);

    log_exec_query(sess, query);
}

void THostProcessor::thr_xml_db(size_t db_num, const TUserIdMap &users) {
    std::stringstream query;
    ui32 cur_insert_num = 0;

    for (const TUserIdMap::value_type &user : users) {
        construct_xml_query(query, cur_insert_num, user);

        if (cur_insert_num >= insert_pack) {
            query << " ON DUPLICATE KEY UPDATE calculated_limit=VALUES(calculated_limit)";
            log_exec_xml_query(db_num, query);
            query.str(std::string());
            cur_insert_num = 0;
        }
    }

    if (cur_insert_num > 0) {
        query << " ON DUPLICATE KEY UPDATE calculated_limit=VALUES(calculated_limit)";
        log_exec_xml_query(db_num, query);
    }
}

} //namespace NWebmaster
