// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

//go:generate packer-sdc struct-markdown
//go:generate packer-sdc mapstructure-to-hcl2 -type DatasourceOutput,Config
package http

import (
	"context"
	"fmt"
	"io"
	"mime"
	"net/http"
	"regexp"
	"strings"

	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/hashicorp/packer-plugin-sdk/common"
	"github.com/hashicorp/packer-plugin-sdk/hcl2helper"
	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
	"github.com/hashicorp/packer-plugin-sdk/template/config"
	"github.com/zclconf/go-cty/cty"
)

type Config struct {
	common.PackerConfig `mapstructure:",squash"`
	// The URL to request data from. This URL must respond with a `2xx` range response code and a `text/*` or `application/json` Content-Type.
	Url string `mapstructure:"url" required:"true"`
	// HTTP method used for the request. Supported methods are `HEAD`, `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS`, `PATCH`. Default is `GET`.
	Method string `mapstructure:"method" required:"false"`
	// A map of strings representing additional HTTP headers to include in the request.
	RequestHeaders map[string]string `mapstructure:"request_headers" required:"false"`
	// HTTP request payload send with the request. Default is empty.
	RequestBody string `mapstructure:"request_body" required:"false"`
}

type Datasource struct {
	config Config
}

type DatasourceOutput struct {
	// The URL the data was requested from.
	Url string `mapstructure:"url"`
	// The raw body of the HTTP response.
	ResponseBody string `mapstructure:"body"`
	// A map of strings representing the response HTTP headers.
	// Duplicate headers are concatenated with, according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).
	ResponseHeaders map[string]string `mapstructure:"request_headers"`
}

func (d *Datasource) ConfigSpec() hcldec.ObjectSpec {
	return d.config.FlatMapstructure().HCL2Spec()
}

func (d *Datasource) Configure(raws ...interface{}) error {
	err := config.Decode(&d.config, nil, raws...)
	if err != nil {
		return err
	}

	var errs *packersdk.MultiError

	if d.config.Url == "" {
		errs = packersdk.MultiErrorAppend(
			errs,
			fmt.Errorf("the `url` must be specified"))
	}

	// Default to GET if no method is specified
	if d.config.Method == "" {
		d.config.Method = "GET"
	}

	// Check if the input is in the list of allowed methods
	validMethod := false
	allowedMethods := []string{"HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}
	for _, method := range allowedMethods {
		if method == d.config.Method {
			validMethod = true
			break
		}
	}
	if !validMethod {
		errs = packersdk.MultiErrorAppend(
			errs,
			fmt.Errorf("the `method` must be one of %v", allowedMethods))
	}

	if errs != nil && len(errs.Errors) > 0 {
		return errs
	}
	return nil
}

func (d *Datasource) OutputSpec() hcldec.ObjectSpec {
	return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec()
}

// This is to prevent potential issues w/ binary files
// and generally unprintable characters
// See https://github.com/hashicorp/terraform/pull/3858#issuecomment-156856738
func isContentTypeText(contentType string) bool {

	parsedType, params, err := mime.ParseMediaType(contentType)
	if err != nil {
		return false
	}

	allowedContentTypes := []*regexp.Regexp{
		regexp.MustCompile("^text/.+"),
		regexp.MustCompile("^application/json$"),
		regexp.MustCompile(`^application/samlmetadata\+xml`),
	}

	for _, r := range allowedContentTypes {
		if r.MatchString(parsedType) {
			charset := strings.ToLower(params["charset"])
			return charset == "" || charset == "utf-8" || charset == "us-ascii"
		}
	}

	return false
}

// Most of this code comes from http terraform provider data source
// https://github.com/hashicorp/terraform-provider-http/blob/main/internal/provider/data_source.go
func (d *Datasource) Execute() (cty.Value, error) {
	ctx := context.TODO()
	url, method, headers := d.config.Url, d.config.Method, d.config.RequestHeaders
	client := &http.Client{}

	// Create request body if it is provided
	var requestBody io.Reader
	if d.config.RequestBody != "" {
		requestBody = strings.NewReader(d.config.RequestBody)
	}

	req, err := http.NewRequestWithContext(ctx, method, url, requestBody)
	// TODO: How to make a test case for this?
	if err != nil {
		fmt.Println("Error creating http request")
		return cty.NullVal(cty.EmptyObject), err
	}

	for name, value := range headers {
		req.Header.Set(name, value)
	}

	resp, err := client.Do(req)
	// TODO: How to make test case for this
	if err != nil {
		fmt.Println("Error making performing http request")
		return cty.NullVal(cty.EmptyObject), err
	}

	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return cty.NullVal(cty.EmptyObject), fmt.Errorf("HTTP request error. Response code: %d", resp.StatusCode)
	}

	contentType := resp.Header.Get("Content-Type")
	if contentType == "" || isContentTypeText(contentType) == false {
		fmt.Printf("Content-Type is not recognized as a text type, got %q\n",
			contentType)
		fmt.Println("If the content is binary data, Packer may not properly handle the contents of the response.")
	}

	bytes, err := io.ReadAll(resp.Body)
	// TODO: How to make test case for this?
	if err != nil {
		fmt.Println("Error processing response body of call")
		return cty.NullVal(cty.EmptyObject), err
	}

	responseHeaders := make(map[string]string)
	for k, v := range resp.Header {
		// Concatenate according to RFC2616
		// cf. https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
		responseHeaders[k] = strings.Join(v, ", ")
	}

	output := DatasourceOutput{
		Url:             d.config.Url,
		ResponseHeaders: responseHeaders,
		ResponseBody:    string(bytes),
	}
	return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil
}
