package basichost

import (
	
	
	
	
	
	
	
	

	
	
	
	ma 
	manet 
)

type autonatv2Client interface {
	GetReachability(ctx context.Context, reqs []autonatv2.Request) (autonatv2.Result, error)
}

const (

	// maxAddrsPerRequest is the maximum number of addresses to probe in a single request
	maxAddrsPerRequest = 10
	// maxTrackedAddrs is the maximum number of addresses to track
	// 10 addrs per transport for 5 transports
	maxTrackedAddrs = 50
	// defaultMaxConcurrency is the default number of concurrent workers for reachability checks
	defaultMaxConcurrency = 5
	// newAddrsProbeDelay is the delay before probing new addr's reachability.
	newAddrsProbeDelay = 1 * time.Second
)

// addrsReachabilityTracker tracks reachability for addresses.
// Use UpdateAddrs to provide addresses for tracking reachability.
// reachabilityUpdateCh is notified when reachability for any of the tracked address changes.
type addrsReachabilityTracker struct {
	ctx    context.Context
	cancel context.CancelFunc
	wg     sync.WaitGroup

	client autonatv2Client
	// reachabilityUpdateCh is used to notify when reachability may have changed
	reachabilityUpdateCh chan struct{}
	maxConcurrency       int
	newAddrsProbeDelay   time.Duration
	probeManager         *probeManager
	newAddrs             chan []ma.Multiaddr
	clock                clock.Clock
	metricsTracker       MetricsTracker

	mx               sync.Mutex
	reachableAddrs   []ma.Multiaddr
	unreachableAddrs []ma.Multiaddr
	unknownAddrs     []ma.Multiaddr
}

// newAddrsReachabilityTracker returns a new addrsReachabilityTracker.
// reachabilityUpdateCh is notified when reachability for any of the tracked address changes.
func newAddrsReachabilityTracker( autonatv2Client,  chan struct{},  clock.Clock,  MetricsTracker) *addrsReachabilityTracker {
	,  := context.WithCancel(context.Background())
	if  == nil {
		 = clock.New()
	}
	return &addrsReachabilityTracker{
		ctx:                  ,
		cancel:               ,
		client:               ,
		reachabilityUpdateCh: ,
		probeManager:         newProbeManager(.Now),
		newAddrsProbeDelay:   newAddrsProbeDelay,
		maxConcurrency:       defaultMaxConcurrency,
		newAddrs:             make(chan []ma.Multiaddr, 1),
		clock:                ,
		metricsTracker:       ,
	}
}

func ( *addrsReachabilityTracker) ( []ma.Multiaddr) {
	select {
	case .newAddrs <- slices.Clone():
	case <-.ctx.Done():
	}
}

func ( *addrsReachabilityTracker) () (, ,  []ma.Multiaddr) {
	.mx.Lock()
	defer .mx.Unlock()
	return slices.Clone(.reachableAddrs), slices.Clone(.unreachableAddrs), slices.Clone(.unknownAddrs)
}

func ( *addrsReachabilityTracker) () error {
	.wg.Add(1)
	go .background()
	return nil
}

func ( *addrsReachabilityTracker) () error {
	.cancel()
	.wg.Wait()
	return nil
}

const (
	// defaultReachabilityRefreshInterval is the default interval to refresh reachability.
	// In steady state, we check for any required probes every refresh interval.
	// This doesn't mean we'll probe for any particular address, only that we'll check
	// if any address needs to be probed.
	defaultReachabilityRefreshInterval = 5 * time.Minute
	// maxBackoffInterval is the maximum back off in case we're unable to probe for reachability.
	// We may be unable to confirm addresses in case there are no valid peers with autonatv2
	// or the autonatv2 subsystem is consistently erroring.
	maxBackoffInterval = 5 * time.Minute
	// backoffStartInterval is the initial back off in case we're unable to probe for reachability.
	backoffStartInterval = 5 * time.Second
)

