package p2p

import (
	"crypto/ecdsa"
	"fmt"
	"net"
	"time"

	"github.com/OffchainLabs/prysm/v7/config/features"
	ecdsaprysm "github.com/OffchainLabs/prysm/v7/crypto/ecdsa"
	"github.com/OffchainLabs/prysm/v7/runtime/version"
	"github.com/libp2p/go-libp2p"
	mplex "github.com/libp2p/go-libp2p-mplex"
	"github.com/libp2p/go-libp2p/core/network"
	"github.com/libp2p/go-libp2p/core/peer"
	"github.com/libp2p/go-libp2p/p2p/net/connmgr"
	"github.com/libp2p/go-libp2p/p2p/security/noise"
	libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
	libp2ptcp "github.com/libp2p/go-libp2p/p2p/transport/tcp"
	gomplex "github.com/libp2p/go-mplex"
	ma "github.com/multiformats/go-multiaddr"
	"github.com/pkg/errors"
)

type internetProtocol string

const (
	udp  = "udp"
	tcp  = "tcp"
	quic = "quic"
)

// MultiAddressBuilder takes in an ip address string and port to produce a go multiaddr format.
func MultiAddressBuilder(ip net.IP, tcpPort, quicPort uint) ([]ma.Multiaddr, error) {
	ipType, err := extractIpType(ip)
	if err != nil {
		return nil, errors.Wrap(err, "unable to determine IP type")
	}

	// Example: /ip4/1.2.3.4./tcp/5678
	multiaddrStr := fmt.Sprintf("/%s/%s/tcp/%d", ipType, ip, tcpPort)
	multiAddrTCP, err := ma.NewMultiaddr(multiaddrStr)
	if err != nil {
		return nil, errors.Wrapf(err, "cannot produce TCP multiaddr format from %s:%d", ip, tcpPort)
	}

	multiaddrs := []ma.Multiaddr{multiAddrTCP}

	if features.Get().EnableQUIC {
		// Example: /ip4/1.2.3.4/udp/5678/quic-v1
		multiAddrQUIC, err := ma.NewMultiaddr(fmt.Sprintf("/%s/%s/udp/%d/quic-v1", ipType, ip, quicPort))
		if err != nil {
			return nil, errors.Wrapf(err, "cannot produce QUIC multiaddr format from %s:%d", ip, tcpPort)
		}

		multiaddrs = append(multiaddrs, multiAddrQUIC)
	}

	return multiaddrs, nil
}

// setConnManagerOption sets the connection manager option for libp2p based on the
// MaxPeers setting in the p2p config. If MaxPeers is set to a value higher than the
// default high water mark, we create a new connection manager with a high water mark
// that is higher than MaxPeers. Otherwise, we do not set a connection manager option
// and allow the libp2p fallback defaults to be applied. Rationale below:
// see: https://github.com/OffchainLabs/prysm/issues/15607
func setConnManagerOption(cfg *Config, opts []libp2p.Option) ([]libp2p.Option, error) {
	low, high := cfg.connManagerLowHigh()
	cm, err := connmgr.NewConnManager(low, high)
	if err != nil {
		return nil, errors.Wrap(err, "new ConnManager")
	}
	opts = append(opts, libp2p.ConnectionManager(cm))
	return opts, nil
}

