package ffuf

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"net/textproto"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"

	"github.com/pelletier/go-toml"
)

type ConfigOptions struct {
	Filter  FilterOptions  `json:"filters"`
	General GeneralOptions `json:"general"`
	HTTP    HTTPOptions    `json:"http"`
	Input   InputOptions   `json:"input"`
	Matcher MatcherOptions `json:"matchers"`
	Output  OutputOptions  `json:"output"`
}

type HTTPOptions struct {
	Cookies           []string `json:"-"` // this is appended in headers
	Data              string   `json:"data"`
	FollowRedirects   bool     `json:"follow_redirects"`
	Headers           []string `json:"headers"`
	IgnoreBody        bool     `json:"ignore_body"`
	Method            string   `json:"method"`
	ProxyURL          string   `json:"proxy_url"`
	Raw               bool     `json:"raw"`
	Recursion         bool     `json:"recursion"`
	RecursionDepth    int      `json:"recursion_depth"`
	RecursionStrategy string   `json:"recursion_strategy"`
	ReplayProxyURL    string   `json:"replay_proxy_url"`
	SNI               string   `json:"sni"`
	Timeout           int      `json:"timeout"`
	URL               string   `json:"url"`
	Http2             bool     `json:"http2"`
	ClientCert        string   `json:"client-cert"`
	ClientKey         string   `json:"client-key"`
}

type GeneralOptions struct {
	AutoCalibration           bool     `json:"autocalibration"`
	AutoCalibrationKeyword    string   `json:"autocalibration_keyword"`
	AutoCalibrationPerHost    bool     `json:"autocalibration_per_host"`
	AutoCalibrationStrategies []string `json:"autocalibration_strategies"`
	AutoCalibrationStrings    []string `json:"autocalibration_strings"`
	Colors                    bool     `json:"colors"`
	ConfigFile                string   `toml:"-" json:"config_file"`
	Delay                     string   `json:"delay"`
	Json                      bool     `json:"json"`
	MaxTime                   int      `json:"maxtime"`
	MaxTimeJob                int      `json:"maxtime_job"`
	Noninteractive            bool     `json:"noninteractive"`
	Quiet                     bool     `json:"quiet"`
	Rate                      int      `json:"rate"`
	ScraperFile               string   `json:"scraperfile"`
	Scrapers                  string   `json:"scrapers"`
	Searchhash                string   `json:"-"`
	ShowVersion               bool     `toml:"-" json:"-"`
	StopOn403                 bool     `json:"stop_on_403"`
	StopOnAll                 bool     `json:"stop_on_all"`
	StopOnErrors              bool     `json:"stop_on_errors"`
	Threads                   int      `json:"threads"`
	Verbose                   bool     `json:"verbose"`
}

type InputOptions struct {
	DirSearchCompat        bool     `json:"dirsearch_compat"`
	Encoders               []string `json:"encoders"`
	Extensions             string   `json:"extensions"`
	IgnoreWordlistComments bool     `json:"ignore_wordlist_comments"`
	InputMode              string   `json:"input_mode"`
	InputNum               int      `json:"input_num"`
	InputShell             string   `json:"input_shell"`
	Inputcommands          []string `json:"input_commands"`
	Request                string   `json:"request_file"`
	RequestProto           string   `json:"request_proto"`
	Wordlists              []string `json:"wordlists"`
}

type OutputOptions struct {
	DebugLog            string `json:"debug_log"`
	OutputDirectory     string `json:"output_directory"`
	OutputFile          string `json:"output_file"`
	OutputFormat        string `json:"output_format"`
	OutputSkipEmptyFile bool   `json:"output_skip_empty"`
}

type FilterOptions struct {
	Mode   string `json:"mode"`
	Lines  string `json:"lines"`
	Regexp string `json:"regexp"`
	Size   string `json:"size"`
	Status string `json:"status"`
	Time   string `json:"time"`
	Words  string `json:"words"`
}

