package autonat

import (
	
	
	
	
	

	
	
	
	
	

	logging 
	ma 
	manet 
)

var log = logging.Logger("autonat")

const maxConfidence = 3

// AmbientAutoNAT is the implementation of ambient NAT autodiscovery
type AmbientAutoNAT struct {
	host host.Host

	*config

	ctx               context.Context
	ctxCancel         context.CancelFunc // is closed when Close is called
	backgroundRunning chan struct{}      // is closed when the background go routine exits

	inboundConn   chan network.Conn
	dialResponses chan error
	// Used when testing the autonat service
	observations chan network.Reachability
	// status is an autoNATResult reflecting current status.
	status atomic.Pointer[network.Reachability]
	// Reflects the confidence on of the NATStatus being private, as a single
	// dialback may fail for reasons unrelated to NAT.
	// If it is <3, then multiple autoNAT peers may be contacted for dialback
	// If only a single autoNAT peer is known, then the confidence increases
	// for each failure until it reaches 3.
	confidence    int
	lastInbound   time.Time
	lastProbe     time.Time
	recentProbes  map[peer.ID]time.Time
	pendingProbes int
	ourAddrs      map[string]struct{}

	service *autoNATService

	emitReachabilityChanged event.Emitter
	subscriber              event.Subscription
}

// StaticAutoNAT is a simple AutoNAT implementation when a single NAT status is desired.
type StaticAutoNAT struct {
	host         host.Host
	reachability network.Reachability
	service      *autoNATService
}

// New creates a new NAT autodiscovery system attached to a host
func ( host.Host,  ...Option) (AutoNAT, error) {
	var  error
	 := new(config)
	.host = 
	.dialPolicy.host = 

	if  = defaults();  != nil {
		return nil, 
	}
	if .addressFunc == nil {
		if ,  := .(interface{ () []ma.Multiaddr });  {
			.addressFunc = .
		} else {
			.addressFunc = .Addrs
		}
	}

	for ,  := range  {
		if  = ();  != nil {
			return nil, 
		}
	}
	,  := .EventBus().Emitter(new(event.EvtLocalReachabilityChanged), eventbus.Stateful)

	var  *autoNATService
	if (!.forceReachability || .reachability == network.ReachabilityPublic) && .dialer != nil {
		,  = newAutoNATService()
		if  != nil {
			return nil, 
		}
		.Enable()
	}

	if .forceReachability {
		.Emit(event.EvtLocalReachabilityChanged{Reachability: .reachability})

		return &StaticAutoNAT{
			host:         ,
			reachability: .reachability,
			service:      ,
		}, nil
	}

	,  := context.WithCancel(context.Background())
	 := &AmbientAutoNAT{
		ctx:               ,
		ctxCancel:         ,
		backgroundRunning: make(chan struct{}),
		host:              ,
		config:            ,
		inboundConn:       make(chan network.Conn, 5),
		dialResponses:     make(chan error, 1),
		observations:      make(chan network.Reachability, 1),

		emitReachabilityChanged: ,
		service:                 ,
		recentProbes:            make(map[peer.ID]time.Time),
		ourAddrs:                make(map[string]struct{}),
	}
	 := network.ReachabilityUnknown
	.status.Store(&)

	,  := .host.EventBus().Subscribe(
		[]any{new(event.EvtLocalAddressesUpdated), new(event.EvtPeerIdentificationCompleted)},
		eventbus.Name("autonat"),
	)
	if  != nil {
		return nil, 
	}
	.subscriber = 

	go .background()

	return , nil
}

// Status returns the AutoNAT observed reachability status.
func ( *AmbientAutoNAT) () network.Reachability {
	 := .status.Load()
	return *
}

func ( *AmbientAutoNAT) () {
	 := *.status.Load()
	.emitReachabilityChanged.Emit(event.EvtLocalReachabilityChanged{Reachability: })
	if .metricsTracer != nil {
		.metricsTracer.ReachabilityStatus()
	}
}

