// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package turn

import (
	b64 
	
	
	
	
	

	
	
	
	
	
	
)

const (
	defaultRTO        = 200 * time.Millisecond
	maxRtxCount       = 7              // Total 7 requests (Rc)
	maxDataBufferSize = math.MaxUint16 // Message size limit for Chromium
)

//              interval [msec]
// 0: 0 ms      +500
// 1: 500 ms	+1000
// 2: 1500 ms   +2000
// 3: 3500 ms   +4000
// 4: 7500 ms   +8000
// 5: 15500 ms  +16000
// 6: 31500 ms  +32000
// -: 63500 ms  failed

// ClientConfig is a bag of config parameters for Client.
type ClientConfig struct {
	STUNServerAddr string // STUN server address (e.g. "stun.abc.com:3478")
	TURNServerAddr string // TURN server address (e.g. "turn.abc.com:3478")
	Username       string
	Password       string
	Realm          string
	Software       string
	RTO            time.Duration
	Conn           net.PacketConn // Listening socket (net.PacketConn)
	Net            transport.Net
	LoggerFactory  logging.LoggerFactory
}

// Client is a STUN server client.
type Client struct {
	conn           net.PacketConn // Read-only
	net            transport.Net  // Read-only
	stunServerAddr net.Addr       // Read-only
	turnServerAddr net.Addr       // Read-only

	username      stun.Username          // Read-only
	password      string                 // Read-only
	realm         stun.Realm             // Read-only
	integrity     stun.MessageIntegrity  // Read-only
	software      stun.Software          // Read-only
	trMap         *client.TransactionMap // Thread-safe
	rto           time.Duration          // Read-only
	relayedConn   *client.UDPConn        // Protected by mutex ***
	tcpAllocation *client.TCPAllocation  // Protected by mutex ***
	allocTryLock  client.TryLock         // Thread-safe
	listenTryLock client.TryLock         // Thread-safe
	mutex         sync.RWMutex           // Thread-safe
	mutexTrMap    sync.Mutex             // Thread-safe
	log           logging.LeveledLogger  // Read-only
}

// NewClient returns a new Client instance. listeningAddress is the address and port to listen on,
// default "0.0.0.0:0".
func ( *ClientConfig) (*Client, error) {
	 := .LoggerFactory
	if  == nil {
		 = logging.NewDefaultLoggerFactory()
	}

	 := .NewLogger("turnc")

	if .Conn == nil {
		return nil, errNilConn
	}

	 := defaultRTO
	if .RTO > 0 {
		 = .RTO
	}

	if .Net == nil {
		,  := stdnet.NewNet()
		if  != nil {
			return nil, 
		}
		.Net = 
	}

	var ,  net.Addr
	var  error

	if len(.STUNServerAddr) > 0 {
		,  = .Net.ResolveUDPAddr("udp4", .STUNServerAddr)
		if  != nil {
			return nil, 
		}

		.Debugf("Resolved STUN server %s to %s", .STUNServerAddr, )
	}

	if len(.TURNServerAddr) > 0 {
		,  = .Net.ResolveUDPAddr("udp4", .TURNServerAddr)
		if  != nil {
			return nil, 
		}

		.Debugf("Resolved TURN server %s to %s", .TURNServerAddr, )
	}

	 := &Client{
		conn:           .Conn,
		stunServerAddr: ,
		turnServerAddr: ,
		username:       stun.NewUsername(.Username),
		password:       .Password,
		realm:          stun.NewRealm(.Realm),
		software:       stun.NewSoftware(.Software),
		trMap:          client.NewTransactionMap(),
		net:            .Net,
		rto:            ,
		log:            ,
	}

	return , nil
}

// TURNServerAddr return the TURN server address.
func ( *Client) () net.Addr {
	return .turnServerAddr
}

// STUNServerAddr return the STUN server address.
func ( *Client) () net.Addr {
	return .stunServerAddr
}

// Username returns username.
func ( *Client) () stun.Username {
	return .username
}

// Realm return realm.
func ( *Client) () stun.Realm {
	return .realm
}

// WriteTo sends data to the specified destination using the base socket.
func ( *Client) ( []byte,  net.Addr) (int, error) {
	return .conn.WriteTo(, )
}

