package cmd

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/cookiejar"
	"reflect"
	"time"

	"github.com/SAP/jenkins-library/pkg/abaputils"
	"github.com/SAP/jenkins-library/pkg/command"
	piperhttp "github.com/SAP/jenkins-library/pkg/http"
	"github.com/SAP/jenkins-library/pkg/log"
	"github.com/SAP/jenkins-library/pkg/telemetry"
	"github.com/pkg/errors"
)

func abapEnvironmentCloneGitRepo(config abapEnvironmentCloneGitRepoOptions, _ *telemetry.CustomData) {

	c := command.Command{}

	c.Stdout(log.Writer())
	c.Stderr(log.Writer())

	var autils = abaputils.AbapUtils{
		Exec: &c,
	}

	client := piperhttp.Client{}

	// error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end
	err := runAbapEnvironmentCloneGitRepo(&config, &autils, &client)
	if err != nil {
		log.Entry().WithError(err).Fatal("step execution failed")
	}
}

func runAbapEnvironmentCloneGitRepo(config *abapEnvironmentCloneGitRepoOptions, com abaputils.Communication, client piperhttp.Sender) error {
	// Mapping for options
	subOptions := convertCloneConfig(config)

	// Determine the host, user and password, either via the input parameters or via a cloud foundry service key
	connectionDetails, errorGetInfo := com.GetAbapCommunicationArrangementInfo(subOptions, "")
	if errorGetInfo != nil {
		return errors.Wrap(errorGetInfo, "Parameters for the ABAP Connection not available")
	}

	// Configuring the HTTP Client and CookieJar
	cookieJar, errorCookieJar := cookiejar.New(nil)
	if errorCookieJar != nil {
		return errors.Wrap(errorCookieJar, "Could not create a Cookie Jar")
	}

	client.SetOptions(piperhttp.ClientOptions{
		MaxRequestDuration: 180 * time.Second,
		CookieJar:          cookieJar,
		Username:           connectionDetails.User,
		Password:           connectionDetails.Password,
	})

	errConfig := checkConfiguration(config)
	if errConfig != nil {
		return errors.Wrap(errConfig, "The provided configuration is not allowed")
	}

	repositories, errGetRepos := abaputils.GetRepositories(&abaputils.RepositoriesConfig{BranchName: config.BranchName, RepositoryName: config.RepositoryName, Repositories: config.Repositories}, true)
	if errGetRepos != nil {
		return fmt.Errorf("Something failed during the clone: %w", errGetRepos)
	}

	log.Entry().Infof("Start cloning %v repositories", len(repositories))
	for _, repo := range repositories {

		logString := repo.GetCloneLogString()
		errorString := "Clone of " + logString + " failed on the ABAP system"

		log.Entry().Info("-------------------------")
		log.Entry().Info("Start cloning " + logString)
		log.Entry().Info("-------------------------")

		// Triggering the Clone of the repository into the ABAP Environment system
		uriConnectionDetails, errorTriggerClone, didCheckoutPullInstead := triggerClone(repo, connectionDetails, client)
		if errorTriggerClone != nil {
			return errors.Wrapf(errorTriggerClone, errorString)
		}

		if !didCheckoutPullInstead {
			// Polling the status of the repository import on the ABAP Environment system
			// If the repository had been cloned already, as checkout/pull has been done - polling the status is not necessary anymore
			status, errorPollEntity := abaputils.PollEntity(repo.Name, uriConnectionDetails, client, com.GetPollIntervall())
			if errorPollEntity != nil {
				return errors.Wrapf(errorPollEntity, errorString)
			}
			if status == "E" {
				return errors.New("Clone of " + logString + " failed on the ABAP System")
			}
			log.Entry().Info("The " + logString + " was cloned successfully")
		}
	}
	log.Entry().Info("-------------------------")
	log.Entry().Info("All repositories were cloned successfully")
	return nil
}

func checkConfiguration(config *abapEnvironmentCloneGitRepoOptions) error {
	if config.Repositories != "" && config.RepositoryName != "" {
		return errors.New("It is not allowed to configure the parameters `repositories`and `repositoryName` at the same time")
	}
	if config.Repositories == "" && config.RepositoryName == "" {
		return errors.New("Please provide one of the following parameters: `repositories` or `repositoryName`")
	}
	return nil
}

