package iroh

import (
	
	
	
	
	
	
	
	
	

	
	
	
)

// PkarrProvenance is the provenance string for [PkarrResolver] items.
const PkarrProvenance = "pkarr"

// pkarr relay URLs and publishing defaults.
//
// iroh/src/address_lookup/pkarr.rs.
const (
	// N0DNSPkarrRelayProd is the number0 production pkarr relay, which also
	// serves the records over DNS.
	N0DNSPkarrRelayProd = "https://dns.iroh.link/pkarr"
	// N0DNSPkarrRelayStaging is the number0 staging pkarr relay.
	N0DNSPkarrRelayStaging = "https://staging-dns.iroh.link/pkarr"

	// DefaultPkarrTTL is the default record TTL, in seconds, of published pkarr
	// signed packets.
	DefaultPkarrTTL uint32 = 30
	// DefaultRepublishInterval is how often the publisher republishes the
	// endpoint info even when unchanged.
	DefaultRepublishInterval = 5 * time.Minute
)

// PkarrPublisher publishes endpoint addressing information to a pkarr relay
// over HTTP. Pair it with a [PkarrResolver] or [DNSAddressLookup] to resolve.
//
// Publishing is fire-and-forget: [PkarrPublisher.Publish] updates an internal
// value and returns immediately while a background goroutine performs the HTTP
// PUT. The publisher republishes every [DefaultRepublishInterval] even when the
// data is unchanged, and retries with backoff on failure. By default only relay
// addresses are published (see [RelayOnlyFilter]).
//
// The zero value is not usable; create one with [NewPkarrPublisher] or
// [N0PkarrPublisher]. Stop the background goroutine with [PkarrPublisher.Close].
//
// It is the Go analog of iroh's PkarrPublisher.
type PkarrPublisher struct {
	endpointID key.EndpointID
	addrFilter AddrFilter
	value      *watch.Value[*dns.EndpointInfo]
	cancel     context.CancelFunc
	done       chan struct{}
}

// PkarrPublisherConfig configures a [PkarrPublisher].
type PkarrPublisherConfig struct {
	// TTL is the record TTL, in seconds, of published packets. If zero,
	// [DefaultPkarrTTL] is used.
	TTL uint32
	// RepublishInterval is how often packets are republished even when
	// unchanged. If zero, [DefaultRepublishInterval] is used.
	RepublishInterval time.Duration
	// AddrFilter controls which addresses are published. If nil,
	// [RelayOnlyFilter] is used. Use a filter that returns its input unchanged
	// to publish all addresses.
	AddrFilter AddrFilter
	// HTTPClient is used for relay requests. If nil, a client with a per-request
	// timeout is used.
	HTTPClient *http.Client
}

// NewPkarrPublisher creates a publisher that signs packets with secretKey,
// publishes to the pkarr relay at relayURL, and starts its background publish
// goroutine.
func ( key.SecretKey,  string,  *PkarrPublisherConfig) (*PkarrPublisher, error) {
	 := DefaultPkarrTTL
	 := DefaultRepublishInterval
	 := RelayOnlyFilter
	var  *http.Client
	if  != nil {
		if .TTL != 0 {
			 = .TTL
		}
		if .RepublishInterval != 0 {
			 = .RepublishInterval
		}
		if .AddrFilter != nil {
			 = .AddrFilter
		}
		 = .HTTPClient
	}

	,  := newPkarrRelayClient(, )
	if  != nil {
		return nil, fmt.Errorf("pkarr publisher: %w", )
	}
	,  := context.WithCancel(context.Background())
	 := &PkarrPublisher{
		endpointID: .Public().EndpointID(),
		addrFilter: ,
		value:      watch.NewValue[*dns.EndpointInfo](nil),
		cancel:     ,
		done:       make(chan struct{}),
	}
	 := &publisherService{
		secretKey:         ,
		client:            ,
		watcher:           .value.Watch(),
		ttl:               ,
		republishInterval: ,
	}
	go func() {
		defer close(.done)
		.run()
	}()
	return , nil
}

