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

package client

import (
	
	
	
	
	
	

	
	
	
)

var (
	_ transport.TCPListener = (*TCPAllocation)(nil) // Includes type check for net.Listener
	_ transport.Dialer      = (*TCPAllocation)(nil)
)

func noDeadline() time.Time {
	return time.Time{}
}

// TCPAllocation is an active TCP allocation on the TURN server
// as specified by RFC 6062.
// The allocation can be used to Dial/Accept relayed outgoing/incoming TCP connections.
type TCPAllocation struct {
	connAttemptCh chan *connectionAttempt
	acceptTimer   *time.Timer
	allocation
}

// NewTCPAllocation creates a new instance of TCPConn.
func ( *AllocationConfig) *TCPAllocation {
	 := &TCPAllocation{
		connAttemptCh: make(chan *connectionAttempt, 10),
		acceptTimer:   time.NewTimer(time.Duration(math.MaxInt64)),
		allocation: allocation{
			client:      .Client,
			relayedAddr: .RelayedAddr,
			serverAddr:  .ServerAddr,
			username:    .Username,
			realm:       .Realm,
			permMap:     newPermissionMap(),
			integrity:   .Integrity,
			_nonce:      .Nonce,
			_lifetime:   .Lifetime,
			net:         .Net,
			log:         .Log,
		},
	}

	.log.Debugf("Initial lifetime: %d seconds", int(.lifetime().Seconds()))

	.refreshAllocTimer = NewPeriodicTimer(
		timerIDRefreshAlloc,
		.onRefreshTimers,
		.lifetime()/2,
	)

	.refreshPermsTimer = NewPeriodicTimer(
		timerIDRefreshPerms,
		.onRefreshTimers,
		permRefreshInterval,
	)

	if .refreshAllocTimer.Start() {
		.log.Debug("Started refreshAllocTimer")
	}
	if .refreshPermsTimer.Start() {
		.log.Debug("Started refreshPermsTimer")
	}

	return 
}

// Connect sends a Connect request to the turn server and returns a chosen connection ID.
func ( *TCPAllocation) ( net.Addr) (proto.ConnectionID, error) {
	 := []stun.Setter{
		stun.TransactionID,
		stun.NewType(stun.MethodConnect, stun.ClassRequest),
		addr2PeerAddress(),
		.username,
		.realm,
		.nonce(),
		.integrity,
		stun.Fingerprint,
	}

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

	.log.Debugf("Send connect request (peer=%v)", )
	,  := .client.PerformTransaction(, .serverAddr, false)
	if  != nil {
		return 0, 
	}

	 := .Msg

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

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

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

	.log.Debugf("Connect request successful (cid=%v)", )

	return , nil
}

// Dial connects to the address on the named network.
func ( *TCPAllocation) (,  string) (net.Conn, error) {
	,  := net.ResolveTCPAddr(, )
	if  != nil {
		return nil, 
	}

	return .DialTCP(, nil, )
}

// DialWithConn connects to the address on the named network with an already existing connection.
// The provided connection must be an already connected TCP connection to the TURN server.
func ( *TCPAllocation) ( net.Conn, ,  string) (*TCPConn, error) {
	,  := net.ResolveTCPAddr(, )
	if  != nil {
		return nil, 
	}

	return .DialTCPWithConn(, , )
}

// DialTCP acts like Dial for TCP networks.
func ( *TCPAllocation) ( string, ,  *net.TCPAddr) (*TCPConn, error) {
	var  *net.TCPAddr
	if ,  := .serverAddr.(*net.TCPAddr);  {
		 = &net.TCPAddr{
			IP:   .IP,
			Port: .Port,
		}
	} else if ,  := .serverAddr.(*net.UDPAddr);  {
		 = &net.TCPAddr{
			IP:   .IP,
			Port: .Port,
		}
	} else {
		return nil, errInvalidTURNAddress
	}

	,  := .net.DialTCP(, , )
	if  != nil {
		return nil, 
	}

	,  := .DialTCPWithConn(, , )
	if  != nil {
		.Close() //nolint:errcheck,gosec
	}

	return , 
}

// DialTCPWithConn acts like DialWithConn for TCP networks.
func ( *TCPAllocation) ( net.Conn,  string,  *net.TCPAddr) (*TCPConn, error) {
	var  error

	// Check if we have a permission for the destination IP addr
	,  := .permMap.find()
	if ! {
		 = &permission{}
		.permMap.insert(, )
	}

	for  := 0;  < maxRetryAttempts; ++ {
		if  = .createPermission(, ); !errors.Is(, errTryAgain) {
			break
		}
	}
	if  != nil {
		return nil, 
	}

	// Send connect request if haven't done so.
	,  := .Connect()
	if  != nil {
		return nil, 
	}

	,  := .(transport.TCPConn)
	if ! {
		return nil, errTCPAddrCast
	}

	 := &TCPConn{
		TCPConn:       ,
		ConnectionID:  ,
		remoteAddress: ,
		allocation:    ,
	}

	if  := .BindConnection(, );  != nil {
		return nil, fmt.Errorf("failed to bind connection: %w", )
	}

	return , nil
}

