import com.sap.piper.JenkinsUtils

import static com.sap.piper.Prerequisites.checkScript

import com.sap.piper.BashUtils
import com.sap.piper.ConfigurationHelper
import com.sap.piper.GenerateDocumentation
import com.sap.piper.Utils

import groovy.transform.Field

import hudson.AbortException

@Field String STEP_NAME = getClass().getName()

@Field Set GENERAL_CONFIG_KEYS = STEP_CONFIG_KEYS

@Field Set STEP_CONFIG_KEYS = [
    'action',
    'apiUrl',
    'credentialsId',
    'deploymentId',
    'deployIdLogPattern',
    'deployOpts',
    /** A map containing properties forwarded to dockerExecute. For more details see [here][dockerExecute] */
    'docker',
    'loginOpts',
    'mode',
    'mtaPath',
    'org',
    'space',
    'xsSessionFile',
]

@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS

enum DeployMode {
    DEPLOY,
    BG_DEPLOY,
    NONE

    String toString() {
        name().toLowerCase(Locale.ENGLISH).replaceAll('_', '-')
    }
}

enum Action {
    RESUME,
    ABORT,
    RETRY,
    NONE

    String toString() {
        name().toLowerCase(Locale.ENGLISH)
    }
}

/**
  * Performs an XS deployment
  *
  * In case of blue-green deployments the step is called for the deployment in the narrower sense
  * and later again for resuming or aborting. In this case both calls needs to be performed from the
  * same directory.
  */
@GenerateDocumentation
void call(Map parameters = [:]) {

    handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) {

        def utils = parameters.juStabUtils ?: new Utils()

        final script = checkScript(this, parameters) ?: this

        ConfigurationHelper configHelper = ConfigurationHelper.newInstance(this)
            .loadStepDefaults()
            .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS)
            .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
            .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS)
            .addIfEmpty('mtaPath', script.commonPipelineEnvironment.getMtarFilePath())
            .addIfEmpty('deploymentId', script.commonPipelineEnvironment.xsDeploymentId)
            .mixin(parameters, PARAMETER_KEYS)

        Map config = configHelper.use()

        DeployMode mode = config.mode

        if(mode == DeployMode.NONE) {
            echo "Deployment skipped intentionally. Deploy mode '${mode.toString()}'."
            return
        }

        Action action = config.action

        if(mode == DeployMode.DEPLOY && action != Action.NONE) {
            error "Cannot perform action '${action.toString()}' in mode '${mode.toString()}'. Only action '${Action.NONE.toString()}' is allowed."
        }

        boolean performLogin  = ((mode == DeployMode.DEPLOY) || (mode == DeployMode.BG_DEPLOY && !(action in [Action.RESUME, Action.ABORT])))
        boolean performLogout = ((mode == DeployMode.DEPLOY) || (mode == DeployMode.BG_DEPLOY && action != Action.NONE))

        boolean sessionExists = fileExists file: config.xsSessionFile

        if( (! performLogin) && (! sessionExists) ) {
            error 'For the current configuration an already existing session is required. But there is no already existing session.'
        }

        configHelper
            .collectValidationFailures()
            /**
              * Used for finalizing the blue-green deployment.
              * @possibleValues RESUME, ABORT, RETRY
              */
            .withMandatoryProperty('action')
            /** The file name of the file representing the sesssion after `xs login`. Should not be changed normally. */
            .withMandatoryProperty('xsSessionFile')
            /** Regex pattern for retrieving the ID of the deployment. */
            .withMandatoryProperty('deployIdLogPattern')
            /**
              * Controls if there is a standard deployment or a blue green deployment
              * @possibleValues DEPLOY, BG_DEPLOY
              */
            .withMandatoryProperty('mode')
            /** The endpoint */
            .withMandatoryProperty('apiUrl')
            /** The organization */
            .withMandatoryProperty('org')
            /** The space */
            .withMandatoryProperty('space')
            /** Additional options appended to the login command. Only needed for sophisticated cases.
              * When provided it is the duty of the provider to ensure proper quoting / escaping.
              */
            .withMandatoryProperty('loginOpts')
            /** Additional options appended to the deploy command. Only needed for sophisticated cases.
              * When provided it is the duty of the provider to ensure proper quoting / escaping.
              */
            .withMandatoryProperty('deployOpts')
            /** The credentialsId */
            .withMandatoryProperty('credentialsId')
            /** The path to the deployable. If not provided explicitly it is retrieved from the common pipeline environment
              * (Parameter `mtarFilePath`).
              */
            .withMandatoryProperty('mtaPath', null, {action == Action.NONE})
            .withMandatoryProperty('deploymentId',
                'No deployment id provided, neither via parameters nor via common pipeline environment. Was there a deployment before?',
                {action in [Action.RESUME, Action.ABORT, Action.RETRY]})
            .use()

        utils.pushToSWA([
            step: STEP_NAME,
        ], config)

        if(action == Action.NONE) {
            boolean deployableExists = fileExists file: config.mtaPath
            if(! deployableExists)
                error "Deployable '${config.mtaPath}' does not exist."
        }

        if(performLogin) {
            login(script, config)
        }

        def failures = []

        if(action in [Action.RESUME, Action.ABORT, Action.RETRY]) {

            complete(script, mode, action, config, failures)

        } else {

            deploy(script, mode, config, failures)
        }

        if (performLogout || failures) {
            logout(script, config, failures)

        } else {
            echo "Skipping logout in order to be able to resume or abort later."
        }

        if(failures) {
            error "Failed command(s): ${failures}. Check earlier log for details."
        }
    }
}

