package node

import (
	"encoding/json"
	"io"
	"net/http"
	"strings"

	corenet "github.com/libp2p/go-libp2p/core/network"
	"github.com/libp2p/go-libp2p/core/peer"
	"github.com/pkg/errors"
	"github.com/prysmaticlabs/prysm/v4/beacon-chain/p2p"
	"github.com/prysmaticlabs/prysm/v4/beacon-chain/p2p/peers"
	"github.com/prysmaticlabs/prysm/v4/beacon-chain/p2p/peers/peerdata"
	http2 "github.com/prysmaticlabs/prysm/v4/network/http"
	eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
)

// ListTrustedPeer retrieves data about the node's trusted peers.
func (s *Server) ListTrustedPeer(w http.ResponseWriter, r *http.Request) {
	peerStatus := s.PeersFetcher.Peers()
	allIds := s.PeersFetcher.Peers().GetTrustedPeers()
	allPeers := make([]*Peer, 0, len(allIds))
	for _, id := range allIds {
		p, err := httpPeerInfo(peerStatus, id)
		if err != nil {
			errJson := &http2.DefaultErrorJson{
				Message: errors.Wrapf(err, "Could not get peer info").Error(),
				Code:    http.StatusInternalServerError,
			}
			http2.WriteError(w, errJson)
			return
		}
		// peers added into trusted set but never connected should also be listed
		if p == nil {
			p = &Peer{
				PeerID:             id.String(),
				Enr:                "",
				LastSeenP2PAddress: "",
				State:              eth.ConnectionState(corenet.NotConnected).String(),
				Direction:          eth.PeerDirection(corenet.DirUnknown).String(),
			}
		}
		allPeers = append(allPeers, p)
	}
	response := &PeersResponse{Peers: allPeers}
	http2.WriteJson(w, response)
}

// AddTrustedPeer adds a new peer into node's trusted peer set by Multiaddr
func (s *Server) AddTrustedPeer(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		errJson := &http2.DefaultErrorJson{
			Message: errors.Wrapf(err, "Could not read request body").Error(),
			Code:    http.StatusInternalServerError,
		}
		http2.WriteError(w, errJson)
		return
	}
	var addrRequest *AddrRequest
	err = json.Unmarshal(body, &addrRequest)
	if err != nil {
		errJson := &http2.DefaultErrorJson{
			Message: errors.Wrapf(err, "Could not decode request body into peer address").Error(),
			Code:    http.StatusBadRequest,
		}
		http2.WriteError(w, errJson)
		return
	}
	info, err := peer.AddrInfoFromString(addrRequest.Addr)
	if err != nil {
		errJson := &http2.DefaultErrorJson{
			Message: errors.Wrapf(err, "Could not derive peer info from multiaddress").Error(),
			Code:    http.StatusBadRequest,
		}
		http2.WriteError(w, errJson)
		return
	}

	// also add new peerdata to peers
	direction, err := s.PeersFetcher.Peers().Direction(info.ID)
	if err != nil {
		s.PeersFetcher.Peers().Add(nil, info.ID, info.Addrs[0], corenet.DirUnknown)
	} else {
		s.PeersFetcher.Peers().Add(nil, info.ID, info.Addrs[0], direction)
	}

	peers := []peer.ID{}
	peers = append(peers, info.ID)
	s.PeersFetcher.Peers().SetTrustedPeers(peers)
	w.WriteHeader(http.StatusOK)
}

// RemoveTrustedPeer removes peer from our trusted peer set but does not close connection.
func (s *Server) RemoveTrustedPeer(w http.ResponseWriter, r *http.Request) {
	segments := strings.Split(r.URL.Path, "/")
	id := segments[len(segments)-1]
	peerId, err := peer.Decode(id)
	if err != nil {
		errJson := &http2.DefaultErrorJson{
			Message: errors.Wrapf(err, "Could not decode peer id").Error(),
			Code:    http.StatusBadRequest,
		}
		http2.WriteError(w, errJson)
		return
	}

	// if the peer is not a trusted peer, do nothing but return 200
	if !s.PeersFetcher.Peers().IsTrustedPeers(peerId) {
		w.WriteHeader(http.StatusOK)
		return
	}

	peers := []peer.ID{}
	peers = append(peers, peerId)
	s.PeersFetcher.Peers().DeleteTrustedPeers(peers)
	w.WriteHeader(http.StatusOK)
}

// httpPeerInfo does the same thing as peerInfo function in node.go but returns the
// http peer response.
func httpPeerInfo(peerStatus *peers.Status, id peer.ID) (*Peer, error) {
	enr, err := peerStatus.ENR(id)
	if err != nil {
		if errors.Is(err, peerdata.ErrPeerUnknown) {
			return nil, nil
		}
		return nil, errors.Wrap(err, "could not obtain ENR")
	}
	var serializedEnr string
	if enr != nil {
		serializedEnr, err = p2p.SerializeENR(enr)
		if err != nil {
			return nil, errors.Wrap(err, "could not serialize ENR")
		}
	}
	address, err := peerStatus.Address(id)
	if err != nil {
		if errors.Is(err, peerdata.ErrPeerUnknown) {
			return nil, nil
		}
		return nil, errors.Wrap(err, "could not obtain address")
	}
	connectionState, err := peerStatus.ConnectionState(id)
	if err != nil {
		if errors.Is(err, peerdata.ErrPeerUnknown) {
			return nil, nil
		}
		return nil, errors.Wrap(err, "could not obtain connection state")
	}
	direction, err := peerStatus.Direction(id)
	if err != nil {
		if errors.Is(err, peerdata.ErrPeerUnknown) {
			return nil, nil
		}
		return nil, errors.Wrap(err, "could not obtain direction")
	}
	if eth.PeerDirection(direction) == eth.PeerDirection_UNKNOWN {
		return nil, nil
	}
	v1ConnState := eth.ConnectionState(connectionState).String()
	v1PeerDirection := eth.PeerDirection(direction).String()
	p := Peer{
		PeerID:    id.String(),
		State:     v1ConnState,
		Direction: v1PeerDirection,
	}
	if address != nil {
		p.LastSeenP2PAddress = address.String()
	}
	if serializedEnr != "" {
		p.Enr = "enr:" + serializedEnr
	}

	return &p, nil
}
