import com.cloudbees.groovy.cps.NonCPS
import com.sap.piper.Utils
import com.sap.piper.ConfigurationHelper
import com.sap.piper.ConfigurationLoader
import com.sap.piper.DebugReport
import com.sap.piper.k8s.ContainerMap
import groovy.transform.Field

import static com.sap.piper.Prerequisites.checkScript

@Field String STEP_NAME = 'piperStageWrapper'

void call(Map parameters = [:], body) {

    final script = checkScript(this, parameters) ?: this
    def utils = parameters.juStabUtils ?: new Utils()

    def stageName = parameters.stageName ?: env.STAGE_NAME

    // load default & individual configuration
    Map config = ConfigurationHelper.newInstance(this)
        .loadStepDefaults()
        .mixin(ConfigurationLoader.defaultStageConfiguration(script, stageName))
        .mixinGeneralConfig(script.commonPipelineEnvironment)
        .mixinStageConfig(script.commonPipelineEnvironment, stageName)
        .mixin(parameters)
        .addIfEmpty('stageName', stageName)
        .addIfEmpty('lockingResourceGroup', script.commonPipelineEnvironment.projectName)
        .dependingOn('stageName').mixin('ordinal')
        .use()

    stageLocking(config) {
        def containerMap = ContainerMap.instance.getMap().get(stageName) ?: [:]
        List environment = []
        if (stageName && stageName != env.STAGE_NAME) {
            // Avoid two sources of truth with regards to stageName.
            // env.STAGE_NAME is filled from stage('Display name') {, it only serves the purpose of
            // easily getting to the stage name from steps.
            environment.add("STAGE_NAME=${stageName}")
        }
        if (config.sidecarImage) {
            echo "sidecarImage configured for stage '${stageName}': '${config.sidecarImage}'"
            environment.add("SIDECAR_IMAGE=${config.sidecarImage}")
        }
        if (Boolean.valueOf(env.ON_K8S) && (containerMap.size() > 0 || config.runStageInPod)) {
            environment.add("POD_NAME=${stageName}")
            withEnv(environment) {
                dockerExecuteOnKubernetes(script: script, containerMap: containerMap, stageName: stageName) {
                    executeStage(script, body, stageName, config, utils, parameters.telemetryDisabled)
                }
            }
        } else {
            withEnvWrapper(environment) {
                node(config.nodeLabel) {
                    executeStage(script, body, stageName, config, utils, parameters.telemetryDisabled)
                }
            }
        }
    }
}

private void withEnvWrapper(List environment, Closure body) {
    if (environment) {
        withEnv(environment) {
            body()
        }
    } else {
        body()
    }
}

private void stageLocking(Map config, Closure body) {
    if (config.stageLocking) {
        String resource = config.lockingResourceGroup?:env.JOB_NAME
        if(config.lockingResource){
            resource += "/${config.lockingResource}"
        }
        else if(config.ordinal){
            resource += "/${config.ordinal}"
        }
        lock(resource: resource, inversePrecedence: true) {
            if(config.ordinal) {
                milestone config.ordinal
            }
            body()
        }
    } else {
        body()
    }
}

