package time

import (
	"fmt"
	"io"
	"net/http/httptrace"
	"strconv"
	"strings"
	"time"

	"github.com/pkg/errors"
	"github.com/projectdiscovery/gologger"
	"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
	"github.com/projectdiscovery/retryablehttp-go"
)

// Analyzer is a time delay analyzer for the fuzzer
type Analyzer struct {
}

const (
	DefaultSleepDuration             = int(7)
	DefaultRequestsLimit             = int(4)
	DefaultTimeCorrelationErrorRange = float64(0.15)
	DefaultTimeSlopeErrorRange       = float64(0.30)
	DefaultLowSleepTimeSeconds       = float64(3)

	defaultSleepTimeDuration = 7 * time.Second
)

var _ analyzers.Analyzer = &Analyzer{}

func init() {
	analyzers.RegisterAnalyzer("time_delay", &Analyzer{})
}

// Name is the name of the analyzer
func (a *Analyzer) Name() string {
	return "time_delay"
}

// ApplyInitialTransformation applies the transformation to the initial payload.
//
// It supports the below payloads -
//   - [SLEEPTIME] => sleep_duration
//   - [INFERENCE] => Inference payload for time delay analyzer
//
// It also applies the payload transformations to the payload
// which includes [RANDNUM] and [RANDSTR]
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
	duration := DefaultSleepDuration
	if len(params) > 0 {
		if v, ok := params["sleep_duration"]; ok {
			duration, ok = v.(int)
			if !ok {
				duration = DefaultSleepDuration
				gologger.Warning().Msgf("Invalid sleep_duration parameter type, using default value: %d", duration)
			}
		}
	}
	data = strings.ReplaceAll(data, "[SLEEPTIME]", strconv.Itoa(duration))
	data = analyzers.ApplyPayloadTransformations(data)

	// Also support [INFERENCE] for the time delay analyzer
	if strings.Contains(data, "[INFERENCE]") {
		randInt := analyzers.GetRandomInteger()
		data = strings.ReplaceAll(data, "[INFERENCE]", fmt.Sprintf("%d=%d", randInt, randInt))
	}
	return data
}

func (a *Analyzer) parseAnalyzerParameters(params map[string]interface{}) (int, int, float64, float64, error) {
	requestsLimit := DefaultRequestsLimit
	sleepDuration := DefaultSleepDuration
	timeCorrelationErrorRange := DefaultTimeCorrelationErrorRange
	timeSlopeErrorRange := DefaultTimeSlopeErrorRange

	if len(params) == 0 {
		return requestsLimit, sleepDuration, timeCorrelationErrorRange, timeSlopeErrorRange, nil
	}
	var ok bool
	for k, v := range params {
		switch k {
		case "sleep_duration":
			sleepDuration, ok = v.(int)
		case "requests_limit":
			requestsLimit, ok = v.(int)
		case "time_correlation_error_range":
			timeCorrelationErrorRange, ok = v.(float64)
		case "time_slope_error_range":
			timeSlopeErrorRange, ok = v.(float64)
		}
		if !ok {
			return 0, 0, 0, 0, errors.Errorf("invalid parameter type for %s", k)
		}
	}
	return requestsLimit, sleepDuration, timeCorrelationErrorRange, timeSlopeErrorRange, nil
}

// Analyze is the main function for the analyzer
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
	if options.ResponseTimeDelay < defaultSleepTimeDuration {
		return false, "", nil
	}

	// Parse parameters for this analyzer if any or use default values
	requestsLimit, sleepDuration, timeCorrelationErrorRange, timeSlopeErrorRange, err :=
		a.parseAnalyzerParameters(options.AnalyzerParameters)
	if err != nil {
		return false, "", err
	}

	reqSender := func(delay int) (float64, error) {
		gr := options.FuzzGenerated
		replaced := strings.ReplaceAll(gr.OriginalPayload, "[SLEEPTIME]", strconv.Itoa(delay))
		replaced = a.ApplyInitialTransformation(replaced, options.AnalyzerParameters)

		if err := gr.Component.SetValue(gr.Key, replaced); err != nil {
			return 0, errors.Wrap(err, "could not set value in component")
		}

		rebuilt, err := gr.Component.Rebuild()
		if err != nil {
			return 0, errors.Wrap(err, "could not rebuild request")
		}

		// copy headers from the original request since Rebuild() only
		// carries headers present at parse time, not post-parse injections
		for k, vs := range gr.Request.Header {
			if gr.Component.Name() == "header" && k == gr.Key {
				continue
			}
			rebuilt.Header[k] = vs
		}

		gologger.Verbose().Msgf("[%s] Sending request with %d delay for: %s", a.Name(), delay, rebuilt.String())

		timeTaken, err := doHTTPRequestWithTimeTracing(rebuilt, options.HttpClient)
		if err != nil {
			return 0, errors.Wrap(err, "could not do request with time tracing")
		}
		return timeTaken, nil
	}

	// Check the baseline delay of the request by doing two requests
	baselineDelay, err := getBaselineDelay(reqSender)
	if err != nil {
		return false, "", err
	}

	matched, matchReason, err := checkTimingDependency(
		requestsLimit,
		sleepDuration,
		timeCorrelationErrorRange,
		timeSlopeErrorRange,
		baselineDelay,
		reqSender,
	)
	if err != nil {
		return false, "", err
	}
	if matched {
		return true, matchReason, nil
	}
	return false, "", nil
}

func getBaselineDelay(reqSender timeDelayRequestSender) (float64, error) {
	var delays []float64
	// Use zero or a very small delay to measure baseline
	for i := 0; i < 3; i++ {
		delay, err := reqSender(0)
		if err != nil {
			return 0, errors.Wrap(err, "could not get baseline delay")
		}
		delays = append(delays, delay)
	}

	var total float64
	for _, d := range delays {
		total += d
	}
	avg := total / float64(len(delays))
	return avg, nil
}

// doHTTPRequestWithTimeTracing does a http request with time tracing
func doHTTPRequestWithTimeTracing(req *retryablehttp.Request, httpclient *retryablehttp.Client) (float64, error) {
	var serverTime time.Duration
	var wroteRequest time.Time

	trace := &httptrace.ClientTrace{
		WroteHeaders: func() {
			wroteRequest = time.Now()
		},
		GotFirstResponseByte: func() {
			serverTime = time.Since(wroteRequest)
		},
	}
	req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
	resp, err := httpclient.Do(req)
	if err != nil {
		return 0, errors.Wrap(err, "could not do request")
	}

	_, err = io.ReadAll(resp.Body)
	if err != nil {
		return 0, errors.Wrap(err, "could not read response body")
	}
	return serverTime.Seconds(), nil
}
