//go:generate ../../../tools/readme_config_includer/generator
package http_response

import (
	_ "embed"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/benbjohnson/clock"
	"github.com/seancfoley/ipaddress-go/ipaddr"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/internal"
	"github.com/influxdata/telegraf/plugins/common/cookie"
	"github.com/influxdata/telegraf/plugins/common/tls"
	"github.com/influxdata/telegraf/plugins/inputs"
)

//go:embed sample.conf
var sampleConfig string

const (
	// defaultResponseBodyMaxSize is the default maximum response body size, in bytes.
	// if the response body is over this size, we will raise a body_read_error.
	defaultResponseBodyMaxSize = 32 * 1024 * 1024
)

type HTTPResponse struct {
	Address         string              `toml:"address" deprecated:"1.12.0;1.35.0;use 'urls' instead"`
	URLs            []string            `toml:"urls"`
	HTTPProxy       string              `toml:"http_proxy"`
	Body            string              `toml:"body"`
	BodyForm        map[string][]string `toml:"body_form"`
	Method          string              `toml:"method"`
	ResponseTimeout config.Duration     `toml:"response_timeout"`
	HTTPHeaderTags  map[string]string   `toml:"http_header_tags"`
	Headers         map[string]string   `toml:"headers"`
	FollowRedirects bool                `toml:"follow_redirects"`
	// Absolute path to file with Bearer token
	BearerToken         string      `toml:"bearer_token"`
	ResponseBodyField   string      `toml:"response_body_field"`
	ResponseBodyMaxSize config.Size `toml:"response_body_max_size"`
	ResponseStringMatch string      `toml:"response_string_match"`
	ResponseStatusCode  int         `toml:"response_status_code"`
	Interface           string      `toml:"interface"`
	// HTTP Basic Auth Credentials
	Username config.Secret `toml:"username"`
	Password config.Secret `toml:"password"`
	tls.ClientConfig
	cookie.CookieAuthConfig

	Log telegraf.Logger `toml:"-"`

	compiledStringMatch *regexp.Regexp
	clients             []client
}

type client struct {
	httpClient httpClient
	address    string
}

type httpClient interface {
	// Do implements [http.Client]
	Do(req *http.Request) (*http.Response, error)
}

func (*HTTPResponse) SampleConfig() string {
	return sampleConfig
}

func (h *HTTPResponse) Init() error {
	// Compile the body regex if it exists
	if h.ResponseStringMatch != "" {
		var err error
		h.compiledStringMatch, err = regexp.Compile(h.ResponseStringMatch)
		if err != nil {
			return fmt.Errorf("failed to compile regular expression %q: %w", h.ResponseStringMatch, err)
		}
	}

	// Set default values
	if h.ResponseTimeout < config.Duration(time.Second) {
		h.ResponseTimeout = config.Duration(time.Second * 5)
	}
	if h.Method == "" {
		h.Method = "GET"
	}

	if len(h.URLs) == 0 {
		if h.Address == "" {
			h.URLs = []string{"http://localhost"}
		} else {
			h.URLs = []string{h.Address}
		}
	}

	h.clients = make([]client, 0, len(h.URLs))
	for _, u := range h.URLs {
		addr, err := url.Parse(u)
		if err != nil {
			return fmt.Errorf("%q is not a valid address: %w", u, err)
		}

		if addr.Scheme != "http" && addr.Scheme != "https" {
			return fmt.Errorf("%q is not a valid address: only http and https types are supported", u)
		}

		cl, err := h.createHTTPClient(*addr)
		if err != nil {
			return err
		}

		h.clients = append(h.clients, client{httpClient: cl, address: u})
	}

	return nil
}

func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error {
	for _, c := range h.clients {
		// Prepare data
		var fields map[string]interface{}
		var tags map[string]string

		// Gather data
		fields, tags, err := h.httpGather(c)
		if err != nil {
			acc.AddError(err)
			continue
		}

		// Add metrics
		acc.AddFields("http_response", fields, tags)
	}

	return nil
}

// Set the proxy. A configured proxy overwrites the system-wide proxy.
func getProxyFunc(httpProxy string) func(*http.Request) (*url.URL, error) {
	if httpProxy == "" {
		return http.ProxyFromEnvironment
	}
	proxyURL, err := url.Parse(httpProxy)
	if err != nil {
		return func(_ *http.Request) (*url.URL, error) {
			return nil, errors.New("bad proxy: " + err.Error())
		}
	}
	return func(*http.Request) (*url.URL, error) {
		return proxyURL, nil
	}
}