func ( *addrsReachabilityTracker) () {
	defer .wg.Done()

	// probeTicker is used to trigger probes at regular intervals
	 := .clock.Ticker(defaultReachabilityRefreshInterval)
	defer .Stop()

	// probeTimer is used to trigger probes at specific times
	 := .clock.Timer(time.Duration(math.MaxInt64))
	defer .Stop()
	 := time.Time{}

	var  reachabilityTask
	var  time.Duration
	var , , , , ,  []ma.Multiaddr
	for {
		select {
		case <-.C:
			// don't start a probe if we have a scheduled probe
			if .BackoffCh == nil && .IsZero() {
				 = .refreshReachability()
			}
		case <-.C:
			if .BackoffCh == nil {
				 = .refreshReachability()
			}
			 = time.Time{}
		case  := <-.BackoffCh:
			 = reachabilityTask{}
			// On completion, start the next probe immediately, or wait for backoff.
			// In case there are no further probes, the reachability tracker will return an empty task,
			// which hangs forever. Eventually, we'll refresh again when the ticker fires.
			if  {
				 = newBackoffInterval()
			} else {
				 = -1 * time.Second // negative to trigger next probe immediately
			}
			 = .clock.Now().Add()
		case  := <-.newAddrs:
			if .BackoffCh != nil { // cancel running task.
				.Cancel()
				<-.BackoffCh // ignore backoff from cancelled task
				 = reachabilityTask{}
			}
			.updateTrackedAddrs()
			 := .clock.Now().Add(.newAddrsProbeDelay)
			if .Before() {
				 = 
			}
		case <-.ctx.Done():
			if .BackoffCh != nil {
				.Cancel()
				<-.BackoffCh
				 = reachabilityTask{}
			}
			if .metricsTracker != nil {
				.metricsTracker.ReachabilityTrackerClosed()
			}
			return
		}

		, ,  = .appendConfirmedAddrs([:0], [:0], [:0])
		if areAddrsDifferent(, ) || areAddrsDifferent(, ) || areAddrsDifferent(, ) {
			if .metricsTracker != nil {
				.metricsTracker.ConfirmedAddrsChanged(, , )
			}
			.notify()
		}
		 = append([:0], ...)
		 = append([:0], ...)
		 = append([:0], ...)
		if !.IsZero() {
			.Reset(.Sub(.clock.Now()))
		}
	}
}

func newBackoffInterval( time.Duration) time.Duration {
	if  <= 0 {
		return backoffStartInterval
	}
	 *= 2
	if  > maxBackoffInterval {
		return maxBackoffInterval
	}
	return 
}

func ( *addrsReachabilityTracker) (, ,  []ma.Multiaddr) (, ,  []ma.Multiaddr) {
	, ,  = .probeManager.AppendConfirmedAddrs(, , )
	.mx.Lock()
	.reachableAddrs = append(.reachableAddrs[:0], ...)
	.unreachableAddrs = append(.unreachableAddrs[:0], ...)
	.unknownAddrs = append(.unknownAddrs[:0], ...)
	.mx.Unlock()

	return , , 
}

func ( *addrsReachabilityTracker) () {
	select {
	case .reachabilityUpdateCh <- struct{}{}:
	default:
	}
}

func ( *addrsReachabilityTracker) ( []ma.Multiaddr) {
	 = slices.DeleteFunc(, func( ma.Multiaddr) bool {
		return !manet.IsPublicAddr()
	})
	if len() > maxTrackedAddrs {
		log.Errorf("too many addresses (%d) for addrs reachability tracker; dropping %d", len(), len()-maxTrackedAddrs)
		 = [:maxTrackedAddrs]
	}
	.probeManager.UpdateAddrs()
}

type probe = []autonatv2.Request

const probeTimeout = 30 * time.Second

// reachabilityTask is a task to refresh reachability.
// Waiting on the zero value blocks forever.
type reachabilityTask struct {
	Cancel context.CancelFunc
	// BackoffCh returns whether the caller should backoff before
	// refreshing reachability
	BackoffCh chan bool
}