func triggerClone(repo abaputils.Repository, cloneConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (abaputils.ConnectionDetailsHTTP, error, bool) {

	uriConnectionDetails := cloneConnectionDetails
	cloneConnectionDetails.XCsrfToken = "fetch"

	cloneConnectionDetails.URL = cloneConnectionDetails.URL + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Clones"

	// Loging into the ABAP System - getting the x-csrf-token and cookies
	resp, err := abaputils.GetHTTPResponse("HEAD", cloneConnectionDetails, nil, client)
	if err != nil {
		err = abaputils.HandleHTTPError(resp, err, "Authentication on the ABAP system failed", cloneConnectionDetails)
		return uriConnectionDetails, err, false
	}
	defer resp.Body.Close()

	log.Entry().WithField("StatusCode", resp.Status).WithField("ABAP Endpoint", cloneConnectionDetails.URL).Debug("Authentication on the ABAP system successful")
	uriConnectionDetails.XCsrfToken = resp.Header.Get("X-Csrf-Token")
	cloneConnectionDetails.XCsrfToken = uriConnectionDetails.XCsrfToken

	// Trigger the Clone of a Repository
	if repo.Name == "" {
		return uriConnectionDetails, errors.New("An empty string was passed for the parameter 'repositoryName'"), false
	}

	jsonBody := []byte(repo.GetCloneRequestBody())
	resp, err = abaputils.GetHTTPResponse("POST", cloneConnectionDetails, jsonBody, client)
	if err != nil {
		err, alreadyCloned := handleCloneError(resp, err, cloneConnectionDetails, client, repo)
		return uriConnectionDetails, err, alreadyCloned
	}
	defer resp.Body.Close()
	log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", repo.Name).WithField("branchName", repo.Branch).WithField("commitID", repo.CommitID).WithField("Tag", repo.Tag).Info("Triggered Clone of Repository / Software Component")

	// Parse Response
	var body abaputils.CloneEntity
	var abapResp map[string]*json.RawMessage
	bodyText, errRead := ioutil.ReadAll(resp.Body)
	if errRead != nil {
		return uriConnectionDetails, err, false
	}
	if err := json.Unmarshal(bodyText, &abapResp); err != nil {
		return uriConnectionDetails, err, false
	}
	if err := json.Unmarshal(*abapResp["d"], &body); err != nil {
		return uriConnectionDetails, err, false
	}
	if reflect.DeepEqual(abaputils.CloneEntity{}, body) {
		log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", repo.Name).WithField("branchName", repo.Branch).WithField("commitID", repo.CommitID).WithField("Tag", repo.Tag).Error("Could not Clone the Repository / Software Component")
		err := errors.New("Request to ABAP System not successful")
		return uriConnectionDetails, err, false
	}

	// The entity "Clones" does not allow for polling. To poll the progress, the related entity "Pull" has to be called
	// While "Clones" has the key fields UUID, SC_NAME and BRANCH_NAME, "Pull" only has the key field UUID
	uriConnectionDetails.URL = uriConnectionDetails.URL + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Pull(uuid=guid'" + body.UUID + "')"
	return uriConnectionDetails, nil, false
}

func handleCloneError(resp *http.Response, err error, cloneConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, repo abaputils.Repository) (returnedError error, alreadyCloned bool) {
	alreadyCloned = false
	returnedError = nil
	if resp == nil {
		log.Entry().WithError(err).WithField("ABAP Endpoint", cloneConnectionDetails.URL).Error("Request failed")
		returnedError = errors.New("Response is nil")
		return
	}
	defer resp.Body.Close()
	errorText, errorCode, parsingError := abaputils.GetErrorDetailsFromResponse(resp)
	if parsingError != nil {
		returnedError = err
		return
	}
	if errorCode == "A4C_A2G/257" {
		// With the latest release, a repeated "clone" was prohibited
		// As an intermediate workaround, we react to the error message A4C_A2G/257 that gets thrown, if the repository had already been cloned
		// In this case, a checkout branch and a pull will be performed
		alreadyCloned = true
		log.Entry().Infof("-------------------------")
		log.Entry().Infof("-------------------------")
		log.Entry().Infof("%s", "The repository / software component has already been cloned on the ABAP Environment system ")
		log.Entry().Infof("%s", "A `checkout branch` and a `pull` will be performed instead")
		log.Entry().Infof("-------------------------")
		log.Entry().Infof("-------------------------")
		checkoutOptions := abapEnvironmentCheckoutBranchOptions{
			Username:       cloneConnectionDetails.User,
			Password:       cloneConnectionDetails.Password,
			Host:           cloneConnectionDetails.Host,
			RepositoryName: repo.Name,
			BranchName:     repo.Branch,
		}
		c := command.Command{}
		c.Stdout(log.Writer())
		c.Stderr(log.Writer())
		com := abaputils.AbapUtils{
			Exec: &c,
		}
		returnedError = runAbapEnvironmentCheckoutBranch(&checkoutOptions, &com, client)
		if returnedError != nil {
			return
		}
		log.Entry().Infof("-------------------------")
		log.Entry().Infof("-------------------------")
		pullOptions := abapEnvironmentPullGitRepoOptions{
			Username:       cloneConnectionDetails.User,
			Password:       cloneConnectionDetails.Password,
			Host:           cloneConnectionDetails.Host,
			RepositoryName: repo.Name,
			CommitID:       repo.CommitID,
		}
		returnedError = runAbapEnvironmentPullGitRepo(&pullOptions, &com, client)
		if returnedError != nil {
			return
		}
	} else {
		log.Entry().WithField("StatusCode", resp.Status).Error("Could not clone the " + repo.GetCloneLogString())
		abapError := errors.New(fmt.Sprintf("%s - %s", errorCode, errorText))
		returnedError = errors.Wrap(abapError, err.Error())
	}
	return
}

func convertCloneConfig(config *abapEnvironmentCloneGitRepoOptions) abaputils.AbapEnvironmentOptions {
	subOptions := abaputils.AbapEnvironmentOptions{}

	subOptions.CfAPIEndpoint = config.CfAPIEndpoint
	subOptions.CfServiceInstance = config.CfServiceInstance
	subOptions.CfServiceKeyName = config.CfServiceKeyName
	subOptions.CfOrg = config.CfOrg
	subOptions.CfSpace = config.CfSpace
	subOptions.Host = config.Host
	subOptions.Password = config.Password
	subOptions.Username = config.Username
	return subOptions
}
