/*
File Name:  Network Init.go
Copyright:  2021 Peernet s.r.o.
Author:     Peter Kleissner

Magic 🪄 to start the network configuration with 0 manual input. Users may specify the list of IPs (and optional ports) to listen; otherwise it listens on all.
IPv6 is always preferred.
*/

package core

import (
	"errors"
	"math/rand"
	"net"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/PeernetOfficial/core/btcec"
)

// networkWire is an incoming packet
type networkWire struct {
	network           *Network         // network which received the packet
	sender            *net.UDPAddr     // sender of the packet
	receiverPublicKey *btcec.PublicKey // public key associated with the receiver
	raw               []byte           // buffer
	unicast           bool             // True if the message was sent via unicast. False if sent via IPv4 broadcast or IPv6 multicast.
}

// initNetwork sets up the network configuration and starts listening.
func initNetwork() {
	rand.Seed(time.Now().UnixNano()) // we are not using "crypto/rand" for speed tradeoff

	// start listen workers
	if config.ListenWorkers == 0 {
		config.ListenWorkers = 2
	}
	for n := 0; n < config.ListenWorkers; n++ {
		go networks.packetWorker()
	}

	// check if user specified where to listen
	if len(config.Listen) > 0 {
		for _, listenA := range config.Listen {
			host, portA, err := net.SplitHostPort(listenA)
			if err != nil && strings.Contains(err.Error(), "missing port in address") { // port is optional
				host = listenA
				portA = "0"
			} else if err != nil {
				Filters.LogError("initNetwork", "invalid input listen address '%s': %s\n", listenA, err.Error())
				continue
			}

			portI, _ := strconv.Atoi(portA)

			if _, err := networks.PrepareListen(host, portI); err != nil {
				Filters.LogError("initNetwork", "listen on '%s': %s\n", listenA, err.Error())
				continue
			}
		}

		return
	}

	// Listen on all IPv4 and IPv6 addresses
	//if _, err := networks.PrepareListen("0.0.0.0", 0); err != nil {
	//	Filters.LogError("initNetwork", "listen on all IPv4 addresses (0.0.0.0): %s\n", err.Error())
	//}
	//if _, err := networks.PrepareListen("::", 0); err != nil {
	//	Filters.LogError("initNetwork", "listen on all IPv6 addresses (::): %s\n", err.Error())
	//}

	// Listen on each network adapter on each IP. This guarantees the highest deliverability, even though it brings on additional challenges such as:
	// * Packet duplicates on IPv6 Multicast (listening on multiple IPs and joining the group on the same adapter) and IPv4 Broadcast (listening on multiple IPs on the same adapter).
	// * Local peers are more likely to connect on the same adapter via multiple IPs (i.e. link-local and others, including public IPv6 and temporary public IPv6).
	// * Network adapters and IPs might change. Simplest case is if someone changes Wifi network.
	interfaceList, err := net.Interfaces()
	if err != nil {
		Filters.LogError("initNetwork", "enumerating network adapters failed: %s\n", err.Error())
		return
	}

	for _, iface := range interfaceList {
		addresses, err := iface.Addrs()
		if err != nil {
			Filters.LogError("initNetwork", "enumerating IPs for network adapter '%s': %s\n", iface.Name, err.Error())
			continue
		}

		networks.ipListen.ifacesExist[iface.Name] = addresses

		networks.InterfaceStart(iface, addresses)
	}
}

// InterfaceStart will start the listeners on all the IP addresses for the network
func (nets *Networks) InterfaceStart(iface net.Interface, addresses []net.Addr) (networksNew []*Network) {
	for _, address := range addresses {
		net1 := address.(*net.IPNet)

		// Do not listen on lookpback IPs. They are not even needed for discovery of machine-local peers (they will be discovered via regular multicast/broadcast).
		if net1.IP.IsLoopback() {
			continue
		}

		networkNew, err := nets.PrepareListen(net1.IP.String(), 0)

		if err != nil {
			// Do not log common errors:
			// * "listen udp4 169.254.X.X:X: bind: The requested address is not valid in its context."
			// Windows reports link-local addresses for inactive network adapters.
			if net1.IP.IsLinkLocalUnicast() {
				continue
			}

			Filters.LogError("networks.InterfaceStart", "listening on network adapter '%s' IPv4 '%s': %s\n", iface.Name, net1.IP.String(), err.Error())
			continue
		}

		nets.ipListen.Add(networkNew.address)

		Filters.LogError("networks.InterfaceStart", "listen on network '%s' UDP %s\n", iface.Name, networkNew.address.String())

		networksNew = append(networksNew, networkNew)
	}

	return
}

// PrepareListen creates a new network and prepares to listen on the given IP address. If port is 0, one is chosen automatically.
func (nets *Networks) PrepareListen(ipA string, port int) (network *Network, err error) {
	ip := net.ParseIP(ipA)
	if ip == nil {
		return nil, errors.New("invalid input IP")
	}

	network = &Network{networkGroup: nets}
	network.terminateSignal = make(chan interface{})

	// get the network interface that belongs to the IP
	if !ip.IsUnspecified() { // checks for IPv4 "0.0.0.0" and IPv6 "::"
		network.iface, network.ipnet = FindInterfaceByIP(ip)
		if network.iface == nil {
			return nil, errors.New("error finding the network interface belonging to IP")
		}
	}

	// open up the port
	if err = network.AutoAssignPort(ip, port); err != nil {
		return nil, err
	}

	nets.Lock()

	// Success - port is open. Add to the list and start accepting incoming messages.
	if IsIPv4(ip) {
		nets.networks4 = append(nets.networks4, network)
		nets.Unlock()
		network.BroadcastIPv4()
	} else {
		nets.networks6 = append(nets.networks6, network)
		nets.Unlock()
		network.MulticastIPv6Join()
	}

	go network.Listen()

	return network, nil
}

// ipList keeps track of listened IP addresses and observed interfaces
type ipList struct {
	ipListen     map[string]struct{}   // list of IPs currently listening on
	sync.RWMutex                       // Mutex for list
	ifacesExist  map[string][]net.Addr // list of currently known interfaces with list of IP addresses
}

// NewIPList creates a new list
func NewIPList() (list *ipList) {
	return &ipList{
		ipListen:    make(map[string]struct{}),
		ifacesExist: make(map[string][]net.Addr),
	}
}

// Add adds a listening IP:Port to the list.
func (list *ipList) Add(addr *net.UDPAddr) {
	list.Lock()
	list.ipListen[net.JoinHostPort(addr.IP.String(), strconv.Itoa(addr.Port))] = struct{}{}
	list.Unlock()
}

// Remove removes a listening address from the list
func (list *ipList) Remove(addr *net.UDPAddr) {
	list.Lock()
	delete(list.ipListen, net.JoinHostPort(addr.IP.String(), strconv.Itoa(addr.Port)))
	list.Unlock()
}

// IsAddressSelf checks if the senders address is actually listening address. This prevents loopback packets from being considered.
// Note: This does not work when listening on 0.0.0.0 or ::1 and binding the sending socket to that.
func (list *ipList) IsAddressSelf(addr *net.UDPAddr) bool {
	if addr == nil {
		return false
	}

	// do not use addr.String() since it addds the Zone for IPv6 which may be ambiguous (can be adapter name or address literal).
	list.RLock()
	_, ok := list.ipListen[net.JoinHostPort(addr.IP.String(), strconv.Itoa(addr.Port))]
	list.RUnlock()
	return ok
}