func ipInList( ma.Multiaddr,  []ma.Multiaddr) bool {
	,  := manet.ToIP()
	for ,  := range  {
		if ,  := manet.ToIP();  == nil && .Equal() {
			return true
		}
	}
	return false
}

func ( *AmbientAutoNAT) () {
	defer close(.backgroundRunning)
	// wait a bit for the node to come online and establish some connections
	// before starting autodetection
	 := .config.bootDelay

	 := .subscriber.Out()
	defer .subscriber.Close()
	defer .emitReachabilityChanged.Close()

	// Fallback timer to update address in case EvtLocalAddressesUpdated is not emitted.
	// TODO: The event not emitting properly is a bug. This is a workaround.
	 := time.NewTicker(30 * time.Minute)
	defer .Stop()

	 := time.NewTimer()
	defer .Stop()
	 := true
	 := false
	for {
		select {
		case  := <-.inboundConn:
			 := .host.Addrs()
			if manet.IsPublicAddr(.RemoteMultiaddr()) &&
				!ipInList(.RemoteMultiaddr(), ) {
				.lastInbound = time.Now()
			}
		case <-.C:
			// schedule a new probe if addresses have changed
		case  := <-:
			switch e := .(type) {
			case event.EvtPeerIdentificationCompleted:
				if ,  := .host.Peerstore().SupportsProtocols(.Peer, AutoNATProto);  == nil && len() > 0 {
					 = true
				}
			case event.EvtLocalAddressesUpdated:
				// schedule a new probe if addresses have changed
			default:
				log.Errorf("unknown event type: %T", )
			}
		case  := <-.observations:
			.recordObservation()
			continue
		case ,  := <-.dialResponses:
			if ! {
				return
			}
			.pendingProbes--
			if IsDialRefused() {
				 = true
			} else {
				.handleDialResponse()
			}
		case <-.C:
			 = false
			 = false
			// Update the last probe time. We use it to ensure
			// that we don't spam the peerstore.
			.lastProbe = time.Now()
			 := .getPeerToProbe()
			.tryProbe()
		case <-.ctx.Done():
			return
		}
		// On address update, reduce confidence from maximum so that we schedule
		// the next probe sooner
		 := .checkAddrs()
		if  && .confidence == maxConfidence {
			.confidence--
		}

		if  && !.Stop() {
			<-.C
		}
		.Reset(.scheduleProbe())
		 = true
	}
}

func ( *AmbientAutoNAT) () ( bool) {
	 := .addressFunc()
	 = slices.ContainsFunc(, func( ma.Multiaddr) bool {
		,  := .ourAddrs[string(.Bytes())]
		return !
	})
	clear(.ourAddrs)
	for ,  := range  {
		if !manet.IsPublicAddr() {
			continue
		}
		.ourAddrs[string(.Bytes())] = struct{}{}
	}
	return 
}

// scheduleProbe calculates when the next probe should be scheduled for.
func ( *AmbientAutoNAT) ( bool) time.Duration {
	 := time.Now()
	 := *.status.Load()
	 := .config.refreshInterval
	 := .lastInbound.After(.lastProbe)
	switch {
	case  &&  == network.ReachabilityUnknown:
		// retry very quicky if forceProbe is true *and* we don't know our reachability
		// limit all peers fetch from peerstore to 1 per second.
		 = 2 * time.Second
	case  == network.ReachabilityUnknown,
		.confidence < maxConfidence,
		 != network.ReachabilityPublic && :
		// Retry quickly in case:
		// 1. Our reachability is Unknown
		// 2. We don't have enough confidence in our reachability.
		// 3. We're private but we received an inbound connection.
		 = .config.retryInterval
	case  == network.ReachabilityPublic && :
		// We are public and we received an inbound connection recently,
		// wait a little longer
		 *= 2
		 = min(, maxRefreshInterval)
	}
	 := .lastProbe.Add()
	if .Before() {
		 = 
	}
	if .metricsTracer != nil {
		.metricsTracer.NextProbeTime()
	}

	return .Sub()
}

