package netreport

import (
	
	
	
	
	
	
	

	itls 
	quic 
	
	
)

// IfStateDetails describes the host's interface capabilities, used to decide
// when enough probes have completed. It is a port of net_report's
// IfStateDetails (iroh/src/net_report/reportgen.rs:74).
type IfStateDetails struct {
	// HaveV4 reports whether the host has IPv4 connectivity.
	HaveV4 bool
	// HaveV6 reports whether the host has IPv6 connectivity.
	HaveV6 bool
}

// Client runs net_report probes against a set of relays and tracks report
// history so it can apply preferred-relay hysteresis across runs. It is a port
// of net_report::Client (iroh/src/net_report.rs:90).
//
// A Client is safe for concurrent use; GetReport serializes internally.
//
// The zero value is not usable; construct a Client with [NewClient].
type Client struct {
	relayMap *relay.Map

	// dnsResolver resolves relay hostnames for HTTPS and QAD probes. If nil,
	// net.DefaultResolver is used.
	dnsResolver *net.Resolver
	// tlsConfig overrides TLS verification for HTTPS probes (used in tests).
	tlsConfig *tls.Config
	// qadTLS supplies the QAD QUIC TLS verification policy. If nil, the relay's
	// WebPKI certificate is verified against the system roots. Tests set a
	// config with InsecureSkipVerify to trust a self-signed relay.
	qadTLS *itls.Config
	// quicConfig overrides QAD transport defaults; if nil, defaultQADConfig is
	// used.
	quicConfig *quic.Config
	// qadDialer, if non-nil, opens QAD probe connections; see WithQADDialer.
	qadDialer QADDialer
	// now returns the current time; overridable in tests for deterministic
	// hysteresis-window pruning.
	now func() time.Time

	mu       sync.Mutex
	last     *Report               // the most recent report
	prev     map[time.Time]*Report // reports within reportHistoryMaxAge
	lastFull time.Time             // when the last full report was generated
}

// NewClient returns a Client that probes the relays in relayMap.
func ( *relay.Map) *Client {
	if  == nil {
		 = relay.NewMap()
	}
	return &Client{
		relayMap: ,
		now:      time.Now,
		prev:     map[time.Time]*Report{},
	}
}

// WithDNSResolver sets the resolver used to look up relay hostnames.
func ( *Client) ( *net.Resolver) *Client {
	.dnsResolver = 
	return 
}

// WithTLSConfig sets the TLS configuration used for HTTPS probes. It is used in
// tests to trust self-signed relay certificates.
func ( *Client) ( *tls.Config) *Client {
	.tlsConfig = 
	return 
}

// WithQUICConfig sets the QAD QUIC transport configuration.
func ( *Client) ( *quic.Config) *Client {
	.quicConfig = 
	return 
}

// WithQADTLSConfig sets the TLS verification policy for QAD QUIC connections.
// It is used in tests to trust a self-signed relay certificate.
func ( *Client) ( *itls.Config) *Client {
	.qadTLS = 
	return 
}

// QADDialer opens the QUIC connection for one QAD probe to addr; tlsConf
// already carries the QAD ALPN and server name.
type QADDialer func(ctx context.Context, addr netip.AddrPort, tlsConf *itls.Config, cfg *quic.Config) (*quic.Conn, error)

// WithQADDialer routes QAD probes through d instead of a private per-probe
// UDP socket, so the observed address is the mapping of the dialer's own
// socket. A per-probe socket's mapping dies with it and its port is nobody's
// dial candidate; per-probe mappings also differ between relays, which makes
// MappingVariesByDest misreport symmetric NAT.
func ( *Client) ( QADDialer) *Client {
	.qadDialer = 
	return 
}

// dialQAD opens the QAD connection for a probe to addr, via the configured
// dialer or a private per-probe socket.
func ( *Client) ( context.Context,  netip.AddrPort,  string) (*qadConn, error) {
	if .qadDialer == nil {
		return newQADClient(, , .qadTLS, .quicConfig)
	}
	 := .quicConfig
	if  == nil {
		 = defaultQADConfig()
	}
	,  := context.WithTimeout(, probesTimeout)
	defer ()
	,  := .qadDialer(, , qadTLSConfig(, .qadTLS), )
	if  != nil {
		return nil, 
	}
	// ownsTransport stays false: the dialer's transport outlives the probe.
	return &qadConn{conn: }, nil
}

