package iroh

import (
	
	
	
	
	
	
	

	
	
	
)

// AddressPublisher publishes the endpoint's addressing information.
type AddressPublisher interface {
	// Publish records endpoint data with the service. It is fire-and-forget:
	// the call must not block, starting any background work itself.
	Publish(data dns.EndpointData)
}

// AddressPublisherFunc adapts a function to [AddressPublisher].
type AddressPublisherFunc func(data dns.EndpointData)

// Publish calls f(data).
func ( AddressPublisherFunc) ( dns.EndpointData) {
	()
}

// AddressResolver resolves the addressing information of a [key.EndpointID].
// It lets an [Endpoint] connect to a peer knowing only its id, by looking up a
// [netaddr.EndpointAddr] (a relay URL and/or direct addresses) through one or
// more lookup services.
//
// Multiple implementations coexist: pkarr-relay ([PkarrResolver]), DNS
// ([DNSAddressLookup]), and in-memory ([MemoryLookup]). An [Endpoint] combines
// them with [AddressLookupServices].
//
// It is the Go analog of iroh's address lookup resolution path.
type AddressResolver interface {
	// Resolve looks up addressing information for id. It returns a sequence of
	// discovered [Item] values and per-service errors. Cancel ctx to stop
	// pending work.
	Resolve(ctx context.Context, id key.EndpointID) iter.Seq2[Item, error]
}

// AddressResolverFunc adapts a function to [AddressResolver].
type AddressResolverFunc func(ctx context.Context, id key.EndpointID) iter.Seq2[Item, error]

// Resolve calls f(ctx, id).
func ( AddressResolverFunc) ( context.Context,  key.EndpointID) iter.Seq2[Item, error] {
	return (, )
}

// Item is a single address-lookup result: the [dns.EndpointInfo] discovered for
// an endpoint plus metadata about the lookup source. It is the item carried in
// the streams returned by [AddressResolver.Resolve].
//
// It is the Go analog of iroh's address_lookup::Item.
type Item struct {
	info        dns.EndpointInfo
	provenance  string
	lastUpdated uint64 // microseconds since the unix epoch, 0 if unknown
	hasUpdated  bool
}

// NewItem returns an Item for info from a lookup source identified by
// provenance. lastUpdated is microseconds since the unix epoch, or nil if the
// source does not track it.
func ( dns.EndpointInfo,  string,  *uint64) Item {
	 := Item{info: , provenance: }
	if  != nil {
		.lastUpdated = *
		.hasUpdated = true
	}
	return 
}

// EndpointID returns the id of the discovered endpoint.
func ( Item) () key.EndpointID { return .info.ID }

// EndpointInfo returns the discovered endpoint info.
func ( Item) () dns.EndpointInfo { return .info }

// UserData returns the discovered user data, if set.
func ( Item) () (dns.UserData, bool) {
	 := .info.Data.UserData()
	if  == nil {
		return dns.UserData{}, false
	}
	return *, true
}

// Provenance returns a stable string identifying the lookup source that
// produced this item, such as "pkarr", "dns", or "memory_lookup".
func ( Item) () string { return .provenance }

// LastUpdated returns the time the source last updated this info, in
// microseconds since the unix epoch, and whether the source tracks it.
func ( Item) () (uint64, bool) { return .lastUpdated, .hasUpdated }

// LastUpdatedTime returns the time the source last updated this info, and
// whether the source tracks it.
func ( Item) () (time.Time, bool) {
	if !.hasUpdated {
		return time.Time{}, false
	}
	return time.UnixMicro(int64(.lastUpdated)), true
}

// Addr converts the item into a [netaddr.EndpointAddr].
func ( Item) () netaddr.EndpointAddr { return .info.Addr() }

// LookupError reports a failed address lookup from a single service. The
// provenance identifies which service failed.
//
// It is the Go analog of iroh's address_lookup::Error.
type LookupError struct {
	Provenance string
	Err        error
}

// Error implements error.
func ( *LookupError) () string {
	return fmt.Sprintf("address lookup service %q failed: %v", .Provenance, .Err)
}

// Unwrap returns the wrapped error for use with [errors.Is] and [errors.As].
func ( *LookupError) () error { return .Err }

// lookupErr wraps err as a [LookupError] from the named service.
func lookupErr( string,  error) *LookupError {
	return &LookupError{Provenance: , Err: }
}

type lookupResult struct {
	item Item
	err  error
}

// Errors returned by [AddressLookupServices.Resolve] when no service produces a
// result.
var (
	// ErrNoServiceConfigured is reported when resolution is attempted with no
	// services registered.
	ErrNoServiceConfigured = errors.New("no address lookup configured")
	// ErrNoResults is reported when every configured service finished without
	// yielding an item. The per-service errors, if any, are joined into it.
	ErrNoResults = errors.New("all address lookup services failed or produced no results")
)

// AddrFilter selects and orders the transport addresses published to a lookup
// service. It receives the full address set and returns the subset to publish,
// in priority order. A nil AddrFilter publishes all addresses unchanged.
//
// It is the Go analog of iroh's address_lookup::AddrFilter.
type AddrFilter func(addrs []netaddr.TransportAddr) []netaddr.TransportAddr

