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

package ice

import (
	
	

	
	
)

type pairCandidateSelector interface {
	Start()
	ContactCandidates()
	PingCandidate(local, remote Candidate)
	HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr)
	HandleBindingRequest(m *stun.Message, local, remote Candidate)
}

type controllingSelector struct {
	startTime     time.Time
	agent         *Agent
	nominatedPair *CandidatePair
	log           logging.LeveledLogger
}

func ( *controllingSelector) () {
	.startTime = time.Now()
	.nominatedPair = nil
}

func ( *controllingSelector) ( Candidate) bool {
	switch {
	case .Type() == CandidateTypeHost:
		return time.Since(.startTime).Nanoseconds() > .agent.hostAcceptanceMinWait.Nanoseconds()
	case .Type() == CandidateTypeServerReflexive:
		return time.Since(.startTime).Nanoseconds() > .agent.srflxAcceptanceMinWait.Nanoseconds()
	case .Type() == CandidateTypePeerReflexive:
		return time.Since(.startTime).Nanoseconds() > .agent.prflxAcceptanceMinWait.Nanoseconds()
	case .Type() == CandidateTypeRelay:
		return time.Since(.startTime).Nanoseconds() > .agent.relayAcceptanceMinWait.Nanoseconds()
	}

	.log.Errorf("Invalid candidate type: %s", .Type())

	return false
}

func ( *controllingSelector) () {
	switch {
	case .agent.getSelectedPair() != nil:
		if .agent.validateSelectedPair() {
			.log.Trace("Checking keepalive")
			.agent.checkKeepalive()
		}
	case .nominatedPair != nil:
		.nominatePair(.nominatedPair)
	default:
		 := .agent.getBestValidCandidatePair()
		if  != nil && .isNominatable(.Local) && .isNominatable(.Remote) {
			.log.Tracef("Nominatable pair found, nominating (%s, %s)", .Local, .Remote)
			.nominated = true
			.nominatedPair = 
			.nominatePair()

			return
		}
		.agent.pingAllCandidates()
	}
}

func ( *controllingSelector) ( *CandidatePair) {
	// The controlling agent MUST include the USE-CANDIDATE attribute in
	// order to nominate a candidate pair (Section 8.1.1).  The controlled
	// agent MUST NOT include the USE-CANDIDATE attribute in a Binding
	// request.
	,  := stun.Build(stun.BindingRequest, stun.TransactionID,
		stun.NewUsername(.agent.remoteUfrag+":"+.agent.localUfrag),
		UseCandidate(),
		AttrControlling(.agent.tieBreaker),
		PriorityAttr(.Local.Priority()),
		stun.NewShortTermIntegrity(.agent.remotePwd),
		stun.Fingerprint,
	)
	if  != nil {
		.log.Error(.Error())

		return
	}

	.log.Tracef("Ping STUN (nominate candidate pair) from %s to %s", .Local, .Remote)
	.agent.sendBindingRequest(, .Local, .Remote)
}

func ( *controllingSelector) ( *stun.Message, ,  Candidate) { //nolint:cyclop
	.agent.sendBindingSuccess(, , )

	 := .agent.findPair(, )

	if  == nil {
		 = .agent.addPair(, )
		.UpdateRequestReceived()

		return
	}
	.UpdateRequestReceived()

	if .state == CandidatePairStateSucceeded && .nominatedPair == nil && .agent.getSelectedPair() == nil {
		 := .agent.getBestAvailableCandidatePair()
		if  == nil {
			.log.Tracef("No best pair available")
		} else if .equal() && .isNominatable(.Local) && .isNominatable(.Remote) {
			.log.Tracef(
				"The candidate (%s, %s) is the best candidate available, marking it as nominated",
				.Local,
				.Remote,
			)
			.nominatedPair = 
			.nominatePair()
		}
	}

	if .agent.userBindingRequestHandler != nil {
		if  := .agent.userBindingRequestHandler(, , , );  {
			.agent.setSelectedPair()
		}
	}
}

func ( *controllingSelector) ( *stun.Message, ,  Candidate,  net.Addr) {
	, ,  := .agent.handleInboundBindingSuccess(.TransactionID)
	if ! {
		.log.Warnf("Discard message from (%s), unknown TransactionID 0x%x", , .TransactionID)

		return
	}

	 := .destination

	// Assert that NAT is not symmetric
	// https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1
	if !addrEqual(, ) {
		.log.Debugf(
			"Discard message: transaction source and destination does not match expected(%s), actual(%s)",
			,
			,
		)

		return
	}

	.log.Tracef("Inbound STUN (SuccessResponse) from %s to %s", , )
	 := .agent.findPair(, )

	if  == nil {
		// This shouldn't happen
		.log.Error("Success response from invalid candidate pair")

		return
	}

	.state = CandidatePairStateSucceeded
	.log.Tracef("Found valid candidate pair: %s", )
	if .isUseCandidate && .agent.getSelectedPair() == nil {
		.agent.setSelectedPair()
	}

	.UpdateRoundTripTime()
}