type MatcherOptions struct {
	Mode   string `json:"mode"`
	Lines  string `json:"lines"`
	Regexp string `json:"regexp"`
	Size   string `json:"size"`
	Status string `json:"status"`
	Time   string `json:"time"`
	Words  string `json:"words"`
}

// NewConfigOptions returns a newly created ConfigOptions struct with default values
func NewConfigOptions() *ConfigOptions {
	c := &ConfigOptions{}
	c.Filter.Mode = "or"
	c.Filter.Lines = ""
	c.Filter.Regexp = ""
	c.Filter.Size = ""
	c.Filter.Status = ""
	c.Filter.Time = ""
	c.Filter.Words = ""
	c.General.AutoCalibration = false
	c.General.AutoCalibrationKeyword = "FUZZ"
	c.General.AutoCalibrationStrategies = []string{"basic"}
	c.General.Colors = false
	c.General.Delay = ""
	c.General.Json = false
	c.General.MaxTime = 0
	c.General.MaxTimeJob = 0
	c.General.Noninteractive = false
	c.General.Quiet = false
	c.General.Rate = 0
	c.General.Searchhash = ""
	c.General.ScraperFile = ""
	c.General.Scrapers = "all"
	c.General.ShowVersion = false
	c.General.StopOn403 = false
	c.General.StopOnAll = false
	c.General.StopOnErrors = false
	c.General.Threads = 40
	c.General.Verbose = false
	c.HTTP.Data = ""
	c.HTTP.FollowRedirects = false
	c.HTTP.IgnoreBody = false
	c.HTTP.Method = ""
	c.HTTP.ProxyURL = ""
	c.HTTP.Raw = false
	c.HTTP.Recursion = false
	c.HTTP.RecursionDepth = 0
	c.HTTP.RecursionStrategy = "default"
	c.HTTP.ReplayProxyURL = ""
	c.HTTP.Timeout = 10
	c.HTTP.SNI = ""
	c.HTTP.URL = ""
	c.HTTP.Http2 = false
	c.Input.DirSearchCompat = false
	c.Input.Encoders = []string{}
	c.Input.Extensions = ""
	c.Input.IgnoreWordlistComments = false
	c.Input.InputMode = "clusterbomb"
	c.Input.InputNum = 100
	c.Input.Request = ""
	c.Input.RequestProto = "https"
	c.Matcher.Mode = "or"
	c.Matcher.Lines = ""
	c.Matcher.Regexp = ""
	c.Matcher.Size = ""
	c.Matcher.Status = "200-299,301,302,307,401,403,405,500"
	c.Matcher.Time = ""
	c.Matcher.Words = ""
	c.Output.DebugLog = ""
	c.Output.OutputDirectory = ""
	c.Output.OutputFile = ""
	c.Output.OutputFormat = "json"
	c.Output.OutputSkipEmptyFile = false
	return c
}