// RelayOnlyFilter keeps only relay addresses. It is the default filter for
// [PkarrPublisher], avoiding leaking direct IP addresses to a public pkarr
// relay.
func ( []netaddr.TransportAddr) []netaddr.TransportAddr {
	 := make([]netaddr.TransportAddr, 0, len())
	for ,  := range  {
		if ,  := .(netaddr.RelayAddr);  {
			 = append(, )
		}
	}
	return 
}

// IPOnlyFilter keeps only direct IP and custom addresses, dropping relays.
func ( []netaddr.TransportAddr) []netaddr.TransportAddr {
	 := make([]netaddr.TransportAddr, 0, len())
	for ,  := range  {
		if ,  := .(netaddr.RelayAddr); ! {
			 = append(, )
		}
	}
	return 
}

// applyFilter returns data with f applied to its addresses, preserving the user
// data. A nil filter returns data unchanged.
func applyFilter( dns.EndpointData,  AddrFilter) dns.EndpointData {
	if  == nil {
		return 
	}
	 := dns.NewEndpointData((.Addrs())...)
	if  := .UserData();  != nil {
		 = .WithUserData()
	}
	return 
}

// AddressLookupServices is the registry of address lookup services for an
// [Endpoint]. It publishes the endpoint's own info to every publisher and merges
// resolver streams.
//
// The zero value is an empty, ready-to-use registry. It is safe for concurrent
// use.
//
// It is the Go analog of iroh's AddressLookupServices.
type AddressLookupServices struct {
	mu         sync.RWMutex
	publishers []AddressPublisher
	resolvers  []AddressResolver
	lastData   *dns.EndpointData
	addrFilter AddrFilter
}

// SetAddrFilter sets a filter applied to all data before publishing to any
// service, ensuring consistent filtering across services.
func ( *AddressLookupServices) ( AddrFilter) {
	.mu.Lock()
	defer .mu.Unlock()
	.addrFilter = 
}

// AddPublisher registers a publisher. If data has already been published, it is
// published to the new service immediately.
func ( *AddressLookupServices) ( AddressPublisher) {
	.mu.Lock()
	defer .mu.Unlock()
	if .lastData != nil {
		.Publish(*.lastData)
	}
	.publishers = append(.publishers, )
}

// AddResolver registers a resolver.
func ( *AddressLookupServices) ( AddressResolver) {
	.mu.Lock()
	defer .mu.Unlock()
	.resolvers = append(.resolvers, )
}

// Len returns the number of registered publishers and resolvers.
func ( *AddressLookupServices) () int {
	.mu.RLock()
	defer .mu.RUnlock()
	return len(.publishers) + len(.resolvers)
}

// IsEmpty reports whether no publishers or resolvers are registered.
func ( *AddressLookupServices) () bool { return .Len() == 0 }

// Clear removes all registered publishers and resolvers.
func ( *AddressLookupServices) () {
	.mu.Lock()
	defer .mu.Unlock()
	.publishers = nil
	.resolvers = nil
}

// Publish publishes data on every registered publisher, applying the registry's
// address filter first, and records it for services added later.
func ( *AddressLookupServices) ( dns.EndpointData) {
	.mu.Lock()
	defer .mu.Unlock()
	 := applyFilter(, .addrFilter)
	for ,  := range .publishers {
		.Publish()
	}
	.lastData = &
}

// Resolve looks up id across all registered services concurrently, merging
// their streams into the returned sequence. Each successful [Item] is yielded as
// it is produced, letting the caller act on the first usable address while
// slower services run.
//
// A per-service error is yielded inline and does not end the sequence. If every
// configured service finishes without yielding an item, a final
// [ErrNoResults] wrapping the per-service errors is yielded. If no services are
// registered, [ErrNoServiceConfigured] is yielded once.
//
// Cancel ctx to stop all services and end the sequence.
func ( *AddressLookupServices) ( context.Context,  key.EndpointID) iter.Seq2[Item, error] {
	.mu.RLock()
	 := slices.Clone(.resolvers)
	.mu.RUnlock()

	return func( func(Item, error) bool) {
		,  := context.WithCancel()
		defer ()

		if len() == 0 {
			if .Err() == nil {
				(Item{}, ErrNoServiceConfigured)
			}
			return
		}
		var  sync.WaitGroup
		 := make(chan lookupResult)
		for ,  := range  {
			 := .Resolve(, )
			.Add(1)
			go func( iter.Seq2[Item, error]) {
				defer .Done()
				for ,  := range  {
					select {
					case  <- lookupResult{item: , err: }:
					case <-.Done():
						return
					}
				}
			}()
		}
		go func() {
			.Wait()
			close()
		}()

		var  bool
		var  []error
		for {
			select {
			case ,  := <-:
				if ! {
					if ! {
						if .Err() == nil {
							(Item{}, fmt.Errorf("%w: %w", ErrNoResults, errors.Join(...)))
						}
					}
					return
				}
				if .err != nil {
					 = append(, .err)
				} else {
					 = true
				}
				if !(.item, .err) {
					return
				}
			case <-.Done():
				return
			}
		}
	}
}