package nat

import (
	
	
	
	
	
	
	

	logging 

	
)

// ErrNoMapping signals no mapping exists for an address
var ErrNoMapping = errors.New("mapping not established")

var log = logging.Logger("nat")

// MappingDuration is a default port mapping duration.
// Port mappings are renewed every (MappingDuration / 3)
const MappingDuration = time.Minute

// CacheTime is the time a mapping will cache an external address for
const CacheTime = 15 * time.Second

type entry struct {
	protocol string
	port     int
}

// so we can mock it in tests
var discoverGateway = nat.DiscoverGateway

// DiscoverNAT looks for a NAT device in the network and returns an object that can manage port mappings.
func ( context.Context) (*NAT, error) {
	,  := discoverGateway()
	if  != nil {
		return nil, 
	}
	var  netip.Addr
	,  := .GetExternalAddress()
	if  == nil {
		, _ = netip.AddrFromSlice()
	}

	// Log the device addr.
	,  := .GetDeviceAddress()
	if  != nil {
		log.Debug("DiscoverGateway address error:", )
	} else {
		log.Debug("DiscoverGateway address:", )
	}

	,  := context.WithCancel(context.Background())
	 := &NAT{
		nat:       ,
		mappings:  make(map[entry]int),
		ctx:       ,
		ctxCancel: ,
	}
	.extAddr.Store(&)
	.refCount.Add(1)
	go func() {
		defer .refCount.Done()
		.background()
	}()
	return , nil
}

// NAT is an object that manages address port mappings in
// NATs (Network Address Translators). It is a long-running
// service that will periodically renew port mappings,
// and keep an up-to-date list of all the external addresses.
type NAT struct {
	natmu sync.Mutex
	nat   nat.NAT
	// External IP of the NAT. Will be renewed periodically (every CacheTime).
	extAddr atomic.Pointer[netip.Addr]

	refCount  sync.WaitGroup
	ctx       context.Context
	ctxCancel context.CancelFunc

	mappingmu sync.RWMutex // guards mappings
	closed    bool
	mappings  map[entry]int
}

// Close shuts down all port mappings. NAT can no longer be used.
func ( *NAT) () error {
	.mappingmu.Lock()
	.closed = true
	.mappingmu.Unlock()

	.ctxCancel()
	.refCount.Wait()
	return nil
}

func ( *NAT) ( string,  int) ( netip.AddrPort,  bool) {
	.mappingmu.Lock()
	defer .mappingmu.Unlock()

	if !.extAddr.Load().IsValid() {
		return netip.AddrPort{}, false
	}
	,  := .mappings[entry{protocol: , port: }]
	// The mapping may have an invalid port.
	if ! ||  == 0 {
		return netip.AddrPort{}, false
	}
	return netip.AddrPortFrom(*.extAddr.Load(), uint16()), true
}

// AddMapping attempts to construct a mapping on protocol and internal port.
// It blocks until a mapping was established. Once added, it periodically renews the mapping.
//
// May not succeed, and mappings may change over time;
// NAT devices may not respect our port requests, and even lie.
func ( *NAT) ( context.Context,  string,  int) error {
	switch  {
	case "tcp", "udp":
	default:
		return fmt.Errorf("invalid protocol: %s", )
	}

	.mappingmu.Lock()
	defer .mappingmu.Unlock()

	if .closed {
		return errors.New("closed")
	}

	// do it once synchronously, so first mapping is done right away, and before exiting,
	// allowing users -- in the optimistic case -- to use results right after.
	 := .establishMapping(, , )
	// Don't validate the mapping here, we refresh the mappings based on this map.
	// We can try getting a port again in case it succeeds. In the worst case,
	// this is one extra LAN request every few minutes.
	.mappings[entry{protocol: , port: }] = 
	return nil
}

// RemoveMapping removes a port mapping.
// It blocks until the NAT has removed the mapping.
func ( *NAT) ( context.Context,  string,  int) error {
	.mappingmu.Lock()
	defer .mappingmu.Unlock()

	switch  {
	case "tcp", "udp":
		 := entry{protocol: , port: }
		if ,  := .mappings[];  {
			delete(.mappings, )
			return .nat.DeletePortMapping(, , )
		}
		return errors.New("unknown mapping")
	default:
		return fmt.Errorf("invalid protocol: %s", )
	}
}

func ( *NAT) () {
	const  = MappingDuration / 3

	 := time.Now()
	 := .Add()
	 := .Add(CacheTime)

	 := time.NewTimer(minTime(, ).Sub()) // don't use a ticker here. We don't know how long establishing the mappings takes.
	defer .Stop()

	var  []entry
	var  []int // port numbers
	for {
		select {
		case  := <-.C:
			if .After() {
				 = [:0]
				 = [:0]
				.mappingmu.Lock()
				for  := range .mappings {
					 = append(, )
				}
				.mappingmu.Unlock()
				// Establishing the mapping involves network requests.
				// Don't hold the mutex, just save the ports.
				for ,  := range  {
					 = append(, .establishMapping(.ctx, .protocol, .port))
				}
				.mappingmu.Lock()
				for ,  := range  {
					if ,  := .mappings[]; ! {
						continue // entry might have been deleted
					}
					.mappings[] = []
				}
				.mappingmu.Unlock()
				 = time.Now().Add()
			}
			if .After() {
				var  netip.Addr
				,  := .nat.GetExternalAddress()
				if  == nil {
					, _ = netip.AddrFromSlice()
				}
				.extAddr.Store(&)
				 = time.Now().Add(CacheTime)
			}
			.Reset(time.Until(minTime(, )))
		case <-.ctx.Done():
			.mappingmu.Lock()
			,  := context.WithTimeout(context.Background(), 10*time.Second)
			defer ()
			for  := range .mappings {
				delete(.mappings, )
				.nat.DeletePortMapping(, .protocol, .port)
			}
			.mappingmu.Unlock()
			return
		}
	}
}

func ( *NAT) ( context.Context,  string,  int) ( int) {
	log.Debugf("Attempting port map: %s/%d", , )
	const  = "libp2p"

	.natmu.Lock()
	var  error
	,  = .nat.AddPortMapping(, , , , MappingDuration)
	if  != nil {
		// Some hardware does not support mappings with timeout, so try that
		,  = .nat.AddPortMapping(, , , , 0)
	}
	.natmu.Unlock()

	if  != nil ||  == 0 {
		if  != nil {
			log.Warnf("NAT port mapping failed: protocol=%s internal_port=%d error=%q", , , )
		} else {
			log.Warnf("NAT port mapping failed: protocol=%s internal_port=%d external_port=0", , )
		}
		// we do not close if the mapping failed,
		// because it may work again next time.
		return 0
	}

	log.Debugf("NAT Mapping: %d --> %d (%s)", , , )
	return 
}

func minTime(,  time.Time) time.Time {
	if .Before() {
		return 
	}
	return 
}