// createHTTPClient creates an http client which will time out at the specified
// timeout period and can follow redirects if specified
func (h *HTTPResponse) createHTTPClient(address url.URL) (*http.Client, error) {
	tlsCfg, err := h.ClientConfig.TLSConfig()
	if err != nil {
		return nil, err
	}

	dialer := &net.Dialer{}

	if h.Interface != "" {
		dialer.LocalAddr, err = localAddress(h.Interface, address)
		if err != nil {
			return nil, err
		}
	}

	client := &http.Client{
		Transport: &http.Transport{
			Proxy:             getProxyFunc(h.HTTPProxy),
			DialContext:       dialer.DialContext,
			DisableKeepAlives: true,
			TLSClientConfig:   tlsCfg,
		},
		Timeout: time.Duration(h.ResponseTimeout),
	}

	if !h.FollowRedirects {
		client.CheckRedirect = func(*http.Request, []*http.Request) error {
			return http.ErrUseLastResponse
		}
	}

	if h.CookieAuthConfig.URL != "" {
		if err := h.CookieAuthConfig.Start(client, h.Log, clock.New()); err != nil {
			return nil, err
		}
	}

	return client, nil
}

func localAddress(interfaceName string, address url.URL) (net.Addr, error) {
	i, err := net.InterfaceByName(interfaceName)
	if err != nil {
		return nil, err
	}

	addrs, err := i.Addrs()
	if err != nil {
		return nil, err
	}

	urlInIPv6, zone := isURLInIPv6(address)
	for _, addr := range addrs {
		if naddr, ok := addr.(*net.IPNet); ok {
			ipNetInIPv6 := isIPNetInIPv6(naddr)

			// choose interface address in the same format as server address
			if ipNetInIPv6 == urlInIPv6 {
				// leaving port set to zero to let kernel pick, but set zone
				return &net.TCPAddr{IP: naddr.IP, Zone: zone}, nil
			}
		}
	}

	return nil, fmt.Errorf("cannot create local address for interface %q and server address %q", interfaceName, address.String())
}

// isURLInIPv6 returns (true, zoneName) only when URL is in IPv6 format.
// For other cases (host part of url cannot be successfully validated, doesn't contain address at all or is in IPv4 format), it returns (false, "").
func isURLInIPv6(address url.URL) (bool, string) {
	host := ipaddr.NewHostName(address.Host)
	if err := host.Validate(); err != nil {
		return false, ""
	}
	if hostAddr := host.AsAddress(); hostAddr != nil {
		if ipv6 := hostAddr.ToIPv6(); ipv6 != nil {
			return true, ipv6.GetZone().String()
		}
	}

	return false, ""
}

// isIPNetInIPv6 returns true only when IPNet can be represented in IPv6 format.
// For other cases (address cannot be successfully parsed or is in IPv4 format), it returns false.
func isIPNetInIPv6(address *net.IPNet) bool {
	ipAddr, err := ipaddr.NewIPAddressFromNetIPNet(address)
	return err == nil && ipAddr.ToIPv6() != nil
}

func setResult(resultString string, fields map[string]interface{}, tags map[string]string) {
	resultCodes := map[string]int{
		"success":                       0,
		"response_string_mismatch":      1,
		"body_read_error":               2,
		"connection_failed":             3,
		"timeout":                       4,
		"dns_error":                     5,
		"response_status_code_mismatch": 6,
	}

	tags["result"] = resultString
	fields["result_type"] = resultString
	fields["result_code"] = resultCodes[resultString]
}

func setError(err error, fields map[string]interface{}, tags map[string]string) error {
	var timeoutError net.Error
	if errors.As(err, &timeoutError) && timeoutError.Timeout() {
		setResult("timeout", fields, tags)
		return timeoutError
	}

	var urlErr *url.Error
	if !errors.As(err, &urlErr) {
		return nil
	}

	var opErr *net.OpError
	if errors.As(urlErr, &opErr) {
		var dnsErr *net.DNSError
		var parseErr *net.ParseError

		if errors.As(opErr, &dnsErr) {
			setResult("dns_error", fields, tags)
			return dnsErr
		} else if errors.As(opErr, &parseErr) {
			// Parse error has to do with parsing of IP addresses, so we
			// group it with address errors
			setResult("address_error", fields, tags)
			return parseErr
		}
	}

	return nil
}

