package cmd

import (
	"bytes"
	b64 "encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"path/filepath"
	"strings"

	"github.com/Jeffail/gabs/v2"
	"github.com/SAP/jenkins-library/pkg/cpi"
	piperhttp "github.com/SAP/jenkins-library/pkg/http"
	"github.com/SAP/jenkins-library/pkg/log"
	"github.com/SAP/jenkins-library/pkg/piperutils"
	"github.com/SAP/jenkins-library/pkg/telemetry"
	"github.com/pkg/errors"
)

type integrationArtifactResourceData struct {
	Method     string
	URL        string
	IFlowID    string
	ScsMessage string
	FlrMessage string
	StatusCode int
}

func integrationArtifactResource(config integrationArtifactResourceOptions, telemetryData *telemetry.CustomData) {
	// Utils can be used wherever the command.ExecRunner interface is expected.
	// It can also be used for example as a mavenExecRunner.
	httpClient := &piperhttp.Client{}
	fileUtils := &piperutils.Files{}

	// For HTTP calls import  piperhttp "github.com/SAP/jenkins-library/pkg/http"
	// and use a  &piperhttp.Client{} in a custom system
	// Example: step checkmarxExecuteScan.go

	// Error situations should be bubbled up until they reach the line below which will then stop execution
	// through the log.Entry().Fatal() call leading to an os.Exit(1) in the end.
	err := runIntegrationArtifactResource(&config, telemetryData, fileUtils, httpClient)
	if err != nil {
		log.Entry().WithError(err).Fatal("step execution failed")
	}
}

func runIntegrationArtifactResource(config *integrationArtifactResourceOptions, telemetryData *telemetry.CustomData, fileUtils piperutils.FileUtils, httpClient piperhttp.Sender) error {
	serviceKey, err := cpi.ReadCpiServiceKey(config.APIServiceKey)
	if err != nil {
		return err
	}

	clientOptions := piperhttp.ClientOptions{}
	header := make(http.Header)
	header.Add("Accept", "application/json")
	tokenParameters := cpi.TokenParameters{TokenURL: serviceKey.OAuth.OAuthTokenProviderURL, Username: serviceKey.OAuth.ClientID, Password: serviceKey.OAuth.ClientSecret, Client: httpClient}
	token, err := cpi.CommonUtils.GetBearerToken(tokenParameters)
	if err != nil {
		return errors.Wrap(err, "failed to fetch Bearer Token")
	}
	clientOptions.Token = fmt.Sprintf("Bearer %s", token)
	httpClient.SetOptions(clientOptions)
	mode := strings.ToLower(strings.TrimSpace(config.Operation))
	switch mode {
	case "create":
		return UploadIntegrationArtifactResource(config, httpClient, fileUtils, serviceKey.OAuth.Host)
	case "update":
		return UpdateIntegrationArtifactResource(config, httpClient, fileUtils, serviceKey.OAuth.Host)
	case "delete":
		return DeleteIntegrationArtifactResource(config, httpClient, fileUtils, serviceKey.OAuth.Host)
	default:
		return errors.New("invalid input for resource operation")
	}
}

//UploadIntegrationArtifactResource - Upload new resource file to existing integration flow design time artefact
func UploadIntegrationArtifactResource(config *integrationArtifactResourceOptions, httpClient piperhttp.Sender, fileUtils piperutils.FileUtils, apiHost string) error {
	httpMethod := "POST"
	uploadIflowStatusURL := fmt.Sprintf("%s/api/v1/IntegrationDesigntimeArtifacts(Id='%s',Version='%s')/Resources", apiHost, config.IntegrationFlowID, "Active")
	header := make(http.Header)
	header.Add("content-type", "application/json")
	payload, jsonError := GetJSONPayload(config, "create", fileUtils)
	if jsonError != nil {
		return errors.Wrapf(jsonError, "Failed to get json payload for file %v, failed with error", config.ResourcePath)
	}

	uploadIflowStatusResp, httpErr := httpClient.SendRequest(httpMethod, uploadIflowStatusURL, payload, header, nil)

	successMessage := "Successfully create a new resource file in the integration flow artefact"
	failureMessage := "Failed to create a new resource file in the integration flow artefact"
	integrationArtifactResourceData := integrationArtifactResourceData{
		Method:     httpMethod,
		URL:        uploadIflowStatusURL,
		IFlowID:    config.IntegrationFlowID,
		ScsMessage: successMessage,
		FlrMessage: failureMessage,
		StatusCode: http.StatusCreated,
	}

	return HttpResponseHandler(uploadIflowStatusResp, httpErr, &integrationArtifactResourceData)
}