// GetReport runs a single net_report. doFull forces a full report (captive
// portal check and reset probe history); otherwise the report is full only if
// no report has been generated within [fullReportInterval]. ifState informs the
// sufficiency check.
//
// The whole call is bounded by [overallReportTimeout]; the probes within it are
// bounded by [probesTimeout].
//
// If the relay map is empty the report is empty: no probes run and
// PreferredRelay is the zero value.
func ( *Client) ( context.Context,  IfStateDetails,  bool) (*Report, error) {
	,  := context.WithTimeout(, overallReportTimeout)
	defer ()

	.mu.Lock()
	 :=  || .last == nil || .now().Sub(.lastFull) >= fullReportInterval
	 := .relayMap
	.mu.Unlock()

	 := &Report{Full: }

	if !.IsEmpty() {
		.runProbes(, , )

		// The captive-portal check runs only on full reports, and only after a
		// short delay so good QAD probes finish first
		// (iroh/src/net_report/reportgen.rs:278).
		if  {
			.runCaptivePortal(, , )
		}
	}

	.addReportHistoryAndSetPreferredRelay()

	.mu.Lock()
	if  {
		.lastFull = .now()
	}
	.mu.Unlock()

	return , .Err()
}

// runProbes runs the HTTPS and QAD probes for each relay in parallel, bounded
// by probesTimeout, and folds the results into report. QAD probes run on at
// most maxRelays relays.
func ( *Client) ( context.Context,  *relay.Map,  *Report) {
	,  := context.WithTimeout(, probesTimeout)
	defer ()

	 := .Configs()

	var (
		      sync.WaitGroup
		      sync.Mutex
		 []*probeReport
	)
	 := func( *probeReport) {
		if  == nil {
			return
		}
		.Lock()
		 = append(, )
		.Unlock()
	}

	 := 0
	for ,  := range  {
		 := 
		// HTTPS probe for every relay.
		.Add(1)
		go func() {
			defer .Done()
			,  := runHTTPSProbe(, .URL, .tlsConfig)
			if  == nil {
				()
			}
		}()

		// QAD probes (v4 and v6) for up to maxRelays relays that enable QUIC.
		if .QUIC != nil &&  < maxRelays {
			++
			.Add(2)
			go func() {
				defer .Done()
				(.runQADProbe(, , ProbeQADv4))
			}()
			go func() {
				defer .Done()
				(.runQADProbe(, , ProbeQADv6))
			}()
		}
	}

	 := make(chan struct{})
	go func() { .Wait(); close() }()
	select {
	case <-:
	case <-.Done():
	}

	// Fold results in a stable order so a report is deterministic given the
	// same probe outcomes.
	.Lock()
	sort.Slice(, func(,  int) bool {
		if [].probe != [].probe {
			return [].probe < [].probe
		}
		return [].relay.Compare([].relay) < 0
	})
	for ,  := range  {
		.update()
	}
	.Unlock()
}

// runQADProbe resolves the relay's QUIC address for the given family, opens a
// QAD connection, records its RTT, captures any observed-address report that has
// already arrived, and gracefully closes it. If no report is available yet, the
// probe is latency-only (see the package doc).
func ( *Client) ( context.Context,  relay.Config,  Probe) *probeReport {
	 := .URL.Host()
	if  == "" {
		return nil
	}
	 := defaultRelayQuicPort
	if .QUIC != nil && .QUIC.Port != 0 {
		 = int(.QUIC.Port)
	}

	,  := .resolveQADAddr(, , , )
	if ! {
		return nil
	}

	,  := .dialQAD(, , )
	if  != nil {
		return nil
	}
	defer .close(qadCloseCode, qadCloseReason)

	// Read the connection RTT (iroh-relay/src/quic.rs:345) and the relay's
	// observed-address report, which observedAddr waits briefly for because it
	// is sent just after the handshake this dial already completed. It returns
	// ErrExtensionNotNegotiated when the relay does not report or none arrives
	// in time, in which case the probe is latency-only.
	 := .rtt(0)
	,  := .observedAddr()
	 := &probeReport{probe: , relay: .URL, latency: }
	if  == nil {
		.addr = 
	}
	return 
}

