package netreport

import (
	
	

	
)

// Report describes the network environment as measured by a single
// [Client.GetReport] run. It is a port of iroh's net_report::Report
// (iroh/src/net_report/report.rs:14).
//
// The zero Report is a valid empty report.
type Report struct {
	// Full reports whether this run used the full probe plan.
	Full bool

	// UDPv4 reports whether a QAD IPv4 round trip completed and reported an
	// observed IPv4 address.
	UDPv4 bool
	// UDPv6 reports whether a QAD IPv6 round trip completed and reported an
	// observed IPv6 address.
	UDPv6 bool

	// MappingVariesByDestV4 reports whether the observed public IPv4 address
	// differs across relays, when known.
	MappingVariesByDestV4 *bool
	// MappingVariesByDestV6 reports whether the observed public IPv6 address
	// differs across relays, when known.
	MappingVariesByDestV6 *bool

	// PreferredRelay is the relay with the best recent latency, chosen with
	// hysteresis. It is the zero RelayURL when no relay responded.
	PreferredRelay netaddr.RelayURL

	// RelayLatency holds per-relay, per-probe latencies.
	RelayLatency RelayLatencies

	// GlobalV4 is the host's public IPv4 address as seen by a relay. It is
	// absent when no QAD IPv4 observed-address report arrived.
	GlobalV4 netip.AddrPort
	// GlobalV6 is the host's public IPv6 address as seen by a relay. It is
	// absent when no QAD IPv6 observed-address report arrived.
	GlobalV6 netip.AddrPort

	// CaptivePortal reports whether a captive portal is intercepting HTTP, when
	// the check ran (full reports only).
	CaptivePortal *bool
}

// HasUDP reports whether any QAD round trip succeeded.
func ( *Report) () bool { return .UDPv4 || .UDPv6 }

// probeReport is the result of one probe, fed to [Report.update].
type probeReport struct {
	probe   Probe
	relay   netaddr.RelayURL
	latency time.Duration

	// addr is the reflexive address observed by a QAD probe. It is the zero
	// AddrPort when no observed-address report arrived, in which case the QAD
	// branch records latency only.
	addr netip.AddrPort
}

// update folds a probeReport into the report, mirroring Report::update
// (iroh/src/net_report/report.rs:63).
func ( *Report) ( *probeReport) {
	.RelayLatency.updateRelay(.relay, .latency, .probe)

	switch .probe {
	case ProbeQADv4:
		// Without an observed-address report a QAD probe yields no addr, so we
		// cannot set UDPv4 or GlobalV4. Latency was already recorded above.
		if !.addr.IsValid() {
			return
		}
		 := canonicalAddrPort(.addr)
		if !.Addr().Is4() {
			return // received IPv6 address from IPv4 QAD; ignore.
		}
		.UDPv4 = true
		if .GlobalV4.IsValid() {
			if .GlobalV4 ==  {
				if .MappingVariesByDestV4 == nil {
					.MappingVariesByDestV4 = boolPtr(false)
				}
			} else {
				.MappingVariesByDestV4 = boolPtr(true)
			}
		} else {
			.GlobalV4 = 
		}
	case ProbeQADv6:
		if !.addr.IsValid() {
			return
		}
		 := .addr
		if !.Addr().Is6() || .Addr().Is4In6() {
			return // received IPv4 address from IPv6 QAD; ignore.
		}
		.UDPv6 = true
		if .GlobalV6.IsValid() {
			if .GlobalV6 ==  {
				if .MappingVariesByDestV6 == nil {
					.MappingVariesByDestV6 = boolPtr(false)
				}
			} else {
				.MappingVariesByDestV6 = boolPtr(true)
			}
		} else {
			.GlobalV6 = 
		}
	}
}

// canonicalAddrPort unmaps an IPv4-mapped IPv6 address (::ffff:a.b.c.d) to a
// plain IPv4 address, mirroring SocketAddr::ip().to_canonical()
// (iroh/src/net_report.rs:806, reportgen.rs:819).
func canonicalAddrPort( netip.AddrPort) netip.AddrPort {
	return netip.AddrPortFrom(.Addr().Unmap(), .Port())
}

func boolPtr( bool) *bool { return & }

// RelayLatencies records the lowest latency seen per relay for each probe type.
// It is a port of net_report::RelayLatencies (report.rs:130). The zero value is
// empty and ready to use.
type RelayLatencies struct {
	ipv4  map[string]time.Duration // key: RelayURL.String()
	ipv6  map[string]time.Duration
	https map[string]time.Duration
	// urls remembers the RelayURL behind each key so iteration and lookup can
	// return a netaddr.RelayURL without re-parsing.
	urls map[string]netaddr.RelayURL
}

// updateRelay records latency for url under probe, keeping the minimum seen.
// Mirrors RelayLatencies::update_relay (report.rs:132).
func ( *RelayLatencies) ( netaddr.RelayURL,  time.Duration,  Probe) {
	if .urls == nil {
		.urls = map[string]netaddr.RelayURL{}
	}
	 := .String()
	.urls[] = 

	 := .bucket(, true)
	if ,  := []; ! ||  <  {
		[] = 
	}
}

// merge folds other into rl, keeping the minimum latency per (url, probe).
// Mirrors RelayLatencies::merge (report.rs:150).
func ( *RelayLatencies) ( *RelayLatencies) {
	for ,  := range .https {
		.updateRelay(.urls[], , ProbeHTTPS)
	}
	for ,  := range .ipv4 {
		.updateRelay(.urls[], , ProbeQADv4)
	}
	for ,  := range .ipv6 {
		.updateRelay(.urls[], , ProbeQADv6)
	}
}

// get returns the lowest latency recorded for url across all probe types and
// whether any was recorded. Mirrors RelayLatencies::get (report.rs:200).
func ( *RelayLatencies) ( netaddr.RelayURL) (time.Duration, bool) {
	 := .String()
	 := time.Duration(0)
	 := false
	for ,  := range []map[string]time.Duration{.https, .ipv4, .ipv6} {
		if ,  := [];  && (! ||  < ) {
			 = 
			 = true
		}
	}
	return , 
}

// Snapshot returns the lowest latency recorded for each relay.
func ( *RelayLatencies) () map[netaddr.RelayURL]time.Duration {
	 := .relays()
	if len() == 0 {
		return nil
	}
	 := make(map[netaddr.RelayURL]time.Duration, len())
	for ,  := range  {
		if ,  := .get();  {
			[] = 
		}
	}
	return 
}

// isEmpty reports whether no latencies have been recorded.
func ( *RelayLatencies) () bool {
	return len(.https) == 0 && len(.ipv4) == 0 && len(.ipv6) == 0
}

// urlsByKey returns each relay url that has at least one recorded latency, in
// no particular order. Callers needing determinism must sort.
func ( *RelayLatencies) () []netaddr.RelayURL {
	 := map[string]struct{}{}
	var  []netaddr.RelayURL
	for ,  := range []map[string]time.Duration{.https, .ipv4, .ipv6} {
		for  := range  {
			if ,  := [];  {
				continue
			}
			[] = struct{}{}
			 = append(, .urls[])
		}
	}
	return 
}

func ( *RelayLatencies) ( Probe,  bool) map[string]time.Duration {
	var  *map[string]time.Duration
	switch  {
	case ProbeHTTPS:
		 = &.https
	case ProbeQADv4:
		 = &.ipv4
	case ProbeQADv6:
		 = &.ipv6
	default:
		 = &.https
	}
	if * == nil &&  {
		* = map[string]time.Duration{}
	}
	return *
}