// Listen will have this client start listening on the conn provided via the config.
// This is optional. If not used, you will need to call HandleInbound method
// to supply incoming data, instead.
func ( *Client) () error {
	if  := .listenTryLock.Lock();  != nil {
		return fmt.Errorf("%w: %s", errAlreadyListening, .Error())
	}

	go func() {
		 := make([]byte, maxDataBufferSize)
		for {
			, ,  := .conn.ReadFrom()
			if  != nil {
				.log.Debugf("Failed to read: %s. Exiting loop", )

				break
			}

			_,  = .HandleInbound([:], )
			if  != nil {
				.log.Debugf("Failed to handle inbound message: %s. Exiting loop", )

				break
			}
		}

		.listenTryLock.Unlock()
	}()

	return nil
}

// Close closes this client.
func ( *Client) () {
	.mutexTrMap.Lock()
	defer .mutexTrMap.Unlock()

	.trMap.CloseAndDeleteAll()
}

// TransactionID & Base64: https://play.golang.org/p/EEgmJDI971P

// SendBindingRequestTo sends a new STUN request to the given transport address.
func ( *Client) ( net.Addr) (net.Addr, error) {
	 := []stun.Setter{stun.TransactionID, stun.BindingRequest}
	if len(.software) > 0 {
		 = append(, .software)
	}

	,  := stun.Build(...)
	if  != nil {
		return nil, 
	}
	,  := .PerformTransaction(, , false)
	if  != nil {
		return nil, 
	}

	var  stun.XORMappedAddress
	if  := .GetFrom(.Msg);  != nil {
		return nil, 
	}

	return &net.UDPAddr{
		IP:   .IP,
		Port: .Port,
	}, nil
}

// SendBindingRequest sends a new STUN request to the STUN server.
func ( *Client) () (net.Addr, error) {
	if .stunServerAddr == nil {
		return nil, errSTUNServerAddressNotSet
	}

	return .SendBindingRequestTo(.stunServerAddr)
}

func ( *Client) ( proto.Protocol) ( //nolint:cyclop
	proto.RelayedAddress,
	proto.Lifetime,
	stun.Nonce,
	error,
) {
	var  proto.RelayedAddress
	var  proto.Lifetime
	var  stun.Nonce

	,  := stun.Build(
		stun.TransactionID,
		stun.NewType(stun.MethodAllocate, stun.ClassRequest),
		proto.RequestedTransport{Protocol: },
		stun.Fingerprint,
	)
	if  != nil {
		return , , , 
	}

	,  := .PerformTransaction(, .turnServerAddr, false)
	if  != nil {
		return , , , 
	}

	 := .Msg

	// Anonymous allocate failed, trying to authenticate.
	if  = .GetFrom();  != nil {
		return , , , 
	}
	if  = .realm.GetFrom();  != nil {
		return , , , 
	}
	.realm = append([]byte(nil), .realm...)
	.integrity = stun.NewLongTermIntegrity(
		.username.String(), .realm.String(), .password,
	)
	// Trying to authorize.
	,  = stun.Build(
		stun.TransactionID,
		stun.NewType(stun.MethodAllocate, stun.ClassRequest),
		proto.RequestedTransport{Protocol: },
		&.username,
		&.realm,
		&,
		&.integrity,
		stun.Fingerprint,
	)
	if  != nil {
		return , , , 
	}

	,  = .PerformTransaction(, .turnServerAddr, false)
	if  != nil {
		return , , , 
	}
	 = .Msg

	if .Type.Class == stun.ClassErrorResponse {
		var  stun.ErrorCodeAttribute
		if  = .GetFrom();  == nil {
			return , , , fmt.Errorf("%s (error %s)", .Type, ) //nolint:goerr113
		}

		return , , , fmt.Errorf("%s", .Type) //nolint:goerr113
	}

	// Getting relayed addresses from response.
	if  := .GetFrom();  != nil {
		return , , , 
	}

	// Getting lifetime from response
	if  := .GetFrom();  != nil {
		return , , , 
	}

	return , , , nil
}

// Allocate sends a TURN allocation request to the given transport address.
func ( *Client) () (net.PacketConn, error) {
	if  := .allocTryLock.Lock();  != nil {
		return nil, fmt.Errorf("%w: %s", errOneAllocateOnly, .Error())
	}
	defer .allocTryLock.Unlock()

	 := .relayedUDPConn()
	if  != nil {
		return nil, fmt.Errorf("%w: %s", errAlreadyAllocated, .LocalAddr().String())
	}

	, , ,  := .sendAllocateRequest(proto.ProtoUDP)
	if  != nil {
		return nil, 
	}

	 := &net.UDPAddr{
		IP:   .IP,
		Port: .Port,
	}

	 = client.NewUDPConn(&client.AllocationConfig{
		Client:      ,
		RelayedAddr: ,
		ServerAddr:  .turnServerAddr,
		Realm:       .realm,
		Username:    .username,
		Integrity:   .integrity,
		Nonce:       ,
		Lifetime:    .Duration,
		Net:         .net,
		Log:         .log,
	})
	.setRelayedUDPConn()

	return , nil
}