func ( *addrsReachabilityTracker) () reachabilityTask {
	if len(.probeManager.GetProbe()) == 0 {
		return reachabilityTask{}
	}
	 := make(chan bool, 1)
	,  := context.WithTimeout(.ctx, 5*time.Minute)
	.wg.Add(1)
	// We run probes provided by addrsTracker. It stops probing when any
	// of the following happens:
	// - there are no more probes to run
	// - context is completed
	// - there are too many consecutive failures from the client
	// - the client has no valid peers to probe
	go func() {
		defer .wg.Done()
		defer ()
		 := &errCountingClient{autonatv2Client: .client, MaxConsecutiveErrors: maxConsecutiveErrors}
		var  atomic.Bool
		var  sync.WaitGroup
		.Add(.maxConcurrency)
		for range .maxConcurrency {
			go func() {
				defer .Done()
				for {
					if .Err() != nil {
						return
					}
					 := .probeManager.GetProbe()
					if len() == 0 {
						return
					}
					.probeManager.MarkProbeInProgress()
					,  := context.WithTimeout(, probeTimeout)
					,  := .GetReachability(, )
					()
					.probeManager.CompleteProbe(, , )
					if isErrorPersistent() {
						.Store(true)
						return
					}
				}
			}()
		}
		.Wait()
		 <- .Load()
	}()
	return reachabilityTask{Cancel: , BackoffCh: }
}

var errTooManyConsecutiveFailures = errors.New("too many consecutive failures")

// errCountingClient counts errors from autonatv2Client and wraps the errors in response with a
// errTooManyConsecutiveFailures in case of persistent failures from autonatv2 module.
type errCountingClient struct {
	autonatv2Client
	MaxConsecutiveErrors int
	mx                   sync.Mutex
	consecutiveErrors    int
}

func ( *errCountingClient) ( context.Context,  probe) (autonatv2.Result, error) {
	,  := .autonatv2Client.GetReachability(, )
	.mx.Lock()
	defer .mx.Unlock()
	if  != nil && !errors.Is(, context.Canceled) { // ignore canceled errors, they're not errors from autonatv2
		.consecutiveErrors++
		if .consecutiveErrors > .MaxConsecutiveErrors {
			 = fmt.Errorf("%w:%w", errTooManyConsecutiveFailures, )
		}
		if errors.Is(, autonatv2.ErrPrivateAddrs) {
			log.Errorf("private IP addr in autonatv2 request: %s", )
		}
	} else {
		.consecutiveErrors = 0
	}
	return , 
}

const maxConsecutiveErrors = 20

// isErrorPersistent returns whether the error will repeat on future probes for a while
func isErrorPersistent( error) bool {
	if  == nil {
		return false
	}
	return errors.Is(, autonatv2.ErrPrivateAddrs) || errors.Is(, autonatv2.ErrNoPeers) ||
		errors.Is(, errTooManyConsecutiveFailures)
}

const (
	// recentProbeInterval is the interval to probe addresses that have been refused
	// these are generally addresses with newer transports for which we don't have many peers
	// capable of dialing the transport
	recentProbeInterval = 10 * time.Minute
	// maxConsecutiveRefusals is the maximum number of consecutive refusals for an address after which
	// we wait for `recentProbeInterval` before probing again
	maxConsecutiveRefusals = 5
	// maxRecentDialsPerAddr is the maximum number of dials on an address before we stop probing for the address.
	// This is used to prevent infinite probing of an address whose status is indeterminate for any reason.
	maxRecentDialsPerAddr = 10
	// confidence is the absolute difference between the number of successes and failures for an address
	// targetConfidence is the confidence threshold for an address after which we wait for `maxProbeInterval`
	// before probing again.
	targetConfidence = 3
	// minConfidence is the confidence threshold for an address to be considered reachable or unreachable
	// confidence is the absolute difference between the number of successes and failures for an address
	minConfidence = 2
	// maxRecentDialsWindow is the maximum number of recent probe results to consider for a single address
	//
	// +2 allows for 1 invalid probe result. Consider a string of successes, after which we have a single failure
	// and then a success(...S S S S F S). The confidence in the targetConfidence window  will be equal to
	// targetConfidence, the last F and S cancel each other, and we won't probe again for maxProbeInterval.
	maxRecentDialsWindow = targetConfidence + 2
	// highConfidenceAddrProbeInterval is the maximum interval between probes for an address
	highConfidenceAddrProbeInterval = 1 * time.Hour
	// maxProbeResultTTL is the maximum time to keep probe results for an address
	maxProbeResultTTL = maxRecentDialsWindow * highConfidenceAddrProbeInterval
)

