package abaputils

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/SAP/jenkins-library/pkg/cloudfoundry"
	"github.com/SAP/jenkins-library/pkg/command"
	piperhttp "github.com/SAP/jenkins-library/pkg/http"
	"github.com/SAP/jenkins-library/pkg/log"
	"github.com/ghodss/yaml"
	"github.com/pkg/errors"
)

// AbapUtils Struct
type AbapUtils struct {
	Exec      command.ExecRunner
	Intervall time.Duration
}

/*
Communication for defining function used for communication
*/
type Communication interface {
	GetAbapCommunicationArrangementInfo(options AbapEnvironmentOptions, oDataURL string) (ConnectionDetailsHTTP, error)
	GetPollIntervall() time.Duration
}

// GetAbapCommunicationArrangementInfo function fetches the communcation arrangement information in SAP CP ABAP Environment
func (abaputils *AbapUtils) GetAbapCommunicationArrangementInfo(options AbapEnvironmentOptions, oDataURL string) (ConnectionDetailsHTTP, error) {
	c := abaputils.Exec
	var connectionDetails ConnectionDetailsHTTP
	var error error

	if options.Host != "" {
		// Host, User and Password are directly provided -> check for host schema (double https)
		match, err := regexp.MatchString(`^(https|HTTPS):\/\/.*`, options.Host)
		if err != nil {
			log.SetErrorCategory(log.ErrorConfiguration)
			return connectionDetails, errors.Wrap(err, "Schema validation for host parameter failed. Check for https.")
		}
		var hostOdataURL = options.Host + oDataURL
		if match {
			connectionDetails.URL = hostOdataURL
			connectionDetails.Host = options.Host
		} else {
			connectionDetails.URL = "https://" + hostOdataURL
			connectionDetails.Host = "https://" + options.Host
		}
		connectionDetails.User = options.Username
		connectionDetails.Password = options.Password
	} else {
		if options.CfAPIEndpoint == "" || options.CfOrg == "" || options.CfSpace == "" || options.CfServiceInstance == "" || options.CfServiceKeyName == "" {
			var err = errors.New("Parameters missing. Please provide EITHER the Host of the ABAP server OR the Cloud Foundry ApiEndpoint, Organization, Space, Service Instance and a corresponding Service Key for the Communication Scenario SAP_COM_0510")
			log.SetErrorCategory(log.ErrorConfiguration)
			return connectionDetails, err
		}
		// Url, User and Password should be read from a cf service key
		var abapServiceKey, error = ReadServiceKeyAbapEnvironment(options, c)
		if error != nil {
			return connectionDetails, errors.Wrap(error, "Read service key failed")
		}
		connectionDetails.Host = abapServiceKey.URL
		connectionDetails.URL = abapServiceKey.URL + oDataURL
		connectionDetails.User = abapServiceKey.Abap.Username
		connectionDetails.Password = abapServiceKey.Abap.Password
	}
	return connectionDetails, error
}

// ReadServiceKeyAbapEnvironment from Cloud Foundry and returns it. Depending on user/developer requirements if he wants to perform further Cloud Foundry actions
func ReadServiceKeyAbapEnvironment(options AbapEnvironmentOptions, c command.ExecRunner) (AbapServiceKey, error) {

	var abapServiceKey AbapServiceKey
	var serviceKeyJSON string
	var err error

	cfconfig := cloudfoundry.ServiceKeyOptions{
		CfAPIEndpoint:     options.CfAPIEndpoint,
		CfOrg:             options.CfOrg,
		CfSpace:           options.CfSpace,
		CfServiceInstance: options.CfServiceInstance,
		CfServiceKeyName:  options.CfServiceKeyName,
		Username:          options.Username,
		Password:          options.Password,
	}

	cf := cloudfoundry.CFUtils{Exec: c}

	serviceKeyJSON, err = cf.ReadServiceKey(cfconfig)

	if err != nil {
		// Executing cfReadServiceKeyScript failed
		return abapServiceKey, err
	}

	// parse
	json.Unmarshal([]byte(serviceKeyJSON), &abapServiceKey)
	if abapServiceKey == (AbapServiceKey{}) {
		log.SetErrorCategory(log.ErrorInfrastructure)
		return abapServiceKey, errors.New("Parsing the service key failed. Service key is empty")
	}

	log.Entry().Info("Service Key read successfully")
	return abapServiceKey, nil
}

/*
GetPollIntervall returns the specified intervall from AbapUtils or a default value of 10 seconds
*/
func (abaputils *AbapUtils) GetPollIntervall() time.Duration {
	if abaputils.Intervall != 0 {
		return abaputils.Intervall
	}
	return 10 * time.Second
}