// handleDialResponse updates the current status based on dial response.
func ( *AmbientAutoNAT) ( error) {
	var  network.Reachability
	switch {
	case  == nil:
		 = network.ReachabilityPublic
	case IsDialError():
		 = network.ReachabilityPrivate
	default:
		 = network.ReachabilityUnknown
	}

	.recordObservation()
}

// recordObservation updates NAT status and confidence
func ( *AmbientAutoNAT) ( network.Reachability) {

	 := *.status.Load()

	if  == network.ReachabilityPublic {
		 := false
		if  != network.ReachabilityPublic {
			// Aggressively switch to public from other states ignoring confidence
			log.Debugf("NAT status is public")

			// we are flipping our NATStatus, so confidence drops to 0
			.confidence = 0
			if .service != nil {
				.service.Enable()
			}
			 = true
		} else if .confidence < maxConfidence {
			.confidence++
		}
		.status.Store(&)
		if  {
			.emitStatus()
		}
	} else if  == network.ReachabilityPrivate {
		if  != network.ReachabilityPrivate {
			if .confidence > 0 {
				.confidence--
			} else {
				log.Debugf("NAT status is private")

				// we are flipping our NATStatus, so confidence drops to 0
				.confidence = 0
				.status.Store(&)
				if .service != nil {
					.service.Disable()
				}
				.emitStatus()
			}
		} else if .confidence < maxConfidence {
			.confidence++
			.status.Store(&)
		}
	} else if .confidence > 0 {
		// don't just flip to unknown, reduce confidence first
		.confidence--
	} else {
		log.Debugf("NAT status is unknown")
		.status.Store(&)
		if  != network.ReachabilityUnknown {
			if .service != nil {
				.service.Enable()
			}
			.emitStatus()
		}
	}
	if .metricsTracer != nil {
		.metricsTracer.ReachabilityStatusConfidence(.confidence)
	}
}

func ( *AmbientAutoNAT) ( peer.ID) {
	if  == "" || .pendingProbes > 5 {
		return
	}
	 := .host.Peerstore().PeerInfo()
	.recentProbes[] = time.Now()
	.pendingProbes++
	go .probe(&)
}

func ( *AmbientAutoNAT) ( *peer.AddrInfo) {
	 := NewAutoNATClient(.host, .config.addressFunc, .metricsTracer)
	,  := context.WithTimeout(.ctx, .config.requestTimeout)
	defer ()

	 := .DialBack(, .ID)
	log.Debugf("Dialback through peer %s completed: err: %s", .ID, )

	select {
	case .dialResponses <- :
	case <-.ctx.Done():
		return
	}
}

func ( *AmbientAutoNAT) () peer.ID {
	 := .host.Network().Peers()
	if len() == 0 {
		return ""
	}

	// clean old probes
	 := time.Now()
	for ,  := range .recentProbes {
		if .Sub() > .throttlePeerPeriod {
			delete(.recentProbes, )
		}
	}

	// Shuffle peers
	for  := len();  > 0; -- {
		 := rand.Intn()
		[-1], [] = [], [-1]
	}

	for ,  := range  {
		 := .host.Peerstore().PeerInfo()
		// Exclude peers which don't support the autonat protocol.
		if ,  := .host.Peerstore().SupportsProtocols(, AutoNATProto); len() == 0 ||  != nil {
			continue
		}

		if .config.dialPolicy.skipPeer(.Addrs) {
			continue
		}
		return 
	}

	return ""
}

func ( *AmbientAutoNAT) () error {
	.ctxCancel()
	if .service != nil {
		return .service.Close()
	}
	<-.backgroundRunning
	return nil
}

// Status returns the AutoNAT observed reachability status.
func ( *StaticAutoNAT) () network.Reachability {
	return .reachability
}

func ( *StaticAutoNAT) () error {
	if .service != nil {
		return .service.Close()
	}
	return nil
}