// buildOptions for the libp2p host.
func (s *Service) buildOptions(ip net.IP, priKey *ecdsa.PrivateKey) ([]libp2p.Option, error) {
	cfg := s.cfg
	multiaddrs, err := MultiAddressBuilder(ip, cfg.TCPPort, cfg.QUICPort)
	if err != nil {
		return nil, errors.Wrapf(err, "cannot produce multiaddr format from %s:%d", ip, cfg.TCPPort)
	}
	if cfg.LocalIP != "" {
		localIP := net.ParseIP(cfg.LocalIP)
		if localIP == nil {
			return nil, errors.Wrapf(err, "invalid local ip provided: %s:%d", cfg.LocalIP, cfg.TCPPort)
		}

		multiaddrs, err = MultiAddressBuilder(localIP, cfg.TCPPort, cfg.QUICPort)
		if err != nil {
			return nil, errors.Wrapf(err, "cannot produce multiaddr format from %s:%d", cfg.LocalIP, cfg.TCPPort)
		}
	}
	ifaceKey, err := ecdsaprysm.ConvertToInterfacePrivkey(priKey)
	if err != nil {
		return nil, errors.Wrap(err, "cannot convert private key to interface private key. (Private key not displayed in logs for security reasons)")
	}
	id, err := peer.IDFromPublicKey(ifaceKey.GetPublic())
	if err != nil {
		return nil, errors.Wrapf(err, "cannot get ID from public key: %s", ifaceKey.GetPublic().Type().String())
	}
	log.Infof("Running node with peer id of %s ", id.String())

	options := []libp2p.Option{
		privKeyOption(priKey),
		libp2p.ListenAddrs(multiaddrs...),
		libp2p.UserAgent(version.BuildData()),
		libp2p.ConnectionGater(s),
		libp2p.Transport(libp2ptcp.NewTCPTransport),
		libp2p.DefaultMuxers,
		libp2p.Muxer("/mplex/6.7.0", mplex.DefaultTransport),
		libp2p.Security(noise.ID, noise.New),
		libp2p.Ping(false), // Disable Ping Service.
	}
	options, err = setConnManagerOption(s.cfg, options)
	if err != nil {
		return nil, errors.Wrap(err, "set connection manager option")
	}

	if features.Get().EnableQUIC {
		options = append(options, libp2p.Transport(libp2pquic.NewTransport))
	}

	if cfg.EnableUPnP {
		options = append(options, libp2p.NATPortMap()) // Allow to use UPnP
	}

	if cfg.RelayNodeAddr != "" {
		options = append(options, libp2p.AddrsFactory(withRelayAddrs(cfg.RelayNodeAddr)))
	} else {
		// Disable relay if it has not been set.
		options = append(options, libp2p.DisableRelay())
	}

	if cfg.HostAddress != "" {
		options = append(options, libp2p.AddrsFactory(func(addrs []ma.Multiaddr) []ma.Multiaddr {
			externalMultiaddrs, err := MultiAddressBuilder(net.ParseIP(cfg.HostAddress), cfg.TCPPort, cfg.QUICPort)
			if err != nil {
				log.WithError(err).Error("Unable to create external multiaddress")
			} else {
				addrs = append(addrs, externalMultiaddrs...)
			}
			return addrs
		}))
	}

	if cfg.HostDNS != "" {
		options = append(options, libp2p.AddrsFactory(func(addrs []ma.Multiaddr) []ma.Multiaddr {
			external, err := ma.NewMultiaddr(fmt.Sprintf("/dns4/%s/tcp/%d", cfg.HostDNS, cfg.TCPPort))
			if err != nil {
				log.WithError(err).Error("Unable to create external multiaddress")
			} else {
				addrs = append(addrs, external)
			}
			return addrs
		}))
	}

	if features.Get().DisableResourceManager {
		options = append(options, libp2p.ResourceManager(&network.NullResourceManager{}))
	}

	return options, nil
}

func extractIpType(ip net.IP) (string, error) {
	if ip.To4() != nil {
		return "ip4", nil
	}

	if ip.To16() != nil {
		return "ip6", nil
	}

	return "", errors.Errorf("provided IP address is neither IPv4 nor IPv6: %s", ip)
}

func multiAddressBuilderWithID(ip net.IP, protocol internetProtocol, port uint, id peer.ID) (ma.Multiaddr, error) {
	var multiaddrStr string

	if id == "" {
		return nil, errors.Errorf("empty peer id given: %s", id)
	}

	ipType, err := extractIpType(ip)
	if err != nil {
		return nil, errors.Wrap(err, "unable to determine IP type")
	}

	switch protocol {
	case udp, tcp:
		// Example with UDP: /ip4/1.2.3.4/udp/5678/p2p/16Uiu2HAkum7hhuMpWqFj3yNLcmQBGmThmqw2ohaCRThXQuKU9ohs
		// Example with TCP: /ip6/1.2.3.4/tcp/5678/p2p/16Uiu2HAkum7hhuMpWqFj3yNLcmQBGmThmqw2ohaCRThXQuKU9ohs
		multiaddrStr = fmt.Sprintf("/%s/%s/%s/%d/p2p/%s", ipType, ip, protocol, port, id)
	case quic:
		// Example: /ip4/1.2.3.4/udp/5678/quic-v1/p2p/16Uiu2HAkum7hhuMpWqFj3yNLcmQBGmThmqw2ohaCRThXQuKU9ohs
		multiaddrStr = fmt.Sprintf("/%s/%s/udp/%d/quic-v1/p2p/%s", ipType, ip, port, id)
	default:
		return nil, errors.Errorf("unsupported protocol: %s", protocol)
	}

	return ma.NewMultiaddr(multiaddrStr)
}

// Adds a private key to the libp2p option if the option was provided.
// If the private key file is missing or cannot be read, or if the
// private key contents cannot be marshaled, an exception is thrown.
func privKeyOption(privkey *ecdsa.PrivateKey) libp2p.Option {
	return func(cfg *libp2p.Config) error {
		ifaceKey, err := ecdsaprysm.ConvertToInterfacePrivkey(privkey)
		if err != nil {
			return err
		}
		log.Debug("ECDSA private key generated")
		return cfg.Apply(libp2p.Identity(ifaceKey))
	}
}

// Configures stream timeouts on mplex.
func configureMplex() {
	gomplex.ResetStreamTimeout = 5 * time.Second
}
