#include <memory>

#include <mesytec-mvlc/mesytec-mvlc.h>
#include <mesytec-mvlc/util/signal_handling.h>
#include <mesytec-mnode/mnode_cpp_types.h>
#include <mesytec-mnode/mnode_nng_async.h>
#include <mesytec-mnode/mnode_nng_proto.h>
#include "internal/argh.h"
#include "proto/google/rpc/code.pb.h"
#include "proto/mrpc.pb.h"
#include "proto/mvlc.pb.h"
#include "proto/service.pb.h"

using namespace mesytec;
using namespace std::literals;
using namespace mesytec::mnode;

void print_service_descriptors(google::protobuf::Service &service)
{
    auto sd = service.GetDescriptor();
    spdlog::info("service: full_name={}, index={}, method_count={}",
        sd->full_name(), sd->index(), sd->method_count());

    for (int i = 0; i < sd->method_count(); ++i)
    {
        auto md = sd->method(i);
        auto it = md->input_type();
        auto ot = md->output_type();
        spdlog::info("  method: full_name={}, index={}, input_type={}, output_type={}",
            md->full_name(), md->index(), it->full_name(), ot->full_name());
    }
}

struct RpcClosure: public google::protobuf::Closure
{
    void Run() override { }
};

struct RpcController: public google::protobuf::RpcController
{
    void Reset() override
    {
        failed_ = false;
        canceled_ = false;
        errorText_.clear();
        cancelCallback_ = nullptr;
    }

    bool Failed() const override { return failed_; }
    std::string ErrorText() const override { return errorText_; }
    void StartCancel() override { }
    void SetFailed(const std::string &reason) override { errorText_ = reason; failed_ = true; }
    bool IsCanceled() const override { return canceled_; }
    void NotifyOnCancel(google::protobuf::Closure *callback) override { cancelCallback_ = callback; }

    bool failed_ = false;
    bool canceled_ = false;
    std::string errorText_;
    google::protobuf::Closure *cancelCallback_ = nullptr;
};

struct RpcChannel: public google::protobuf::RpcChannel
{
    explicit RpcChannel(nng_socket socket)
        : socket_(socket)
    {
    }

    void fail(google::protobuf::RpcController *controller, const std::string &reason, google::rpc::Code code)
    {
        controller->SetFailed(reason);
        callResponse_.mutable_status()->set_code(code);
        callResponse_.mutable_status()->set_message(reason);
        callResponse_.mutable_status()->clear_details();
    }

    void CallMethod(const google::protobuf::MethodDescriptor *method,
                    google::protobuf::RpcController *controller,
                    const google::protobuf::Message *request,
                    google::protobuf::Message *response,
                    google::protobuf::Closure *done) override
    {
        spdlog::info("RpcChannel::CallMethod: method={}, request={}, response={}",
            method->full_name(), request->ShortDebugString(), response->ShortDebugString());

        methodCall_.set_service(method->service()->full_name());
        methodCall_.set_method(method->full_name());

        if (!methodCall_.mutable_request()->PackFrom(*request))
        {
            fail(controller, "PackFrom failed", google::rpc::Code::INTERNAL);
            spdlog::error("PackFrom failed");
            return;
        }

        auto msg = mnode::nng::allocate_message(0);

        if (mnode::nng::serialize_proto_to_nng(methodCall_, msg.get()) == 0)
        {
            spdlog::error("serialize_proto_to_nng failed");
            return;
        }

        spdlog::info("sending request: nng_msg_len={}", nng_msg_len(msg.get()));

        if (auto nng_res = mnode::nng::send_message(socket_, msg))
        {
            spdlog::error("nng_sendmsg: {}", nng_strerror(nng_res));
            return;
        }

        auto [nng_response, nng_res] = mnode::nng::receive_message(socket_);

        if (nng_res)
        {
            spdlog::error("nng_recvmsg: {}", nng_strerror(nng_res));
            return;
        }
        else if (nng_res == NNG_ETIMEDOUT)
        {
            spdlog::trace("nng_recvmsg: NNG_ETIMEDOUT");
            return;
        }

        if (mnode::nng::deserialize_proto_from_nng_trim(callResponse_, nng_response.get()) == 0)
        {
            spdlog::error("deserialize_proto_from_nng_trim failed");
            return;
        }

        if (!callResponse_.response().UnpackTo(response))
        {
            spdlog::error("UnpackTo failed");
            return;
        }

        done->Run();
    }

    nng_socket socket_;
    mnode::proto::mrpc::MethodCall methodCall_;
    mnode::proto::mrpc::MethodCallResponse callResponse_;

};

int main()
{
    namespace nng = mnode::nng;
    spdlog::set_level(spdlog::level::debug);
    auto socket = nng::make_req_socket();

    if (int res = nng_dial(socket, "tcp://localhost:5555", nullptr, NNG_FLAG_NONBLOCK))
    {
        nng::mnode_nng_error("nng_dial", res);
        return res;
    }

    RpcController rpcController;
    RpcClosure done;
    RpcChannel rpcChannel(socket);
    mnode::proto::mvlc::MVLCService::Stub mvlcService(&rpcChannel);

    for (int i = 0; i < 10; ++i)
    {
        mnode::proto::vme::ReadRequest request;
        request.set_address(0xffff600e);
        request.mutable_amod()->set_value(mesytec::mvlc::vme_amods::A32);
        request.set_data_width(mnode::proto::vme::DataWidth::D32);
        mnode::proto::vme::ReadResponse response;
        spdlog::info("request: {}", request.ShortDebugString());
        mvlcService.VMERead(&rpcController, &request, &response, &done);
        spdlog::info("response: {}", response.ShortDebugString());
    }

    return 0;
}