// ConfigFromOptions parses the values in ConfigOptions struct, ensures that the values are sane,
// and creates a Config struct out of them.
func ConfigFromOptions(parseOpts *ConfigOptions, ctx context.Context, cancel context.CancelFunc) (*Config, error) {
	//TODO: refactor in a proper flag library that can handle things like required flags
	errs := NewMultierror()
	conf := NewConfig(ctx, cancel)

	var err error
	var err2 error
	if len(parseOpts.HTTP.URL) == 0 && parseOpts.Input.Request == "" {
		errs.Add(fmt.Errorf("-u flag or -request flag is required"))
	}

	// prepare extensions
	if parseOpts.Input.Extensions != "" {
		extensions := strings.Split(parseOpts.Input.Extensions, ",")
		conf.Extensions = extensions
	}

	// Convert cookies to a header
	if len(parseOpts.HTTP.Cookies) > 0 {
		parseOpts.HTTP.Headers = append(parseOpts.HTTP.Headers, "Cookie: "+strings.Join(parseOpts.HTTP.Cookies, "; "))
	}

	//Prepare inputproviders
	conf.InputMode = parseOpts.Input.InputMode

	validmode := false
	for _, mode := range []string{"clusterbomb", "pitchfork", "sniper"} {
		if conf.InputMode == mode {
			validmode = true
		}
	}
	if !validmode {
		errs.Add(fmt.Errorf("Input mode (-mode) %s not recognized", conf.InputMode))
	}

	template := ""
	// sniper mode needs some additional checking
	if conf.InputMode == "sniper" {
		template = "§"

		if len(parseOpts.Input.Wordlists) > 1 {
			errs.Add(fmt.Errorf("sniper mode only supports one wordlist"))
		}

		if len(parseOpts.Input.Inputcommands) > 1 {
			errs.Add(fmt.Errorf("sniper mode only supports one input command"))
		}
	}
	tmpEncoders := make(map[string]string)
	for _, e := range parseOpts.Input.Encoders {
		if strings.Contains(e, ":") {
			key := strings.Split(e, ":")[0]
			val := strings.Split(e, ":")[1]
			tmpEncoders[key] = val
		}
	}
	tmpWordlists := make([]string, 0)
	for _, v := range parseOpts.Input.Wordlists {
		var wl []string
		if runtime.GOOS == "windows" {
			// Try to ensure that Windows file paths like C:\path\to\wordlist.txt:KEYWORD are treated properly
			if FileExists(v) {
				// The wordlist was supplied without a keyword parameter
				wl = []string{v}
			} else {
				filepart := v
				if strings.Contains(filepart, ":") {
					filepart = v[:strings.LastIndex(filepart, ":")]
				}

				if FileExists(filepart) {
					wl = []string{filepart, v[strings.LastIndex(v, ":")+1:]}
				} else {
					// The file was not found. Use full wordlist parameter value for more concise error message down the line
					wl = []string{v}
				}
			}
		} else {
			wl = strings.SplitN(v, ":", 2)
		}
		// Try to use absolute paths for wordlists
		fullpath := ""
		if wl[0] != "-" {
			fullpath, err = filepath.Abs(wl[0])
		} else {
			fullpath = wl[0]
		}

		if err == nil {
			wl[0] = fullpath
		}
		if len(wl) == 2 {
			if conf.InputMode == "sniper" {
				errs.Add(fmt.Errorf("sniper mode does not support wordlist keywords"))
			} else {
				newp := InputProviderConfig{
					Name:    "wordlist",
					Value:   wl[0],
					Keyword: wl[1],
				}
				// Add encoders if set
				enc, ok := tmpEncoders[wl[1]]
				if ok {
					newp.Encoders = enc
				}
				conf.InputProviders = append(conf.InputProviders, newp)
			}
		} else {
			newp := InputProviderConfig{
				Name:     "wordlist",
				Value:    wl[0],
				Keyword:  "FUZZ",
				Template: template,
			}
			// Add encoders if set
			enc, ok := tmpEncoders["FUZZ"]
			if ok {
				newp.Encoders = enc
			}
			conf.InputProviders = append(conf.InputProviders, newp)
		}
		tmpWordlists = append(tmpWordlists, strings.Join(wl, ":"))
	}
	conf.Wordlists = tmpWordlists

	for _, v := range parseOpts.Input.Inputcommands {
		ic := strings.SplitN(v, ":", 2)
		if len(ic) == 2 {
			if conf.InputMode == "sniper" {
				errs.Add(fmt.Errorf("sniper mode does not support command keywords"))
			} else {
				newp := InputProviderConfig{
					Name:    "command",
					Value:   ic[0],
					Keyword: ic[1],
				}
				enc, ok := tmpEncoders[ic[1]]
				if ok {
					newp.Encoders = enc
				}
				conf.InputProviders = append(conf.InputProviders, newp)
				conf.CommandKeywords = append(conf.CommandKeywords, ic[0])
			}
		} else {
			newp := InputProviderConfig{
				Name:     "command",
				Value:    ic[0],
				Keyword:  "FUZZ",
				Template: template,
			}
			enc, ok := tmpEncoders["FUZZ"]
			if ok {
				newp.Encoders = enc
			}
			conf.InputProviders = append(conf.InputProviders, newp)
			conf.CommandKeywords = append(conf.CommandKeywords, "FUZZ")
		}
	}

	if len(conf.InputProviders) == 0 {
		errs.Add(fmt.Errorf("Either -w or --input-cmd flag is required"))
	}

	// Prepare the request using body
	if parseOpts.Input.Request != "" {
		err := parseRawRequest(parseOpts, &conf)
		if err != nil {
			errmsg := fmt.Sprintf("Could not parse raw request: %s", err)
			errs.Add(fmt.Errorf(errmsg))
		}
	}

	//Prepare URL
	if parseOpts.HTTP.URL != "" {
		conf.Url = parseOpts.HTTP.URL
	}

	// Prepare SNI
	if parseOpts.HTTP.SNI != "" {
		conf.SNI = parseOpts.HTTP.SNI
	}

	// prepare cert
	if parseOpts.HTTP.ClientCert != "" {
		conf.ClientCert = parseOpts.HTTP.ClientCert
	}
	if parseOpts.HTTP.ClientKey != "" {
		conf.ClientKey = parseOpts.HTTP.ClientKey
	}

	//Prepare headers and make canonical
	for _, v := range parseOpts.HTTP.Headers {
		hs := strings.SplitN(v, ":", 2)
		if len(hs) == 2 {
			// trim and make canonical
			// except if used in custom defined header
			var CanonicalNeeded = true
			for _, a := range conf.CommandKeywords {
				if strings.Contains(hs[0], a) {
					CanonicalNeeded = false
				}
			}
			// check if part of InputProviders
			if CanonicalNeeded {
				for _, b := range conf.InputProviders {
					if strings.Contains(hs[0], b.Keyword) {
						CanonicalNeeded = false
					}
				}
			}
			if CanonicalNeeded {
				var CanonicalHeader = textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(hs[0]))
				conf.Headers[CanonicalHeader] = strings.TrimSpace(hs[1])
			} else {
				conf.Headers[strings.TrimSpace(hs[0])] = strings.TrimSpace(hs[1])
			}
		} else {
			errs.Add(fmt.Errorf("Header defined by -H needs to have a value. \":\" should be used as a separator"))
		}
	}

	//Prepare delay
	d := strings.Split(parseOpts.General.Delay, "-")
	if len(d) > 2 {
		errs.Add(fmt.Errorf("Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\""))
	} else if len(d) == 2 {
		conf.Delay.IsRange = true
		conf.Delay.HasDelay = true
		conf.Delay.Min, err = strconv.ParseFloat(d[0], 64)
		conf.Delay.Max, err2 = strconv.ParseFloat(d[1], 64)
		if err != nil || err2 != nil {
			errs.Add(fmt.Errorf("Delay range min and max values need to be valid floats. For example: 0.1-0.5"))
		}
	} else if len(parseOpts.General.Delay) > 0 {
		conf.Delay.IsRange = false
		conf.Delay.HasDelay = true
		conf.Delay.Min, err = strconv.ParseFloat(parseOpts.General.Delay, 64)
		if err != nil {
			errs.Add(fmt.Errorf("Delay needs to be either a single float: \"0.1\" or a range of floats, delimited by dash: \"0.1-0.8\""))
		}
	}

	// Verify proxy url format
	if len(parseOpts.HTTP.ProxyURL) > 0 {
		u, err := url.Parse(parseOpts.HTTP.ProxyURL)
		if err != nil || u.Opaque != "" || (u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "socks5") {
			errs.Add(fmt.Errorf("Bad proxy url (-x) format. Expected http, https or socks5 url"))
		} else {
			conf.ProxyURL = parseOpts.HTTP.ProxyURL
		}
	}

	// Verify replayproxy url format
	if len(parseOpts.HTTP.ReplayProxyURL) > 0 {
		u, err := url.Parse(parseOpts.HTTP.ReplayProxyURL)
		if err != nil || u.Opaque != "" || (u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "socks5" && u.Scheme != "socks5h") {
			errs.Add(fmt.Errorf("Bad replay-proxy url (-replay-proxy) format. Expected http, https or socks5 url"))
		} else {
			conf.ReplayProxyURL = parseOpts.HTTP.ReplayProxyURL
		}
	}

	//Check the output file format option
	if parseOpts.Output.OutputFile != "" {
		//No need to check / error out if output file isn't defined
		outputFormats := []string{"all", "json", "ejson", "html", "md", "csv", "ecsv"}
		found := false
		for _, f := range outputFormats {
			if f == parseOpts.Output.OutputFormat {
				conf.OutputFormat = f
				found = true
			}
		}
		if !found {
			errs.Add(fmt.Errorf("Unknown output file format (-of): %s", parseOpts.Output.OutputFormat))
		}
	}

	// Auto-calibration strings
	if len(parseOpts.General.AutoCalibrationStrings) > 0 {
		conf.AutoCalibrationStrings = parseOpts.General.AutoCalibrationStrings
	}
	// Auto-calibration strategies
	if len(parseOpts.General.AutoCalibrationStrategies) > 0 {
		conf.AutoCalibrationStrategies = parseOpts.General.AutoCalibrationStrategies
	}
	// Using -acc implies -ac
	if len(parseOpts.General.AutoCalibrationStrings) > 0 {
		conf.AutoCalibration = true
	}
	// Using -acs implies -ac
	if len(parseOpts.General.AutoCalibrationStrategies) > 0 {
		conf.AutoCalibration = true
	}

	if parseOpts.General.Rate < 0 {
		conf.Rate = 0
	} else {
		conf.Rate = int64(parseOpts.General.Rate)
	}

	if conf.Method == "" {
		if parseOpts.HTTP.Method == "" {
			// Only set if defined on command line, because we might be reparsing the CLI after
			// populating it through raw request in the first iteration
			conf.Method = "GET"
		} else {
			conf.Method = parseOpts.HTTP.Method
		}
	} else {
		if parseOpts.HTTP.Method != "" {
			// Method overridden in CLI
			conf.Method = parseOpts.HTTP.Method
		}
	}

	if parseOpts.HTTP.Data != "" {
		// Only set if defined on command line, because we might be reparsing the CLI after
		// populating it through raw request in the first iteration
		conf.Data = parseOpts.HTTP.Data
	}

	// Common stuff
	conf.IgnoreWordlistComments = parseOpts.Input.IgnoreWordlistComments
	conf.DirSearchCompat = parseOpts.Input.DirSearchCompat
	conf.Colors = parseOpts.General.Colors
	conf.InputNum = parseOpts.Input.InputNum

	conf.InputShell = parseOpts.Input.InputShell
	conf.OutputFile = parseOpts.Output.OutputFile
	conf.OutputDirectory = parseOpts.Output.OutputDirectory
	conf.OutputSkipEmptyFile = parseOpts.Output.OutputSkipEmptyFile
	conf.IgnoreBody = parseOpts.HTTP.IgnoreBody
	conf.Quiet = parseOpts.General.Quiet
	conf.ScraperFile = parseOpts.General.ScraperFile
	conf.Scrapers = parseOpts.General.Scrapers
	conf.StopOn403 = parseOpts.General.StopOn403
	conf.StopOnAll = parseOpts.General.StopOnAll
	conf.StopOnErrors = parseOpts.General.StopOnErrors
	conf.FollowRedirects = parseOpts.HTTP.FollowRedirects
	conf.Raw = parseOpts.HTTP.Raw
	conf.Recursion = parseOpts.HTTP.Recursion
	conf.RecursionDepth = parseOpts.HTTP.RecursionDepth
	conf.RecursionStrategy = parseOpts.HTTP.RecursionStrategy
	conf.AutoCalibration = parseOpts.General.AutoCalibration
	conf.AutoCalibrationPerHost = parseOpts.General.AutoCalibrationPerHost
	conf.AutoCalibrationStrategies = parseOpts.General.AutoCalibrationStrategies
	conf.Threads = parseOpts.General.Threads
	conf.Timeout = parseOpts.HTTP.Timeout
	conf.MaxTime = parseOpts.General.MaxTime
	conf.MaxTimeJob = parseOpts.General.MaxTimeJob
	conf.Noninteractive = parseOpts.General.Noninteractive
	conf.Verbose = parseOpts.General.Verbose
	conf.Json = parseOpts.General.Json
	conf.Http2 = parseOpts.HTTP.Http2

	// Check that fmode and mmode have sane values
	valid_opmodes := []string{"and", "or"}
	fmode_found := false
	mmode_found := false
	for _, v := range valid_opmodes {
		if v == parseOpts.Filter.Mode {
			fmode_found = true
		}
		if v == parseOpts.Matcher.Mode {
			mmode_found = true
		}
	}
	if !fmode_found {
		errmsg := fmt.Sprintf("Unrecognized value for parameter fmode: %s, valid values are: and, or", parseOpts.Filter.Mode)
		errs.Add(fmt.Errorf(errmsg))
	}
	if !mmode_found {
		errmsg := fmt.Sprintf("Unrecognized value for parameter mmode: %s, valid values are: and, or", parseOpts.Matcher.Mode)
		errs.Add(fmt.Errorf(errmsg))
	}
	conf.FilterMode = parseOpts.Filter.Mode
	conf.MatcherMode = parseOpts.Matcher.Mode

	if conf.AutoCalibrationPerHost {
		// AutoCalibrationPerHost implies AutoCalibration
		conf.AutoCalibration = true
	}

	// Handle copy as curl situation where POST method is implied by --data flag. If method is set to anything but GET, NOOP
	if len(conf.Data) > 0 &&
		conf.Method == "GET" &&
		//don't modify the method automatically if a request file is being used as input
		len(parseOpts.Input.Request) == 0 {

		conf.Method = "POST"
	}

	conf.CommandLine = strings.Join(os.Args, " ")

	newInputProviders := []InputProviderConfig{}
	for _, provider := range conf.InputProviders {
		if provider.Template != "" {
			if !templatePresent(provider.Template, &conf) {
				errmsg := fmt.Sprintf("Template %s defined, but not found in pairs in headers, method, URL or POST data.", provider.Template)
				errs.Add(fmt.Errorf(errmsg))
			} else {
				newInputProviders = append(newInputProviders, provider)
			}
		} else {
			if !keywordPresent(provider.Keyword, &conf) {
				errmsg := fmt.Sprintf("Keyword %s defined, but not found in headers, method, URL or POST data.", provider.Keyword)
				_, _ = fmt.Fprintf(os.Stderr, "%s\n", fmt.Errorf(errmsg))
			} else {
				newInputProviders = append(newInputProviders, provider)
			}
		}
	}
	conf.InputProviders = newInputProviders

	// If sniper mode, ensure there is no FUZZ keyword
	if conf.InputMode == "sniper" {
		if keywordPresent("FUZZ", &conf) {
			errs.Add(fmt.Errorf("FUZZ keyword defined, but we are using sniper mode."))
		}
	}

	// Do checks for recursion mode
	if parseOpts.HTTP.Recursion {
		if !strings.HasSuffix(conf.Url, "FUZZ") {
			errmsg := "When using -recursion the URL (-u) must end with FUZZ keyword."
			errs.Add(fmt.Errorf(errmsg))
		}
	}

	// Make verbose mutually exclusive with json
	if parseOpts.General.Verbose && parseOpts.General.Json {
		errs.Add(fmt.Errorf("Cannot have -json and -v"))
	}
	return &conf, errs.ErrorOrNil()
}