private void executeStage(script, originalStage, stageName, config, utils, telemetryDisabled = false) {
    boolean projectExtensions
    boolean globalExtensions
    def startTime = System.currentTimeMillis()

    try {
        // Add general stage stashes to config.stashContent
        config.stashContent = utils.unstashStageFiles(script, stageName, config.stashContent)

        /* Defining the sources where to look for a project extension and a repository extension.
        * Files need to be named like the executed stage to be recognized.
        */
        def projectInterceptorFile = "${config.projectExtensionsDirectory}${stageName}.groovy"
        def globalInterceptorFile = "${config.globalExtensionsDirectory}${stageName}.groovy"
        projectExtensions = fileExists(projectInterceptorFile)
        globalExtensions = fileExists(globalInterceptorFile)
        // Pre-defining the real originalStage in body variable, might be overwritten later if extensions exist
        def body = originalStage

        // First, check if a global extension exists via a dedicated repository
        if (globalExtensions && allowExtensions(script)) {
            echo "[${STEP_NAME}] Found global interceptor '${globalInterceptorFile}' for ${stageName}."
            // If we call the global interceptor, we will pass on originalStage as parameter
            DebugReport.instance.globalExtensions.put(stageName, "Overwrites")
            Closure modifiedOriginalStage = {
                DebugReport.instance.globalExtensions.put(stageName, "Extends")
                originalStage()
            }

            body = {
                callInterceptor(script, globalInterceptorFile, modifiedOriginalStage, stageName, config)
            }
        }

        // Second, check if a project extension (within the same repository) exists
        if (projectExtensions && allowExtensions(script)) {
            echo "[${STEP_NAME}] Running project interceptor '${projectInterceptorFile}' for ${stageName}."
            // If we call the project interceptor, we will pass on body as parameter which contains either originalStage or the repository interceptor
            if (projectExtensions && globalExtensions) {
                DebugReport.instance.globalExtensions.put(stageName, "Unknown (Overwritten by local extension)")
            }
            DebugReport.instance.localExtensions.put(stageName, "Overwrites")
            Closure modifiedOriginalBody = {
                DebugReport.instance.localExtensions.put(stageName, "Extends")
                if (projectExtensions && globalExtensions) {
                    DebugReport.instance.globalExtensions.put(stageName, "Overwrites")
                }
                body.call()
            }

            callInterceptor(script, projectInterceptorFile, modifiedOriginalBody, stageName, config)

        } else {
            // NOTE: It may appear more elegant to re-assign 'body' more than once and then call 'body()' after the
            // if-block. This could lead to infinite loops however, as any change to the local variable 'body' will
            // become visible in all of the closures at the time they run. I.e. 'body' inside any of the closures will
            // reflect the last assignment and not its value at the time of constructing the closure!
            body()
        }

    } finally {
        //Perform stashing of selected files in workspace
        utils.stashStageFiles(script, stageName)

        // In general telemetry reporting is disabled by the config settings. This flag is used to disable the reporting when the config is not yet read (e.g. init stage).
        if (!telemetryDisabled) {
            def duration = System.currentTimeMillis() - startTime
            utils.pushToSWA([
                eventType       : 'library-os-stage',
                stageName       : stageName,
                stepParamKey1   : 'buildResult',
                stepParam1      : "${script.currentBuild.currentResult}",
                buildResult     : "${script.currentBuild.currentResult}",
                stepParamKey2   : 'stageStartTime',
                stepParam2      : "${startTime}",
                stageStartTime  : "${startTime}",
                stepParamKey3   : 'stageDuration',
                stepParam3      : "${duration}",
                stageDuration   : "${duration}",
                stepParamKey4   : 'projectExtension',
                stepParam4      : "${projectExtensions}",
                projectExtension: "${projectExtensions}",
                stepParamKey5   : 'globalExtension',
                stepParam5      : "${globalExtensions}",
                globalExtension : "${globalExtensions}"
            ], config)
        }
    }
}

private void callInterceptor(Script script, String extensionFileName, Closure originalStage, String stageName, Map configuration) {
    try {
        Script interceptor = load(extensionFileName)
        if (isOldInterceptorInterfaceUsed(interceptor)) {
            echo("[Warning] The interface to implement extensions has changed. " +
                "The extension $extensionFileName has to implement a method named 'call' with exactly one parameter of type Map. " +
                "This map will have the properties script, originalStage, stageName, config. " +
                "For example: def call(Map parameters) { ... }")
            interceptor.call(originalStage, stageName, configuration, configuration)
        } else {
            validateInterceptor(interceptor, extensionFileName)
            interceptor.call([
                script       : script,
                originalStage: originalStage,
                stageName    : stageName,
                config       : configuration
            ])
        }
    } catch (Throwable error) {
        if (!DebugReport.instance.failedBuild.step) {
            DebugReport.instance.storeStepFailure("${stageName}(extended)", error, true)
        }
        throw error
    }
}

@NonCPS
private boolean isInterceptorValid(Script interceptor) {
    MetaMethod method = interceptor.metaClass.pickMethod("call", [Map.class] as Class[])
    return method != null
}

private void validateInterceptor(Script interceptor, String extensionFileName) {
    if (!isInterceptorValid(interceptor)) {
        error("The extension $extensionFileName has to implement a method named 'call' with exactly one parameter of type Map. " +
            "This map will have the properties script, originalStage, stageName, config. " +
            "For example: def call(Map parameters) { ... }")
    }
}

@NonCPS
private boolean isOldInterceptorInterfaceUsed(Script interceptor) {
    MetaMethod method = interceptor.metaClass.pickMethod("call", [Closure.class, String.class, Map.class, Map.class] as Class[])
    return method != null
}

private static boolean allowExtensions(Script script) {
    return script.env.PIPER_DISABLE_EXTENSIONS == null || Boolean.valueOf(script.env.PIPER_DISABLE_EXTENSIONS) == false
}
