// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.sam

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.text.SemVer
import software.aws.toolkits.jetbrains.core.executables.ExecutableManager
import software.aws.toolkits.jetbrains.core.executables.getExecutableIfPresent
import software.aws.toolkits.jetbrains.services.cloudformation.CloudFormationTemplate
import software.aws.toolkits.jetbrains.services.cloudformation.SERVERLESS_FUNCTION_TYPE
import software.aws.toolkits.resources.message
import java.io.FileFilter
import java.nio.file.Paths

object SamCommon {
    val mapper = jacksonObjectMapper()
    const val SAM_BUILD_DIR = ".aws-sam"
    const val SAM_INFO_VERSION_KEY = "version"
    const val SAM_INVALID_OPTION_SUBSTRING = "no such option"
    const val SAM_NAME = "SAM CLI"

    // The minimum SAM CLI version required for images. TODO remove when sam min > 1.13.0
    val minImageVersion = SemVer("1.13.0", 1, 13, 0)

    /**
     * @return The string representation of the SAM version else "UNKNOWN"
     */
    fun getVersionString(): String = ExecutableManager.getInstance().getExecutableIfPresent<SamExecutable>().version ?: "UNKNOWN"

    fun getTemplateFromDirectory(projectRoot: VirtualFile): VirtualFile? {
        // Use Java File so we don't need to do a full VFS refresh
        val projectRootFile = VfsUtil.virtualToIoFile(projectRoot)
        val yamlFiles = projectRootFile.listFiles(
            FileFilter {
                it.isFile && it.name.endsWith("yaml") || it.name.endsWith("yml")
            }
        )?.toList() ?: emptyList()
        assert(yamlFiles.size == 1) { message("cloudformation.yaml.too_many_files", yamlFiles.size) }
        return LocalFileSystem.getInstance().refreshAndFindFileByIoFile(yamlFiles.first())
    }

    fun getCodeUrisFromTemplate(project: Project, template: VirtualFile): List<VirtualFile> {
        val templatePath = Paths.get(template.parent.path)

        val codeDirs = runReadAction {
            val cfTemplate = CloudFormationTemplate.parse(project, template)

            cfTemplate.resources()
                .filter { it.isType(SERVERLESS_FUNCTION_TYPE) }
                .map { templatePath.resolve(it.getScalarProperty("CodeUri")) }
                .toList()
        }

        val localFileSystem = LocalFileSystem.getInstance()
        return codeDirs.mapNotNull { localFileSystem.refreshAndFindFileByIoFile(it.toFile()) }
            .filter { it.isDirectory }
    }
}


// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.sam

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.runInEdtAndGet
import kotlinx.coroutines.future.await
import kotlinx.coroutines.runBlocking
import org.jetbrains.yaml.YAMLFileType
import software.amazon.awssdk.services.cloudformation.model.StackSummary
import software.aws.toolkits.jetbrains.ToolkitPlaces
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance
import software.aws.toolkits.jetbrains.core.executables.ExecutableManager
import software.aws.toolkits.jetbrains.core.executables.getExecutable
import software.aws.toolkits.jetbrains.services.cloudformation.Parameter
import software.aws.toolkits.jetbrains.services.cloudformation.validateSamTemplateHasResources
import software.aws.toolkits.jetbrains.ui.KeyValueTextField
import software.aws.toolkits.jetbrains.ui.ResourceSelector
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.ui.find
import software.aws.toolkits.resources.message
import java.util.regex.PatternSyntaxException

object SamTemplateFileUtils {
    val templateYamlRegex = Regex("template\\.y[a]?ml", RegexOption.IGNORE_CASE)

    /**
     * Determines the relevant Sam Template, returns null if one can't be found.
     */
    fun getSamTemplateFile(e: AnActionEvent): VirtualFile? = runReadAction {
        val virtualFiles = e.getData(PlatformDataKeys.VIRTUAL_FILE_ARRAY) ?: return@runReadAction null
        val virtualFile = virtualFiles.singleOrNull() ?: return@runReadAction null

        if (templateYamlRegex.matches(virtualFile.name)) {
            return@runReadAction virtualFile
        }

        // If the module node was selected, see if there is a template file in the top level folder
        val module = e.getData(LangDataKeys.MODULE_CONTEXT)
        if (module != null) {
            // It is only acceptable if one template file is found
            val childTemplateFiles = ModuleRootManager.getInstance(module).contentRoots.flatMap { root ->
                root.children.filter { child -> templateYamlRegex.matches(child.name) }
            }

            if (childTemplateFiles.size == 1) {
                return@runReadAction childTemplateFiles.single()
            }
        }

        return@runReadAction null
    }

    fun validateTemplateFile(project: Project, templateFile: VirtualFile): String? =
        try {
            runReadAction {
                project.validateSamTemplateHasResources(templateFile)
            }
        } catch (e: Exception) {
            message("serverless.application.deploy.error.bad_parse", templateFile.path, e)
        }

    fun retrieveSamTemplate(e: AnActionEvent, project: Project): VirtualFile? {
        if (e.place == ToolkitPlaces.EXPLORER_TOOL_WINDOW) {
            return runInEdtAndGet {
                FileChooser.chooseFile(
                    FileChooserDescriptorFactory.createSingleFileDescriptor(YAMLFileType.YML),
                    project,
                    project.guessProjectDir()
                )
            } ?: return null
        } else {
            val file = getSamTemplateFile(e)
            if (file == null) {
                Exception(message("serverless.application.deploy.toast.template_file_failure"))
                    .notifyError(message("aws.notification.title"), project)
                return null
            }
            return file
        }
    }
}

fun getSamCli(): GeneralCommandLine {
    val executable = runBlocking {
        ExecutableManager.getInstance().getExecutable<SamExecutable>().await()
    }

    val samExecutable = when (executable) {
        is ExecutableInstance.Executable -> executable
        else -> {
            throw RuntimeException((executable as? ExecutableInstance.BadExecutable)?.validationError.orEmpty())
        }
    }

    return samExecutable.getCommandLine()
}

object ValidateSamParameters {
    // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-using-console-create-stack-parameters.html
    //  A stack name can contain only alphanumeric characters (case-sensitive) and hyphens. It must start with an alphabetic character and can't be longer than 128 characters.
    private val STACK_NAME_PATTERN = "[a-zA-Z][a-zA-Z0-9-]*".toRegex()
    const val MAX_STACK_NAME_LENGTH = 128
    fun validateStackName(name: String?, availableStacks: ResourceSelector<StackSummary>): String? {
        if (name.isNullOrEmpty()) {
            return message("serverless.application.deploy.validation.new.stack.name.missing")
        }
        if (!STACK_NAME_PATTERN.matches(name)) {
            return message("serverless.application.deploy.validation.new.stack.name.invalid")
        }
        if (name.length > MAX_STACK_NAME_LENGTH) {
            return message("serverless.application.deploy.validation.new.stack.name.too.long", MAX_STACK_NAME_LENGTH)
        }
        // Check if the new stack name is same as an existing stack name
        availableStacks.model.find { it.stackName() == name }?.let {
            return message("serverless.application.deploy.validation.new.stack.name.duplicate")
        }
        return null
    }

    fun validateParameters(parametersComponent: KeyValueTextField, templateFileParameters: List<Parameter>): ValidationInfo? {
        // validate on ui element because value hasn't been committed yet
        val parameters = parametersComponent.envVars
        val parameterDeclarations = templateFileParameters.associateBy { it.logicalName }

        val invalidParameters = parameters.entries.mapNotNull { (name, value) ->
            val cfnParameterDeclaration = parameterDeclarations[name] ?: return ValidationInfo("parameter declared but not in template")
            when (cfnParameterDeclaration.getOptionalScalarProperty("Type")) {
                "String" -> validateStringParameter(name, value, cfnParameterDeclaration)
                "Number" -> validateNumberParameter(name, value, cfnParameterDeclaration)
                // not implemented: List<Number>, CommaDelimitedList, AWS-specific parameters, SSM parameters
                else -> null
            }
        }

        return invalidParameters.firstOrNull()
    }

    private fun validateStringParameter(name: String, providedValue: String?, parameterDeclaration: Parameter): ValidationInfo? {
        val value = providedValue.orEmpty()
        val minValue = parameterDeclaration.getOptionalScalarProperty("MinLength")
        val maxValue = parameterDeclaration.getOptionalScalarProperty("MaxLength")
        val allowedPattern = parameterDeclaration.getOptionalScalarProperty("AllowedPattern")

        minValue?.toIntOrNull()?.let {
            if (value.length < it) {
                return ValidationInfo(message("serverless.application.deploy.validation.template.values.tooShort", name, minValue))
            }
        }

        maxValue?.toIntOrNull()?.let {
            if (value.length > it) {
                return ValidationInfo(message("serverless.application.deploy.validation.template.values.tooLong", name, maxValue))
            }
        }

        allowedPattern?.let {
            try {
                val regex = it.toRegex()
                if (!regex.matches(value)) {
                    return ValidationInfo(message("serverless.application.deploy.validation.template.values.failsRegex", name, regex))
                }
            } catch (e: PatternSyntaxException) {
                return ValidationInfo(message("serverless.application.deploy.validation.template.values.badRegex", name, e.message ?: it))
            }
        }

        return null
    }