// resolveQADAddr resolves host to an address of the family implied by probe and
// returns it with port. It returns ok=false if no matching address is found.
func ( *Client) ( context.Context,  string,  int,  Probe) (netip.AddrPort, bool) {
	if ,  := netip.ParseAddr();  == nil {
		if !addrMatchesProbe(, ) {
			return netip.AddrPort{}, false
		}
		return netip.AddrPortFrom(, uint16()), true
	}
	,  := lookupIPStaggered(, .dnsResolver, )
	if  != nil {
		return netip.AddrPort{}, false
	}
	for ,  := range  {
		,  := netip.AddrFromSlice(.IP)
		if ! {
			continue
		}
		 = .Unmap()
		if addrMatchesProbe(, ) {
			return netip.AddrPortFrom(, uint16()), true
		}
	}
	return netip.AddrPort{}, false
}

func addrMatchesProbe( netip.Addr,  Probe) bool {
	switch  {
	case ProbeQADv4:
		return .Is4()
	case ProbeQADv6:
		return .Is6() && !.Is4In6()
	default:
		return false
	}
}

// runCaptivePortal runs the captive-portal check against one relay after a
// short delay, bounded by captivePortalTimeout, and records the result in
// report.CaptivePortal. iroh/src/net_report/reportgen.rs:614.
func ( *Client) ( context.Context,  *relay.Map,  *Report) {
	select {
	case <-time.After(captivePortalDelay):
	case <-.Done():
		return
	}

	 := .URLs()
	if len() == 0 {
		return
	}

	,  := context.WithTimeout(, captivePortalTimeout)
	defer ()

	,  := checkCaptivePortal(, [0], .tlsConfig)
	if  != nil {
		return
	}
	.CaptivePortal = boolPtr()
}

// addReportHistoryAndSetPreferredRelay adds r to the recent-report history,
// prunes reports older than reportHistoryMaxAge, and sets r.PreferredRelay to
// the relay with the best recent latency, applying hysteresis. It is a port of
// add_report_history_and_set_preferred_relay (iroh/src/net_report.rs:698).
func ( *Client) ( *Report) {
	.mu.Lock()
	defer .mu.Unlock()

	var  netaddr.RelayURL
	if .last != nil {
		 = .last.PreferredRelay
		// Carry forward mapping-varies info when this report lacks it.
		if .MappingVariesByDestV4 == nil {
			.MappingVariesByDestV4 = .last.MappingVariesByDestV4
		}
		if .MappingVariesByDestV6 == nil {
			.MappingVariesByDestV6 = .last.MappingVariesByDestV6
		}
	}

	 := .now()

	// Best recent latency per relay across the history window and this report.
	var  RelayLatencies
	for ,  := range .prev {
		if .Sub() > reportHistoryMaxAge {
			delete(.prev, )
			continue
		}
		.merge(&.RelayLatency)
	}
	.merge(&.RelayLatency)

	// Pick the currently-alive relay with the best recent latency, recording
	// the old preferred relay's current latency for the hysteresis check.
	var  time.Duration
	var  time.Duration

	// Iterate this report's relays in a deterministic order.
	 := .RelayLatency.relays()
	sort.Slice(, func(,  int) bool { return [].Compare([]) < 0 })

	 := false
	for ,  := range  {
		,  := .RelayLatency.get()
		if !.IsZero() && .Equal() {
			 = 
		}
		,  := .get()
		if  && (! ||  < ) {
			 = 
			.PreferredRelay = 
			 = true
		}
	}

	// Hysteresis: if we changed away from a still-responsive old relay but the
	// new one is not at least 1/3 faster, stick with the old one.
	// iroh/src/net_report.rs:760.
	if !.IsZero() &&
		!.PreferredRelay.Equal() &&
		 != 0 &&
		 > /3*2 {
		.PreferredRelay = 
	}

	 := *
	.prev[] = &
	 := *
	.last = &
}