// HTTPGather gathers all fields and returns any errors it encounters
func (h *HTTPResponse) httpGather(cl client) (map[string]interface{}, map[string]string, error) {
	// Prepare fields and tags
	fields := make(map[string]interface{})
	tags := map[string]string{"server": cl.address, "method": h.Method}

	var body io.Reader
	if h.Body != "" {
		body = strings.NewReader(h.Body)
	} else if len(h.BodyForm) != 0 {
		values := url.Values{}
		for k, vs := range h.BodyForm {
			for _, v := range vs {
				values.Add(k, v)
			}
		}
		body = strings.NewReader(values.Encode())
	}

	request, err := http.NewRequest(h.Method, cl.address, body)
	if err != nil {
		return nil, nil, err
	}

	if _, uaPresent := h.Headers["User-Agent"]; !uaPresent {
		request.Header.Set("User-Agent", internal.ProductToken())
	}

	if h.BearerToken != "" {
		token, err := os.ReadFile(h.BearerToken)
		if err != nil {
			return nil, nil, err
		}
		bearer := "Bearer " + strings.Trim(string(token), "\n")
		request.Header.Add("Authorization", bearer)
	}

	for key, val := range h.Headers {
		request.Header.Add(key, val)
		if key == "Host" {
			request.Host = val
		}
	}

	if err := h.setRequestAuth(request); err != nil {
		return nil, nil, err
	}

	// Start Timer
	start := time.Now()
	resp, err := cl.httpClient.Do(request)
	responseTime := time.Since(start).Seconds()

	// If an error in returned, it means we are dealing with a network error, as
	// HTTP error codes do not generate errors in the net/http library
	if err != nil {
		// Log error
		h.Log.Debugf("Network error while polling %s: %s", cl.address, err.Error())

		// Get error details
		if setError(err, fields, tags) == nil {
			// Any error not recognized by `set_error` is considered a "connection_failed"
			setResult("connection_failed", fields, tags)
		}

		return fields, tags, nil
	}

	if _, ok := fields["response_time"]; !ok {
		fields["response_time"] = responseTime
	}

	// This function closes the response body, as
	// required by the net/http library
	defer resp.Body.Close()

	// Add the response headers
	for headerName, tag := range h.HTTPHeaderTags {
		headerValues, foundHeader := resp.Header[headerName]
		if foundHeader && len(headerValues) > 0 {
			tags[tag] = headerValues[0]
		}
	}

	// Set log the HTTP response code
	tags["status_code"] = strconv.Itoa(resp.StatusCode)
	fields["http_response_code"] = resp.StatusCode

	if h.ResponseBodyMaxSize == 0 {
		h.ResponseBodyMaxSize = config.Size(defaultResponseBodyMaxSize)
	}
	bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, int64(h.ResponseBodyMaxSize)+1))
	// Check first if the response body size exceeds the limit.
	if err == nil && int64(len(bodyBytes)) > int64(h.ResponseBodyMaxSize) {
		h.setBodyReadError("The body of the HTTP Response is too large", bodyBytes, fields, tags)
		return fields, tags, nil
	} else if err != nil {
		h.setBodyReadError("Failed to read body of HTTP Response : "+err.Error(), bodyBytes, fields, tags)
		return fields, tags, nil
	}

	// Add the body of the response if expected
	if len(h.ResponseBodyField) > 0 {
		// Check that the content of response contains only valid utf-8 characters.
		if !utf8.Valid(bodyBytes) {
			h.setBodyReadError("The body of the HTTP Response is not a valid utf-8 string", bodyBytes, fields, tags)
			return fields, tags, nil
		}
		fields[h.ResponseBodyField] = string(bodyBytes)
	}
	fields["content_length"] = len(bodyBytes)

	var success = true

	// Check the response for a regex
	if h.ResponseStringMatch != "" {
		if h.compiledStringMatch.Match(bodyBytes) {
			fields["response_string_match"] = 1
		} else {
			success = false
			setResult("response_string_mismatch", fields, tags)
			fields["response_string_match"] = 0
		}
	}

	// Check the response status code
	if h.ResponseStatusCode > 0 {
		if resp.StatusCode == h.ResponseStatusCode {
			fields["response_status_code_match"] = 1
		} else {
			success = false
			setResult("response_status_code_mismatch", fields, tags)
			fields["response_status_code_match"] = 0
		}
	}

	if success {
		setResult("success", fields, tags)
	}

	return fields, tags, nil
}

// Set result in case of a body read error
func (h *HTTPResponse) setBodyReadError(errorMsg string, bodyBytes []byte, fields map[string]interface{}, tags map[string]string) {
	h.Log.Debug(errorMsg)
	setResult("body_read_error", fields, tags)
	fields["content_length"] = len(bodyBytes)
	if h.ResponseStringMatch != "" {
		fields["response_string_match"] = 0
	}
}

func (h *HTTPResponse) setRequestAuth(request *http.Request) error {
	if h.Username.Empty() || h.Password.Empty() {
		return nil
	}

	username, err := h.Username.Get()
	if err != nil {
		return fmt.Errorf("getting username failed: %w", err)
	}
	defer username.Destroy()
	password, err := h.Password.Get()
	if err != nil {
		return fmt.Errorf("getting password failed: %w", err)
	}
	defer password.Destroy()
	request.SetBasicAuth(username.String(), password.String())

	return nil
}

func init() {
	inputs.Add("http_response", func() telegraf.Input {
		return &HTTPResponse{}
	})
}
