package autonatv2

import (
	
	
	
	
	
	

	

	
	
	
	
	
	ma 
)

// client implements the client for making dial requests for AutoNAT v2. It verifies successful
// dials and provides an option to send data for dial requests.
type client struct {
	host               host.Host
	dialData           []byte
	normalizeMultiaddr func(ma.Multiaddr) ma.Multiaddr
	metricsTracer      MetricsTracer

	mu sync.Mutex
	// dialBackQueues maps nonce to the channel for providing the local multiaddr of the connection
	// the nonce was received on
	dialBackQueues map[uint64]chan ma.Multiaddr
}

type normalizeMultiaddrer interface {
	NormalizeMultiaddr(ma.Multiaddr) ma.Multiaddr
}

func newClient( *autoNATSettings) *client {
	return &client{
		dialData:       make([]byte, 4000),
		dialBackQueues: make(map[uint64]chan ma.Multiaddr),
		metricsTracer:  .metricsTracer,
	}
}

func ( *client) ( host.Host) {
	 := func( ma.Multiaddr) ma.Multiaddr { return  }
	if ,  := .(normalizeMultiaddrer);  {
		 = .NormalizeMultiaddr
	}
	.host = 
	.normalizeMultiaddr = 
	.host.SetStreamHandler(DialBackProtocol, .handleDialBack)
}

func ( *client) () {
	.host.RemoveStreamHandler(DialBackProtocol)
}

// GetReachability verifies address reachability with a AutoNAT v2 server p.
func ( *client) ( context.Context,  peer.ID,  []Request) (Result, error) {
	,  := .getReachability(, , )

	// Track metrics
	if .metricsTracer != nil {
		.metricsTracer.ClientCompletedRequest(, , )
	}

	return , 
}

func ( *client) ( context.Context,  peer.ID,  []Request) (Result, error) {
	,  := context.WithTimeout(, streamTimeout)
	defer ()

	,  := .host.NewStream(, , DialProtocol)
	if  != nil {
		return Result{}, fmt.Errorf("open %s stream failed: %w", DialProtocol, )
	}

	if  := .Scope().SetService(ServiceName);  != nil {
		.Reset()
		return Result{}, fmt.Errorf("attach stream %s to service %s failed: %w", DialProtocol, ServiceName, )
	}

	if  := .Scope().ReserveMemory(maxMsgSize, network.ReservationPriorityAlways);  != nil {
		.Reset()
		return Result{}, fmt.Errorf("failed to reserve memory for stream %s: %w", DialProtocol, )
	}
	defer .Scope().ReleaseMemory(maxMsgSize)

	.SetDeadline(time.Now().Add(streamTimeout))
	defer .Close()

	 := rand.Uint64()
	 := make(chan ma.Multiaddr, 1)
	.mu.Lock()
	.dialBackQueues[] = 
	.mu.Unlock()
	defer func() {
		.mu.Lock()
		delete(.dialBackQueues, )
		.mu.Unlock()
	}()

	 := newDialRequest(, )
	 := pbio.NewDelimitedWriter()
	if  := .WriteMsg(&);  != nil {
		.Reset()
		return Result{}, fmt.Errorf("dial request write failed: %w", )
	}

	 := pbio.NewDelimitedReader(, maxMsgSize)
	if  := .ReadMsg(&);  != nil {
		.Reset()
		return Result{}, fmt.Errorf("dial msg read failed: %w", )
	}

	switch {
	case .GetDialResponse() != nil:
		break
	// provide dial data if appropriate
	case .GetDialDataRequest() != nil:
		if  := validateDialDataRequest(, &);  != nil {
			.Reset()
			return Result{}, fmt.Errorf("invalid dial data request: %s %w", .Conn().RemoteMultiaddr(), )
		}
		// dial data request is valid and we want to send data
		if  := sendDialData(.dialData, int(.GetDialDataRequest().GetNumBytes()), , &);  != nil {
			.Reset()
			return Result{}, fmt.Errorf("dial data send failed: %w", )
		}
		if  := .ReadMsg(&);  != nil {
			.Reset()
			return Result{}, fmt.Errorf("dial response read failed: %w", )
		}
		if .GetDialResponse() == nil {
			.Reset()
			return Result{}, fmt.Errorf("invalid response type: %T", .Msg)
		}
	default:
		.Reset()
		return Result{}, fmt.Errorf("invalid msg type: %T", .Msg)
	}

	 := .GetDialResponse()
	if .GetStatus() != pb.DialResponse_OK {
		// E_DIAL_REFUSED has implication for deciding future address verificiation priorities
		// wrap a distinct error for convenient errors.Is usage
		if .GetStatus() == pb.DialResponse_E_DIAL_REFUSED {
			return Result{AllAddrsRefused: true}, nil
		}
		return Result{}, fmt.Errorf("dial request failed: response status %d %s", .GetStatus(),
			pb.DialResponse_ResponseStatus_name[int32(.GetStatus())])
	}
	if .GetDialStatus() == pb.DialStatus_UNUSED {
		return Result{}, fmt.Errorf("invalid response: invalid dial status UNUSED")
	}
	if int(.AddrIdx) >= len() {
		return Result{}, fmt.Errorf("invalid response: addr index out of range: %d [0-%d)", .AddrIdx, len())
	}
	// wait for nonce from the server
	var  ma.Multiaddr
	if .GetDialStatus() == pb.DialStatus_OK {
		 := time.NewTimer(dialBackStreamTimeout)
		select {
		case  := <-:
			 = 
		case <-.Done():
		case <-.C:
		}
		.Stop()
	}
	return .newResult(, , )
}