    private fun validateNumberParameter(name: String, value: String?, parameterDeclaration: Parameter): ValidationInfo? {
        // cfn numbers can be integer or float. assume real implementation refers to java floats
        val number = value?.toFloatOrNull()
            ?: return ValidationInfo(message("serverless.application.deploy.validation.template.values.notANumber", name, value.orEmpty()))
        val minValue = parameterDeclaration.getOptionalScalarProperty("MinValue")
        val maxValue = parameterDeclaration.getOptionalScalarProperty("MaxValue")

        minValue?.toFloatOrNull()?.let {
            if (number < it) {
                return ValidationInfo(message("serverless.application.deploy.validation.template.values.tooSmall", name, minValue))
            }
        }

        maxValue?.toFloatOrNull()?.let {
            if (number > it) {
                return ValidationInfo(message("serverless.application.deploy.validation.template.values.tooBig", name, maxValue))
            }
        }

        return null
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.sam

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.text.SemVer
import software.aws.toolkits.core.lambda.LambdaArchitecture
import software.aws.toolkits.jetbrains.core.executables.AutoResolvable
import software.aws.toolkits.jetbrains.core.executables.ExecutableCommon
import software.aws.toolkits.jetbrains.core.executables.ExecutableType
import software.aws.toolkits.jetbrains.core.executables.Validatable
import software.aws.toolkits.jetbrains.services.lambda.deploy.DeployServerlessApplicationSettings
import software.aws.toolkits.jetbrains.services.lambda.sam.sync.SyncServerlessApplicationSettings
import software.aws.toolkits.jetbrains.services.lambda.wizard.AppBasedImageTemplate
import software.aws.toolkits.jetbrains.services.lambda.wizard.AppBasedZipTemplate
import software.aws.toolkits.jetbrains.services.lambda.wizard.LocationBasedTemplate
import software.aws.toolkits.jetbrains.services.lambda.wizard.TemplateParameters
import software.aws.toolkits.jetbrains.settings.ExecutableDetector
import java.nio.file.Path
import java.nio.file.Paths

class SamExecutable : ExecutableType<SemVer>, AutoResolvable, Validatable {
    companion object {
        // inclusive
        val minVersion = SemVer("1.0.0", 1, 0, 0)

        // exclusive
        val maxVersion = SemVer("2.0.0", 2, 0, 0)

        /**
         * The text SAM cli prints after the user code finishes running and
         * the debugger is disconnected.
         */
        const val endDebuggingText = "END RequestId: "

        // Reliable start message printed when delve starts
        // Comes from: https://github.com/go-delve/delve/blob/f5d2e132bca763d222680815ace98601c2396517/service/debugger/debugger.go#L187
        const val goStartMessage = "launching process with args"
    }

    override val displayName: String = "sam"
    override val id: String = "samCli"

    override fun version(path: Path): SemVer = ExecutableCommon.getVersion(
        path.toString(),
        SamVersionCache,
        this.displayName
    )

    override fun validate(path: Path) {
        val version = this.version(path)
        ExecutableCommon.checkSemVerVersion(
            version,
            minVersion,
            maxVersion,
            this.displayName
        )
    }

    override fun resolve(): Path? {
        val path = (
            if (SystemInfo.isWindows) {
                ExecutableDetector().find(
                    arrayOf("C:\\Program Files\\Amazon\\AWSSAMCLI\\bin", "C:\\Program Files (x86)\\Amazon\\AWSSAMCLI\\bin"),
                    arrayOf("sam.cmd", "sam.exe")
                )
            } else {
                ExecutableDetector().find(
                    arrayOf("/usr/local/bin", "/usr/bin"),
                    arrayOf("sam")
                )
            }
            ) ?: return null

        return Paths.get(path)
    }
}

fun GeneralCommandLine.samBuildCommand(
    templatePath: Path,
    logicalId: String? = null,
    buildDir: Path,
    environmentVariables: Map<String, String>,
    samOptions: SamOptions
) = this.apply {
    withEnvironment(environmentVariables)
    withWorkDirectory(templatePath.toAbsolutePath().parent.toString())

    addParameter("build")

    // Add logical id if known to perform min build
    logicalId?.let {
        withParameters(logicalId)
    }

    addParameter("--template")
    addParameter(templatePath.toString())
    addParameter("--build-dir")
    addParameter(buildDir.toString())
    if (samOptions.buildInContainer) {
        withParameters("--use-container")
    }

    if (samOptions.skipImagePull) {
        withParameters("--skip-pull-image")
    }

    samOptions.dockerNetwork?.let { network ->
        val sanitizedNetwork = network.trim()
        if (sanitizedNetwork.isNotBlank()) {
            withParameters("--docker-network").withParameters(sanitizedNetwork)
        }
    }

    samOptions.additionalBuildArgs?.let { buildArgs ->
        if (buildArgs.isNotBlank()) {
            withParameters(*buildArgs.split(" ").toTypedArray())
        }
    }
}

fun GeneralCommandLine.samPackageCommand(
    environmentVariables: Map<String, String>,
    templatePath: Path,
    packagedTemplatePath: Path,
    s3Bucket: String?,
    ecrRepo: String?
) = this.apply {
    withEnvironment(environmentVariables)
    withWorkDirectory(templatePath.parent.toAbsolutePath().toString())

    addParameter("package")
    addParameter("--template-file")
    addParameter(templatePath.toString())
    addParameter("--output-template-file")
    addParameter(packagedTemplatePath.toString())
    s3Bucket?.let {
        addParameter("--s3-bucket")
        addParameter(s3Bucket)
    }
    ecrRepo?.let {
        addParameter("--image-repository")
        addParameter(ecrRepo)
    }
}

fun GeneralCommandLine.samDeployCommand(
    environmentVariables: Map<String, String>,
    templatePath: Path,
    settings: DeployServerlessApplicationSettings
) = this.apply {
    withEnvironment(environmentVariables)
    withWorkDirectory(templatePath.parent.toAbsolutePath().toString())

    addParameter("deploy")
    addParameter("--template-file")
    addParameter(templatePath.toString())
    addParameter("--stack-name")
    addParameter(settings.stackName)
    addParameter("--s3-bucket")
    addParameter(settings.bucket)
    settings.ecrRepo?.let {
        addParameter("--image-repository")
        addParameter(it)
    }

    if (settings.capabilities.isNotEmpty()) {
        addParameter("--capabilities")
        addParameters(settings.capabilities.map { it.capability })
    }

    addParameter("--no-execute-changeset")

    if (settings.parameters.isNotEmpty()) {
        addParameter("--parameter-overrides")
        // Even though keys must be alphanumeric, escape it so that it is "valid" enough so that CFN can return a validation error instead of us failing
        settings.parameters.forEach { (key, value) ->
            addParameter(
                "${escapeParameter(key)}=${escapeParameter(value)}"
            )
        }
    }

    if (settings.tags.isNotEmpty()) {
        addParameter("--tags")
        // Even though keys must be alphanumeric, escape it so that it is "valid" enough so that CFN can return a validation error instead of us failing
        settings.tags.forEach { (key, value) ->
            addParameter(
                "${escapeParameter(key)}=${escapeParameter(value)}"
            )
        }
    }
}

private fun escapeParameter(param: String): String {
    // Invert the quote if the string is already quoted
    val quote = if (param.startsWith("\"") || param.endsWith("\"")) {
        "'"
    } else {
        "\""
    }

    return quote + param + quote
}

fun GeneralCommandLine.samInitCommand(
    outputDir: Path,
    parameters: TemplateParameters,
    extraContext: Map<String, String>
) = this.apply {
    addParameter("init")
    addParameter("--no-input")
    addParameter("--output-dir")
    addParameter(outputDir.toAbsolutePath().toString())

    when (parameters) {
        is AppBasedZipTemplate -> {
            addParameter("--name")
            addParameter(parameters.name)
            addParameter("--runtime")
            addParameter(parameters.runtime.toString())
            if (parameters.architecture != LambdaArchitecture.DEFAULT) {
                addParameter("--architecture")
                addParameter(parameters.architecture.toString())
            }
            addParameter("--dependency-manager")
            addParameter(parameters.dependencyManager)
            addParameter("--app-template")
            addParameter(parameters.appTemplate)
        }
        is AppBasedImageTemplate -> {
            addParameter("--package-type")
            addParameter("Image")
            addParameter("--name")
            addParameter(parameters.name)
            addParameter("--base-image")
            addParameter(parameters.baseImage)
            if (parameters.architecture != LambdaArchitecture.DEFAULT) {
                addParameter("--architecture")
                addParameter(parameters.architecture.toString())
            }
            addParameter("--dependency-manager")
            addParameter(parameters.dependencyManager)
            addParameter("--app-template")
            addParameter(parameters.appTemplate)
        }
        is LocationBasedTemplate -> {
            addParameter("--location")
            addParameter(parameters.location)
        }
    }

    if (extraContext.isNotEmpty()) {
        val extraContextAsJson = jacksonObjectMapper().writeValueAsString(extraContext)

        addParameter("--extra-context")
        addParameter(extraContextAsJson)
    }
}

fun GeneralCommandLine.samSyncCommand(
    environmentVariables: Map<String, String>,
    templatePath: Path,
    settings: SyncServerlessApplicationSettings
) = this.apply {
    withEnvironment(environmentVariables)
    withWorkDirectory(templatePath.toAbsolutePath().parent.toString())
    addParameter("sync")
    addParameter("--stack-name")
    addParameter(settings.stackName)
    addParameter("--template-file")
    addParameter(templatePath.toString())
    addParameter("--s3-bucket")
    addParameter(settings.bucket)
    settings.ecrRepo?.let {
        addParameter("--image-repository")
        addParameter(it)
    }
    if (settings.capabilities.isNotEmpty()) {
        addParameter("--capabilities")
        addParameters(settings.capabilities.map { it.capability })
    }
    if (settings.parameters.isNotEmpty()) {
        addParameter("--parameter-overrides")
        settings.parameters.forEach { (key, value) ->
            addParameter(
                "${escapeParameter(key)}=${escapeParameter(value)}"
            )
        }
    }

    if (settings.tags.isNotEmpty()) {
        addParameter("--tags")
        settings.tags.forEach { (key, value) ->
            addParameter(
                "${escapeParameter(key)}=${escapeParameter(value)}"
            )
        }
    }
    if (settings.useContainer) {
        addParameter("--use-container")
    }
    addParameter("--no-dependency-layer")

    addParameter("--no-watch")
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.sam

import com.intellij.util.xmlb.annotations.Tag

@Tag("sam")
data class SamOptions(
    var dockerNetwork: String? = null,
    var buildInContainer: Boolean = false,
    var skipImagePull: Boolean = false,
    var additionalBuildArgs: String? = null,
    var additionalLocalArgs: String? = null
)


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.jetbrains.services.lambda.sam

import com.intellij.openapi.progress.ProgressIndicator
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.services.schemas.SchemaCodeLangs
import software.aws.toolkits.jetbrains.services.schemas.SchemaTemplateParameters
import software.aws.toolkits.jetbrains.services.schemas.code.SchemaCodeDownloadRequestDetails
import software.aws.toolkits.jetbrains.services.schemas.code.SchemaCodeDownloader
import java.nio.file.Path

class SamSchemaDownloadPostCreationAction {
    fun downloadCodeIntoWorkspace(
        schemaTemplateParameters: SchemaTemplateParameters,
        schemaSourceRoot: Path,
        language: SchemaCodeLangs,
        connectionSettings: ConnectionSettings,
        indicator: ProgressIndicator
    ) {
        val codeGenDownloader = SchemaCodeDownloader.create(connectionSettings)

        codeGenDownloader.downloadCode(
            SchemaCodeDownloadRequestDetails(
                schemaTemplateParameters.schema,
                schemaTemplateParameters.schemaVersion,
                language,
                schemaSourceRoot.toFile()
            ),
            indicator
        ).toCompletableFuture().get()
    }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.jetbrains.services.lambda.sam

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.convertValue
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.testFramework.LightVirtualFile
import software.amazon.awssdk.services.lambda.model.Architecture
import software.amazon.awssdk.services.lambda.model.PackageType
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.core.utils.exists
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.inputStream
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.core.utils.writeText
import software.aws.toolkits.jetbrains.services.cloudformation.CloudFormationTemplate
import software.aws.toolkits.jetbrains.services.cloudformation.Function
import software.aws.toolkits.jetbrains.services.cloudformation.SERVERLESS_FUNCTION_TYPE
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits
import software.aws.toolkits.jetbrains.services.lambda.steps.UploadedCode
import software.aws.toolkits.jetbrains.services.lambda.steps.UploadedEcrCode
import software.aws.toolkits.jetbrains.services.lambda.steps.UploadedS3Code
import software.aws.toolkits.jetbrains.utils.YamlWriter
import software.aws.toolkits.jetbrains.utils.yaml
import software.aws.toolkits.resources.message
import java.io.File
import java.nio.file.Files
import java.nio.file.Path

object SamTemplateUtils {
    private val LOG = getLogger<SamTemplateUtils>()
    private val MAPPER = ObjectMapper(YAMLFactory())
    private const val S3_URI_PREFIX = "s3://"

    fun getFunctionEnvironmentVariables(template: Path, logicalId: String): Map<String, String> = readTemplate(template) {
        val function = requiredAt("/Resources").get(logicalId)
            ?: throw IllegalArgumentException("No resource with the logical ID $logicalId")
        val globals = at("/Globals/Function/Environment/Variables")
        val variables = function.at("/Properties/Environment/Variables")
        val globalVars = runCatching { MAPPER.convertValue<Map<String, String>?>(globals) ?: emptyMap() }.getOrDefault(emptyMap())
        val vars = runCatching { MAPPER.convertValue<Map<String, String>?>(variables) ?: emptyMap() }.getOrDefault(emptyMap())
        // function vars overwrite global ones if they overlap, so this works as expected
        globalVars + vars
    }

    fun getUploadedCodeUri(template: Path, logicalId: String): UploadedCode = readTemplate(template) {
        val function = findFunction(logicalId)
        if (function.isImageBased()) {
            UploadedEcrCode(function.requiredAt("/Properties/ImageUri").textValue())
        } else {
            val codeUri = function.requiredAt("/Properties/CodeUri")

            // CodeUri: s3://<bucket>>/<key>
            // or
            // CodeUri:
            //  Bucket: mybucket-name
            //  Key: code.zip
            //  Version: 121212

            when {
                codeUri.isTextual -> convertCodeUriString(codeUri.textValue())
                codeUri.isObject -> convertCodeUriObject(codeUri)
                else -> throw IllegalStateException("Unable to parse codeUri $codeUri")
            }
        }
    }

    private fun convertCodeUriString(codeUri: String): UploadedS3Code {
        if (!codeUri.startsWith(S3_URI_PREFIX)) {
            throw IllegalStateException("$codeUri does not start with $S3_URI_PREFIX")
        }

        val s3bucketKey = codeUri.removePrefix(S3_URI_PREFIX)
        val split = s3bucketKey.split("/", limit = 2)
        if (split.size != 2) {
            throw IllegalStateException("$codeUri does not follow the format $S3_URI_PREFIX<bucket>/<key>")
        }

        return UploadedS3Code(
            bucket = split.first(),
            key = split.last(),
            version = null
        )
    }

    private fun convertCodeUriObject(codeUri: JsonNode): UploadedS3Code = UploadedS3Code(
        bucket = codeUri.required("Bucket").textValue(),
        key = codeUri.required("Key").textValue(),
        version = codeUri.get("Version").textValue()
    )

    /**
     * Returns the location of the Lambda source code as per SAM build requirements
     */
    fun getCodeLocation(template: Path, logicalId: String): String = readTemplate(template) {
        val function = findFunction(logicalId)
        if (function.isServerlessFunction()) {
            if (function.isImageBased()) {
                function.getPathOrThrow(logicalId, "/Metadata/DockerContext").textValue()
            } else {
                function.getPathOrThrow(logicalId, "/Properties/CodeUri").textValue()
            }
        } else {
            function.getPathOrThrow(logicalId, "/Properties/Code").textValue()
        }
    }

    private fun JsonNode.getPathOrThrow(logicalId: String, path: String): JsonNode {
        val node = at(path)
        if (node.isMissingNode) {
            throw RuntimeException(message("cloudformation.key_not_found", path, logicalId))
        }
        return node
    }

    private fun JsonNode.findFunction(logicalId: String): JsonNode = this.requiredAt("/Resources").get(logicalId)
        ?: throw IllegalArgumentException("No resource with the logical ID $logicalId")

    private fun JsonNode.isImageBased(): Boolean = this.packageType() == PackageType.IMAGE

    private fun JsonNode.packageType(): PackageType {
        val type = this.at("/Properties/PackageType")?.textValue() ?: return PackageType.ZIP
        return PackageType.knownValues().firstOrNull { it.toString() == type }
            ?: throw IllegalStateException(message("cloudformation.invalid_property", "PackageType", type))
    }

    private fun JsonNode.isServerlessFunction(): Boolean = this.get("Type")?.textValue() == SERVERLESS_FUNCTION_TYPE

    private fun <T> readTemplate(template: Path, function: JsonNode.() -> T): T = template.inputStream().use {
        function(MAPPER.readTree(it))
    }

    @JvmStatic
    fun findFunctionsFromTemplate(project: Project, file: File): List<Function> {
        val virtualFile = file.readFileIntoMemory() ?: return emptyList()
        return findFunctionsFromTemplate(project, virtualFile)
    }

    @JvmStatic
    fun findFunctionsFromTemplate(project: Project, file: VirtualFile): List<Function> = try {
        ReadAction.compute<List<Function>, Throwable> {
            CloudFormationTemplate.parse(project, file).resources()
                .filterIsInstance<Function>()
                .toList()
        }
    } catch (e: Exception) {
        LOG.warn(e) { "Failed to parse template: $file" }
        emptyList()
    }

    fun findImageFunctionsFromTemplate(project: Project, file: VirtualFile): List<Function> =
        findFunctionsFromTemplate(project, file).filter { it.packageType() == PackageType.IMAGE }

    @JvmStatic
    fun findZipFunctionsFromTemplate(project: Project, file: File): List<Function> {
        val virtualFile = file.readFileIntoMemory() ?: return emptyList()
        return findZipFunctionsFromTemplate(project, virtualFile)
    }

    @JvmStatic
    fun findZipFunctionsFromTemplate(project: Project, file: VirtualFile): List<Function> =
        findFunctionsFromTemplate(project, file).filter { it.packageType() == PackageType.ZIP }

    @JvmStatic
    fun functionFromElement(element: PsiElement): Function? = CloudFormationTemplate.convertPsiToResource(element) as? Function

    fun writeDummySamTemplate(
        tempFile: Path,
        logicalId: String,
        runtime: Runtime,
        architecture: Architecture? = Architecture.X86_64,
        codeUri: String,
        handler: String,
        timeout: Int = LambdaLimits.DEFAULT_TIMEOUT,
        memorySize: Int = LambdaLimits.DEFAULT_MEMORY_SIZE,
        envVars: Map<String, String> = emptyMap()
    ) {
        templateCommon(
            tempFile = tempFile,
            logicalId = logicalId,
            timeout = timeout,
            memorySize = memorySize,
            envVars = envVars,
            properties = {
                keyValue("Handler", handler)
                keyValue("CodeUri", codeUri)
                keyValue("Runtime", runtime.toString())
                mapping("Architectures") {
                    listValue(architecture.toString())
                }
            }
        )
    }

    fun writeDummySamImageTemplate(
        tempFile: Path,
        logicalId: String,
        dockerfile: Path,
        timeout: Int = LambdaLimits.DEFAULT_TIMEOUT,
        memorySize: Int = LambdaLimits.DEFAULT_MEMORY_SIZE,
        envVars: Map<String, String> = emptyMap()
    ) {
        templateCommon(
            tempFile = tempFile,
            logicalId = logicalId,
            timeout = timeout,
            memorySize = memorySize,
            envVars = envVars,
            properties = {
                keyValue("PackageType", "Image")
            },
            metadata = {
                keyValue("DockerContext", dockerfile.parent.toString())
                keyValue("Dockerfile", dockerfile.fileName.toString())
            }
        )
    }

    private fun templateCommon(
        tempFile: Path,
        logicalId: String,
        timeout: Int = LambdaLimits.DEFAULT_TIMEOUT,
        memorySize: Int = LambdaLimits.DEFAULT_MEMORY_SIZE,
        envVars: Map<String, String> = emptyMap(),
        properties: YamlWriter.() -> Unit,
        metadata: (YamlWriter.() -> Unit)? = null
    ) {
        if (!tempFile.exists()) {
            Files.createDirectories(tempFile.parent)
            Files.createFile(tempFile)
        }
        tempFile.writeText(
            yaml {
                mapping("Resources") {
                    mapping(logicalId) {
                        keyValue("Type", SERVERLESS_FUNCTION_TYPE)
                        mapping("Properties") {
                            keyValue("Timeout", timeout.toString())
                            keyValue("MemorySize", memorySize.toString())

                            properties(this)

                            if (envVars.isNotEmpty()) {
                                mapping("Environment") {
                                    mapping("Variables") {
                                        envVars.forEach { (key, value) ->
                                            keyValue(key, value)
                                        }
                                    }
                                }
                            }
                        }

                        metadata?.let {
                            mapping("Metadata") {
                                metadata.invoke(this)
                            }
                        }
                    }
                }
            }
        )
    }

    private fun File.readFileIntoMemory(): VirtualFile? {
        if (!isFile) {
            return null
        }

        // Use in-memory file since we can't refresh since we are most likely in a read action
        val templateContent = readText()
        return LightVirtualFile(name, templateContent)
    }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.sam

import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.util.text.SemVer
import com.intellij.util.text.nullize
import software.aws.toolkits.jetbrains.core.executables.ExecutableCommon
import software.aws.toolkits.jetbrains.core.executables.ExecutableType
import software.aws.toolkits.jetbrains.utils.FileInfoCache
import software.aws.toolkits.resources.message

object SamVersionCache : FileInfoCache<SemVer>() {
    override fun getFileInfo(path: String): SemVer {
        val executableName = "sam"
        val sanitizedPath = path.nullize(true) ?: throw RuntimeException(message("executableCommon.cli_not_configured", executableName))
        val commandLine = ExecutableCommon.getCommandLine(sanitizedPath, executableName, ExecutableType.getInstance<SamExecutable>()).withParameters("--info")
        val process = CapturingProcessHandler(commandLine).runProcess()

        if (process.exitCode != 0) {
            val output = process.stderr.trimEnd()
            if (output.contains(SamCommon.SAM_INVALID_OPTION_SUBSTRING)) {
                throw IllegalStateException(message("executableCommon.unexpected_output", SamCommon.SAM_NAME, output))
            }
            throw IllegalStateException(output)
        } else {
            val output = process.stdout.trimEnd()
            if (output.isEmpty()) {
                throw IllegalStateException(message("executableCommon.empty_info", SamCommon.SAM_NAME))
            }
            val tree = SamCommon.mapper.readTree(output)
            val version = tree.get(SamCommon.SAM_INFO_VERSION_KEY).asText()
            return SemVer.parseFromText(version) ?: throw IllegalStateException(message("executableCommon.version_parse_error", SamCommon.SAM_NAME, version))
        }
    }
}


// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.sam.sync

import com.intellij.execution.Executor
import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.RunProfile
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.process.KillableColoredProcessHandler
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.ProgramRunner
import com.intellij.execution.ui.RunContentManager
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import icons.AwsIcons
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.toEnvironmentVariables
import software.aws.toolkits.jetbrains.services.lambda.sam.getSamCli
import software.aws.toolkits.jetbrains.services.lambda.sam.samSyncCommand
import java.io.IOException
import java.nio.charset.Charset
import java.nio.file.Path
import javax.swing.Icon

class SyncApplicationRunProfile(
    private val project: Project,
    private val settings: SyncServerlessApplicationSettings,
    private val connection: ConnectionSettings,
    private val templatePath: Path
) : RunProfile {
    override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState = SyncApplicationRunProfileState(environment)

    override fun getName(): String = settings.stackName

    override fun getIcon(): Icon? = AwsIcons.Resources.SERVERLESS_APP

    inner class SyncApplicationRunProfileState(environment: ExecutionEnvironment) : CommandLineState(environment) {

        override fun startProcess(): ProcessHandler {
            val processHandler = KillableColoredProcessHandler(getSamSyncCommand())
            ProcessTerminatedListener.attach(processHandler)
            return processHandler
        }

        private fun getSamSyncCommand(): GeneralCommandLine = getSamCli().samSyncCommand(
            connection.toEnvironmentVariables(),
            templatePath,
            settings
        )

        override fun execute(executor: Executor, runner: ProgramRunner<*>) =
            super.execute(executor, runner).apply {
                var isDevStack = false
                processHandler?.addProcessListener(object : ProcessAdapter() {
                    override fun startNotified(event: ProcessEvent) {
                        super.startNotified(event)
                        runInEdt {
                            RunContentManager.getInstance(project).toFrontRunContent(executor, processHandler)
                        }
                    }
                    private var insertAssertionNow = false
                    override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
                        if (outputType === ProcessOutputTypes.STDOUT ||
                            outputType === ProcessOutputTypes.STDERR
                        ) {
                            if (event.text.contains("Confirm that you are synchronizing a development stack.")) {
                                isDevStack = true
                            }
                            if (event.text.contains("[Y/n]:") && isDevStack) {
                                insertAssertionNow = true
                                isDevStack = false
                                ApplicationManager.getApplication().executeOnPooledThread {
                                    try {
                                        while (insertAssertionNow) {
                                            processHandler.processInput?.write("Y\n".toByteArray(Charset.defaultCharset()))
                                        }
                                    } catch (e: IOException) {
                                        /* no-op */
                                    }
                                }
                            } else {
                                insertAssertionNow = false
                            }
                        }
                    }
                })
            }
    }
}


// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.sam.sync

import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.MutableCollectionComboBoxModel
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.actionListener
import com.intellij.ui.dsl.builder.bind
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.toMutableProperty
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
import com.intellij.ui.layout.selected
import com.intellij.util.text.nullize
import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.amazon.awssdk.services.cloudformation.model.StackSummary
import software.amazon.awssdk.services.cloudformation.model.Tag
import software.amazon.awssdk.services.ecr.EcrClient
import software.amazon.awssdk.services.lambda.model.PackageType
import software.amazon.awssdk.services.s3.S3Client
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.map
import software.aws.toolkits.jetbrains.services.cloudformation.CloudFormationTemplate
import software.aws.toolkits.jetbrains.services.cloudformation.Parameter
import software.aws.toolkits.jetbrains.services.cloudformation.SamFunction
import software.aws.toolkits.jetbrains.services.cloudformation.describeStackForSync
import software.aws.toolkits.jetbrains.services.cloudformation.mergeRemoteParameters
import software.aws.toolkits.jetbrains.services.cloudformation.resources.CloudFormationResources
import software.aws.toolkits.jetbrains.services.ecr.CreateEcrRepoDialog
import software.aws.toolkits.jetbrains.services.ecr.resources.EcrResources
import software.aws.toolkits.jetbrains.services.ecr.resources.Repository
import software.aws.toolkits.jetbrains.services.lambda.deploy.CreateCapabilities
import software.aws.toolkits.jetbrains.services.lambda.sam.SamTemplateUtils
import software.aws.toolkits.jetbrains.services.lambda.sam.ValidateSamParameters.validateParameters
import software.aws.toolkits.jetbrains.services.lambda.sam.ValidateSamParameters.validateStackName
import software.aws.toolkits.jetbrains.services.s3.CreateS3BucketDialog
import software.aws.toolkits.jetbrains.services.s3.resources.S3Resources
import software.aws.toolkits.jetbrains.settings.SyncSettings
import software.aws.toolkits.jetbrains.settings.relativeSamPath
import software.aws.toolkits.jetbrains.ui.KeyValueTextField
import software.aws.toolkits.jetbrains.ui.ResourceSelector
import software.aws.toolkits.jetbrains.utils.ui.validationInfo
import software.aws.toolkits.jetbrains.utils.ui.withBinding
import software.aws.toolkits.resources.message
import java.awt.Component

data class SyncServerlessApplicationSettings(
    val stackName: String,
    val bucket: String,
    val ecrRepo: String?,
    val parameters: Map<String, String>,
    val tags: Map<String, String>,
    val useContainer: Boolean,
    val capabilities: List<CreateCapabilities>
)

class SyncServerlessApplicationDialog(
    private val project: Project,
    private val templateFile: VirtualFile,
    private val activeStacks: List<StackSummary>,
    private val loadResourcesOnCreate: Boolean = true
) : DialogWrapper(project) {
    var useContainer: Boolean = false
    var newStackName: String = ""
    var templateParameters: Map<String, String> = emptyMap()
    var tags: Map<String, String> = emptyMap()
    var showImageOptions: Boolean = false

    private val stackNameField = JBTextField().apply {
        this.isEnabled = false
    }

    private val stackSelector = ResourceSelector.builder()
        .resource(CloudFormationResources.ACTIVE_STACKS)
        .awsConnection(project)
        .customRenderer(SimpleListCellRenderer.create("", StackSummary::stackName))
        .apply {
            if (!loadResourcesOnCreate) {
                disableAutomaticLoading()
            }
        }
        .build()

    private val s3BucketSelector = ResourceSelector.builder()
        .resource(S3Resources.LIST_BUCKETS.map { it.name() })
        .awsConnection(project)
        .apply {
            if (!loadResourcesOnCreate) {
                disableAutomaticLoading()
            }
        }
        .build()

    private val ecrRepoSelector = ResourceSelector.builder()
        .resource(EcrResources.LIST_REPOS)
        .awsConnection(project)
        .customRenderer(SimpleListCellRenderer.create("", Repository::repositoryName))
        .apply {
            if (!loadResourcesOnCreate) {
                disableAutomaticLoading()
            }
        }
        .build()

    private val parametersField = KeyValueTextField(message("serverless.application.sync.template.parameters"))
    private val tagsField = KeyValueTextField(message("tags.title"))
    private var templateFileParameters = CloudFormationTemplate.parse(project, templateFile).parameters().toList()
    private val module = ModuleUtilCore.findModuleForFile(templateFile, project)
    private val settings: SyncSettings? = module?.let { SyncSettings.getInstance(it) }
    private val samPath: String = module?.let { relativeSamPath(it, templateFile) } ?: templateFile.name
    private val templateFunctions = SamTemplateUtils.findFunctionsFromTemplate(project, templateFile)
    private val hasImageFunctions: Boolean = templateFunctions.any { (it as? SamFunction)?.packageType() == PackageType.IMAGE }
    private val checkStack = checkIfStackInSettingsExists()

    private var syncType: SyncType = if (checkStack) SyncType.CREATE else SyncType.UPDATE
    private var createNewStack = checkStack
    private val capabilitiesList = settings?.enabledCapabilities(samPath)?.toMutableList()
        ?: mutableListOf(CreateCapabilities.NAMED_IAM, CreateCapabilities.AUTO_EXPAND)

    private val s3Client: S3Client = project.awsClient()
    private val ecrClient: EcrClient = project.awsClient()
    private val cloudFormationClient: CloudFormationClient = project.awsClient()
    private fun checkIfStackInSettingsExists(): Boolean = if (!settings?.samStackName(samPath).isNullOrEmpty()) {
        !activeStacks.map { it.stackName() }.contains(settings?.samStackName(samPath))
    } else {
        true
    }

    fun settings() = SyncServerlessApplicationSettings(
        stackName = if (createNewStack) {
            newStackName.nullize()
        } else {
            stackSelector.selected()?.stackName()
        } ?: throw RuntimeException(message("serverless.application.sync.validation.stack.missing")),
        bucket = s3BucketSelector.selected() ?: throw RuntimeException("s3 bucket selected was null"),
        ecrRepo = if (hasImageFunctions) {
            ecrRepoSelector.selected()?.repositoryUri
        } else {
            null
        },
        parameters = templateParameters,
        tags = tags,
        useContainer = useContainer,
        capabilities = capabilitiesList
    )

    // TODO: Add Help for Dialog

    private val component by lazy {
        panel {
            buttonsGroup {
                row {
                    // TODO: Find a better way to bind the radio buttons
                    val createStackButton = radioButton(message("serverless.application.sync.label.stack.new"), true).applyToComponent {
                        this.isSelected = createNewStack
                        this.toolTipText = (message("serverless.application.sync.tooltip.createStack"))
                    }.bindSelected(
                        { createNewStack },
                        {
                            if (it) {
                                createNewStack = true
                                syncType = SyncType.CREATE
                            }
                        }
                    )
                        .actionListener { event, component ->
                            if (syncType != SyncType.CREATE) {
                                syncType = SyncType.CREATE
                                refreshTemplateParametersAndTags()
                                createNewStack = true
                            }
                        }
                    cell(stackNameField)
                        .horizontalAlign(HorizontalAlign.FILL)
                        .enabledIf(createStackButton.component.selected)
                        .bindText(::newStackName)
                        .validationOnApply { field ->
                            if (!field.isEnabled) {
                                null
                            } else {
                                validateStackName(field.text, stackSelector)?.let { field.validationInfo(it) }
                            }
                        }.component.toolTipText = message("serverless.application.sync.tooltip.createStack")
                }

                row {
                    val updateStackButton = radioButton(message("serverless.application.sync.label.stack.select"), false).applyToComponent {
                        isSelected = !createNewStack
                        this.toolTipText = (message("serverless.application.sync.tooltip.createStack"))
                    }.bindSelected(
                        { !createNewStack },
                        {
                            if (it) {
                                createNewStack = false
                                syncType = SyncType.UPDATE
                            }
                        }
                    ).actionListener { event, component ->
                        if (syncType != SyncType.UPDATE) {
                            syncType = SyncType.UPDATE
                            refreshTemplateParametersAndTags()
                            createNewStack = false
                        }
                    }
                    stackSelector.reload(forceFetch = true)
                    cell(stackSelector)
                        .horizontalAlign(HorizontalAlign.FILL)
                        .enabledIf(updateStackButton.component.selected)
                        .errorOnApply(message("serverless.application.sync.validation.stack.missing")) {
                            it.isEnabled && (it.isLoading || it.selected() == null)
                        }.component.toolTipText = message("serverless.application.sync.tooltip.updateStack")
                }
            }.bind({ createNewStack }, { createNewStack = it })

            row(message("serverless.application.sync.template.parameters")) {
                cell(parametersField)
                    .withBinding(::templateParameters.toMutableProperty())
                    .validationOnApply {
                        validateParameters(it, templateFileParameters)
                    }.horizontalAlign(HorizontalAlign.FILL)
                    .component.toolTipText = message("serverless.application.sync.tooltip.template.parameters")
            }
            val tagsString = message("tags.title")
            row(tagsString) {
                cell(tagsField)
                    .horizontalAlign(HorizontalAlign.FILL)
                    .withBinding(::tags.toMutableProperty())
            }

            row(message("serverless.application.sync.label.bucket")) {
                cell(s3BucketSelector)
                    .horizontalAlign(HorizontalAlign.FILL)
                    .errorOnApply(message("serverless.application.sync.validation.s3.bucket.empty")) { it.isLoading || it.selected() == null }
                    .component.toolTipText = message("serverless.application.sync.tooltip.s3Bucket")

                button(message("general.create")) { actionEvent ->
                    val bucketDialog = CreateS3BucketDialog(
                        project = project,
                        s3Client = s3Client,
                        parent = actionEvent.source as? Component
                    )

                    if (bucketDialog.showAndGet()) {
                        bucketDialog.bucketName().let {
                            s3BucketSelector.reload(forceFetch = true)
                            s3BucketSelector.selectedItem = it
                        }
                    }
                }
            }

            row(message("serverless.application.sync.label.repo")) {
                cell(ecrRepoSelector)
                    .horizontalAlign(HorizontalAlign.FILL)
                    .errorOnApply(message("serverless.application.sync.validation.ecr.repo.empty")) {
                        it.isVisible && (it.isLoading || it.selected() == null)
                    }.component.toolTipText = message("serverless.application.sync.tooltip.ecrRepo")

                button(message("general.create")) { actionEvent ->
                    val ecrDialog = CreateEcrRepoDialog(
                        project = project,
                        ecrClient = ecrClient,
                        parent = actionEvent.source as? Component
                    )

                    if (ecrDialog.showAndGet()) {
                        ecrRepoSelector.reload(forceFetch = true)
                        ecrRepoSelector.selectedItem { it.repositoryName == ecrDialog.repoName }
                    }
                }
            }.visible(showImageOptions)

            row {
                label(message("cloudformation.capabilities"))
                    .component.toolTipText = message("cloudformation.capabilities.toolTipText")
                CreateCapabilities.values().forEach {
                    checkBox(it.text).actionListener { event, component ->
                        if (component.isSelected) capabilitiesList.add(it) else capabilitiesList.remove(it)
                    }.applyToComponent {
                        this.isSelected = it in capabilitiesList
                        this.toolTipText = it.toolTipText
                    }
                }
            }

            row {
                checkBox(message("serverless.application.sync.use_container"))
                    .bindSelected(::useContainer)
                    .component.toolTipText = message("lambda.sam.buildInContainer.tooltip")
            }
        }
    }

    override fun createCenterPanel() = component

    init {
        title = message("serverless.application.sync")
        setOKButtonText(message("serverless.application.sync.action.name"))
        setOKButtonTooltip(message("serverless.application.sync.action.description"))
        showImageOptions = hasImageFunctions

        settings?.samStackName(samPath)?.let { stackName ->
            if (activeStacks.map { it.stackName() }.contains(stackName)) {
                syncType = SyncType.UPDATE
                createNewStack = false
                stackSelector.selectedItem { it.stackName() == stackName }
                refreshTemplateParametersAndTags(stackName)
            }
        } ?: refreshTemplateParametersAndTags()

        if (showImageOptions) {
            ecrRepoSelector.selectedItem = settings?.samEcrRepoUri(samPath)
        }

        s3BucketSelector.selectedItem = settings?.samBucketName(samPath)
        useContainer = (settings?.samUseContainer(samPath) ?: false)
        tagsField.envVars = settings?.samTags(samPath).orEmpty()
        parametersField.envVars = settings?.samTempParameterOverrides(samPath).orEmpty()

        init()
    }

    private fun refreshTemplateParametersAndTags(stackName: String? = null) {
        when (createNewStack) {
            true -> {
                populateParameters(templateFileParameters)
            }

            false -> {
                val selectedStackName = stackName ?: stackSelector.selected()?.stackName()
                if (selectedStackName == null) {
                    populateParameters(emptyList())
                } else {
                    cloudFormationClient.describeStackForSync(selectedStackName, ::enableParamsAndTags) {
                        it?.let {
                            runInEdt(ModalityState.any()) {
                                // This check is here in-case createStack was selected before we got this update back
                                // TODO: should really create a queuing pattern here so we can cancel on user-action
                                if (!createNewStack) {
                                    populateParameters(templateFileParameters.mergeRemoteParameters(it.parameters()))
                                    populateTags(it.tags())
                                }
                            }
                        } ?: populateParameters(templateFileParameters)
                    }
                }
            }
        }
    }

    private fun enableParamsAndTags(enabled: Boolean) {
        runInEdt(ModalityState.any()) {
            tagsField.isEnabled = enabled
            parametersField.isEnabled = enabled
        }
    }

    @TestOnly
    fun getParameterDialog(): DialogPanel = component

    @TestOnly
    fun forceUi(
        panel: DialogPanel,
        isCreateStack: Boolean? = null,
        hasImageFunctions: Boolean? = null,
        stacks: List<StackSummary>? = null,
        buckets: List<String>? = null,
        ecrRepos: List<Repository>? = null,
        forceStackName: Boolean = false,
        stackName: String? = null,
        forceBucket: Boolean = false,
        bucket: String? = null,
        forceEcrRepo: Boolean = false,
        ecrRepo: String? = null,
        useContainer: Boolean? = null
    ) {
        if (stacks != null) {
            stackSelector.model = MutableCollectionComboBoxModel(stacks)
            stackSelector.forceLoaded()
        }

        if (isCreateStack == true) {
            syncType = SyncType.CREATE
            stackNameField.isEnabled = true
            stackSelector.isEnabled = false
        } else if (isCreateStack == false) {
            syncType = SyncType.UPDATE
            stackNameField.isEnabled = false
            stackSelector.isEnabled = true
        }

        if (forceStackName || stackName != null) {
            if (syncType == SyncType.CREATE) {
                newStackName = stackName.orEmpty()
            } else {
                stackSelector.selectedItem = stacks?.first { it.stackName() == stackName }
            }
        }

        if (buckets != null) {
            s3BucketSelector.model = MutableCollectionComboBoxModel(buckets)
            s3BucketSelector.forceLoaded()
        }

        if (forceBucket || bucket != null) {
            s3BucketSelector.selectedItem = bucket
        }

        if (ecrRepos != null) {
            ecrRepoSelector.model = MutableCollectionComboBoxModel(ecrRepos)
            ecrRepoSelector.forceLoaded()
        }

        if (hasImageFunctions != null) {
            showImageOptions = hasImageFunctions
        }

        if (forceEcrRepo || ecrRepo != null) {
            ecrRepoSelector.selectedItem = ecrRepo
        }

        if (useContainer != null) {
            this.useContainer = useContainer
        }

        panel.reset()
    }

    // visible for testing
    internal fun populateParameters(parameters: List<Parameter>, templateFileDeclarationOverrides: List<Parameter>? = null) {
        // TODO: would be nice to be able to pipe through the description
        parametersField.envVars = parameters.associate { it.logicalName to (it.defaultValue().orEmpty()) }
        templateFileParameters = templateFileDeclarationOverrides ?: CloudFormationTemplate.parse(project, templateFile).parameters().toList()
    }

    private fun populateTags(tags: List<Tag>) {
        tagsField.envVars = tags.associate { it.key() to it.value() }
    }

    enum class SyncType {
        CREATE,
        UPDATE
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.settings

import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.project.Project
import com.intellij.ui.layout.panel
import software.aws.toolkits.jetbrains.settings.LambdaSettings
import software.aws.toolkits.resources.message

class LambdaSettingsConfigurable(private val project: Project) : BoundConfigurable(message("aws.settings.lambda.configurable.title")), SearchableConfigurable {
    private val lambdaSettings get() = LambdaSettings.getInstance(project)
    override fun getId() = "aws.lambda"
    override fun createPanel() = panel {
        row {
            checkBox(
                message("aws.settings.sam.show_all_gutter_icons"),
                lambdaSettings::showAllHandlerGutterIcons,
                message("aws.settings.sam.show_all_gutter_icons_tooltip")
            )
        }
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import com.intellij.execution.ExecutionException
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.ui.ConsoleView
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.util.Key
import com.intellij.xdebugger.XDebuggerManager
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.SamDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.SamRunningState
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.resolveDebuggerSupport
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.jetbrains.services.lambda.steps.GetPorts.Companion.DEBUG_PORTS
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.Step
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.resources.message
import java.util.concurrent.atomic.AtomicBoolean

class AttachDebugger(val environment: ExecutionEnvironment, val state: SamRunningState) : Step() {
    override val stepName = message("sam.debug.attach")
    override val hidden = false

    override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
        try {
            val samProcessHandler = getSamProcess(context)
            val samCompleted = AtomicBoolean(false)

            samProcessHandler.addProcessListener(object : ProcessAdapter() {
                override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
                    if (event.text.contains(SamExecutable.endDebuggingText)) {
                        samCompleted.set(true)
                    }
                }
            })

            val debugPorts = context.getRequiredAttribute(DEBUG_PORTS)
            val debugProcessStarter = prepAndCreateDebugProcessStart(context, debugPorts)
            val debugManager = XDebuggerManager.getInstance(environment.project)

            // Note: Do NOT use coroutines here due to it will deadlock Rider when starting the DotnetDebugProcess due to in storeUtil.kt
            // saveDocumentsAndProjectsAndApp has a runBlocking call that needs EDT and deadlocks coroutines
            invokeAndWaitIfNeeded {
                val session = debugManager.startSessionAndShowTab(environment.runProfile.name, environment.contentToReuse, debugProcessStarter)
                // Tie SAM output to the Debug tab's console so that it is viewable in both spots
                samProcessHandler.addProcessListener(buildProcessAdapter { session.consoleView })

                // Tie the debug session to the overall workflow, so if it gets cancelled (Stop button on SAM tab), we tell the debugger to stop too
                context.addListener(object : Context.Listener {
                    override fun onCancel() {
                        session.stop()
                    }

                    override fun onComplete() {
                        session.stop()
                    }
                })

                session.debugProcess.processHandler.addProcessListener(object : ProcessAdapter() {
                    override fun processWillTerminate(event: ProcessEvent, willBeDestroyed: Boolean) {
                        // If the user pressed Stop on the debug tab, cancel the entire flow
                        val requestedTermination = event.processHandler.getUserData(ProcessHandler.TERMINATION_REQUESTED) ?: false
                        if (requestedTermination) {
                            context.cancel()
                        }
                    }
                })
            }
        } catch (e: TimeoutCancellationException) {
            throw ExecutionException(message("lambda.debug.process.start.timeout"))
        } catch (e: Throwable) {
            throw ExecutionException(e)
        }
    }

    private fun prepAndCreateDebugProcessStart(context: Context, debugPorts: List<Int>) = runBlocking {
        withTimeout(SamDebugSupport.debuggerConnectTimeoutMs()) {
            state.settings
                .resolveDebuggerSupport()
                .createDebugProcess(context, environment, state, state.settings.debugHost, debugPorts)
        }
    }

    private fun getSamProcess(context: Context) = runBlocking {
        withTimeout(SamDebugSupport.debuggerConnectTimeoutMs()) {
            context.pollingGet(SamRunnerStep.SAM_PROCESS_HANDLER)
        }
    }

    private fun buildProcessAdapter(console: (() -> ConsoleView?)) = object : ProcessAdapter() {
        override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
            // Skip system messages
            if (outputType == ProcessOutputTypes.SYSTEM) {
                return
            }
            val viewType = if (outputType == ProcessOutputTypes.STDERR) {
                ConsoleViewContentType.ERROR_OUTPUT
            } else {
                ConsoleViewContentType.NORMAL_OUTPUT
            }
            console()?.print(event.text, viewType)
        }
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.Step
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.resources.message

class AttachDebuggerParent(private val childSteps: List<Step>) : Step() {
    override val stepName = message("sam.debug.attach.parent")
    override val hidden = childSteps.size <= 1
    override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
        childSteps.forEach {
            it.run(context, stepEmitter, ignoreCancellation)
        }
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import com.intellij.execution.configurations.GeneralCommandLine
import software.aws.toolkits.core.utils.AttributeBagKey
import software.aws.toolkits.jetbrains.services.lambda.sam.SamOptions
import software.aws.toolkits.jetbrains.services.lambda.sam.samBuildCommand
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.Step
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.resources.message
import java.nio.file.Path

data class BuildLambdaRequest(
    val templatePath: Path,
    val logicalId: String? = null,
    val buildDir: Path,
    val buildEnvVars: Map<String, String> = emptyMap(),
    val samOptions: SamOptions,
    val preBuildSteps: List<Step> = emptyList()
)

class BuildLambda(private val request: BuildLambdaRequest) : SamCliStep() {
    override val stepName: String = message("lambda.create.step.build")

    override fun constructCommandLine(context: Context): GeneralCommandLine = getCli().samBuildCommand(
        templatePath = request.templatePath,
        logicalId = request.logicalId,
        buildDir = request.buildDir,
        environmentVariables = request.buildEnvVars,
        samOptions = request.samOptions
    )

    override fun handleSuccessResult(output: String, stepEmitter: StepEmitter, context: Context) {
        context.putAttribute(BUILT_LAMBDA, BuiltLambda(request.buildDir.resolve("template.yaml"), request.logicalId))
    }

    companion object {
        val BUILT_LAMBDA = AttributeBagKey.create<BuiltLambda>("BUILT_LAMBDA")
    }
}

/**
 * Represents the result of building a Lambda
 *
 * @param templateLocation The path to the build generated template
 * @param logicalId Optional logical id if we are building a specific function
 */
data class BuiltLambda(
    val templateLocation: Path,
    val logicalId: String?
)


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import software.amazon.awssdk.services.lambda.LambdaClient
import software.amazon.awssdk.services.lambda.model.PackageType
import software.aws.toolkits.core.utils.AttributeBagKey
import software.aws.toolkits.jetbrains.services.lambda.steps.PackageLambda.Companion.UPLOADED_CODE_LOCATION
import software.aws.toolkits.jetbrains.services.lambda.upload.FunctionDetails
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.Step
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.resources.message

class CreateLambda(private val lambdaClient: LambdaClient, private val details: FunctionDetails) : Step() {
    override val stepName = message("lambda.create.step.create_lambda")

    override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
        lambdaClient.createFunction {
            it.functionName(details.name)
            it.description(details.description)
            it.role(details.iamRole.arn)
            it.timeout(details.timeout)
            it.memorySize(details.memorySize)

            it.code { code ->
                when (val codeLocation = context.getRequiredAttribute(UPLOADED_CODE_LOCATION)) {
                    is UploadedS3Code -> {
                        it.packageType(PackageType.ZIP)
                        it.handler(details.handler)
                        it.runtime(details.runtime)

                        code.s3Bucket(codeLocation.bucket)
                        code.s3Key(codeLocation.key)
                        code.s3ObjectVersion(codeLocation.version)
                    }
                    is UploadedEcrCode -> {
                        it.packageType(PackageType.IMAGE)
                        code.imageUri(codeLocation.imageUri)
                    }
                }
            }

            it.environment { env ->
                env.variables(details.envVars)
            }
            it.tracingConfig { tracing ->
                tracing.mode(details.tracingMode)
            }
        }

        stepEmitter.emitMessage(message("lambda.workflow.update_code.wait_for_stable"), isError = false)
        val response = lambdaClient.waiter().waitUntilFunctionExists { it.functionName(details.name) }.matched().response().get()

        // Also wait for it to become active
        lambdaClient.waiter().waitUntilFunctionActive { it.functionName(details.name) }

        context.putAttribute(FUNCTION_ARN, response.configuration().functionArn())
    }

    companion object {
        val FUNCTION_ARN = AttributeBagKey.create<String>("LAMBDA_FUNCTION_ARN")
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import com.intellij.execution.configurations.GeneralCommandLine
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.utils.AttributeBagKey
import software.aws.toolkits.jetbrains.services.lambda.deploy.DeployServerlessApplicationSettings
import software.aws.toolkits.jetbrains.services.lambda.sam.samDeployCommand
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.resources.message
import java.nio.file.Path

class DeployLambda(
    private val packagedTemplateFile: Path,
    private val envVars: Map<String, String>,
    private val settings: DeployServerlessApplicationSettings,
    region: AwsRegion
) : SamCliStep() {
    override val stepName = message("serverless.application.deploy.step_name.create_change_set")
    private val changeSetRegex = "(arn:${region.partitionId}:cloudformation:.*changeSet/[^\\s]*)".toRegex()

    override fun constructCommandLine(context: Context): GeneralCommandLine = getCli().samDeployCommand(
        environmentVariables = envVars,
        templatePath = packagedTemplateFile,
        settings = settings
    )

    override fun handleSuccessResult(output: String, stepEmitter: StepEmitter, context: Context) {
        val changeSet = changeSetRegex.find(output)?.value ?: throw RuntimeException(message("serverless.application.deploy.change_set_not_found"))
        context.putAttribute(CHANGE_SET_ARN, changeSet)
    }

    companion object {
        val CHANGE_SET_ARN = AttributeBagKey.create<String>("CHANGE_SET_ARN")
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import com.intellij.util.net.NetUtils
import software.aws.toolkits.core.utils.AttributeBagKey
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.LocalLambdaRunSettings
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.resolveDebuggerSupport
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.Step
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter

class GetPorts(val settings: LocalLambdaRunSettings) : Step() {
    override val stepName: String = ""
    override val hidden: Boolean = true

    override fun execute(context: Context, messageEmitter: StepEmitter, ignoreCancellation: Boolean) {
        val debugExtension = settings.resolveDebuggerSupport()
        val debugPorts = NetUtils.findAvailableSocketPorts(debugExtension.numberOfDebugPorts()).toList()
        context.putAttribute(DEBUG_PORTS, debugPorts)
    }

    companion object {
        val DEBUG_PORTS = AttributeBagKey.create<List<Int>>("DEBUG_PORTS")
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import software.aws.toolkits.core.credentials.toEnvironmentVariables
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.utils.buildList
import software.aws.toolkits.jetbrains.services.lambda.deploy.DeployServerlessApplicationSettings
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.ValidateDocker
import software.aws.toolkits.jetbrains.services.lambda.sam.SamCommon
import software.aws.toolkits.jetbrains.services.lambda.sam.SamOptions
import software.aws.toolkits.jetbrains.services.lambda.sam.SamTemplateUtils
import software.aws.toolkits.jetbrains.services.lambda.upload.FunctionDetails
import software.aws.toolkits.jetbrains.services.lambda.upload.ImageBasedCode
import software.aws.toolkits.jetbrains.services.lambda.upload.ZipBasedCode
import software.aws.toolkits.jetbrains.utils.execution.steps.StepWorkflow
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

fun createLambdaWorkflowForZip(
    project: Project,
    functionDetails: FunctionDetails,
    codeDetails: ZipBasedCode,
    buildDir: Path,
    buildEnvVars: Map<String, String>,
    codeStorageLocation: String,
    samOptions: SamOptions
): StepWorkflow {
    val (dummyTemplate, dummyLogicalId) = createTemporaryZipTemplate(buildDir, codeDetails)
    val packagedTemplate = buildDir.resolve("packaged-temp-template.yaml")
    val builtTemplate = buildDir.resolve("template.yaml")
    val envVars = createAwsEnvVars(project)

    return StepWorkflow(
        buildList {
            if (samOptions.buildInContainer) {
                add(ValidateDocker())
            }

            add(
                BuildLambda(
                    BuildLambdaRequest(
                        templatePath = dummyTemplate,
                        buildDir = buildDir,
                        buildEnvVars = buildEnvVars,
                        samOptions = samOptions
                    )
                ),
            )
            add(
                PackageLambda(
                    templatePath = builtTemplate,
                    packagedTemplatePath = packagedTemplate,
                    logicalId = dummyLogicalId,
                    envVars = envVars,
                    s3Bucket = codeStorageLocation
                ),
            )
            add(
                CreateLambda(
                    lambdaClient = project.awsClient(),
                    details = functionDetails
                )
            )
        }
    )
}

fun createLambdaWorkflowForImage(
    project: Project,
    functionDetails: FunctionDetails,
    codeDetails: ImageBasedCode,
    codeStorageLocation: String,
    samOptions: SamOptions
): StepWorkflow {
    val (dummyTemplate, dummyLogicalId) = createTemporaryImageTemplate(codeDetails)
    val buildDir = codeDetails.dockerfile.resolveSibling(".aws-sam").resolve("build")
    val builtTemplate = buildDir.resolve("template.yaml")
    val packagedTemplate = buildDir.resolve("packaged-temp-template.yaml")
    val envVars = createAwsEnvVars(project)

    return StepWorkflow(
        ValidateDocker(),
        BuildLambda(
            BuildLambdaRequest(
                templatePath = dummyTemplate,
                buildDir = buildDir,
                samOptions = samOptions
            )
        ),
        PackageLambda(
            templatePath = builtTemplate,
            packagedTemplatePath = packagedTemplate,
            logicalId = dummyLogicalId,
            envVars = envVars,
            ecrRepo = codeStorageLocation
        ),
        CreateLambda(project.awsClient(), functionDetails)
    )
}

/**
 * Creates a [StepWorkflow] for updating a Lambda's code and optionally its handler
 *
 * @param updatedHandler If provided, we will call update function configuration with the provided handler.
 */
fun updateLambdaCodeWorkflowForZip(
    project: Project,
    functionName: String,
    codeDetails: ZipBasedCode,
    buildDir: Path,
    buildEnvVars: Map<String, String>,
    codeStorageLocation: String,
    samOptions: SamOptions,
    updatedHandler: String?
): StepWorkflow {
    val (dummyTemplate, dummyLogicalId) = createTemporaryZipTemplate(buildDir, codeDetails)
    val builtTemplate = buildDir.resolve("template.yaml")
    val packagedTemplate = buildDir.resolve("packaged-temp-template.yaml")
    val envVars = createAwsEnvVars(project)

    return StepWorkflow(
        buildList {
            if (samOptions.buildInContainer) {
                add(ValidateDocker())
            }

            add(
                BuildLambda(
                    BuildLambdaRequest(
                        templatePath = dummyTemplate,
                        buildDir = buildDir,
                        buildEnvVars = buildEnvVars,
                        samOptions = samOptions
                    )
                ),
            )
            add(
                PackageLambda(
                    templatePath = builtTemplate,
                    packagedTemplatePath = packagedTemplate,
                    logicalId = dummyLogicalId,
                    envVars = envVars,
                    s3Bucket = codeStorageLocation
                ),
            )
            add(
                UpdateLambdaCode(
                    lambdaClient = project.awsClient(),
                    functionName = functionName,
                    updatedHandler = updatedHandler
                )
            )
        }
    )
}

fun updateLambdaCodeWorkflowForImage(
    project: Project,
    functionName: String,
    codeDetails: ImageBasedCode,
    codeStorageLocation: String,
    samOptions: SamOptions
): StepWorkflow {
    val (dummyTemplate, dummyLogicalId) = createTemporaryImageTemplate(codeDetails)
    val buildDir = codeDetails.dockerfile.resolveSibling(".aws-sam").resolve("build")
    val builtTemplate = buildDir.resolve("template.yaml")
    val packagedTemplate = buildDir.resolve("packaged-temp-template.yaml")
    val envVars = createAwsEnvVars(project)

    return StepWorkflow(
        ValidateDocker(),
        BuildLambda(
            BuildLambdaRequest(
                templatePath = dummyTemplate,
                buildDir = buildDir,
                samOptions = samOptions
            )
        ),
        PackageLambda(
            templatePath = builtTemplate,
            packagedTemplatePath = packagedTemplate,
            logicalId = dummyLogicalId,
            envVars = envVars,
            ecrRepo = codeStorageLocation
        ),
        UpdateLambdaCode(
            lambdaClient = project.awsClient(),
            functionName = functionName,
            updatedHandler = null
        )
    )
}

fun createDeployWorkflow(
    project: Project,
    template: VirtualFile,
    settings: DeployServerlessApplicationSettings
): StepWorkflow {
    val envVars = createAwsEnvVars(project)
    val region = AwsConnectionManager.getInstance(project).activeRegion
    val buildDir = Paths.get(template.parent.path, SamCommon.SAM_BUILD_DIR, "build")
    val builtTemplate = buildDir.resolve("template.yaml")
    val packagedTemplate = builtTemplate.parent.resolve("packaged-${builtTemplate.fileName}")
    val templatePath = Paths.get(template.path)

    Files.createDirectories(buildDir)

    return StepWorkflow(
        buildList {
            if (settings.useContainer) {
                add(ValidateDocker())
            }

            add(
                BuildLambda(
                    BuildLambdaRequest(
                        templatePath = templatePath,
                        logicalId = null,
                        buildDir = buildDir,
                        buildEnvVars = envVars,
                        samOptions = SamOptions(buildInContainer = settings.useContainer)
                    )
                )
            )
            add(
                PackageLambda(
                    templatePath = builtTemplate,
                    packagedTemplatePath = packagedTemplate,
                    logicalId = null,
                    envVars = envVars,
                    s3Bucket = settings.bucket,
                    ecrRepo = settings.ecrRepo
                )
            )
            add(
                DeployLambda(
                    packagedTemplateFile = packagedTemplate,
                    region = region,
                    envVars = envVars,
                    settings = settings
                )
            )
        }
    )
}

private fun createAwsEnvVars(project: Project): Map<String, String> {
    val connectSettings = AwsConnectionManager.getInstance(project).connectionSettings()
        ?: throw IllegalStateException("Tried to update a lambda without valid AWS connection")

    return connectSettings.credentials.resolveCredentials().toEnvironmentVariables() + connectSettings.region.toEnvironmentVariables()
}

private fun createTemporaryZipTemplate(buildDir: Path, codeDetails: ZipBasedCode): Pair<Path, String> {
    Files.createDirectories(buildDir)

    val dummyTemplate = Files.createTempFile("temp-template", ".yaml")
    val dummyLogicalId = "Function"

    SamTemplateUtils.writeDummySamTemplate(
        tempFile = dummyTemplate,
        logicalId = dummyLogicalId,
        runtime = codeDetails.runtime,
        handler = codeDetails.handler,
        codeUri = codeDetails.baseDir.toString()
    )

    return Pair(dummyTemplate, dummyLogicalId)
}

private fun createTemporaryImageTemplate(codeDetails: ImageBasedCode): Pair<Path, String> {
    val dummyTemplate = Files.createTempFile("temp-template", ".yaml")
    val dummyLogicalId = "Function"

    SamTemplateUtils.writeDummySamImageTemplate(
        tempFile = dummyTemplate,
        logicalId = dummyLogicalId,
        dockerfile = codeDetails.dockerfile
    )

    return Pair(dummyTemplate, dummyLogicalId)
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import com.intellij.execution.configurations.GeneralCommandLine
import software.aws.toolkits.core.utils.AttributeBagKey
import software.aws.toolkits.jetbrains.services.lambda.sam.SamTemplateUtils
import software.aws.toolkits.jetbrains.services.lambda.sam.samPackageCommand
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.resources.message
import java.nio.file.Path

class PackageLambda(
    private val templatePath: Path,
    private val packagedTemplatePath: Path,
    private val logicalId: String?,
    private val envVars: Map<String, String>,
    private val s3Bucket: String? = null,
    private val ecrRepo: String? = null
) : SamCliStep() {
    override val stepName: String = message("lambda.create.step.package")

    override fun constructCommandLine(context: Context): GeneralCommandLine = getCli().samPackageCommand(
        templatePath = templatePath,
        packagedTemplatePath = packagedTemplatePath,
        environmentVariables = envVars,
        s3Bucket = s3Bucket,
        ecrRepo = ecrRepo
    )

    override fun handleSuccessResult(output: String, stepEmitter: StepEmitter, context: Context) {
        // We finished the upload, extract out the uploaded code location if we have a logicalId
        logicalId ?: return

        context.putAttribute(UPLOADED_CODE_LOCATION, SamTemplateUtils.getUploadedCodeUri(packagedTemplatePath, logicalId))
    }

    companion object {
        val UPLOADED_CODE_LOCATION = AttributeBagKey.create<UploadedCode>("UPLOADED_CODE_LOCATION")
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import com.intellij.execution.configurations.GeneralCommandLine
import software.aws.toolkits.jetbrains.services.lambda.sam.getSamCli
import software.aws.toolkits.jetbrains.utils.execution.steps.CliBasedStep

abstract class SamCliStep : CliBasedStep() {
    fun getCli(): GeneralCommandLine = getSamCli()
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.encoding.EncodingProjectManager
import software.aws.toolkits.core.credentials.toEnvironmentVariables
import software.aws.toolkits.core.utils.AttributeBagKey
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.LocalLambdaRunSettings
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.TemplateSettings
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.resolveDebuggerSupport
import software.aws.toolkits.jetbrains.services.lambda.steps.GetPorts.Companion.DEBUG_PORTS
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.resources.message

class SamRunnerStep(val environment: ExecutionEnvironment, val settings: LocalLambdaRunSettings, val debug: Boolean) : SamCliStep() {
    override val stepName: String = message("lambda.debug.step.start_sam")
    override fun onProcessStart(context: Context, processHandler: ProcessHandler) {
        context.putAttribute(SAM_PROCESS_HANDLER, processHandler)
    }

    override fun constructCommandLine(context: Context): GeneralCommandLine {
        val builtLambda = context.getRequiredAttribute(BuildLambda.BUILT_LAMBDA)
        val totalEnvVars = settings.environmentVariables +
            settings.connection.credentials.resolveCredentials().toEnvironmentVariables() +
            settings.connection.region.toEnvironmentVariables()

        val commandLine = getCli()
            .withParameters("local")
            .withParameters("invoke")
            .apply {
                if (ApplicationManager.getApplication().isUnitTestMode) {
                    withParameters("--debug")
                }

                if (settings is TemplateSettings) {
                    withParameters(settings.logicalId)
                }
            }
            .withParameters("--template")
            .withParameters(builtLambda.templateLocation.toString())
            .withParameters("--event")
            .withParameters(createEventFile())
            // the default charset may not be compatible with the executable.
            // matches pycharm behavior:https://github.com/JetBrains/intellij-community/blob/9aadf09286f1c3707ad25b855b7ce7c45810c9ae/python/src/com/jetbrains/python/run/PythonScriptCommandLineState.java#L243
            .withCharset(EncodingProjectManager.getInstance(environment.project).defaultCharset)
            .withEnvironment(totalEnvVars)
            .withEnvironment("PYTHONUNBUFFERED", "1") // Force SAM to not buffer stdout/stderr so it gets shown in IDE

        if (debug) {
            val debugExtension = settings.resolveDebuggerSupport()
            val debugPorts = context.getRequiredAttribute(DEBUG_PORTS)
            commandLine.addParameters(debugExtension.samArguments(debugPorts))
            debugPorts.forEach {
                commandLine.withParameters("--debug-port").withParameters(it.toString())
            }
        }

        val samOptions = settings.samOptions
        if (samOptions.skipImagePull) {
            commandLine.withParameters("--skip-pull-image")
        }

        samOptions.dockerNetwork?.let {
            if (it.isNotBlank()) {
                commandLine.withParameters("--docker-network")
                    .withParameters(it.trim())
            }
        }

        samOptions.additionalLocalArgs?.let {
            if (it.isNotBlank()) {
                commandLine.withParameters(*it.split(" ").toTypedArray())
            }
        }

        return commandLine
    }

    private fun createEventFile(): String {
        val eventFile = FileUtil.createTempFile("${environment.runProfile.name}-event", ".json", true)
        eventFile.writeText(settings.input)
        return eventFile.absolutePath
    }

    companion object {
        val SAM_PROCESS_HANDLER = AttributeBagKey.create<ProcessHandler>("samProcessHandler")
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

import software.amazon.awssdk.services.lambda.LambdaClient
import software.aws.toolkits.jetbrains.services.lambda.steps.PackageLambda.Companion.UPLOADED_CODE_LOCATION
import software.aws.toolkits.jetbrains.services.lambda.waitForUpdatableState
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.Step
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.resources.message

class UpdateLambdaCode(private val lambdaClient: LambdaClient, private val functionName: String, private val updatedHandler: String?) : Step() {
    override val stepName = message("lambda.create.step.update_lambda")

    override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
        stepEmitter.emitMessageLine(message("lambda.workflow.update_code.wait_for_updatable"), isError = false)
        lambdaClient.waitForUpdatableState(functionName)
        lambdaClient.updateFunctionCode {
            it.functionName(functionName)

            when (val codeLocation = context.getRequiredAttribute(UPLOADED_CODE_LOCATION)) {
                is UploadedS3Code -> {
                    it.s3Bucket(codeLocation.bucket)
                    it.s3Key(codeLocation.key)
                    it.s3ObjectVersion(codeLocation.version)
                }
                is UploadedEcrCode -> {
                    it.imageUri(codeLocation.imageUri)
                }
            }
        }

        updatedHandler?.let { _ ->
            stepEmitter.emitMessageLine(message("lambda.workflow.update_code.wait_for_updatable"), isError = false)
            lambdaClient.waitForUpdatableState(functionName)
            lambdaClient.updateFunctionConfiguration {
                it.functionName(functionName)
                it.handler(updatedHandler)
            }
        }

        stepEmitter.emitMessageLine(message("lambda.workflow.update_code.wait_for_stable"), isError = false)
        lambdaClient.waiter().waitUntilFunctionUpdated { it.functionName(functionName) }
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.steps

sealed class UploadedCode
data class UploadedS3Code(val bucket: String, val key: String, val version: String?) : UploadedCode()
data class UploadedEcrCode(val imageUri: String) : UploadedCode()


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.jetbrains.services.lambda.upload

import com.intellij.ui.IdeBorderFactory
import software.amazon.awssdk.services.lambda.model.PackageType
import software.aws.toolkits.resources.message
import java.awt.BorderLayout
import javax.swing.JCheckBox
import javax.swing.JPanel
import kotlin.properties.Delegates

class BuildSettingsPanel : JPanel(BorderLayout()) {
    lateinit var content: JPanel
        private set
    lateinit var buildInContainerCheckbox: JCheckBox
        private set

    var packagingType: PackageType by Delegates.observable(PackageType.ZIP) { _, _, _ -> updateComponents() }

    init {
        content.border = IdeBorderFactory.createTitledBorder(message("lambda.upload.build_settings"), false)
        add(content, BorderLayout.CENTER)
    }

    private fun updateComponents() {
        content.isVisible = packagingType == PackageType.ZIP
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.upload

import software.amazon.awssdk.services.lambda.model.Runtime
import java.nio.file.Path

sealed class CodeDetails
data class ZipBasedCode(val baseDir: Path, val handler: String, val runtime: Runtime) : CodeDetails()
data class ImageBasedCode(val dockerfile: Path) : CodeDetails()