package relay

import (
	
	
	
	
	
	
	

	
)

// A Prober measures the connect latency to a single relay. It returns the
// round-trip establishment time and nil on success, or a non-nil error if the
// relay could not be reached within ctx.
//
// Prober is the seam for latency-aware relay selection: [RankByLatency] and
// [Map.Nearest] call it once per candidate relay, concurrently. The default
// implementation is [HTTPConnectProber]; tests inject a deterministic Prober.
type Prober func(ctx context.Context, url netaddr.RelayURL) (time.Duration, error)

// RelayLatency pairs a relay URL with its measured connect latency.
//
// Err is non-nil when the relay could not be probed; in that case Latency is
// not meaningful. [RankByLatency] sorts reachable relays (Err == nil) ahead of
// unreachable ones.
type RelayLatency struct {
	URL     netaddr.RelayURL
	Latency time.Duration
	Err     error
}

// ErrNoRelays is returned by [Map.Nearest] when the map has no relays or none
// could be reached.
var ErrNoRelays = errors.New("relay: no reachable relay in map")

// defaultProbeTimeout bounds a single probe when the caller's context has no
// deadline. It is generous enough for a trans-oceanic TLS handshake yet short
// enough that one dead relay does not stall selection.
const defaultProbeTimeout = 3 * time.Second

// RankByLatency probes every relay in m using prober, concurrently, and returns
// the results sorted by ascending latency. Reachable relays (Err == nil) sort
// before unreachable ones; ties and unreachable relays are ordered
// deterministically by relay URL so selection is reproducible.
//
// If prober is nil, [HTTPConnectProber] is used. RankByLatency never returns a
// nil slice for a non-empty map: an unreachable relay appears with its Err set.
func ( context.Context,  *Map,  Prober) []RelayLatency {
	if  == nil {
		 = HTTPConnectProber(nil)
	}
	 := .URLs()
	 := make([]RelayLatency, len())
	var  sync.WaitGroup
	for ,  := range  {
		.Add(1)
		go func() {
			defer .Done()
			,  := probeOne(, , )
			[] = RelayLatency{URL: , Latency: , Err: }
		}()
	}
	.Wait()

	slices.SortFunc(, func(,  RelayLatency) int {
		// Reachable relays sort ahead of unreachable ones.
		if (.Err == nil) != (.Err == nil) {
			if .Err == nil {
				return -1
			}
			return 1
		}
		if .Err == nil {
			if  := int(.Latency - .Latency);  != 0 {
				return sign()
			}
		}
		// Deterministic tie-break (and total order for unreachable relays).
		return .URL.Compare(.URL)
	})
	return 
}

// probeOne runs a single probe, applying defaultProbeTimeout when ctx carries no
// deadline so one unresponsive relay cannot stall the whole ranking.
func probeOne( context.Context,  Prober,  netaddr.RelayURL) (time.Duration, error) {
	if ,  := .Deadline(); ! {
		var  context.CancelFunc
		,  = context.WithTimeout(, defaultProbeTimeout)
		defer ()
	}
	return (, )
}

func sign( int) int {
	switch {
	case  < 0:
		return -1
	case  > 0:
		return 1
	default:
		return 0
	}
}

// Nearest probes the relays in m and returns the URL of the lowest-latency
// reachable relay. It returns [ErrNoRelays] if the map is empty or no relay
// could be reached. If prober is nil, [HTTPConnectProber] is used.
//
// Nearest is the seam a ticket minter (e.g. ccl's transfer layer) calls to pick
// a home relay close to the local machine instead of an arbitrary one.
func ( *Map) ( context.Context,  Prober) (netaddr.RelayURL, error) {
	 := RankByLatency(, , )
	if len() == 0 || [0].Err != nil {
		return netaddr.RelayURL{}, ErrNoRelays
	}
	return [0].URL, nil
}

// PreferNearest returns a Map containing only the lowest-latency reachable relay
// in m, using prober (or [HTTPConnectProber] if nil). It is a convenience for
// wiring nearest-relay selection into a [Mode]:
//
//	m, err := relay.DefaultMap().PreferNearest(ctx, nil)
//	if err == nil {
//		mode = relay.ModeCustom(m)
//	}
//
// On error (empty map or all relays unreachable) it returns the error and a nil
// map so the caller can fall back to the full set.
func ( *Map) ( context.Context,  Prober) (*Map, error) {
	,  := .Nearest(, )
	if  != nil {
		return nil, 
	}
	,  := .Get()
	if ! {
		 = Config{URL: }
	}
	return NewMap(), nil
}

// HTTPConnectProber returns a Prober that measures the time to establish a TLS
// connection to a relay's HTTPS endpoint. It reflects the real connect cost a
// relay client pays, which is dominated by round-trip latency to the relay, and
// is cheaper than a full net-report probe.
//
// tlsConfig, if non-nil, overrides the TLS configuration (used in tests to skip
// verification against a local relay). A relay URL without an explicit port uses
// 443.
func ( *tls.Config) Prober {
	return func( context.Context,  netaddr.RelayURL) (time.Duration, error) {
		 := .Host()
		if  == "" {
			return 0, errors.New("relay: probe url has no host")
		}
		 := "443"
		if  := .URL().Port();  != "" {
			 = 
		}
		 := net.JoinHostPort(, )

		 := time.Now()
		var  net.Dialer
		,  := .DialContext(, "tcp", )
		if  != nil {
			return 0, 
		}
		defer .Close()

		 := 
		if  == nil {
			 = &tls.Config{ServerName: }
		}
		 := tls.Client(, )
		if  := .HandshakeContext();  != nil {
			return 0, 
		}
		_ = .Close()
		return time.Since(), nil
	}
}