package gateway

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"

	"github.com/clbanning/mxj"
	"golang.org/x/net/html/charset"

	"github.com/TykTechnologies/tyk/apidef"
)

func WrappedCharsetReader(s string, i io.Reader) (io.Reader, error) {
	return charset.NewReader(i, s)
}

// TransformMiddleware is a middleware that will apply a template to a request body to transform it's contents ready for an upstream API
type TransformMiddleware struct {
	BaseMiddleware
}

func (t *TransformMiddleware) Name() string {
	return "TransformMiddleware"
}

func (t *TransformMiddleware) EnabledForSpec() bool {
	for _, version := range t.Spec.VersionData.Versions {
		if len(version.ExtendedPaths.Transform) > 0 {
			return true
		}
	}
	return false
}

// ProcessRequest will run any checks on the request on the way through the system, return an error to have the chain fail
func (t *TransformMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) {
	_, versionPaths, _, _ := t.Spec.Version(r)
	found, meta := t.Spec.CheckSpecMatchesStatus(r, versionPaths, Transformed)
	if !found {
		return nil, http.StatusOK
	}
	err := transformBody(r, meta.(*TransformSpec), t.Spec.EnableContextVars)
	if err != nil {
		t.Logger().WithError(err).Error("Body transform failure")
	}
	return nil, http.StatusOK
}

func transformBody(r *http.Request, tmeta *TransformSpec, contextVars bool) error {
	body, _ := ioutil.ReadAll(r.Body)
	defer r.Body.Close()

	// Put into an interface:
	bodyData := make(map[string]interface{})

	switch tmeta.TemplateData.Input {
	case apidef.RequestXML:
		if len(body) == 0 {
			body = []byte("<_/>")
		}
		mxj.XmlCharsetReader = WrappedCharsetReader
		var err error
		bodyData, err = mxj.NewMapXml(body) // unmarshal
		if err != nil {
			return fmt.Errorf("error unmarshalling XML: %v", err)
		}
	case apidef.RequestJSON:
		if len(body) == 0 {
			body = []byte("{}")
		}

		var tempBody interface{}
		if err := json.Unmarshal(body, &tempBody); err != nil {
			return err
		}

		switch tempBody.(type) {
		case []interface{}:
			bodyData["array"] = tempBody
		case map[string]interface{}:
			bodyData = tempBody.(map[string]interface{})
		}
	default:
		return fmt.Errorf("unsupported request input type: %v", tmeta.TemplateData.Input)
	}

	if tmeta.TemplateData.EnableSession {
		if session := ctxGetSession(r); session != nil {
			bodyData["_tyk_meta"] = session.GetMetaData()
		} else {
			log.Error("Session context was enabled but not found.")
		}
	}

	if contextVars {
		bodyData["_tyk_context"] = ctxGetData(r)
	}

	// Apply to template
	var bodyBuffer bytes.Buffer
	if err := tmeta.Template.Execute(&bodyBuffer, bodyData); err != nil {
		return fmt.Errorf("failed to apply template to request: %v", err)
	}
	r.Body = ioutil.NopCloser(&bodyBuffer)
	r.ContentLength = int64(bodyBuffer.Len())
	nopCloseRequestBody(r)

	return nil
}