func parseRawRequest(parseOpts *ConfigOptions, conf *Config) error {
	conf.RequestFile = parseOpts.Input.Request
	conf.RequestProto = parseOpts.Input.RequestProto
	file, err := os.Open(parseOpts.Input.Request)
	if err != nil {
		return fmt.Errorf("could not open request file: %s", err)
	}
	defer file.Close()

	r := bufio.NewReader(file)

	s, err := r.ReadString('\n')
	if err != nil {
		return fmt.Errorf("could not read request: %s", err)
	}
	parts := strings.Split(s, " ")
	if len(parts) < 3 {
		return fmt.Errorf("malformed request supplied")
	}
	// Set the request Method
	conf.Method = parts[0]

	for {
		line, err := r.ReadString('\n')
		line = strings.TrimSpace(line)

		if err != nil || line == "" {
			break
		}

		p := strings.SplitN(line, ":", 2)
		if len(p) != 2 {
			continue
		}

		if strings.EqualFold(p[0], "content-length") {
			continue
		}

		conf.Headers[strings.TrimSpace(p[0])] = strings.TrimSpace(p[1])
	}

	// Handle case with the full http url in path. In that case,
	// ignore any host header that we encounter and use the path as request URL
	if strings.HasPrefix(parts[1], "http") {
		parsed, err := url.Parse(parts[1])
		if err != nil {
			return fmt.Errorf("could not parse request URL: %s", err)
		}
		conf.Url = parts[1]
		conf.Headers["Host"] = parsed.Host
	} else {
		// Build the request URL from the request
		conf.Url = parseOpts.Input.RequestProto + "://" + conf.Headers["Host"] + parts[1]
	}

	// Set the request body
	b, err := io.ReadAll(r)
	if err != nil {
		return fmt.Errorf("could not read request body: %s", err)
	}
	conf.Data = string(b)

	// Remove newline (typically added by the editor) at the end of the file
	//nolint:gosimple // we specifically want to remove just a single newline, not all of them
	if strings.HasSuffix(conf.Data, "\r\n") {
		conf.Data = conf.Data[:len(conf.Data)-2]
	} else if strings.HasSuffix(conf.Data, "\n") {
		conf.Data = conf.Data[:len(conf.Data)-1]
	}
	return nil
}

