#include "action.h"
#include "deserialize.h"
#include "lables.h"

#include <saas/library/behaviour/behaviour.h>
#include <saas/protos/positions.pb.h>
#include <saas/util/json/json.h>
#include <saas/util/types/cast.h>

#include <util/digest/fnv.h>
#include <util/string/hex.h>

using namespace NSaas;
using namespace NRTYServer;

template <>
TMessage::TMessageType enum_cast<NRTYServer::TMessage::TMessageType, TAction::TActionType>(TAction::TActionType value) {
    switch (value) {
    case TAction::atAdd:    return TMessage::ADD_DOCUMENT;
    case TAction::atDelete: return TMessage::DELETE_DOCUMENT;
    case TAction::atModify: return TMessage::MODIFY_DOCUMENT;
    case TAction::atReopen: return TMessage::REOPEN_INDEXERS;
    case TAction::atDeprecated__Update: return TMessage::DEPRECATED__UPDATE_DOCUMENT;
    case TAction::atSwitchPrefix: return TMessage::SWITCH_PREFIX;
    default: Y_FAIL("undefined");
    }
}

template <>
TAction::TActionType enum_cast<TAction::TActionType, NRTYServer::TMessage::TMessageType>(TMessage::TMessageType value) {
    switch (value) {
    case TMessage::ADD_DOCUMENT:    return TAction::atAdd;
    case TMessage::DELETE_DOCUMENT: return TAction::atDelete;
    case TMessage::MODIFY_DOCUMENT: return TAction::atModify;
    case TMessage::REOPEN_INDEXERS: return TAction::atReopen;
    case TMessage::DEPRECATED__UPDATE_DOCUMENT: return TAction::atDeprecated__Update;
    case TMessage::SWITCH_PREFIX:   return TAction::atSwitchPrefix;
    default: Y_FAIL("undefined");
    }
}

TString TAction::BuildJsonMessage() const {
    TToJsonContext context(TToJsonContext::COMMON_JSON);
    return NUtil::JsonToString(ToJson(context), true);
}

TAction::TAction()
    : OwnedProtobufPtr(MakeHolder<TProtobuf>())
    , InternalMessage(*OwnedProtobufPtr)
    , InternalDocument(*InternalMessage.MutableDocument())
{
    SetActionType(atModify);
}

NSaas::TAction::TAction(const TProtobuf& protobuf)
    : TAction()
{
    ParseFromProtobuf(protobuf);
}

TDocument& NSaas::TAction::GetDocument() {
    return HasDocument() ? *Document : AddDocument();
}

TString NSaas::TAction::GetActionDescription() const {
    return NSaas::GetActionDescription(ToProtobuf());
}

bool NSaas::TAction::Validate(TString& error) const {
    try {
        TAction checker;
        TToJsonContext context(TToJsonContext::COMMON_JSON);
        checker.ParseFromJson(ToJson(context));
        return true;
    } catch (const yexception& e) {
        error = e.what();
        return false;
    }
}

TDocument& TAction::AddDocument() {
    if (HasDocument())
        throw yexception() << "deprecated: only one document per json is allowed";
    else
        Document.Reset(new TDocument(InternalDocument));
    return *Document;
}

TAction& TAction::SetActionType(TActionType action) {
    InternalMessage.SetMessageType(enum_cast<NRTYServer::TMessage::TMessageType>(action));
    return *this;
}

TAction::TActionType TAction::GetActionType() const {
    return enum_cast<TActionType>(InternalMessage.GetMessageType());
}

TAction& TAction::SetRequest(const TString& request) {
    Request = request;
    InternalDocument.SetBody(NProtobufFormat::QueryDelBodyPrefix + Request);
    InternalDocument.SetUrl(NProtobufFormat::QueryDelUrlPrefix + ToString(FnvHash<ui64>(Request.data(), Request.size())));
    return *this;
}

TAction& NSaas::TAction::ClearRequest() {
    Request.clear();
    if (InternalDocument.GetBody().StartsWith(NProtobufFormat::QueryDelBodyPrefix)) {
        InternalDocument.ClearBody();
        InternalDocument.ClearUrl();
    }
    return *this;
}

TAction& NSaas::TAction::AddDistributorAttribute(const TString& value) {
    InternalMessage.AddDistributorAttributes(value);
    return *this;
}

TAction& NSaas::TAction::ClearDistributorAttributes() {
    InternalMessage.ClearDistributorAttributes();
    return *this;
}

TAction& NSaas::TAction::SetReceiveTimestamp(TInstant value) {
    InternalMessage.SetReceiveTimestamp(value.Seconds());
    return *this;
}