// probeManager tracks reachability for a set of addresses by periodically probing reachability with autonatv2.
// A Probe is a list of addresses which can be tested for reachability with autonatv2.
// This struct decides the priority order of addresses for testing reachability, and throttles in case there have
// been too many probes for an address in the `ProbeInterval`.
//
// Use the `runProbes` function to execute the probes with an autonatv2 client.
type probeManager struct {
	now func() time.Time

	mx                    sync.Mutex
	inProgressProbes      map[string]int // addr -> count
	inProgressProbesTotal int
	statuses              map[string]*addrStatus
	addrs                 []ma.Multiaddr
}

// newProbeManager creates a new probe manager.
func newProbeManager( func() time.Time) *probeManager {
	return &probeManager{
		statuses:         make(map[string]*addrStatus),
		inProgressProbes: make(map[string]int),
		now:              ,
	}
}

// AppendConfirmedAddrs appends the current confirmed reachable and unreachable addresses.
func ( *probeManager) (, ,  []ma.Multiaddr) (, ,  []ma.Multiaddr) {
	.mx.Lock()
	defer .mx.Unlock()

	for ,  := range .addrs {
		 := .statuses[string(.Bytes())]
		.RemoveBefore(.now().Add(-maxProbeResultTTL)) // cleanup stale results
		switch .Reachability() {
		case network.ReachabilityPublic:
			 = append(, )
		case network.ReachabilityPrivate:
			 = append(, )
		case network.ReachabilityUnknown:
			 = append(, )
		}
	}
	return , , 
}

// UpdateAddrs updates the tracked addrs
func ( *probeManager) ( []ma.Multiaddr) {
	.mx.Lock()
	defer .mx.Unlock()

	slices.SortFunc(, func(,  ma.Multiaddr) int { return .Compare() })
	 := make(map[string]*addrStatus, len())
	for ,  := range  {
		 := string(.Bytes())
		if ,  := .statuses[]; ! {
			[] = &addrStatus{Addr: }
		} else {
			[] = .statuses[]
		}
	}
	.addrs = 
	.statuses = 
}

// GetProbe returns the next probe. Returns zero value in case there are no more probes.
// Probes that are run against an autonatv2 client should be marked in progress with
// `MarkProbeInProgress` before running.
func ( *probeManager) () probe {
	.mx.Lock()
	defer .mx.Unlock()

	 := .now()
	for ,  := range .addrs {
		 := .Bytes()
		 := .statuses[string()].RequiredProbeCount()
		if .inProgressProbes[string()] >=  {
			continue
		}
		 := make(probe, 0, maxAddrsPerRequest)
		 = append(, autonatv2.Request{Addr: , SendDialData: true})
		// We have the first(primary) address. Append other addresses, ignoring inprogress probes
		// on secondary addresses. The expectation is that the primary address will
		// be dialed.
		for  := 1;  < len(.addrs); ++ {
			 := ( + ) % len(.addrs)
			 := .addrs[].Bytes()
			 := .statuses[string()].RequiredProbeCount()
			if  == 0 {
				continue
			}
			 = append(, autonatv2.Request{Addr: .addrs[], SendDialData: true})
			if len() >= maxAddrsPerRequest {
				break
			}
		}
		return 
	}
	return nil
}

// MarkProbeInProgress should be called when a probe is started.
// All in progress probes *MUST* be completed with `CompleteProbe`
func ( *probeManager) ( probe) {
	if len() == 0 {
		return
	}
	.mx.Lock()
	defer .mx.Unlock()
	.inProgressProbes[string([0].Addr.Bytes())]++
	.inProgressProbesTotal++
}

// InProgressProbes returns the number of probes that are currently in progress.
func ( *probeManager) () int {
	.mx.Lock()
	defer .mx.Unlock()
	return .inProgressProbesTotal
}