void login(Script script, Map config) {

    withCredentials([usernamePassword(
        credentialsId: config.credentialsId,
        passwordVariable: 'password',
        usernameVariable: 'username'
    )]) {

        def returnCode = executeXSCommand([script: script].plus(config.docker),
        [
            "xs login -a ${config.apiUrl} -u ${username} -p ${BashUtils.quoteAndEscape(password)} -o ${config.org} -s ${config.space} ${config.loginOpts}",
            'RC=$?',
            "[ \$RC == 0 ]  && cp \"\${HOME}/${config.xsSessionFile}\" .",
            'exit $RC'
        ])

        if(returnCode != 0)
            error "xs login failed."
    }

    boolean existsXsSessionFileAfterLogin = fileExists file: config.xsSessionFile
    if(! existsXsSessionFileAfterLogin)
        error "Session file ${config.xsSessionFile} not found in current working directory after login."
}

void deploy(Script script, DeployMode mode, Map config, def failures) {

    def deploymentLog

    try {
        lock(getLockIdentifier(config)) {
            deploymentLog = executeXSCommand([script: script].plus(config.docker),
            [
                "cp ${config.xsSessionFile} \${HOME}",
                "xs ${mode.toString()} '${config.mtaPath}' -f ${config.deployOpts}"
            ], true)
        }

        echo "Deploy log: ${deploymentLog}"

    } catch(AbortException e) {
        echo "deployment failed. Message: ${e.getMessage()}, Log: ${deploymentLog}}"
        failures << "xs ${mode.toString()}"
    }

    if(mode == DeployMode.BG_DEPLOY) {

        if(! failures.isEmpty()) {

            echo "Retrieval of deploymentId skipped since prior deployment was not successfull."

        } else {

            for (def logLine : deploymentLog.readLines()) {
                def matcher = logLine =~ config.deployIdLogPattern
                if(matcher.find()) {
                    script.commonPipelineEnvironment.xsDeploymentId = matcher[0][1]
                    echo "DeploymentId: ${script.commonPipelineEnvironment.xsDeploymentId}."
                    break
                }
            }
            if(script.commonPipelineEnvironment.xsDeploymentId == null) {
                failures << "Cannot lookup deploymentId. Search pattern was: '${config.deployIdLogPattern}'."
            }
        }
    }
}

void complete(Script script, DeployMode mode, Action action, Map config, def failures) {

    if(mode != DeployMode.BG_DEPLOY)
        error "Action '${action.toString()}' can only be performed for mode '${DeployMode.BG_DEPLOY.toString()}'. Current mode is: '${mode.toString()}'."

    def returnCode = 1

    lock(getLockIdentifier(config)) {
        returnCode = executeXSCommand([script: script].plus(config.docker),
        [
            "cp ${config.xsSessionFile} \${HOME}",
            "xs ${mode.toString()} -i ${config.deploymentId} -a ${action.toString()}"
        ])
    }

    if(returnCode != 0) {
        echo "${mode.toString()} with action '${action.toString()}' failed with return code ${returnCode}."
        failures << "xs ${mode.toString()} -a ${action.toString()}"
    }
}

void logout(Script script, Map config, def failures) {

    def returnCode = executeXSCommand([script: script].plus(config.docker),
    [
        "cp ${config.xsSessionFile} \${HOME}",
        'xs logout'
    ])

    if(returnCode != 0) {
        failures << 'xs logout'
    }

    sh "XSCONFIG=${config.xsSessionFile}; [ -f \${XSCONFIG} ] && rm \${XSCONFIG}"
}

String getLockIdentifier(Map config) {
    "$STEP_NAME:${config.apiUrl}:${config.org}:${config.space}"
}

def executeXSCommand(Map dockerOptions, List commands, boolean returnStdout = false) {

    def r

    dockerExecute(dockerOptions) {

        // in case there are credentials contained in the commands we assume
        // the call is properly wrapped by withCredentials(./.)
        echo "Executing: '${commands}'."

        List prelude = [
            '#!/bin/bash'
        ]

        List script = (prelude + commands)

        params = [
            script: script.join('\n')
        ]

        if(returnStdout) {
            params << [ returnStdout: true ]
        } else {
            params << [ returnStatus: true ]
        }

        r = sh params

        if( (! returnStdout ) && r != 0) {

            try {
                echo "xs logs:"

                sh 'LOG_FOLDER=${HOME}/.xs_logs; [ -d ${LOG_FOLDER} ]  && cat ${LOG_FOLDER}/*'

            } catch(Exception e) {

                echo "Cannot provide xs logs: ${e.getMessage()}."
            }

            echo "Executing of commands '${commands}' failed. Check earlier logs for details."
        }
    }
    r
}