// N0PkarrPublisher creates a publisher using the number0 production pkarr relay
// ([N0DNSPkarrRelayProd]).
func ( key.SecretKey,  *PkarrPublisherConfig) (*PkarrPublisher, error) {
	return NewPkarrPublisher(, N0DNSPkarrRelayProd, )
}

// Publish records data to publish to the pkarr relay. It applies the
// publisher's address filter and returns immediately; the HTTP PUT runs in the
// background.
func ( *PkarrPublisher) ( dns.EndpointData) {
	 := applyFilter(, .addrFilter)
	 := dns.EndpointInfo{ID: .endpointID, Data: }
	.value.Set(&)
}

// Close stops the background publish goroutine and waits for it to exit.
func ( *PkarrPublisher) () error {
	.cancel()
	<-.done
	return nil
}

// publisherService runs the publisher's background loop: it publishes whenever
// the endpoint info changes and republishes on a fixed interval, with backoff
// on failure.
type publisherService struct {
	secretKey         key.SecretKey
	client            *pkarrRelayClient
	watcher           watch.Observer[*dns.EndpointInfo]
	ttl               uint32
	republishInterval time.Duration
}

func ( *publisherService) ( context.Context) {
	// A single goroutine watches for endpoint-info changes and signals them on
	// changed, so the loop below never spawns a watcher goroutine per iteration.
	 := make(chan struct{}, 1)
	go func() {
		for {
			if ,  := .watcher.Updated();  != nil {
				return // ctx cancelled
			}
			select {
			case  <- struct{}{}:
			default: // a pending signal already covers this change
			}
		}
	}()

	var  int
	 := time.NewTimer(time.Duration(1 << 62))
	defer .Stop()
	for {
		if  := .watcher.Current();  != nil {
			if  := .publishCurrent(, *);  != nil {
				if .Err() != nil {
					return
				}
				++
				resetTimer(, time.Duration()*time.Second)
			} else {
				 = 0
				resetTimer(, .republishInterval)
			}
		}
		select {
		case <-.Done():
			return
		case <-.C:
		case <-:
		}
	}
}

func ( *publisherService) ( context.Context,  dns.EndpointInfo) error {
	,  := .ToSignedPacket(.secretKey, .ttl)
	if  != nil {
		return fmt.Errorf("encode signed packet: %w", )
	}
	return .client.publish(, )
}

// resetTimer stops and resets t to fire after d, draining any pending tick.
func resetTimer( *time.Timer,  time.Duration) {
	if !.Stop() {
		select {
		case <-.C:
		default:
		}
	}
	.Reset()
}

// PkarrResolver resolves endpoint addressing information from a pkarr relay over
// HTTP.
//
// The zero value is not usable; create one with [NewPkarrResolver] or
// [N0PkarrResolver].
//
// It is the Go analog of iroh's PkarrResolver.
type PkarrResolver struct {
	client *pkarrRelayClient
}

// PkarrResolverConfig configures a [PkarrResolver].
type PkarrResolverConfig struct {
	// HTTPClient is used for relay requests. If nil, a client with a per-request
	// timeout is used.
	HTTPClient *http.Client
}

// NewPkarrResolver creates a resolver that resolves from the pkarr relay at
// relayURL.
func ( string,  *PkarrResolverConfig) (*PkarrResolver, error) {
	var  *http.Client
	if  != nil {
		 = .HTTPClient
	}
	,  := newPkarrRelayClient(, )
	if  != nil {
		return nil, fmt.Errorf("pkarr resolver: %w", )
	}
	return &PkarrResolver{client: }, nil
}

// N0PkarrResolver creates a resolver using the number0 production pkarr relay
// ([N0DNSPkarrRelayProd]).
func ( *PkarrResolverConfig) (*PkarrResolver, error) {
	return NewPkarrResolver(N0DNSPkarrRelayProd, )
}

