package exchainapi

import (
	"context"
	"errors"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/log"
	"github.com/exchain/go-exchain/exchain/chaindb"
	nodev1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/node/v1"
	"github.com/exchain/go-exchain/exchain/wrapper"
	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"github.com/holiman/uint256"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"net"
	"net/http"
)

type server struct {
	chain chaindb.ChainReader
	nodev1.UnimplementedNodeServer
}

func (s *server) GetAccount(ctx context.Context, request *nodev1.AccountRequest) (*nodev1.AccountResponse, error) {
	//TODO implement me
	panic("implement me")
}

func (s *server) GetBlockHeaderByNumber(ctx context.Context, request *nodev1.GetHeaderRequest) (*nodev1.GetHeaderResponse, error) {
	header := s.chain.GetBlockHeader(uint256.NewInt(request.Number))
	if header == nil {
		return nil, fmt.Errorf("block header not found")
	}
	return &nodev1.GetHeaderResponse{
		Header: header,
	}, nil
}

func (s *server) GetBlockByNumber(ctx context.Context, request *nodev1.GetBlockRequest) (*nodev1.GetBlockResponse, error) {
	blk := s.chain.GetBlock(uint256.NewInt(request.Number))
	if blk == nil {
		return nil, fmt.Errorf("block not found")
	}
	return &nodev1.GetBlockResponse{
		Block: blk,
	}, nil
}

func (s *server) GetBlockByHash(ctx context.Context, request *nodev1.GetBlockRequest) (*nodev1.GetBlockResponse, error) {
	hash := common.HexToHash(request.Hash)
	blk := s.chain.BlockByHash(hash)
	if blk == nil {
		return nil, fmt.Errorf("block not found")
	}
	return &nodev1.GetBlockResponse{
		Block: blk,
	}, nil
}

func (s *server) GetTransactionByHash(ctx context.Context, request *nodev1.GetTransactionRequest) (*nodev1.GetTransactionResponse, error) {
	hash := common.HexToHash(request.Hash)
	tx, err := s.chain.GetTransaction(hash)
	if err != nil {
		return nil, err
	}
	if tx == nil {
		return nil, fmt.Errorf("transaction not found")
	}
	return &nodev1.GetTransactionResponse{
		Transaction: tx,
	}, nil
}

func (s *server) GetTransactionReceipt(ctx context.Context, request *nodev1.GetReceiptRequest) (*nodev1.GetReceiptResponse, error) {
	hash := common.HexToHash(request.Hash)
	receipt := s.chain.GetReceipt(hash)
	if receipt == nil {
		return nil, fmt.Errorf("receipt not found")
	}
	return &nodev1.GetReceiptResponse{
		Receipt: receipt,
	}, nil
}

func (s *server) GetPairList(ctx context.Context, request *nodev1.GetPairListRequest) (*nodev1.GetPairListResponse, error) {
	//TODO implement me
	panic("implement me")
}

func (s *server) GetPairInfo(ctx context.Context, request *nodev1.GetPairInfoRequest) (*nodev1.GetPairInfoResponse, error) {
	//TODO implement me
	panic("implement me")
}

func (s *server) GetCoinList(ctx context.Context, request *nodev1.GetCoinListRequest) (*nodev1.GetCoinListResponse, error) {
	//TODO implement me
	panic("implement me")
}

func (s *server) GetCoinInfo(ctx context.Context, request *nodev1.GetCoinInfoRequest) (*nodev1.GetCoinInfoResponse, error) {
	//TODO implement me
	panic("implement me")
}

func (s *server) GetWithdrawalProof(ctx context.Context, request *nodev1.WithdrawalProofRequest) (*nodev1.WithdrawalProofResponse, error) {
	res := &nodev1.WithdrawalProofResponse{}
	tx, _ := s.chain.GetTransaction(common.HexToHash(request.Txhash))
	receipt := s.chain.GetReceipt(common.HexToHash(request.Txhash))
	if receipt == nil {
		return nil, errors.New("not found tx receipt")
	}
	tree, err := s.chain.GetWDT(receipt.BlockHeight)
	if err != nil {
		return nil, err
	}
	item := wrapper.NewTxWrapper(tx).WithdrawalHash()
	proof, err := tree.Proof(item.Bytes())
	if err != nil {
		return nil, errors.New(fmt.Sprintf("failed to get proof (%s)", err.Error()))
	}
	res.Proof = proof
	return res, nil
}

func StartServer(logger log.Logger, grpcPort int, gwPort int, chain chaindb.ChainReader) error {
	defer logger.Info("grpc and gateway server stopped")
	logger.Info("starting grpc and gateway server", "grpcPort", grpcPort, "gwPort", gwPort)
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	gServer := &server{
		chain: chain,
	}
	listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", grpcPort))
	if err != nil {
		logger.Error("failed to listen", "err", err)
		return err
	}
	errCh := make(chan error)
	grpcServer := grpc.NewServer()
	nodev1.RegisterNodeServer(grpcServer, gServer)
	go func(ch chan error) {
		var e = grpcServer.Serve(listener)
		ch <- e
	}(errCh)

	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
	err = nodev1.RegisterNodeHandlerFromEndpoint(ctx, mux, fmt.Sprintf("127.0.0.1:%d", grpcPort), opts)
	if err != nil {
		return nil
	}
	go func(ch chan error) {
		var e = http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", gwPort), mux)
		ch <- e
	}(errCh)

	select {
	case e := <-errCh:
		return e
	}
}