func ( *controllingSelector) (,  Candidate) {
	,  := stun.Build(stun.BindingRequest, stun.TransactionID,
		stun.NewUsername(.agent.remoteUfrag+":"+.agent.localUfrag),
		AttrControlling(.agent.tieBreaker),
		PriorityAttr(.Priority()),
		stun.NewShortTermIntegrity(.agent.remotePwd),
		stun.Fingerprint,
	)
	if  != nil {
		.log.Error(.Error())

		return
	}

	.agent.sendBindingRequest(, , )
}

type controlledSelector struct {
	agent *Agent
	log   logging.LeveledLogger
}

func ( *controlledSelector) () {
}

func ( *controlledSelector) () {
	if .agent.getSelectedPair() != nil {
		if .agent.validateSelectedPair() {
			.log.Trace("Checking keepalive")
			.agent.checkKeepalive()
		}
	} else {
		.agent.pingAllCandidates()
	}
}

func ( *controlledSelector) (,  Candidate) {
	,  := stun.Build(stun.BindingRequest, stun.TransactionID,
		stun.NewUsername(.agent.remoteUfrag+":"+.agent.localUfrag),
		AttrControlled(.agent.tieBreaker),
		PriorityAttr(.Priority()),
		stun.NewShortTermIntegrity(.agent.remotePwd),
		stun.Fingerprint,
	)
	if  != nil {
		.log.Error(.Error())

		return
	}

	.agent.sendBindingRequest(, , )
}

func ( *controlledSelector) ( *stun.Message, ,  Candidate,  net.Addr) {
	//nolint:godox
	// TODO according to the standard we should specifically answer a failed nomination:
	// https://tools.ietf.org/html/rfc8445#section-7.3.1.5
	// If the controlled agent does not accept the request from the
	// controlling agent, the controlled agent MUST reject the nomination
	// request with an appropriate error code response (e.g., 400)
	// [RFC5389].

	, ,  := .agent.handleInboundBindingSuccess(.TransactionID)
	if ! {
		.log.Warnf("Discard message from (%s), unknown TransactionID 0x%x", , .TransactionID)

		return
	}

	 := .destination

	// Assert that NAT is not symmetric
	// https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1
	if !addrEqual(, ) {
		.log.Debugf(
			"Discard message: transaction source and destination does not match expected(%s), actual(%s)",
			,
			,
		)

		return
	}

	.log.Tracef("Inbound STUN (SuccessResponse) from %s to %s", , )

	 := .agent.findPair(, )
	if  == nil {
		// This shouldn't happen
		.log.Error("Success response from invalid candidate pair")

		return
	}

	.state = CandidatePairStateSucceeded
	.log.Tracef("Found valid candidate pair: %s", )
	if .nominateOnBindingSuccess {
		if  := .agent.getSelectedPair();  == nil ||
			( !=  &&
				(!.agent.needsToCheckPriorityOnNominated() || .priority() <= .priority())) {
			.agent.setSelectedPair()
		} else if  !=  {
			.log.Tracef("Ignore nominate new pair %s, already nominated pair %s", , )
		}
	}

	.UpdateRoundTripTime()
}

func ( *controlledSelector) ( *stun.Message, ,  Candidate) { //nolint:cyclop
	 := .agent.findPair(, )
	if  == nil {
		 = .agent.addPair(, )
	}
	.UpdateRequestReceived()

	if .Contains(stun.AttrUseCandidate) { //nolint:nestif
		// https://tools.ietf.org/html/rfc8445#section-7.3.1.5

		if .state == CandidatePairStateSucceeded {
			// If the state of this pair is Succeeded, it means that the check
			// previously sent by this pair produced a successful response and
			// generated a valid pair (Section 7.2.5.3.2).  The agent sets the
			// nominated flag value of the valid pair to true.
			 := .agent.getSelectedPair()
			if  == nil ||
				( !=  &&
					(!.agent.needsToCheckPriorityOnNominated() ||
						.priority() <= .priority())) {
				.agent.setSelectedPair()
			} else if  !=  {
				.log.Tracef("Ignore nominate new pair %s, already nominated pair %s", , )
			}
		} else {
			// If the received Binding request triggered a new check to be
			// enqueued in the triggered-check queue (Section 7.3.1.4), once the
			// check is sent and if it generates a successful response, and
			// generates a valid pair, the agent sets the nominated flag of the
			// pair to true.  If the request fails (Section 7.2.5.2), the agent
			// MUST remove the candidate pair from the valid list, set the
			// candidate pair state to Failed, and set the checklist state to
			// Failed.
			.nominateOnBindingSuccess = true
		}
	}

	.agent.sendBindingSuccess(, , )
	.PingCandidate(, )

	if .agent.userBindingRequestHandler != nil {
		if  := .agent.userBindingRequestHandler(, , , );  {
			.agent.setSelectedPair()
		}
	}
}

type liteSelector struct {
	pairCandidateSelector
}

// A lite selector should not contact candidates.
func ( *liteSelector) () {
	if ,  := .pairCandidateSelector.(*controllingSelector);  {
		//nolint:godox
		// https://github.com/pion/ice/issues/96
		// TODO: implement lite controlling agent. For now falling back to full agent.
		// This only happens if both peers are lite. See RFC 8445 S6.1.1 and S6.2
		.pairCandidateSelector.ContactCandidates()
	} else if ,  := .pairCandidateSelector.(*controlledSelector);  {
		.agent.validateSelectedPair()
	}
}