func validateDialDataRequest( []Request,  *pb.Message) error {
	 := int(.GetDialDataRequest().AddrIdx)
	if  >= len() { // invalid address index
		return fmt.Errorf("addr index out of range: %d [0-%d)", , len())
	}
	if .GetDialDataRequest().NumBytes > maxHandshakeSizeBytes { // data request is too high
		return fmt.Errorf("requested data too high: %d", .GetDialDataRequest().NumBytes)
	}
	if ![].SendDialData { // low priority addr
		return fmt.Errorf("low priority addr: %s index %d", [].Addr, )
	}
	return nil
}

func ( *client) ( *pb.DialResponse,  []Request,  ma.Multiaddr) (Result, error) {
	 := int(.AddrIdx)
	if  >= len() {
		// This should have been validated by this point, but checking this is cheap.
		return Result{}, fmt.Errorf("addrs index(%d) greater than len(reqs)(%d)", , len())
	}
	 := [].Addr

	 := network.ReachabilityUnknown //nolint:ineffassign
	switch .DialStatus {
	case pb.DialStatus_OK:
		if !.areAddrsConsistent(, ) {
			// the server is misinforming us about the address it successfully dialed
			// either we received no dialback or the address on the dialback is inconsistent with
			// what the server is telling us
			return Result{}, fmt.Errorf("invalid response: dialBackAddr: %s, respAddr: %s", , )
		}
		 = network.ReachabilityPublic
	case pb.DialStatus_E_DIAL_BACK_ERROR:
		if !.areAddrsConsistent(, ) {
			return Result{}, fmt.Errorf("dial-back stream error: dialBackAddr: %s, respAddr: %s", , )
		}
		// We received the dial back but the server claims the dial back errored.
		// As long as we received the correct nonce in dial back it is safe to assume
		// that we are public.
		 = network.ReachabilityPublic
	case pb.DialStatus_E_DIAL_ERROR:
		 = network.ReachabilityPrivate
	default:
		// Unexpected response code. Discard the response and fail.
		log.Warnf("invalid status code received in response for addr %s: %d", , .DialStatus)
		return Result{}, fmt.Errorf("invalid response: invalid status code for addr %s: %d", , .DialStatus)
	}

	return Result{
		Addr:         ,
		Idx:          ,
		Reachability: ,
	}, nil
}