// AllocateTCP creates a new TCP allocation at the TURN server.
func ( *Client) () (*client.TCPAllocation, error) {
	if  := .allocTryLock.Lock();  != nil {
		return nil, fmt.Errorf("%w: %s", errOneAllocateOnly, .Error())
	}
	defer .allocTryLock.Unlock()

	 := .getTCPAllocation()
	if  != nil {
		return nil, fmt.Errorf("%w: %s", errAlreadyAllocated, .Addr())
	}

	, , ,  := .sendAllocateRequest(proto.ProtoTCP)
	if  != nil {
		return nil, 
	}

	 := &net.TCPAddr{
		IP:   .IP,
		Port: .Port,
	}

	 = client.NewTCPAllocation(&client.AllocationConfig{
		Client:      ,
		RelayedAddr: ,
		ServerAddr:  .turnServerAddr,
		Realm:       .realm,
		Username:    .username,
		Integrity:   .integrity,
		Nonce:       ,
		Lifetime:    .Duration,
		Net:         .net,
		Log:         .log,
	})

	.setTCPAllocation()

	return , nil
}

// CreatePermission Issues a CreatePermission request for the supplied addresses
// as described in https://datatracker.ietf.org/doc/html/rfc5766#section-9
func ( *Client) ( ...net.Addr) error {
	if  := .relayedUDPConn();  != nil {
		if  := .CreatePermissions(...);  != nil {
			return 
		}
	}

	if  := .getTCPAllocation();  != nil {
		if  := .CreatePermissions(...);  != nil {
			return 
		}
	}

	return nil
}

// PerformTransaction performs STUN transaction.
func ( *Client) ( *stun.Message,  net.Addr,  bool) (client.TransactionResult,
	error,
) {
	 := b64.StdEncoding.EncodeToString(.TransactionID[:])

	 := make([]byte, len(.Raw))
	copy(, .Raw)

	 := client.NewTransaction(&client.TransactionConfig{
		Key:          ,
		Raw:          ,
		To:           ,
		Interval:     .rto,
		IgnoreResult: ,
	})

	.trMap.Insert(, )

	.log.Tracef("Start %s transaction %s to %s", .Type, , .To)
	,  := .conn.WriteTo(.Raw, )
	if  != nil {
		return client.TransactionResult{}, 
	}

	.StartRtxTimer(.onRtxTimeout)

	// If ignoreResult is true, get the transaction going and return immediately
	if  {
		return client.TransactionResult{}, nil
	}

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

	return , nil
}

// OnDeallocated is called when de-allocation of relay address has been complete.
// (Called by UDPConn).
func ( *Client) (net.Addr) {
	.setRelayedUDPConn(nil)
	.setTCPAllocation(nil)
}

// HandleInbound handles data received.
// This method handles incoming packet de-multiplex it by the source address
// and the types of the message.
// This return a boolean (handled or not) and if there was an error.
// Caller should check if the packet was handled by this client or not.
// If not handled, it is assumed that the packet is application data.
// If an error is returned, the caller should discard the packet regardless.
func ( *Client) ( []byte,  net.Addr) (bool, error) {
	// +-------------------+-------------------------------+
	// |   Return Values   |                               |
	// +-------------------+       Meaning / Action        |
	// | handled |  error  |                               |
	// |=========+=========+===============================+
	// |  false  |   nil   | Handle the packet as app data |
	// |---------+---------+-------------------------------+
	// |  true   |   nil   |        Nothing to do          |
	// |---------+---------+-------------------------------+
	// |  false  |  error  |     (shouldn't happen)        |
	// |---------+---------+-------------------------------+
	// |  true   |  error  | Error occurred while handling |
	// +---------+---------+-------------------------------+
	// Possible causes of the error:
	//  - Malformed packet (parse error)
	//  - STUN message was a request
	//  - Non-STUN message from the STUN server

	switch {
	case stun.IsMessage():
		return true, .handleSTUNMessage(, )
	case proto.IsChannelData():
		return true, .handleChannelData()
	case .stunServerAddr != nil && .String() == .stunServerAddr.String():
		// Received from STUN server but it is not a STUN message
		return true, errNonSTUNMessage
	default:
		// Assume, this is an application data
		.log.Tracef("Ignoring non-STUN/TURN packet")
	}

	return false, nil
}

