#include "user_impl.h"

#include "parser.h"

#include <library/cpp/tvmauth/exception.h>
#include <library/cpp/tvmauth/ticket_status.h>

#include <util/generic/strbuf.h>
#include <util/string/cast.h>
#include <util/string/split.h>

#include <algorithm>

namespace NTvmAuth {
    static const char* EX_MSG = "Method cannot be used in non-valid ticket";

    TStringBuf GetBlackboxEnvAsString(EBlackboxEnv environment) {
        switch (environment) {
            case (EBlackboxEnv::Prod):
                return TStringBuf("Prod");
            case (EBlackboxEnv::Test):
                return TStringBuf("Test");
            case (EBlackboxEnv::ProdYateam):
                return TStringBuf("ProdYateam");
            case (EBlackboxEnv::TestYateam):
                return TStringBuf("TestYateam");
            case (EBlackboxEnv::Stress):
                return TStringBuf("Stress");
            default: 
                throw yexception() << "Unknown environment";
        }
    }

    TCheckedUserTicket::TImpl::operator bool() const {
        return (Status_ == ETicketStatus::Ok);
    }

    TUid TCheckedUserTicket::TImpl::GetDefaultUid() const {
        Y_ENSURE_EX(bool(*this), TNotAllowedException() << EX_MSG);
        return ProtobufTicket_.user().defaultuid();
    }

    time_t TCheckedUserTicket::TImpl::GetExpirationTime() const {
        Y_ENSURE_EX(bool(*this), TNotAllowedException() << EX_MSG);
        return ProtobufTicket_.expirationtime();
    }

    const TScopes& TCheckedUserTicket::TImpl::GetScopes() const {
        Y_ENSURE_EX(bool(*this), TNotAllowedException() << EX_MSG);
        if (CachedScopes_.empty()) {
            for (const auto& el : ProtobufTicket_.user().scopes()) {
                CachedScopes_.push_back(el);
            }
        }
        return CachedScopes_;
    }

    bool TCheckedUserTicket::TImpl::HasScope(TStringBuf scopeName) const {
        Y_ENSURE_EX(bool(*this), TNotAllowedException() << EX_MSG);
        return std::binary_search(ProtobufTicket_.user().scopes().begin(), ProtobufTicket_.user().scopes().end(), scopeName);
    }

    ETicketStatus TCheckedUserTicket::TImpl::GetStatus() const {
        return Status_;
    }

    const TUids& TCheckedUserTicket::TImpl::GetUids() const {
        Y_ENSURE_EX(bool(*this), TNotAllowedException() << EX_MSG);
        if (CachedUids_.empty()) {
            for (const auto& user : ProtobufTicket_.user().users()) {
                CachedUids_.push_back(user.uid());
            }
        }
        return CachedUids_;
    }

    TString TCheckedUserTicket::TImpl::DebugInfo() const {
        if (CachedDebugInfo_) {
            return CachedDebugInfo_;
        }

        if (Status_ == ETicketStatus::Malformed) {
            CachedDebugInfo_ = "status=malformed;";
            return CachedDebugInfo_;
        }

        TString targetString = "ticket_type=";
        targetString.reserve(256);
        if (Status_ == ETicketStatus::InvalidTicketType) {
            targetString.append("not-user;");
            CachedDebugInfo_ = targetString;
            return targetString;
        }

        targetString.append("user");
        if (ProtobufTicket_.expirationtime() > 0)
            targetString.append(";expiration_time=").append(IntToString<10>(ProtobufTicket_.expirationtime()));
        for (const auto& scope : ProtobufTicket_.user().scopes()) {
            targetString.append(";scope=").append(scope);
        }

        if (ProtobufTicket_.user().defaultuid() > 0)
            targetString.append(";default_uid=").append(IntToString<10>(ProtobufTicket_.user().defaultuid()));
        for (const auto& user : ProtobufTicket_.user().users()) {
            targetString.append(";uid=").append(IntToString<10>(user.uid()));
        }

        targetString.append(";env=");
        EBlackboxEnv environment = static_cast<EBlackboxEnv>(ProtobufTicket_.user().env());
        targetString.append(GetBlackboxEnvAsString(environment));
        targetString.append(";");

        CachedDebugInfo_ = targetString;
        return targetString;
    }

    EBlackboxEnv TCheckedUserTicket::TImpl::GetEnv() const {
        return (EBlackboxEnv)ProtobufTicket_.user().env();
    }

    void TCheckedUserTicket::TImpl::SetStatus(ETicketStatus status) {
        Status_ = status;
    }

