package grpc

import (
	"context"
	"time"

	grpc "google.golang.org/grpc"
	"google.golang.org/grpc/status"

	"github.com/tendermint/tendermint/crypto"
	cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
	"github.com/tendermint/tendermint/libs/log"
	privvalproto "github.com/tendermint/tendermint/proto/tendermint/privval"
	tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
	"github.com/tendermint/tendermint/types"
)

// SignerClient implements PrivValidator.
// Handles remote validator connections that provide signing services
type SignerClient struct {
	logger log.Logger

	client  privvalproto.PrivValidatorAPIClient
	conn    *grpc.ClientConn
	chainID string
}

var _ types.PrivValidator = (*SignerClient)(nil)

// NewSignerClient returns an instance of SignerClient.
// it will start the endpoint (if not already started)
func NewSignerClient(conn *grpc.ClientConn,
	chainID string, log log.Logger) (*SignerClient, error) {

	sc := &SignerClient{
		logger:  log,
		chainID: chainID,
		client:  privvalproto.NewPrivValidatorAPIClient(conn), // Create the Private Validator Client
	}

	return sc, nil
}

// Close closes the underlying connection
func (sc *SignerClient) Close() error {
	sc.logger.Info("Stopping service")
	if sc.conn != nil {
		return sc.conn.Close()
	}
	return nil
}

//--------------------------------------------------------
// Implement PrivValidator

// GetPubKey retrieves a public key from a remote signer
// returns an error if client is not able to provide the key
func (sc *SignerClient) GetPubKey() (crypto.PubKey, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Todo: should this be configurable?
	defer cancel()
	resp, err := sc.client.GetPubKey(ctx, &privvalproto.PubKeyRequest{ChainId: sc.chainID})
	if err != nil {
		errStatus, _ := status.FromError(err)
		sc.logger.Error("SignerClient::GetPubKey", "err", errStatus.Message())
		return nil, errStatus.Err()
	}

	pk, err := cryptoenc.PubKeyFromProto(resp.PubKey)
	if err != nil {
		return nil, err
	}

	return pk, nil
}

// SignVote requests a remote signer to sign a vote
func (sc *SignerClient) SignVote(chainID string, vote *tmproto.Vote) error {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	resp, err := sc.client.SignVote(ctx, &privvalproto.SignVoteRequest{ChainId: sc.chainID, Vote: vote})
	if err != nil {
		errStatus, _ := status.FromError(err)
		sc.logger.Error("Client SignVote", "err", errStatus.Message())
		return errStatus.Err()
	}

	*vote = resp.Vote

	return nil
}

// SignProposal requests a remote signer to sign a proposal
func (sc *SignerClient) SignProposal(chainID string, proposal *tmproto.Proposal) error {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	resp, err := sc.client.SignProposal(
		ctx, &privvalproto.SignProposalRequest{ChainId: chainID, Proposal: proposal})

	if err != nil {
		errStatus, _ := status.FromError(err)
		sc.logger.Error("SignerClient::SignProposal", "err", errStatus.Message())
		return errStatus.Err()
	}

	*proposal = resp.Proposal

	return nil
}
