#include "WeChatContext.h"
#include "../ServiceManager.h"
#include "BoostLog.h"
#include "WeChatSession.h"
#include <NetworkUtility.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <boost/asio/defer.hpp>
#include <boost/beast/core.hpp>
#include <boost/format.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <sstream>

std::string WeChatContext::reply(const std::string &body) {
    std::ostringstream oss;
    LOG(info) << "someone send message: \n" << body;
    boost::property_tree::ptree ptree;
    std::istringstream iss(body);
    boost::property_tree::read_xml(iss, ptree);

    auto ToUserName = ptree.get_optional<std::string>("xml.ToUserName");
    if (!ToUserName) {
        LOG(error) << "request dont contain ToUserName.";
        return oss.str();
    }

    auto FromUserName = ptree.get<std::string>("xml.FromUserName");
    auto CreateTime = ptree.get<std::string>("xml.CreateTime");
    auto MsgType = ptree.get<std::string>("xml.MsgType");
    auto content = ptree.get<std::string>("xml.Content");
    auto MsgId = ptree.get<std::string>("xml.MsgId");

    std::shared_ptr<WeChatSession> session;
    if (m_sessions.count(FromUserName) > 0) {
        session = m_sessions.at(FromUserName);
    } else {
        session = std::make_shared<WeChatSession>(FromUserName);
        m_sessions.emplace(FromUserName, session);
    }
    boost::algorithm::trim(content);
    auto reply = session->processInput(content);

    boost::property_tree::ptree sendXml;
    sendXml.put("xml.Content", reply);
    LOG(info) << "send " << FromUserName << ": " << reply;

    sendXml.put("xml.ToUserName", FromUserName);
    sendXml.put("xml.FromUserName", *ToUserName);
    sendXml.put("xml.CreateTime", CreateTime);
    sendXml.put("xml.MsgType", MsgType);

    boost::property_tree::write_xml(oss, sendXml);
    // LOG(info) << "reply content:\n "  << oss.str();
    return oss.str();
}

WeChatContext::WeChatContext(boost::asio::io_context &ioContext)
    : m_ioContext(ioContext), m_timer(ioContext), m_sessionsExpireTimer(ioContext) {
    boost::asio::defer(m_ioContext, [this]() { updateAccessToken(); });
}

void WeChatContext::updateAccessToken() {
    boost::beast::error_code error;

    boost::format target("/cgi-bin/token?grant_type=client_credential&appid=%1%&secret=%2%");
    target % appid % secret;

    auto response = Https::get(m_ioContext, host, port, target.str(), error);
    if (error) {
        LOG(error) << error.message();
        return;
    }
    if (response.empty()) {
        LOG(warning) << "response is empty.";
        return;
    }

    auto json = boost::json::parse(response);
    auto &accessTokenObject = json.as_object();
    if (accessTokenObject.count("errcode")) {
        LOG(error) << "get access_token failed,code: " << accessTokenObject.at("errcode").as_int64()
                   << ", message: " << accessTokenObject.at("errmsg").as_string();
        return;
    }
    m_accessToken = accessTokenObject.at("access_token").as_string();
    auto expires_in = accessTokenObject.at("expires_in").as_int64();
    // LOG(info) << "access_token: " << m_accessToken;
    LOG(info) << "re-access_token after " << expires_in << " s.";
    m_timer.expires_after(std::chrono::seconds(expires_in));
    m_timer.async_wait([this](const boost::system::error_code &error) {
        if (error) {
            LOG(error) << error.message();
            return;
        }
        updateAccessToken();
    });
    broadcast("hello,amass.");
}

WeChatContext::OpenIds WeChatContext::users() {
    boost::beast::error_code error;

    boost::format target("/cgi-bin/user/get?access_token=%1%");

    target % m_accessToken;
    auto response = Https::get(m_ioContext, host, port, target.str(), error);
    if (error) {
        LOG(error) << error.message();
        return {};
    }

    auto json = boost::json::parse(response);
    auto &responseObject = json.as_object();
    if (responseObject.contains("errcode")) {
        LOG(error) << responseObject.at("errmsg").as_string();
        return {};
    }
    auto &users = responseObject.at("data").as_object().at("openid").as_array();
    if (users.empty()) {
        LOG(info) << "now we have no users.";
    }
    OpenIds ret;
    for (auto &id : users) {
        ret.emplace_back(id.as_string());
    }
    return ret;
}

std::string WeChatContext::broadcast(const std::string_view &message) {
    boost::json::object messageObject;
    auto users = this->users();
    LOG(info) << "users: " << users;
    if (users.size() < 2) users.emplace_back("fake_user");
    boost::json::array usersArray;
    for (auto &user : users) {
        usersArray.emplace_back(user);
    }
    messageObject.emplace("touser", std::move(usersArray));
    messageObject.emplace("msgtype", "text");

    boost::json::object textObject;
    textObject.emplace("content", message.data());

    messageObject.emplace("text", std::move(textObject));

    boost::format target("/cgi-bin/message/mass/send?access_token=%1%");

    target % m_accessToken;

    boost::system::error_code error;

    auto response = Https::post(m_ioContext, host, port, target.str(), boost::json::serialize(messageObject), error);
    if (error) {
        // LOG(error) << error.message();
        return response;
    }
    return response;
}

void WeChatContext::cleanExpiredSessions(const boost::system::error_code &error) {
    if (error) {
        LOG(error) << error.message();
        return;
    }
    auto now = std::chrono::system_clock::now();
    for (auto iterator = m_sessions.begin(); iterator != m_sessions.cend();) {
        if (std::chrono::duration_cast<std::chrono::seconds>(now - iterator->second->lastAccessedTime()) >
            sessionExpireTime) {
            iterator = m_sessions.erase(iterator);
        } else {
            ++iterator;
        }
    }
    m_sessionsExpireTimer.expires_after(sessionExpireTime);
    m_sessionsExpireTimer.async_wait([ptr{weak_from_this()}](const boost::system::error_code &error) {
        if (ptr.expired()) return;
        ptr.lock()->cleanExpiredSessions(error);
    });
}