//UpdateIntegrationArtifactResource - Update integration artifact resource file
func UpdateIntegrationArtifactResource(config *integrationArtifactResourceOptions, httpClient piperhttp.Sender, fileUtils piperutils.FileUtils, apiHost string) error {
	httpMethod := "PUT"
	header := make(http.Header)
	header.Add("content-type", "application/json")
	fileName := filepath.Base(config.ResourcePath)
	fileExt := GetResourceFileExtension(fileName)
	if fileExt == "" {
		return errors.New("invalid file extension in resource file")
	}
	updateIflowStatusURL := fmt.Sprintf("%s/api/v1/IntegrationDesigntimeArtifacts(Id='%s',Version='%s')/$links/Resources(Name='%s',ResourceType='%s')", apiHost, config.IntegrationFlowID, "Active", fileName, fileExt)
	payload, jsonError := GetJSONPayload(config, "update", fileUtils)
	if jsonError != nil {
		return errors.Wrapf(jsonError, "Failed to get json payload for file %v, failed with error", config.ResourcePath)
	}
	updateIflowStatusResp, httpErr := httpClient.SendRequest(httpMethod, updateIflowStatusURL, payload, header, nil)

	successMessage := "Successfully updated resource file of the integration flow artefact"
	failureMessage := "Failed to update rsource file of the integration flow artefact"
	integrationArtifactResourceData := integrationArtifactResourceData{
		Method:     httpMethod,
		URL:        updateIflowStatusURL,
		IFlowID:    config.IntegrationFlowID,
		ScsMessage: successMessage,
		FlrMessage: failureMessage,
		StatusCode: http.StatusOK,
	}

	return HttpResponseHandler(updateIflowStatusResp, httpErr, &integrationArtifactResourceData)
}

//DeleteIntegrationArtifactResource - Delete integration artifact resource file
func DeleteIntegrationArtifactResource(config *integrationArtifactResourceOptions, httpClient piperhttp.Sender, fileUtils piperutils.FileUtils, apiHost string) error {
	httpMethod := "DELETE"
	header := make(http.Header)
	header.Add("content-type", "application/json")
	fileName := filepath.Base(config.ResourcePath)
	fileExt := GetResourceFileExtension(fileName)
	if fileExt == "" {
		return errors.New("invalid file extension in resource file")
	}
	deleteIflowResourceStatusURL := fmt.Sprintf("%s/api/v1/IntegrationDesigntimeArtifacts(Id='%s',Version='%s')/$links/Resources(Name='%s',ResourceType='%s')", apiHost, config.IntegrationFlowID, "Active", fileName, fileExt)
	deleteIflowResourceStatusResp, httpErr := httpClient.SendRequest(httpMethod, deleteIflowResourceStatusURL, nil, header, nil)

	successMessage := "Successfully deleted a resource file in the integration flow artefact"
	failureMessage := "Failed to delete a resource file in the integration flow artefact"
	integrationArtifactResourceData := integrationArtifactResourceData{
		Method:     httpMethod,
		URL:        deleteIflowResourceStatusURL,
		IFlowID:    config.IntegrationFlowID,
		ScsMessage: successMessage,
		FlrMessage: failureMessage,
		StatusCode: http.StatusOK,
	}

	return HttpResponseHandler(deleteIflowResourceStatusResp, httpErr, &integrationArtifactResourceData)
}

//GetJSONPayload -return http payload as byte array
func GetJSONPayload(config *integrationArtifactResourceOptions, mode string, fileUtils piperutils.FileUtils) (*bytes.Buffer, error) {
	fileContent, readError := fileUtils.FileRead(config.ResourcePath)
	if readError != nil {
		return nil, errors.Wrapf(readError, "Error reading file")
	}
	fileName := filepath.Base(config.ResourcePath)
	jsonObj := gabs.New()
	if mode == "create" {
		jsonObj.Set(fileName, "Name")
		jsonObj.Set(GetResourceFileExtension(fileName), "ResourceType")
		jsonObj.Set(b64.StdEncoding.EncodeToString(fileContent), "ResourceContent")
	} else if mode == "update" {
		jsonObj.Set(b64.StdEncoding.EncodeToString(fileContent), "ResourceContent")
	} else {
		return nil, fmt.Errorf("Unkown node: '%s'", mode)
	}

	jsonBody, jsonErr := json.Marshal(jsonObj)

	if jsonErr != nil {
		return nil, errors.Wrapf(jsonErr, "json payload is invalid for integration flow artifact %q", config.IntegrationFlowID)
	}
	return bytes.NewBuffer(jsonBody), nil
}

//GetResourceFileExtension -return resource file extension
func GetResourceFileExtension(filename string) string {
	fileExtension := filepath.Ext(filename)
	switch fileExtension {
	case ".xsl":
		return "xslt"
	case ".gsh", ".groovy":
		return "groovy"
	case ".js":
		return "js"
	case ".jar":
		return "jar"
	default:
		return ""
	}
}

//HttpResponseHandler - handle http response object
func HttpResponseHandler(resp *http.Response, httpErr error, integrationArtifactResourceData *integrationArtifactResourceData) error {

	if resp != nil && resp.Body != nil {
		defer resp.Body.Close()
	}

	if resp == nil {
		return errors.Errorf("did not retrieve a HTTP response: %v", httpErr)
	}

	if resp.StatusCode == integrationArtifactResourceData.StatusCode {
		log.Entry().
			WithField("IntegrationFlowID", integrationArtifactResourceData.IFlowID).
			Info(integrationArtifactResourceData.ScsMessage)
		return nil
	}
	if httpErr != nil {
		responseBody, readErr := ioutil.ReadAll(resp.Body)
		if readErr != nil {
			return errors.Wrapf(readErr, "HTTP response body could not be read, Response status code: %v", resp.StatusCode)
		}
		log.Entry().Errorf("a HTTP error occurred! Response body: %v, Response status code: %v", string(responseBody), resp.StatusCode)
		return errors.Wrapf(httpErr, "HTTP %v request to %v failed with error: %v", integrationArtifactResourceData.Method, integrationArtifactResourceData.URL, string(responseBody))
	}
	return errors.Errorf("%s, Response Status code: %v", integrationArtifactResourceData.FlrMessage, resp.StatusCode)
}