func ( *Client) ( []byte,  net.Addr) error { //nolint:cyclop
	 := make([]byte, len())
	copy(, )

	 := &stun.Message{Raw: }
	if  := .Decode();  != nil {
		return fmt.Errorf("%w: %s", errFailedToDecodeSTUN, .Error())
	}

	if .Type.Class == stun.ClassRequest {
		return fmt.Errorf("%w : %s", errUnexpectedSTUNRequestMessage, .String())
	}

	if .Type.Class == stun.ClassIndication { // nolint:nestif
		switch .Type.Method {
		case stun.MethodData:
			var  proto.PeerAddress
			if  := .GetFrom();  != nil {
				return 
			}
			 = &net.UDPAddr{
				IP:   .IP,
				Port: .Port,
			}

			var  proto.Data
			if  := .GetFrom();  != nil {
				return 
			}

			.log.Tracef("Data indication received from %s", )

			 := .relayedUDPConn()
			if  == nil {
				.log.Debug("No relayed conn allocated")

				return nil // Silently discard
			}
			.HandleInbound(, )
		case stun.MethodConnectionAttempt:
			var  proto.PeerAddress
			if  := .GetFrom();  != nil {
				return 
			}

			 := &net.TCPAddr{
				IP:   .IP,
				Port: .Port,
			}

			var  proto.ConnectionID
			if  := .GetFrom();  != nil {
				return 
			}

			.log.Debugf("Connection attempt from %s", )

			 := .getTCPAllocation()
			if  == nil {
				.log.Debug("No TCP allocation exists")

				return nil // Silently discard
			}

			.HandleConnectionAttempt(, )
		default:
			.log.Debug("Received unsupported STUN method")
		}

		return nil
	}

	// This is a STUN response message (transactional)
	// The type is either:
	// - stun.ClassSuccessResponse
	// - stun.ClassErrorResponse

	 := b64.StdEncoding.EncodeToString(.TransactionID[:])

	.mutexTrMap.Lock()
	,  := .trMap.Find()
	if ! {
		.mutexTrMap.Unlock()
		// Silently discard
		.log.Debugf("No transaction for %s", )

		return nil
	}

	// End the transaction
	.StopRtxTimer()
	.trMap.Delete()
	.mutexTrMap.Unlock()

	if !.WriteResult(client.TransactionResult{
		Msg:     ,
		From:    ,
		Retries: .Retries(),
	}) {
		.log.Debugf("No listener for %s", )
	}

	return nil
}

func ( *Client) ( []byte) error {
	 := &proto.ChannelData{
		Raw: make([]byte, len()),
	}
	copy(.Raw, )
	if  := .Decode();  != nil {
		return 
	}

	 := .relayedUDPConn()
	if  == nil {
		.log.Debug("No relayed conn allocated")

		return nil // Silently discard
	}

	,  := .FindAddrByChannelNumber(uint16(.Number))
	if ! {
		return fmt.Errorf("%w: %d", errChannelBindNotFound, int(.Number))
	}

	.log.Tracef("Channel data received from %s (ch=%d)", .String(), int(.Number))

	.HandleInbound(.Data, )

	return nil
}

func ( *Client) ( string,  int) {
	.mutexTrMap.Lock()
	defer .mutexTrMap.Unlock()

	,  := .trMap.Find()
	if ! {
		return // Already gone
	}

	if  == maxRtxCount {
		// All retransmissions failed
		.trMap.Delete()
		if !.WriteResult(client.TransactionResult{
			Err: fmt.Errorf("%w %s", errAllRetransmissionsFailed, ),
		}) {
			.log.Debug("No listener for transaction")
		}

		return
	}

	.log.Tracef("Retransmitting transaction %s to %s (nRtx=%d)",
		, .To, )
	,  := .conn.WriteTo(.Raw, .To)
	if  != nil {
		.trMap.Delete()
		if !.WriteResult(client.TransactionResult{
			Err: fmt.Errorf("%w %s", errFailedToRetransmitTransaction, ),
		}) {
			.log.Debug("No listener for transaction")
		}

		return
	}
	.StartRtxTimer(.)
}

func ( *Client) ( *client.UDPConn) {
	.mutex.Lock()
	defer .mutex.Unlock()

	.relayedConn = 
}

func ( *Client) () *client.UDPConn {
	.mutex.RLock()
	defer .mutex.RUnlock()

	return .relayedConn
}

func ( *Client) ( *client.TCPAllocation) {
	.mutex.Lock()
	defer .mutex.Unlock()

	.tcpAllocation = 
}

func ( *Client) () *client.TCPAllocation {
	.mutex.RLock()
	defer .mutex.RUnlock()

	return .tcpAllocation
}