func keywordPresent(keyword string, conf *Config) bool {
	//Search for keyword from HTTP method, URL and POST data too
	if strings.Contains(conf.Method, keyword) {
		return true
	}
	if strings.Contains(conf.Url, keyword) {
		return true
	}
	if strings.Contains(conf.Data, keyword) {
		return true
	}
	for k, v := range conf.Headers {
		if strings.Contains(k, keyword) {
			return true
		}
		if strings.Contains(v, keyword) {
			return true
		}
	}
	return false
}

func templatePresent(template string, conf *Config) bool {
	// Search for input location identifiers, these must exist in pairs
	sane := false

	if c := strings.Count(conf.Method, template); c > 0 {
		if c%2 != 0 {
			return false
		}
		sane = true
	}
	if c := strings.Count(conf.Url, template); c > 0 {
		if c%2 != 0 {
			return false
		}
		sane = true
	}
	if c := strings.Count(conf.Data, template); c > 0 {
		if c%2 != 0 {
			return false
		}
		sane = true
	}
	for k, v := range conf.Headers {
		if c := strings.Count(k, template); c > 0 {
			if c%2 != 0 {
				return false
			}
			sane = true
		}
		if c := strings.Count(v, template); c > 0 {
			if c%2 != 0 {
				return false
			}
			sane = true
		}
	}

	return sane
}

func ReadConfig(configFile string) (*ConfigOptions, error) {
	conf := NewConfigOptions()
	configData, err := os.ReadFile(configFile)
	if err == nil {
		err = toml.Unmarshal(configData, conf)
	}
	return conf, err
}

func ReadDefaultConfig() (*ConfigOptions, error) {
	// Try to create configuration directory, ignore the potential error
	_ = CheckOrCreateConfigDir()
	conffile := filepath.Join(CONFIGDIR, "ffufrc")
	if !FileExists(conffile) {
		userhome, err := os.UserHomeDir()
		if err == nil {
			conffile = filepath.Join(userhome, ".ffufrc")
		}
	}
	return ReadConfig(conffile)
}
