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

// DiscoveryTimeout is the maximum time to wait for NAT discovery.
// This is based on the underlying UPnP and NAT-PMP/PCP protocols:
//   - SSDP (UPnP discovery) waits 5 seconds for responses
//   - NAT-PMP uses exponential backoff starting at 250ms, up to 9 retries
//     (total ~32 seconds if exhausted, but typically responds in 1-2 seconds)
//   - PCP follows similar timing to NAT-PMP
//   - 10 seconds covers common cases while failing fast when no NAT exists
const DiscoveryTimeout = 10 * time.Second

// rediscoveryThreshold is the number of consecutive connection failures
// before triggering NAT rediscovery. We ignore first few failures to
// distinguish between transient network issues and persistent router
// problems like restarts or port changes that require finding the NAT device again.
const rediscoveryThreshold = 3

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, 
	}

	 := getExternalAddress()

	// Log the device addr.
	,  := .GetDeviceAddress()
	if  != nil {
		log.Warn("DiscoverGateway address error", "err", )
	} else {
		log.Info("DiscoverGateway address", "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.
//
// Locking strategy:
//   - natmu: Protects nat instance and rediscovery state (nat, consecutiveFailures, rediscovering)
//   - mappingmu: Protects port mappings table and closed flag (mappings, closed)
//   - Lock ordering: When both locks are needed, always acquire mappingmu before natmu
//     to prevent deadlocks
//   - We use separate mutexes because the NAT instance may change (e.g., when router
//     restarts and UPnP port changes), but the port mappings must persist and be
//     re-applied across all instances. This separation allows the mappings table to
//     remain stable while the underlying NAT device changes.
type NAT struct {
	natmu sync.Mutex
	nat   nat.NAT

	// Track connection failures for auto-rediscovery
	consecutiveFailures int  // protected by natmu
	rediscovering       bool // protected by natmu

	// 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

	// Port mappings that should persist across NAT instance changes
	mappingmu sync.RWMutex
	closed    bool          // protected by mappingmu
	mappings  map[entry]int // protected by mappingmu
}

// 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")
	}

	// Check if the mapping already exists to avoid duplicate work
	 := entry{protocol: , port: }
	if ,  := .mappings[];  {
		return nil
	}

	log.Info("Starting maintenance of port mapping", "protocol", , "port", )

	// 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[] = 
	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()
	.natmu.Lock()
	defer .natmu.Unlock()

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

func ( *NAT) () {
	// Renew port mappings every 20 seconds (1/3 of 60s lifetime).
	// - NAT-PMP RFC 6886 recommends renewing at 50% of lifetime
	// - We use 33% for added safety against silent lifetime reductions
	// NOTE: This aggressive 60s/20s pattern may be outdated for modern routers
	// but provides quick cleanup and fast failure detection for our rediscovery.
	// TODO: Research longer durations (e.g. 30min/10min) to reduce router load
	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() {
				.natmu.Lock()
				,  := .nat.GetExternalAddress()
				.natmu.Unlock()

				var  netip.Addr
				if  == nil {
					, _ = netip.AddrFromSlice()
				}
				.extAddr.Store(&)
				 = time.Now().Add(CacheTime)
			}
			.Reset(time.Until(minTime(, )))
		case <-.ctx.Done():
			.mappingmu.Lock()
			defer .mappingmu.Unlock()
			.natmu.Lock()
			defer .natmu.Unlock()

			,  := context.WithTimeout(context.Background(), 10*time.Second)
			defer ()
			for  := range .mappings {
				.nat.DeletePortMapping(, .protocol, .port)
			}
			clear(.mappings)
			return
		}
	}
}

func ( *NAT) ( context.Context,  string,  int) ( int) {
	log.Debug("Attempting port map", "protocol", , "internal_port", )
	const  = "libp2p"

	// Try to establish the mapping with both NAT calls under the same lock
	.natmu.Lock()
	defer .natmu.Unlock()

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

	// Handle success
	if  == nil &&  != 0 {
		.consecutiveFailures = 0
		log.Debug("NAT port mapping established", "protocol", , "internal_port", , "external_port", )
		return 
	}

	// Handle failures
	if  != nil {
		log.Warn("NAT port mapping failed", "protocol", , "internal_port", , "err", )

		// Check if this is a connection error that might indicate router restart
		// See: https://github.com/libp2p/go-libp2p/issues/3224#issuecomment-2866844723
		// Note: We use string matching because goupnp doesn't preserve error chains (uses %v instead of %w)
		if strings.Contains(.Error(), "connection refused") {
			.consecutiveFailures++
			if .consecutiveFailures >= rediscoveryThreshold && !.rediscovering {
				.rediscovering = true
				// Spawn in goroutine to avoid blocking the caller while we
				// perform network discovery, which can take up to 30 seconds.
				// The rediscovering flag prevents multiple concurrent attempts.
				go .rediscoverNAT()
			}
		} else {
			// Reset counter for non-connection errors (transient failures)
			.consecutiveFailures = 0
		}
		return 0
	}

	// externalPort is 0 but no error was returned
	log.Warn("NAT port mapping failed", "protocol", , "internal_port", , "external_port", 0)
	return 0
}

// rediscoverNAT attempts to rediscover the NAT device after connection failures
func ( *NAT) () {
	log.Info("NAT rediscovery triggered due to repeated connection failures")

	,  := context.WithTimeout(.ctx, DiscoveryTimeout)
	defer ()

	,  := discoverGateway()
	if  != nil {
		log.Warn("NAT rediscovery failed", "err", )
		.natmu.Lock()
		defer .natmu.Unlock()
		.rediscovering = false
		return
	}

	 := getExternalAddress()

	// Replace the NAT instance
	// No cleanup of the old instance needed because:
	// - Router restart has already wiped all mappings
	// - Old UPnP endpoint is dead (connection refused)
	// - If router didn't actually restart (false positive), any stale mappings
	//   on the router expire naturally (60 second UPnP timeout)
	.natmu.Lock()
	.nat = 
	.extAddr.Store(&)
	.consecutiveFailures = 0
	.rediscovering = false
	.natmu.Unlock()

	// Re-establish all existing mappings on the new NAT instance
	.mappingmu.Lock()
	for  := range .mappings {
		 := .establishMapping(.ctx, .protocol, .port)
		.mappings[] = 
		if  != 0 {
			log.Info("NAT mapping restored after rediscovery", "protocol", .protocol, "internal_port", .port, "external_port", )
		}
	}
	.mappingmu.Unlock()

	log.Info("NAT rediscovery successful")
}

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

// getExternalAddress retrieves and parses the external address from a NAT instance
func getExternalAddress( nat.NAT) netip.Addr {
	,  := .GetExternalAddress()
	if  != nil {
		log.Debug("Failed to get external address", "err", )
		return netip.Addr{}
	}
	,  := netip.AddrFromSlice()
	if ! {
		log.Debug("Failed to parse external address", "ip", )
		return netip.Addr{}
	}
	return 
}