#include <maps/wikimap/mapspro/libs/taskutils/include/token.h>

#include <maps/wikimap/mapspro/libs/taskutils/include/exception.h>
#include <maps/libs/common/include/digest.h>

#include <sstream>
#include <stdexcept>
#include <boost/lexical_cast.hpp>
#include <boost/tokenizer.hpp>

namespace maps::wiki::taskutils {

namespace {

const std::string DATA_DELIMITER = ".";
const size_t MD5_HEX_SIZE = 32;

} // namespace

class TokenImpl {
public:
    TokenImpl(TaskID id, TUid uid, time_t expires, const std::string& secretWord)
        : id_(id)
        , uid_(uid)
        , expires_(expires)
    {
        if (!id) {
            throw InvalidTokenException() << "zero id";
        }

        auto key = std::to_string(id_) + DATA_DELIMITER + std::to_string(expires_);
        auto sign = computeMD5Hex(key + DATA_DELIMITER + secretWord);

        str_ = key + DATA_DELIMITER + sign + DATA_DELIMITER + std::to_string(uid);
    }

    bool expired() const { return expires_ <= time(0); }

    void checkExpires() const
    {
        if (expired()) {
            throw ExpiredException() << "task id: " << id_ << " uid: " << uid_;
        }
    }


    std::string str_;
    TaskID id_;
    TUid uid_;
    time_t expires_;
};


Token::Token(TaskID id, TUid uid, time_t expires, const std::string& secretWord)
    : impl_(new TokenImpl(id, uid, expires, secretWord))
{
}

Token::Token(const std::string& str, const std::string& secretWord)
{
    typedef boost::char_separator<char> Separator;
    typedef boost::tokenizer<Separator> Tokenizer;

    try {
        Tokenizer tok(str, Separator(DATA_DELIMITER.c_str(), 0, boost::keep_empty_tokens));
        auto it = tok.begin();

        REQUIRE(it != tok.end(), "empty id");
        TaskID id = boost::lexical_cast<TaskID>(*it++);

        REQUIRE(it != tok.end(), "empty expires");
        time_t expires = boost::lexical_cast<time_t>(*it++);

        REQUIRE(it != tok.end(), "empty sign");
        std::string sign = *it++;
        REQUIRE(sign.size() == MD5_HEX_SIZE, "wrong sign: " + sign);

        REQUIRE(it != tok.end(), "empty uid");
        auto uid = boost::lexical_cast<TUid>(*it++);
        REQUIRE(it == tok.end(), "strange extra data after uid");

        impl_.reset(new TokenImpl(id, uid, expires, secretWord));
        REQUIRE(impl_->str_ == str, "not equal " + impl_->str_);
    }
    catch (const maps::Exception& e) {
        throw InvalidTokenException() << "invalid token: " << str;
    }
    catch (const std::exception& e) {
        throw InvalidTokenException() << "invalid token: " << str << " " << e.what();
    }
}

Token::~Token()
{
}

TaskID
Token::id() const
{
    return impl_->id_;
}

TUid
Token::uid() const
{
    return impl_->uid_;
}

time_t
Token::expires() const
{
    return impl_->expires_;
}

std::string
Token::str() const
{
    return impl_->str_;
}

bool
Token::expired() const
{
    return impl_->expired();
}

void
Token::checkExpires() const
{
    return impl_->checkExpires();
}

} // namespace maps::wiki::taskutils