    TCheckedUserTicket::TImpl::TImpl(ETicketStatus status, ticket2::Ticket&& protobufTicket)
        : Status_(status)
        , ProtobufTicket_(std::move(protobufTicket))
    {
    }

    TUserTicketImplPtr TCheckedUserTicket::TImpl::CreateTicketForTests(ETicketStatus status,
                                                                       TUid defaultUid,
                                                                       TScopes scopes,
                                                                       TUids uids,
                                                                       EBlackboxEnv env) {
        auto prepareCont = [](auto& cont) {
            std::sort(cont.begin(), cont.end());
            cont.erase(std::unique(cont.begin(), cont.end()), cont.end());
        };
        auto erase = [](auto& cont, auto val) {
            auto it = std::find(cont.begin(), cont.end(), val);
            if (it != cont.end()) {
                cont.erase(it);
            }
        };

        prepareCont(scopes);
        erase(scopes, "");

        uids.push_back(defaultUid);
        prepareCont(uids);
        erase(uids, 0);
        Y_ENSURE(!uids.empty(), "User ticket cannot contain empty uid list");

        ticket2::Ticket proto;
        for (TUid uid : uids) {
            proto.mutable_user()->add_users()->set_uid(uid);
        }
        proto.mutable_user()->set_defaultuid(defaultUid);
        proto.mutable_user()->set_entrypoint(100500);
        for (TStringBuf scope : scopes) {
            proto.mutable_user()->add_scopes(TString(scope));
        }

        proto.mutable_user()->set_env((tvm_keys::BbEnvType)env);

        return MakeHolder<TImpl>(status, std::move(proto));
    }

    TUserContext::TImpl::TImpl(EBlackboxEnv env, TStringBuf tvmKeysResponse)
        : Env_(env)
    {
        ResetKeys(tvmKeysResponse);
    }

    void TUserContext::TImpl::ResetKeys(TStringBuf tvmKeysResponse) {
        tvm_keys::Keys protoKeys;
        if (!protoKeys.ParseFromString(TParserTvmKeys::ParseStrV1(tvmKeysResponse))) {
            ythrow TMalformedTvmKeysException() << "Malformed TVM keys";
        }

        NRw::TPublicKeys keys;
        for (int idx = 0; idx < protoKeys.bb_size(); ++idx) {
            const tvm_keys::BbKey& k = protoKeys.bb(idx);
            if (IsAllowed(k.env())) {
                keys.emplace(k.gen().id(),
                             k.gen().body());
            }
        }

        if (keys.empty()) {
            ythrow TEmptyTvmKeysException() << "Empty TVM keys";
        }

        Keys_ = std::move(keys);
    }

    TUserTicketImplPtr TUserContext::TImpl::Check(TStringBuf ticketBody) const {
        TParserTickets::TRes res = TParserTickets::ParseV3(ticketBody, Keys_, TParserTickets::UserFlag());
        ETicketStatus status = CheckProtobufUserTicket(res.Ticket);

        if (res.Status != ETicketStatus::Ok && !(res.Status == ETicketStatus::MissingKey && status == ETicketStatus::InvalidBlackboxEnv)) {
            status = res.Status;
        }
        return MakeHolder<TCheckedUserTicket::TImpl>(status, std::move(res.Ticket));
    }

    ETicketStatus TUserContext::TImpl::CheckProtobufUserTicket(const ticket2::Ticket& ticket) const {
        if (!ticket.has_user()) {
            return ETicketStatus::Malformed;
        }
        if (!IsAllowed(ticket.user().env())) {
            return ETicketStatus::InvalidBlackboxEnv;
        }
        return ETicketStatus::Ok;
    }

    const NRw::TPublicKeys& TUserContext::TImpl::GetKeys() const {
        return Keys_;
    }

    bool TUserContext::TImpl::IsAllowed(tvm_keys::BbEnvType env) const {
        if (env == tvm_keys::Prod && (Env_ == EBlackboxEnv::Prod || Env_ == EBlackboxEnv::Stress)) {
            return true;
        }
        if (env == tvm_keys::ProdYateam && Env_ == EBlackboxEnv::ProdYateam) {
            return true;
        }
        if (env == tvm_keys::Test && Env_ == EBlackboxEnv::Test) {
            return true;
        }
        if (env == tvm_keys::TestYateam && Env_ == EBlackboxEnv::TestYateam) {
            return true;
        }
        if (env == tvm_keys::Stress && Env_ == EBlackboxEnv::Stress) {
            return true;
        }

        return false;
    }
}