func sendDialData( []byte,  int,  pbio.Writer,  *pb.Message) ( error) {
	 := &pb.DialDataResponse{Data: }
	* = pb.Message{
		Msg: &pb.Message_DialDataResponse{
			DialDataResponse: ,
		},
	}
	for  := ;  > 0; {
		if  < len(.Data) {
			.Data = .Data[:]
		}
		if  := .WriteMsg();  != nil {
			return fmt.Errorf("write failed: %w", )
		}
		 -= len()
	}
	return nil
}

func newDialRequest( []Request,  uint64) pb.Message {
	 := make([][]byte, len())
	for ,  := range  {
		[] = .Addr.Bytes()
	}
	return pb.Message{
		Msg: &pb.Message_DialRequest{
			DialRequest: &pb.DialRequest{
				Addrs: ,
				Nonce: ,
			},
		},
	}
}

// handleDialBack receives the nonce on the dial-back stream
func ( *client) ( network.Stream) {
	defer func() {
		if  := recover();  != nil {
			fmt.Fprintf(os.Stderr, "caught panic: %s\n%s\n", , debug.Stack())
		}
		.Reset()
	}()

	if  := .Scope().SetService(ServiceName);  != nil {
		log.Debugf("failed to attach stream to service %s: %w", ServiceName, )
		.Reset()
		return
	}

	if  := .Scope().ReserveMemory(dialBackMaxMsgSize, network.ReservationPriorityAlways);  != nil {
		log.Debugf("failed to reserve memory for stream %s: %w", DialBackProtocol, )
		.Reset()
		return
	}
	defer .Scope().ReleaseMemory(dialBackMaxMsgSize)

	.SetDeadline(time.Now().Add(dialBackStreamTimeout))
	defer .Close()

	 := pbio.NewDelimitedReader(, dialBackMaxMsgSize)
	var  pb.DialBack
	if  := .ReadMsg(&);  != nil {
		log.Debugf("failed to read dialback msg from %s: %s", .Conn().RemotePeer(), )
		.Reset()
		return
	}
	 := .GetNonce()

	.mu.Lock()
	 := .dialBackQueues[]
	.mu.Unlock()
	if  == nil {
		log.Debugf("dialback received with invalid nonce: localAdds: %s peer: %s nonce: %d", .Conn().LocalMultiaddr(), .Conn().RemotePeer(), )
		.Reset()
		return
	}
	select {
	case  <- .Conn().LocalMultiaddr():
	default:
		log.Debugf("multiple dialbacks received: localAddr: %s peer: %s", .Conn().LocalMultiaddr(), .Conn().RemotePeer())
		.Reset()
		return
	}
	 := pbio.NewDelimitedWriter()
	 := pb.DialBackResponse{}
	if  := .WriteMsg(&);  != nil {
		log.Debugf("failed to write dialback response: %s", )
		.Reset()
	}
}

func ( *client) (,  ma.Multiaddr) bool {
	if len() == 0 || len() == 0 {
		return false
	}
	 = .normalizeMultiaddr()
	 = .normalizeMultiaddr()

	 := .Protocols()
	 := .Protocols()
	if len() != len() {
		return false
	}
	for ,  := range  {
		 := []
		if  == 0 {
			switch .Code {
			case ma.P_DNS, ma.P_DNSADDR:
				if .Code == ma.P_IP4 || .Code == ma.P_IP6 {
					continue
				}
				return false
			case ma.P_DNS4:
				if .Code == ma.P_IP4 {
					continue
				}
				return false
			case ma.P_DNS6:
				if .Code == ma.P_IP6 {
					continue
				}
				return false
			}
			if .Code != .Code {
				return false
			}
		} else if .Code != .Code {
			return false
		}
	}
	return true
}