/*
ReadCOnfigFile reads a file from a specific path and returns the json string as []byte
*/
func ReadConfigFile(path string) (file []byte, err error) {
	filelocation, err := filepath.Glob(path)
	if err != nil {
		return nil, err
	}
	if len(filelocation) == 0 {
		return nil, errors.New("Could not find " + path)
	}
	filename, err := filepath.Abs(filelocation[0])
	if err != nil {
		return nil, err
	}
	yamlFile, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	var jsonFile []byte
	jsonFile, err = yaml.YAMLToJSON(yamlFile)
	return jsonFile, err
}

// GetHTTPResponse wraps the SendRequest function of piperhttp
func GetHTTPResponse(requestType string, connectionDetails ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) {

	header := make(map[string][]string)
	header["Content-Type"] = []string{"application/json"}
	header["Accept"] = []string{"application/json"}
	header["x-csrf-token"] = []string{connectionDetails.XCsrfToken}

	httpResponse, err := client.SendRequest(requestType, connectionDetails.URL, bytes.NewBuffer(body), header, nil)
	return httpResponse, err
}

// HandleHTTPError handles ABAP error messages which can occur when using OData services
//
// The point of this function is to enrich the error received from a HTTP Request (which is passed as a parameter to this function).
// Further error details may be present in the response body of the HTTP response.
// If the response body is parseable, the included details are wrapped around the original error from the HTTP repsponse.
// If this is not possible, the original error is returned.
func HandleHTTPError(resp *http.Response, err error, message string, connectionDetails ConnectionDetailsHTTP) error {
	if resp == nil {
		// Response is nil in case of a timeout
		log.Entry().WithError(err).WithField("ABAP Endpoint", connectionDetails.URL).Error("Request failed")
	} else {

		defer resp.Body.Close()

		log.Entry().WithField("StatusCode", resp.Status).Error(message)

		errorText, errorCode, parsingError := GetErrorDetailsFromResponse(resp)
		if parsingError != nil {
			return err
		}
		abapError := errors.New(fmt.Sprintf("%s - %s", errorCode, errorText))
		err = errors.Wrap(abapError, err.Error())

	}
	return err
}

func GetErrorDetailsFromResponse(resp *http.Response) (errorString string, errorCode string, err error) {

	// Include the error message of the ABAP Environment system, if available
	var abapErrorResponse AbapError
	bodyText, readError := ioutil.ReadAll(resp.Body)
	if readError != nil {
		return "", "", readError
	}
	var abapResp map[string]*json.RawMessage
	errUnmarshal := json.Unmarshal(bodyText, &abapResp)
	if errUnmarshal != nil {
		return "", "", errUnmarshal
	}
	if _, ok := abapResp["error"]; ok {
		json.Unmarshal(*abapResp["error"], &abapErrorResponse)
		if (AbapError{}) != abapErrorResponse {
			log.Entry().WithField("ErrorCode", abapErrorResponse.Code).Debug(abapErrorResponse.Message.Value)
			return abapErrorResponse.Message.Value, abapErrorResponse.Code, nil
		}
	}

	return "", "", errors.New("Could not parse the JSON error response")

}

// ConvertTime formats an ABAP timestamp string from format /Date(1585576807000+0000)/ into a UNIX timestamp and returns it
func ConvertTime(logTimeStamp string) time.Time {
	seconds := strings.TrimPrefix(strings.TrimSuffix(logTimeStamp, "000+0000)/"), "/Date(")
	n, error := strconv.ParseInt(seconds, 10, 64)
	if error != nil {
		return time.Unix(0, 0).UTC()
	}
	t := time.Unix(n, 0).UTC()
	return t
}

/*******************************
 *	Structs for specific steps *
 *******************************/

// AbapEnvironmentPullGitRepoOptions struct for the PullGitRepo piper step
type AbapEnvironmentPullGitRepoOptions struct {
	AbapEnvOptions  AbapEnvironmentOptions
	RepositoryNames []string `json:"repositoryNames,omitempty"`
}

// AbapEnvironmentCheckoutBranchOptions struct for the CheckoutBranch piper step
type AbapEnvironmentCheckoutBranchOptions struct {
	AbapEnvOptions AbapEnvironmentOptions
	RepositoryName string `json:"repositoryName,omitempty"`
}

// AbapEnvironmentRunATCCheckOptions struct for the RunATCCheck piper step
type AbapEnvironmentRunATCCheckOptions struct {
	AbapEnvOptions AbapEnvironmentOptions
	AtcConfig      string `json:"atcConfig,omitempty"`
}

/********************************
 *	Structs for ABAP in general *
 ********************************/