// CompleteProbe should be called when a probe completes.
func ( *probeManager) ( probe,  autonatv2.Result,  error) {
	 := .now()

	if len() == 0 {
		// should never happen
		return
	}

	.mx.Lock()
	defer .mx.Unlock()

	// decrement in-progress count for the first address
	 := string([0].Addr.Bytes())
	.inProgressProbes[]--
	if .inProgressProbes[] <= 0 {
		delete(.inProgressProbes, )
	}
	.inProgressProbesTotal--

	// nothing to do if the request errored.
	if  != nil {
		return
	}

	// Consider only primary address as refused. This increases the number of
	// refused probes, but refused probes are cheap for a server as no dials are made.
	if .AllAddrsRefused {
		if ,  := .statuses[];  {
			.AddRefusal()
		}
		return
	}
	 := string(.Addr.Bytes())
	if  !=  {
		if ,  := .statuses[];  {
			.AddRefusal()
		}
	}

	// record the result for the dialed address
	if ,  := .statuses[];  {
		.AddOutcome(, .Reachability, maxRecentDialsWindow)
	}
}

type dialOutcome struct {
	Success bool
	At      time.Time
}

type addrStatus struct {
	Addr                ma.Multiaddr
	lastRefusalTime     time.Time
	consecutiveRefusals int
	dialTimes           []time.Time
	outcomes            []dialOutcome
}

func ( *addrStatus) () network.Reachability {
	, ,  := .reachabilityAndCounts()
	return 
}

func ( *addrStatus) ( time.Time) int {
	if .consecutiveRefusals >= maxConsecutiveRefusals {
		if .Sub(.lastRefusalTime) < recentProbeInterval {
			return 0
		}
		// reset every `recentProbeInterval`
		.lastRefusalTime = time.Time{}
		.consecutiveRefusals = 0
	}

	// Don't probe if we have probed too many times recently
	 := .recentDialCount()
	if  >= maxRecentDialsPerAddr {
		return 0
	}

	return .requiredProbeCountForConfirmation()
}

func ( *addrStatus) ( time.Time) int {
	, ,  := .reachabilityAndCounts()
	 :=  - 
	if  < 0 {
		 = -
	}
	 := targetConfidence - 
	if  > 0 {
		return 
	}
	// we have enough confirmations; check if we should refresh

	// Should never happen. The confidence logic above should require a few probes.
	if len(.outcomes) == 0 {
		return 0
	}
	 := .outcomes[len(.outcomes)-1]
	// If the last probe result is old, we need to retest
	if .Sub(.At) > highConfidenceAddrProbeInterval {
		return 1
	}
	// if the last probe result was different from reachability, probe again.
	switch  {
	case network.ReachabilityPublic:
		if !.Success {
			return 1
		}
	case network.ReachabilityPrivate:
		if .Success {
			return 1
		}
	default:
		// this should never happen
		return 1
	}
	return 0
}

func ( *addrStatus) ( time.Time) {
	.lastRefusalTime = 
	.consecutiveRefusals++
}

func ( *addrStatus) ( time.Time,  network.Reachability,  int) {
	.lastRefusalTime = time.Time{}
	.consecutiveRefusals = 0

	.dialTimes = append(.dialTimes, )
	for ,  := range .dialTimes {
		if .Sub() < recentProbeInterval {
			.dialTimes = slices.Delete(.dialTimes, 0, )
			break
		}
	}

	.RemoveBefore(.Add(-maxProbeResultTTL)) // remove old outcomes
	 := false
	switch  {
	case network.ReachabilityPublic:
		 = true
	case network.ReachabilityPrivate:
		 = false
	default:
		return // don't store the outcome if reachability is unknown
	}
	.outcomes = append(.outcomes, dialOutcome{At: , Success: })
	if len(.outcomes) >  {
		.outcomes = slices.Delete(.outcomes, 0, len(.outcomes)-)
	}
}

// RemoveBefore removes outcomes before t
func ( *addrStatus) ( time.Time) {
	 := 0
	for ;  < len(.outcomes); ++ {
		if !.outcomes[].At.Before() {
			break
		}
	}
	.outcomes = slices.Delete(.outcomes, 0, )
}

func ( *addrStatus) ( time.Time) int {
	 := 0
	for ,  := range slices.Backward(.dialTimes) {
		if .Sub() > recentProbeInterval {
			break
		}
		++
	}
	return 
}

func ( *addrStatus) () ( network.Reachability,  int,  int) {
	for ,  := range .outcomes {
		if .Success {
			++
		} else {
			++
		}
	}
	if - >= minConfidence {
		return network.ReachabilityPublic, , 
	}
	if - >= minConfidence {
		return network.ReachabilityPrivate, , 
	}
	return network.ReachabilityUnknown, , 
}