// BindConnection associates the provided connection.
func ( *TCPAllocation) ( *TCPConn,  proto.ConnectionID) error { //nolint:cyclop
	,  := stun.Build(
		stun.TransactionID,
		stun.NewType(stun.MethodConnectionBind, stun.ClassRequest),
		,
		.username,
		.realm,
		.nonce(),
		.integrity,
		stun.Fingerprint,
	)
	if  != nil {
		return 
	}

	.log.Debugf("Send connectionBind request (cid=%v)", )

	_,  = .Write(.Raw)
	if  != nil {
		return 
	}

	// Read exactly one STUN message, any data after belongs to the user
	 := make([]byte, stunHeaderSize)
	,  := .Read()
	if  != stunHeaderSize {
		return errIncompleteTURNFrame
	} else if  != nil {
		return 
	}

	if !stun.IsMessage() {
		return errInvalidTURNFrame
	}

	 := binary.BigEndian.Uint16([2:4]) + stunHeaderSize
	 := make([]byte, )
	copy(, )
	_,  = .Read([stunHeaderSize:])
	if  != nil {
		return 
	}
	 := &stun.Message{Raw: }
	if  = .Decode();  != nil {
		return fmt.Errorf("failed to decode STUN message: %w", )
	}

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

		return fmt.Errorf("%s", .Type) //nolint:goerr113
	case stun.ClassSuccessResponse:
		.log.Debug("Successful connectionBind request")

		return nil
	default:
		return fmt.Errorf("%w: %s", errUnexpectedSTUNRequestMessage, .String())
	}
}

// Accept waits for and returns the next connection to the listener.
func ( *TCPAllocation) () (net.Conn, error) {
	return .AcceptTCP()
}

// AcceptTCP accepts the next incoming call and returns the new connection.
func ( *TCPAllocation) () (transport.TCPConn, error) {
	,  := net.ResolveTCPAddr("tcp4", .serverAddr.String())
	if  != nil {
		return nil, 
	}

	,  := .net.DialTCP("tcp", nil, )
	if  != nil {
		return nil, 
	}

	,  := .AcceptTCPWithConn()
	if  != nil {
		.Close() //nolint:errcheck,gosec
	}

	return , 
}

// AcceptTCPWithConn accepts the next incoming call and returns the new connection.
func ( *TCPAllocation) ( net.Conn) (*TCPConn, error) {
	select {
	case  := <-.connAttemptCh:

		,  := .(transport.TCPConn)
		if ! {
			return nil, errTCPAddrCast
		}

		 := &TCPConn{
			TCPConn:       ,
			ConnectionID:  .cid,
			remoteAddress: .from,
			allocation:    ,
		}

		if  := .BindConnection(, .cid);  != nil {
			return nil, fmt.Errorf("failed to bind connection: %w", )
		}

		return , nil
	case <-.acceptTimer.C:
		return nil, &net.OpError{
			Op:   "accept",
			Net:  .Addr().Network(),
			Addr: .Addr(),
			Err:  newTimeoutError("i/o timeout"),
		}
	}
}

// SetDeadline sets the deadline associated with the listener. A zero time value disables the deadline.
func ( *TCPAllocation) ( time.Time) error {
	var  time.Duration
	if  == noDeadline() {
		 = time.Duration(math.MaxInt64)
	} else {
		 = time.Until()
	}
	.acceptTimer.Reset()

	return nil
}

// Close releases the allocation
// Any blocked Accept operations will be unblocked and return errors.
// Any opened connection via Dial/Accept will be closed.
func ( *TCPAllocation) () error {
	.refreshAllocTimer.Stop()
	.refreshPermsTimer.Stop()

	.client.OnDeallocated(.relayedAddr)

	return .refreshAllocation(0, true /* dontWait=true */)
}

// Addr returns the relayed address of the allocation.
func ( *TCPAllocation) () net.Addr {
	return .relayedAddr
}

// HandleConnectionAttempt is called by the TURN client
// when it receives a ConnectionAttempt indication.
func ( *TCPAllocation) ( *net.TCPAddr,  proto.ConnectionID) {
	.connAttemptCh <- &connectionAttempt{
		from: ,
		cid:  ,
	}
}