//AbapEnvironmentOptions contains cloud foundry fields and the host parameter for connections to ABAP Environment instances
type AbapEnvironmentOptions struct {
	Username          string `json:"username,omitempty"`
	Password          string `json:"password,omitempty"`
	Host              string `json:"host,omitempty"`
	CfAPIEndpoint     string `json:"cfApiEndpoint,omitempty"`
	CfOrg             string `json:"cfOrg,omitempty"`
	CfSpace           string `json:"cfSpace,omitempty"`
	CfServiceInstance string `json:"cfServiceInstance,omitempty"`
	CfServiceKeyName  string `json:"cfServiceKeyName,omitempty"`
}

// AbapMetadata contains the URI of metadata files
type AbapMetadata struct {
	URI string `json:"uri"`
}

// ConnectionDetailsHTTP contains fields for HTTP connections including the XCSRF token
type ConnectionDetailsHTTP struct {
	Host       string
	User       string `json:"user"`
	Password   string `json:"password"`
	URL        string `json:"url"`
	XCsrfToken string `json:"xcsrftoken"`
}

// AbapError contains the error code and the error message for ABAP errors
type AbapError struct {
	Code    string           `json:"code"`
	Message AbapErrorMessage `json:"message"`
}

// AbapErrorMessage contains the lanuage and value fields for ABAP errors
type AbapErrorMessage struct {
	Lang  string `json:"lang"`
	Value string `json:"value"`
}

// AbapServiceKey contains information about an ABAP service key
type AbapServiceKey struct {
	SapCloudService    string         `json:"sap.cloud.service"`
	URL                string         `json:"url"`
	SystemID           string         `json:"systemid"`
	Abap               AbapConnection `json:"abap"`
	Binding            AbapBinding    `json:"binding"`
	PreserveHostHeader bool           `json:"preserve_host_header"`
}

// AbapConnection contains information about the ABAP connection for the ABAP endpoint
type AbapConnection struct {
	Username                         string `json:"username"`
	Password                         string `json:"password"`
	CommunicationScenarioID          string `json:"communication_scenario_id"`
	CommunicationArrangementID       string `json:"communication_arrangement_id"`
	CommunicationSystemID            string `json:"communication_system_id"`
	CommunicationInboundUserID       string `json:"communication_inbound_user_id"`
	CommunicationInboundUserAuthMode string `json:"communication_inbound_user_auth_mode"`
}

// AbapBinding contains information about service binding in Cloud Foundry
type AbapBinding struct {
	ID      string `json:"id"`
	Type    string `json:"type"`
	Version string `json:"version"`
	Env     string `json:"env"`
}

/********************************
 *	Testing with a client mock  *
 ********************************/

// ClientMock contains information about the client mock
type ClientMock struct {
	Token              string
	Body               string
	BodyList           []string
	StatusCode         int
	Error              error
	NilResponse        bool
	ErrorInsteadOfDump bool
}

// SetOptions sets clientOptions for a client mock
func (c *ClientMock) SetOptions(opts piperhttp.ClientOptions) {}

// SendRequest sets a HTTP response for a client mock
func (c *ClientMock) SendRequest(method, url string, bdy io.Reader, hdr http.Header, cookies []*http.Cookie) (*http.Response, error) {

	if c.NilResponse {
		return nil, c.Error
	}

	var body []byte
	if c.Body != "" {
		body = []byte(c.Body)
	} else {
		if c.ErrorInsteadOfDump && len(c.BodyList) == 0 {
			return nil, errors.New("No more bodies in the list")
		}
		bodyString := c.BodyList[len(c.BodyList)-1]
		c.BodyList = c.BodyList[:len(c.BodyList)-1]
		body = []byte(bodyString)
	}
	header := http.Header{}
	header.Set("X-Csrf-Token", c.Token)
	return &http.Response{
		StatusCode: c.StatusCode,
		Header:     header,
		Body:       ioutil.NopCloser(bytes.NewReader(body)),
	}, c.Error
}

// DownloadFile : Empty file download
func (c *ClientMock) DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error {
	return nil
}

// AUtilsMock mock
type AUtilsMock struct {
	ReturnedConnectionDetailsHTTP ConnectionDetailsHTTP
	ReturnedError                 error
}

// GetAbapCommunicationArrangementInfo mock
func (autils *AUtilsMock) GetAbapCommunicationArrangementInfo(options AbapEnvironmentOptions, oDataURL string) (ConnectionDetailsHTTP, error) {
	return autils.ReturnedConnectionDetailsHTTP, autils.ReturnedError
}

// GetPollIntervall mock
func (autils *AUtilsMock) GetPollIntervall() time.Duration {
	return 1 * time.Microsecond
}

// Cleanup to reset AUtilsMock
func (autils *AUtilsMock) Cleanup() {
	autils.ReturnedConnectionDetailsHTTP = ConnectionDetailsHTTP{}
	autils.ReturnedError = nil
}