// Resolve fetches the signed packet for id from the pkarr relay and decodes its
// endpoint info.
func ( *PkarrResolver) ( context.Context,  key.EndpointID) iter.Seq2[Item, error] {
	return func( func(Item, error) bool) {
		,  := .client.resolve(, )
		if  != nil {
			if .Err() == nil {
				(Item{}, lookupErr(PkarrProvenance, ))
			}
			return
		}
		,  := dns.EndpointInfoFromSignedPacket()
		if  != nil {
			if .Err() == nil {
				(Item{}, lookupErr(PkarrProvenance, ))
			}
			return
		}
		if .Err() == nil {
			(NewItem(, PkarrProvenance, nil), nil)
		}
	}
}

// pkarrRelayClient publishes and resolves pkarr signed packets to a pkarr relay
// using HTTP PUT and GET on "<relay>/<z32-endpoint-id>".
//
// iroh/src/address_lookup/pkarr.rs PkarrRelayClient; the route and body match
// iroh-dns-server/src/http/pkarr.rs (put/get).
type pkarrRelayClient struct {
	httpClient *http.Client
	relayURL   *url.URL
}

func newPkarrRelayClient( string,  *http.Client) (*pkarrRelayClient, error) {
	,  := url.Parse()
	if  != nil {
		return nil, fmt.Errorf("parse relay url: %w", )
	}
	if  == nil {
		 = &http.Client{Timeout: 30 * time.Second}
	}
	return &pkarrRelayClient{httpClient: , relayURL: }, nil
}

// keyURL returns "<relay>/<z32>" for the endpoint id's z-base-32 encoding.
func ( *pkarrRelayClient) ( string) string {
	 := *.relayURL
	.Path = strings.TrimRight(.Path, "/") + "/" + 
	return .String()
}

// publish PUTs the signed packet's relay payload (signature + timestamp + DNS
// wire bytes, i.e. everything after the public key) to "<relay>/<z32>".
func ( *pkarrRelayClient) ( context.Context,  *dns.SignedPacket) error {
	 := .RelayPayload()
	 := .keyURL(.PublicKey().EndpointID().Z32())
	,  := http.NewRequestWithContext(, http.MethodPut, , bytes.NewReader())
	if  != nil {
		return fmt.Errorf("build request: %w", )
	}
	,  := .httpClient.Do()
	if  != nil {
		return fmt.Errorf("http put: %w", )
	}
	defer .Body.Close()
	io.Copy(io.Discard, .Body)
	if .StatusCode < 200 || .StatusCode >= 300 {
		return fmt.Errorf("pkarr relay returned status %d", .StatusCode)
	}
	return nil
}

// resolve GETs the relay payload from "<relay>/<z32>" and reconstructs (and
// verifies) the signed packet from the public key and payload.
func ( *pkarrRelayClient) ( context.Context,  key.EndpointID) (*dns.SignedPacket, error) {
	 := .keyURL(.Z32())
	,  := http.NewRequestWithContext(, http.MethodGet, , nil)
	if  != nil {
		return nil, fmt.Errorf("build request: %w", )
	}
	,  := .httpClient.Do()
	if  != nil {
		return nil, fmt.Errorf("http get: %w", )
	}
	defer .Body.Close()
	if .StatusCode < 200 || .StatusCode >= 300 {
		io.Copy(io.Discard, .Body)
		return nil, fmt.Errorf("pkarr relay returned status %d", .StatusCode)
	}
	,  := io.ReadAll(.Body)
	if  != nil {
		return nil, fmt.Errorf("read payload: %w", )
	}
	 := .PublicKey().Bytes()
	 := make([]byte, 0, len()+len())
	 = append(, [:]...)
	 = append(, ...)
	,  := dns.SignedPacketFromBytes()
	if  != nil {
		return nil, fmt.Errorf("decode signed packet: %w", )
	}
	return , nil
}