package http

import (
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
)

// requestGenerator generates requests sequentially based on various
// configurations for a http request template.
//
// If payload values are present, an iterator is created for the payload
// values. Paths and Raw requests are supported as base input, so
// it will automatically select between them based on the template.
type requestGenerator struct {
	currentIndex     int
	currentPayloads  map[string]interface{}
	okCurrentPayload bool
	request          *Request
	options          *protocols.ExecutorOptions
	payloadIterator  *generators.Iterator
	interactshURLs   []string
	onceFlow         map[string]struct{}
}

// LeaveDefaultPorts skips normalization of default standard ports
var LeaveDefaultPorts = false

// newGenerator creates a new request generator instance
func (request *Request) newGenerator(disablePayloads bool) *requestGenerator {
	generator := &requestGenerator{
		request:  request,
		options:  request.options,
		onceFlow: make(map[string]struct{}),
	}

	if len(request.Payloads) > 0 && !disablePayloads {
		generator.payloadIterator = request.generator.NewIterator()
	}
	return generator
}

// nextValue returns the next path or the next raw request depending on user input
// It returns false if all the inputs have been exhausted by the generator instance.
func (r *requestGenerator) nextValue() (value string, payloads map[string]interface{}, result bool) {
	// Iterate each payload sequentially for each request path/raw
	//
	// If the sequence has finished for the current payload values
	// then restart the sequence from the beginning and move on to the next payloads values
	// otherwise use the last request.
	var sequence []string
	switch {
	case len(r.request.Path) > 0:
		sequence = r.request.Path
	case len(r.request.Raw) > 0:
		sequence = r.request.Raw
	default:
		return "", nil, false
	}

	hasPayloadIterator := r.payloadIterator != nil

	if hasPayloadIterator && r.currentPayloads == nil {
		r.currentPayloads, r.okCurrentPayload = r.payloadIterator.Value()
	}

	var request string
	var shouldContinue bool
	if nextRequest, nextIndex, found := r.findNextIteration(sequence, r.currentIndex); found {
		r.currentIndex = nextIndex + 1
		request = nextRequest
		shouldContinue = true
	} else {
		// if found is false which happens at end of iteration of reqData(path or raw request)
		// try again from start with index 0
		if nextRequest, nextIndex, found := r.findNextIteration(sequence, 0); found && hasPayloadIterator {
			r.currentIndex = nextIndex + 1
			request = nextRequest
			shouldContinue = true
		}
	}

	if shouldContinue {
		if r.hasMarker(request, Once) {
			r.applyMark(request, Once)
		}
		if hasPayloadIterator {
			return request, generators.CopyMap(r.currentPayloads), r.okCurrentPayload
		}
		// next should return a copy of payloads and not pointer to payload to avoid data race
		return request, generators.CopyMap(r.currentPayloads), true
	} else {
		return "", nil, false
	}
}

// findNextIteration iterates and returns next Request(path or raw request)
// at end of each iteration payload is incremented
func (r *requestGenerator) findNextIteration(sequence []string, index int) (string, int, bool) {
	for i, request := range sequence[index:] {
		if r.wasMarked(request, Once) {
			// if request contains flowmark i.e `@once` and is marked skip it
			continue
		}
		return request, index + i, true

	}
	// move on to next payload if current payload is applied/returned for all Requests(path or raw request)
	if r.payloadIterator != nil {
		r.currentPayloads, r.okCurrentPayload = r.payloadIterator.Value()
	}
	return "", 0, false
}

// applyMark marks given request i.e blacklist request
func (r *requestGenerator) applyMark(request string, mark flowMark) {
	switch mark {
	case Once:
		r.onceFlow[request] = struct{}{}
	}

}

// wasMarked checks if request is marked using request blacklist
func (r *requestGenerator) wasMarked(request string, mark flowMark) bool {
	switch mark {
	case Once:
		_, ok := r.onceFlow[request]
		return ok
	}
	return false
}

// hasMarker returns true if request has a marker (ex: @once which means request should only be executed once)
func (r *requestGenerator) hasMarker(request string, mark flowMark) bool {
	fo, hasOverrides := parseFlowAnnotations(request)
	return hasOverrides && fo == mark
}

// Remaining returns the number of requests that are still left to be
// generated (and therefore to be sent) by this generator.
func (r *requestGenerator) Remaining() int {
	var sequence []string
	switch {
	case len(r.request.Path) > 0:
		sequence = r.request.Path
	case len(r.request.Raw) > 0:
		sequence = r.request.Raw
	default:
		return 0
	}

	remainingInCurrentPass := 0
	for i := r.currentIndex; i < len(sequence); i++ {
		if !r.hasMarker(sequence[i], Once) {
			remainingInCurrentPass++
		}
	}

	if r.payloadIterator == nil {
		return remainingInCurrentPass
	}

	numRemainingPayloadSets := r.payloadIterator.Remaining()
	totalValidInSequence := 0
	for _, req := range sequence {
		if !r.hasMarker(req, Once) {
			totalValidInSequence++
		}
	}

	// Total remaining = remaining in current pass + (remaining payload sets * requests per full pass)
	return remainingInCurrentPass + numRemainingPayloadSets*totalValidInSequence
}

func (r *requestGenerator) Total() int {
	var sequence []string
	switch {
	case len(r.request.Path) > 0:
		sequence = r.request.Path
	case len(r.request.Raw) > 0:
		sequence = r.request.Raw
	default:
		return 0
	}

	applicableRequests := 0
	additionalRequests := 0
	for _, request := range sequence {
		if !r.hasMarker(request, Once) {
			applicableRequests++
		} else {
			additionalRequests++
		}
	}

	if r.payloadIterator == nil {
		return applicableRequests + additionalRequests
	}

	return (applicableRequests * r.payloadIterator.Total()) + additionalRequests
}