TInstant NSaas::TAction::GetReceiveTimestamp() const {
    return TInstant::Seconds(InternalMessage.GetReceiveTimestamp());
}

bool NSaas::TAction::HasReceiveTimestamp() const {
    return InternalMessage.HasReceiveTimestamp();
}

void NSaas::TAction::SetPosition(const TString& key, NRTYServer::TPositionValue value) {
    auto position = InternalDocument.MutablePosition();
    position->SetKey(key);
    position->SetValue(value);
}

TMaybe<NRTYServer::TPosition> NSaas::TAction::GetPosition() const {
    if (InternalDocument.HasPosition()) {
        return InternalDocument.GetPosition();
    }
    return Nothing();
}

void NSaas::TAction::AddExtraPosition(const TString& key, NRTYServer::TPositionValue value) {
    auto position = InternalDocument.AddExtraPositions();
    position->SetKey(key);
    position->SetValue(value);
}

TVector<NRTYServer::TPosition> NSaas::TAction::GetExtraPositions() const {
    TVector<NRTYServer::TPosition> extraPositions;
    for (auto&& position : InternalDocument.GetExtraPositions()) {
        extraPositions.emplace_back(position);
    }
    return extraPositions;
}

TDocument& TAction::GetDocument() const {
    if (!HasDocument())
        throw yexception() << "no document in action";
    return *Document;
}

TAction& TAction::ParseFromProtobuf(const NRTYServer::TMessage& source) {
    TProtobufDeserializer protobuf;
    protobuf.Deserialize(source, *this);
    return *this;
}

TAction& TAction::ParseFromJson(const NJson::TJsonValue& source) {
    TJsonDeserializer json;
    json.Deserialize(source, *this);
    return *this;
}

NJson::TJsonValue TAction::ToJson(TToJsonContext& context) const {
    NJson::TJsonValue message;

    const TRTYMessageBehaviour& behaviour = GetBehaviour(GetProtobufActionType());

    if (HasPrefix() || behaviour.IsContentMessage)
        message.InsertValue(NJsonFormat::PrefixLable, GetPrefix());
    if (HasExternalShard())
        message.InsertValue(NJsonFormat::ExternalShardLable, GetExternalShard());
    message.InsertValue(NJsonFormat::ActionLable, behaviour.Name);
    if (HasRequest() && behaviour.MessageType == NRTYServer::TMessage::DELETE_DOCUMENT) {
        message.InsertValue(NJsonFormat::RequestLable, Request);
    } else if (behaviour.IsContentMessage) {
        if (!HasDocument())
            throw yexception() << "action " << behaviour.Name << " requires a document";
        NJson::TJsonValue& docs = message.InsertValue(NJsonFormat::DocsLable, NJson::JSON_ARRAY);
        docs.AppendValue(GetDocument().ToJson(context));
    } else {
        if (HasDocument() && GetDocument().HasBody())
            throw yexception() << "action " << behaviour.Name << " does not accept body in document";
    }
    return message;
}

const TMessage& TAction::ToProtobuf() const {
    return InternalMessage;
}

NRTYServer::TMessage& NSaas::TAction::MutableProtobuf() {
    return InternalMessage;
}

TActionDebugView NSaas::GetActionDetails(const NRTYServer::TMessage &message) {
    TActionDebugView res;

    const TRTYMessageBehaviour& behaviour = GetBehaviour(message.GetMessageType());
    res.Name = behaviour.Name;
    res.KeyPrefix = message.GetDocument().GetKeyPrefix();
    res.Key = message.GetDocument().GetUrl();
    res.MessageId = message.GetMessageId();
    return res;
}

TString NSaas::GetActionDescription(const NRTYServer::TMessage& message) {
    TActionDebugView view = GetActionDetails(message);
    TString result;
    result.append(view.Name);
    result.append(":");
    result.append(ToString(view.KeyPrefix));
    result.append(":");
    result.append(view.Key);

    const TRTYMessageBehaviour& behaviour = GetBehaviour(message.GetMessageType());
    if (behaviour.IdentifyByContent) {
        const ui32 hash = FnvHash<ui32>(message.GetDocument().SerializeAsString());
        result.append(":");
        result.append(HexEncode(&hash, sizeof(hash)));
    }
    return result;
}

ui64 NSaas::GetActionVersion(const NRTYServer::TMessage& message) {
    const auto& d = message.GetDocument();
    ui64 result = 0;
    result += d.GetVersion();
    result <<= 32;
    result += d.GetModificationTimestamp();
    return result;
}
