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

package software.aws.toolkits.jetbrains.services.lambda.execution.remote

import com.intellij.execution.ExecutionBundle
import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.RuntimeConfigurationError
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.options.SettingsEditorGroup
import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.lambda.LambdaClient
import software.aws.toolkits.core.credentials.ToolkitCredentialsProvider
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.services.lambda.execution.LambdaRunConfigurationBase
import software.aws.toolkits.jetbrains.services.lambda.execution.LambdaRunConfigurationType
import software.aws.toolkits.jetbrains.ui.connection.AwsConnectionSettingsEditor
import software.aws.toolkits.jetbrains.ui.connection.addAwsConnectionEditor
import software.aws.toolkits.resources.message

class RemoteLambdaRunConfigurationFactory(configuration: LambdaRunConfigurationType) : ConfigurationFactory(configuration) {
    override fun createTemplateConfiguration(project: Project) = RemoteLambdaRunConfiguration(project, this)
    override fun getName(): String = "Remote"
    override fun getId(): String = name
}

class RemoteLambdaRunConfiguration(project: Project, factory: ConfigurationFactory) :
    LambdaRunConfigurationBase<RemoteLambdaOptions>(project, factory, "Remote") {

    override val serializableOptions = RemoteLambdaOptions()

    override fun getConfigurationEditor(): SettingsEditorGroup<RemoteLambdaRunConfiguration> {
        val group = SettingsEditorGroup<RemoteLambdaRunConfiguration>()
        val remoteLambdaSettings = RemoteLambdaRunSettingsEditor(project)
        group.addEditor(ExecutionBundle.message("run.configuration.configuration.tab.title"), remoteLambdaSettings)
        group.addAwsConnectionEditor(AwsConnectionSettingsEditor(project, LambdaClient.SERVICE_NAME, remoteLambdaSettings::updateFunctions))
        return group
    }

    override fun checkConfiguration() {
        functionName() ?: throw RuntimeConfigurationError(message("lambda.run_configuration.no_function_specified"))
        resolveRegion()
        resolveCredentials()
        checkInput()
    }

    override fun getState(executor: Executor, environment: ExecutionEnvironment): RemoteLambdaState {
        try {
            val functionName = functionName()
                ?: throw RuntimeConfigurationError(message("lambda.run_configuration.no_function_specified"))

            return RemoteLambdaState(
                environment,
                RemoteLambdaRunSettings(resolveCredentials(), resolveRegion(), functionName, resolveInput())
            )
        } catch (e: Exception) {
            throw ExecutionException(e.message, e)
        }
    }

    override fun suggestedName() = "[${message("lambda.run_configuration.remote")}] ${functionName()}"

    fun functionName(): String? = serializableOptions.functionOptions.functionName

    fun functionName(name: String?) {
        serializableOptions.functionOptions.functionName = name
    }
}

data class RemoteLambdaRunSettings(
    val credentialProvider: ToolkitCredentialsProvider,
    val region: AwsRegion,
    val functionName: String,
    val input: String
)


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

package software.aws.toolkits.jetbrains.services.lambda.execution.remote

import com.intellij.execution.RunManager
import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.actions.ConfigurationContext
import com.intellij.execution.actions.LazyRunConfigurationProducer
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.openapi.util.Ref
import com.intellij.psi.PsiElement
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.services.lambda.execution.LambdaRunConfigurationType

class RemoteLambdaRunConfigurationProducer : LazyRunConfigurationProducer<RemoteLambdaRunConfiguration>() {
    override fun getConfigurationFactory(): ConfigurationFactory = LambdaRunConfigurationType.getInstance()
        .configurationFactories
        .first { it is RemoteLambdaRunConfigurationFactory }

    // Filter all Lambda run configurations down to only ones that are Lambda remote for this run producer
    override fun getConfigurationSettingsList(runManager: RunManager): List<RunnerAndConfigurationSettings> =
        super.getConfigurationSettingsList(runManager).filter { it.configuration is RemoteLambdaRunConfiguration }

    override fun setupConfigurationFromContext(
        configuration: RemoteLambdaRunConfiguration,
        context: ConfigurationContext,
        sourceElement: Ref<PsiElement>
    ): Boolean {
        val location = context.location as? RemoteLambdaLocation ?: return false
        val function = location.lambdaFunction

        val accountSettings = AwsConnectionManager.getInstance(context.project)
        accountSettings.connectionSettings()?.let {
            configuration.credentialProviderId(it.credentials.id)
            configuration.regionId(it.region.id)
        }

        configuration.functionName(function.name)
        configuration.setGeneratedName()

        return true
    }

    override fun isConfigurationFromContext(
        configuration: RemoteLambdaRunConfiguration,
        context: ConfigurationContext
    ): Boolean {
        val location = context.location as? RemoteLambdaLocation ?: return false
        val function = location.lambdaFunction

        val accountSettings = AwsConnectionManager.getInstance(context.project)
        return accountSettings.connectionSettings()?.let {
            configuration.functionName() == function.name &&
                configuration.credentialProviderId() == it.credentials.id &&
                configuration.regionId() == it.region.id
        } ?: false
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.execution.remote

import com.intellij.execution.configurations.RunProfile
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.configurations.RunnerSettings
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.execution.runners.AsyncProgramRunner
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.RunContentBuilder
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import org.jetbrains.concurrency.AsyncPromise
import org.jetbrains.concurrency.Promise

class RemoteLambdaRunner : AsyncProgramRunner<RunnerSettings>() {
    override fun getRunnerId(): String = "Remote AWS Lambda"

    override fun canRun(executorId: String, profile: RunProfile): Boolean =
        DefaultRunExecutor.EXECUTOR_ID == executorId && profile is RemoteLambdaRunConfiguration

    override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise<RunContentDescriptor?> {
        val runPromise = AsyncPromise<RunContentDescriptor?>()
        val remoteState = state as RemoteLambdaState
        ApplicationManager.getApplication().executeOnPooledThread {
            try {
                val executionResult = remoteState.execute(environment.executor, this)
                val builder = RunContentBuilder(executionResult, environment)

                runInEdt(ModalityState.any()) {
                    runPromise.setResult(builder.showRunContent(environment.contentToReuse))
                }
            } catch (e: Exception) {
                runInEdt(ModalityState.any()) {
                    runPromise.setError(e)
                }
            }
        }

        return runPromise
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.execution.remote

import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.map
import software.aws.toolkits.jetbrains.services.lambda.resources.LambdaResources
import software.aws.toolkits.jetbrains.ui.ResourceSelector
import java.util.concurrent.atomic.AtomicReference
import javax.swing.JPanel

class RemoteLambdaRunSettingsEditor(project: Project) : SettingsEditor<RemoteLambdaRunConfiguration>() {
    private val credentialSettingsRef = AtomicReference<ConnectionSettings>()
    private val functionSelector = ResourceSelector
        .builder()
        .resource(LambdaResources.LIST_FUNCTIONS.map { it.functionName() })
        .disableAutomaticLoading()
        .awsConnection { credentialSettingsRef.get() ?: throw IllegalStateException("functionSelector.reload() called before region/credentials set") }
        .build()
    private val view = RemoteLambdaRunSettingsEditorPanel(project, functionSelector)
    private val credentialManager = CredentialManager.getInstance()

    internal fun updateFunctions(region: AwsRegion?, credentialProviderId: String?) {
        region ?: return
        credentialProviderId ?: return

        val credentialIdentifier = credentialManager.getCredentialIdentifierById(credentialProviderId) ?: return
        val credentialProvider = tryOrNull { credentialManager.getAwsCredentialProvider(credentialIdentifier, region) } ?: return

        val newSettings = ConnectionSettings(credentialProvider, region)
        val oldSettings = credentialSettingsRef.getAndUpdate { newSettings }
        if (oldSettings == newSettings) return
        functionSelector.reload()
    }

    override fun createEditor(): JPanel = view.panel

    override fun resetEditorFrom(configuration: RemoteLambdaRunConfiguration) {
        view.functionNames.selectedItem = configuration.functionName()
        if (configuration.isUsingInputFile()) {
            view.lambdaInput.inputFile = configuration.inputSource()
        } else {
            view.lambdaInput.inputText = configuration.inputSource()
        }
    }

    override fun applyEditorTo(configuration: RemoteLambdaRunConfiguration) {
        configuration.functionName(view.functionNames.selected())
        if (view.lambdaInput.isUsingFile) {
            configuration.useInputFile(view.lambdaInput.inputFile)
        } else {
            configuration.useInputText(view.lambdaInput.inputText)
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.execution.remote

import com.intellij.execution.DefaultExecutionResult
import com.intellij.execution.ExecutionResult
import com.intellij.execution.Executor
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.filters.TextConsoleBuilder
import com.intellij.execution.filters.TextConsoleBuilderFactory
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.ProgramRunner
import com.intellij.json.JsonLanguage
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.psi.search.GlobalSearchScopes
import software.amazon.awssdk.core.SdkBytes
import software.amazon.awssdk.services.lambda.LambdaClient
import software.amazon.awssdk.services.lambda.model.LogType
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.utils.formatText
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.LambdaTelemetry
import software.aws.toolkits.telemetry.Result
import java.io.OutputStream
import java.nio.charset.StandardCharsets
import java.util.Base64

class RemoteLambdaState(
    private val environment: ExecutionEnvironment,
    val settings: RemoteLambdaRunSettings
) : RunProfileState {
    private val consoleBuilder: TextConsoleBuilder

    init {
        val project = environment.project
        val searchScope = GlobalSearchScopes.executionScope(project, environment.runProfile)
        consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project, searchScope)
    }

    override fun execute(executor: Executor, runner: ProgramRunner<*>): ExecutionResult {
        val lambdaProcess = LambdaProcess()
        val console = consoleBuilder.console
        console.attachToProcess(lambdaProcess)

        return DefaultExecutionResult(console, lambdaProcess)
    }

    private inner class LambdaProcess : ProcessHandler() {
        override fun startNotify() {
            super.startNotify()

            ApplicationManager.getApplication().executeOnPooledThread { invokeLambda(this) }
        }

        override fun getProcessInput(): OutputStream? = null

        override fun detachIsDefault(): Boolean = true

        override fun detachProcessImpl() {
            notifyProcessDetached()
        }

        override fun destroyProcessImpl() {
            notifyProcessTerminated(0)
        }
    }

    private fun invokeLambda(lambdaProcess: ProcessHandler) {
        val client = AwsClientManager.getInstance()
            .getClient<LambdaClient>(settings.credentialProvider, settings.region)
        var result = Result.Succeeded

        lambdaProcess.notifyTextAvailable(
            message("lambda.execute.invoke", settings.functionName) + '\n',
            ProcessOutputTypes.SYSTEM
        )

        try {
            val response = client.invoke {
                it.logType(LogType.TAIL)
                it.payload(SdkBytes.fromUtf8String(settings.input))
                it.functionName(settings.functionName)
            }

            val logs = Base64.getDecoder().decode(response.logResult()).toString(StandardCharsets.UTF_8)
            val resultPayload = response.payload().asString(StandardCharsets.UTF_8)

            runInEdt {
                lambdaProcess.notifyTextAvailable(
                    message("lambda.execute.logs", logs) + '\n',
                    ProcessOutputTypes.STDOUT
                )

                response.functionError()?.let {
                    lambdaProcess.notifyTextAvailable(
                        message("lambda.execute.function_error", it) + '\n',
                        ProcessOutputTypes.STDERR
                    )
                }

                lambdaProcess.notifyTextAvailable(
                    message("lambda.execute.output", formatJson(resultPayload.trim())) + '\n',
                    ProcessOutputTypes.STDOUT
                )

                lambdaProcess.destroyProcess()
            }
        } catch (e: Exception) {
            result = Result.Failed
            runInEdt {
                lambdaProcess.notifyTextAvailable(
                    message("lambda.execute.service_error", e.message ?: "Unknown") + '\n',
                    ProcessOutputTypes.STDERR
                )

                lambdaProcess.destroyProcess()
            }
        } finally {
            LambdaTelemetry.invokeRemote(
                project = environment.project,
                result = result
            )
        }
    }

    private fun formatJson(input: String) = if (input.isNotEmpty() && input.first() == '{' && input.last() == '}') {
        formatText(environment.project, JsonLanguage.INSTANCE, input)
    } else {
        input
    }
}


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

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

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.util.io.FileUtil
import software.aws.toolkits.jetbrains.core.utils.buildList
import java.util.UUID

interface ImageDebugSupport : SamDebugSupport {
    val id: String

    /**
     * The primary language id, used to find the correct builder for the runtime
     */
    val languageId: String

    fun displayName(): String
    fun supportsPathMappings(): Boolean = false

    override fun samArguments(debugPorts: List<Int>): List<String> = buildList {
        val containerEnvVars = containerEnvVars(debugPorts)
        if (containerEnvVars.isNotEmpty()) {
            val path = createContainerEnvVarsFile(containerEnvVars)
            add("--container-env-vars")
            add(path)
        }
    }

    /**
     * Environment variables added to the execution of the container. These are used for debugging support for OCI
     * runtimes. The SAM CLI sets these for Zip based functions, but not Image based functions. An easy starting point
     * for the arguments is the list SAM cli maintains for Zip functions:
     * https://github.com/aws/aws-sam-cli/blob/develop/samcli/local/docker/lambda_debug_settings.py
     * @param debugPorts The list of debugger ports. Some runtimes (dotnet) require more than one
     */
    fun containerEnvVars(debugPorts: List<Int>): Map<String, String> = emptyMap()

    private fun createContainerEnvVarsFile(envVars: Map<String, String>): String {
        val envVarsFile = FileUtil.createTempFile("${UUID.randomUUID()}-debugArgs", ".json", true)
        envVarsFile.writeText(mapper.writeValueAsString(envVars))
        return envVarsFile.absolutePath
    }

    companion object {
        private val mapper = jacksonObjectMapper()
        val EP_NAME = ExtensionPointName<ImageDebugSupport>("aws.toolkit.lambda.sam.imageDebuggerSupport")

        fun debuggers(): Map<String, ImageDebugSupport> = EP_NAME.extensionList.associateBy { it.id }
    }
}


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

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

import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.PathMappingSettings.PathMapping
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.lambda.LambdaArchitecture
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.RuntimeGroup
import software.aws.toolkits.jetbrains.services.lambda.runtimeGroup
import software.aws.toolkits.jetbrains.services.lambda.sam.SamOptions

interface TemplateSettings {
    val templateFile: VirtualFile
    val logicalId: String
}

interface ZipSettings {
    val runtime: LambdaRuntime
    val architecture: LambdaArchitecture
    val handler: String
}

interface ImageSettings {
    val imageDebugger: ImageDebugSupport
}

sealed class LocalLambdaRunSettings(
    val connection: ConnectionSettings,
    val samOptions: SamOptions,
    val environmentVariables: Map<String, String>,
    val debugHost: String,
    val input: String
) {
    abstract val runtimeGroup: RuntimeGroup
}

class TemplateRunSettings(
    override val templateFile: VirtualFile,
    override val runtime: LambdaRuntime,
    override val architecture: LambdaArchitecture,
    override val handler: String,
    override val logicalId: String,
    environmentVariables: Map<String, String>,
    connection: ConnectionSettings,
    samOptions: SamOptions,
    debugHost: String,
    input: String
) : TemplateSettings, ZipSettings, LocalLambdaRunSettings(connection, samOptions, environmentVariables, debugHost, input) {
    override val runtimeGroup = runtime.runtimeGroup ?: throw IllegalStateException("Attempting to run SAM for unsupported runtime $runtime")
}

class HandlerRunSettings(
    override val runtime: LambdaRuntime,
    override val architecture: LambdaArchitecture,
    override val handler: String,
    val timeout: Int,
    val memorySize: Int,
    environmentVariables: Map<String, String>,
    connection: ConnectionSettings,
    samOptions: SamOptions,
    debugHost: String,
    input: String
) : ZipSettings, LocalLambdaRunSettings(connection, samOptions, environmentVariables, debugHost, input) {
    override val runtimeGroup = runtime.runtimeGroup ?: throw IllegalStateException("Attempting to run SAM for unsupported runtime $runtime")
}

class ImageTemplateRunSettings(
    override val templateFile: VirtualFile,
    override val imageDebugger: ImageDebugSupport,
    override val logicalId: String,
    val dockerFile: VirtualFile,
    val pathMappings: List<PathMapping>,
    environmentVariables: Map<String, String>,
    connection: ConnectionSettings,
    samOptions: SamOptions,
    debugHost: String,
    input: String
) : ImageSettings, TemplateSettings, LocalLambdaRunSettings(connection, samOptions, environmentVariables, debugHost, input) {
    override val runtimeGroup = RuntimeGroup.find { imageDebugger.languageId in it.languageIds }
        ?: throw IllegalStateException("Attempting to run SAM for unsupported language ${imageDebugger.languageId}")
}

fun LocalLambdaRunSettings.resolveDebuggerSupport() = when (this) {
    is ImageTemplateRunSettings -> imageDebugger
    is ZipSettings -> RuntimeDebugSupport.getInstance(runtimeGroup)
    else -> throw IllegalStateException("Can't find debugger support for $this")
}


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

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

import com.intellij.openapi.extensions.ExtensionPointName
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.jetbrains.services.lambda.RuntimeGroupExtensionPointObject

interface RuntimeDebugSupport : SamDebugSupport {
    fun isSupported(runtime: Runtime): Boolean = true // Default behavior is all runtimes in the runtime group are supported

    companion object : RuntimeGroupExtensionPointObject<RuntimeDebugSupport>(ExtensionPointName("aws.toolkit.lambda.sam.runtimeDebugSupport"))
}


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

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

import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.util.registry.Registry
import com.intellij.xdebugger.XDebugProcessStarter
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.Step

interface SamDebugSupport {
    fun numberOfDebugPorts(): Int = 1

    /**
     * SAM arguments added to the execution of `sam local invoke`. These include --debugger-path and --debug-args
     * for debugging, and anything else that is needed on a per-runtime basis
     * @param debugPorts The list of debugger ports. Some runtimes (dotnet) require more than one
     */
    fun samArguments(debugPorts: List<Int>): List<String> = emptyList()

    fun additionalDebugProcessSteps(environment: ExecutionEnvironment, state: SamRunningState): List<Step> = listOf()

    suspend fun createDebugProcess(
        context: Context,
        environment: ExecutionEnvironment,
        state: SamRunningState,
        debugHost: String,
        debugPorts: List<Int>
    ): XDebugProcessStarter

    companion object {
        fun debuggerConnectTimeoutMs() = Registry.intValue("aws.debuggerAttach.timeout", 60000).toLong()
    }
}


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

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

import com.intellij.execution.configurations.RunProfile
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.configurations.RunnerSettings
import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.execution.runners.AsyncProgramRunner
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.RunContentBuilder
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.openapi.fileEditor.FileDocumentManager
import org.jetbrains.concurrency.AsyncPromise
import org.jetbrains.concurrency.Promise
import org.slf4j.event.Level
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.core.lambda.validOrNull
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.services.lambda.execution.local.LocalLambdaRunConfiguration
import software.aws.toolkits.jetbrains.services.lambda.runtimeGroup
import software.aws.toolkits.jetbrains.services.lambda.sam.SamTemplateUtils
import java.io.File

class SamInvokeRunner : AsyncProgramRunner<RunnerSettings>() {
    override fun getRunnerId(): String = "SamInvokeRunner"

    override fun canRun(executorId: String, profile: RunProfile): Boolean {
        if (profile !is LocalLambdaRunConfiguration) {
            return false
        }

        if (DefaultRunExecutor.EXECUTOR_ID == executorId) {
            // Always true so that the run icon is shown, error is then told to user that runtime doesnt work
            return true
        }

        if (DefaultDebugExecutor.EXECUTOR_ID != executorId) {
            // Only support debugging if it is the default executor
            return false
        }

        val runtimeValue = if (profile.isUsingTemplate() && !profile.isImage) {
            LOG.tryOrNull("Failed to get runtime of ${profile.logicalId()}", Level.WARN) {
                SamTemplateUtils.findZipFunctionsFromTemplate(profile.project, File(profile.templateFile()))
                    .find { it.logicalName == profile.logicalId() }
                    ?.runtime()
                    ?.let {
                        Runtime.fromValue(it)?.validOrNull
                    }
            }
        } else if (!profile.isImage) {
            profile.runtime()?.toSdkRuntime()
        } else {
            null
        }
        val runtimeGroup = runtimeValue?.runtimeGroup

        val canRunRuntime = runtimeGroup != null &&
            RuntimeDebugSupport.supportedRuntimeGroups().contains(runtimeGroup) &&
            RuntimeDebugSupport.getInstanceOrNull(runtimeGroup)?.isSupported(runtimeValue) ?: false
        val canRunImage = profile.isImage && profile.imageDebugger() != null

        return canRunRuntime || canRunImage
    }

    override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise<RunContentDescriptor?> {
        val runPromise = AsyncPromise<RunContentDescriptor?>()
        FileDocumentManager.getInstance().saveAllDocuments()
        val runContentDescriptor = state.execute(environment.executor, this)?.let {
            RunContentBuilder(it, environment).showRunContent(environment.contentToReuse)
        }

        runPromise.setResult(runContentDescriptor)

        return runPromise
    }

    private companion object {
        val LOG = getLogger<SamInvokeRunner>()
    }
}


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

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

import com.intellij.build.BuildView
import com.intellij.build.DefaultBuildDescriptor
import com.intellij.build.ViewManager
import com.intellij.execution.DefaultExecutionResult
import com.intellij.execution.ExecutionResult
import com.intellij.execution.Executor
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.ProgramRunner
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import software.aws.toolkits.core.telemetry.DefaultMetricEvent
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.utils.buildList
import software.aws.toolkits.jetbrains.services.PathMapping
import software.aws.toolkits.jetbrains.services.lambda.LambdaBuilder
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.steps.AttachDebugger
import software.aws.toolkits.jetbrains.services.lambda.steps.AttachDebuggerParent
import software.aws.toolkits.jetbrains.services.lambda.steps.BuildLambda
import software.aws.toolkits.jetbrains.services.lambda.steps.BuildLambdaRequest
import software.aws.toolkits.jetbrains.services.lambda.steps.GetPorts
import software.aws.toolkits.jetbrains.services.lambda.steps.SamRunnerStep
import software.aws.toolkits.jetbrains.services.sts.StsResources
import software.aws.toolkits.jetbrains.services.telemetry.MetricEventMetadata
import software.aws.toolkits.jetbrains.utils.execution.steps.BuildViewWorkflowEmitter
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.ParallelStep
import software.aws.toolkits.jetbrains.utils.execution.steps.Step
import software.aws.toolkits.jetbrains.utils.execution.steps.StepExecutor
import software.aws.toolkits.jetbrains.utils.execution.steps.StepWorkflow
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.LambdaPackageType
import software.aws.toolkits.telemetry.LambdaTelemetry
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.Runtime
import java.nio.file.Paths

class SamRunningState(
    val environment: ExecutionEnvironment,
    val settings: LocalLambdaRunSettings
) : RunProfileState {
    lateinit var pathMappings: List<PathMapping>

    override fun execute(executor: Executor, runner: ProgramRunner<*>): ExecutionResult {
        val project = environment.project
        val descriptor = DefaultBuildDescriptor(
            runConfigId(),
            message("lambda.run_configuration.sam"),
            "/unused/location",
            System.currentTimeMillis()
        )

        val buildView = BuildView(
            project,
            descriptor,
            null,
            object : ViewManager {
                override fun isConsoleEnabledByDefault() = false

                override fun isBuildContentView() = true
            }
        )

        val lambdaBuilder = LambdaBuilder.getInstance(settings.runtimeGroup)

        val buildLambdaRequest = buildBuildLambdaRequest(environment.project, settings)

        pathMappings = createPathMappings(lambdaBuilder, settings, buildLambdaRequest)
        val buildWorkflow = buildWorkflow(environment, settings, this, buildLambdaRequest, buildView)

        return DefaultExecutionResult(buildView, buildWorkflow)
    }

    private fun createPathMappings(lambdaBuilder: LambdaBuilder, settings: LocalLambdaRunSettings, buildRequest: BuildLambdaRequest): List<PathMapping> {
        val defaultPathMappings = lambdaBuilder.defaultPathMappings(buildRequest.templatePath, buildRequest.logicalId ?: dummyLogicalId, buildRequest.buildDir)
        return if (settings is ImageTemplateRunSettings) {
            // This needs to be a bit smart. If a user set local path matches a default path, we need to make sure that is the one set
            // by removing the default set one.
            val userMappings = settings.pathMappings.map { PathMapping(it.localRoot, it.remoteRoot) }
            userMappings + defaultPathMappings.filterNot { defaultMapping -> userMappings.any { defaultMapping.localRoot == it.localRoot } }
        } else {
            defaultPathMappings
        }
    }

    private fun reportMetric(lambdaSettings: LocalLambdaRunSettings, result: Result, isDebug: Boolean) {
        val account = AwsResourceCache.getInstance()
            .getResourceIfPresent(StsResources.ACCOUNT, lambdaSettings.connection)

        LambdaTelemetry.invokeLocal(
            metadata = MetricEventMetadata(
                awsAccount = account ?: DefaultMetricEvent.METADATA_INVALID,
                awsRegion = lambdaSettings.connection.region.id
            ),
            debug = isDebug,
            runtime = Runtime.from(
                when (lambdaSettings) {
                    is ZipSettings -> {
                        lambdaSettings.runtime.toString()
                    }
                    is ImageSettings -> {
                        lambdaSettings.imageDebugger.id
                    }
                    else -> {
                        ""
                    }
                }
            ),
            version = SamCommon.getVersionString(),
            lambdaPackageType = if (lambdaSettings is ImageTemplateRunSettings) LambdaPackageType.Image else LambdaPackageType.Zip,
            result = result
        )
    }

    private fun buildWorkflow(
        environment: ExecutionEnvironment,
        settings: LocalLambdaRunSettings,
        state: SamRunningState,
        buildRequest: BuildLambdaRequest,
        buildView: BuildView
    ): ProcessHandler {
        val startSam = SamRunnerStep(environment, settings, environment.isDebug())

        val workflow = StepWorkflow(
            buildList {
                add(ValidateDocker())
                if (buildRequest.preBuildSteps.isNotEmpty()) {
                    addAll(buildRequest.preBuildSteps)
                }
                add(BuildLambda(buildRequest))
                if (environment.isDebug()) {
                    add(GetPorts(settings))
                    add(object : ParallelStep() {
                        override fun buildChildSteps(context: Context): List<Step> = listOf(
                            startSam,
                            AttachDebuggerParent(
                                state.settings.resolveDebuggerSupport().additionalDebugProcessSteps(environment, state) +
                                    AttachDebugger(
                                        environment,
                                        state
                                    )
                            )
                        )

                        override val stepName: String = ""
                        override val hidden: Boolean = true
                    })
                } else {
                    add(startSam)
                }
            }
        )
        val emitter = BuildViewWorkflowEmitter.createEmitter(buildView, message("sam.build.running"), environment.executionId.toString())
        val executor = StepExecutor(environment.project, workflow, emitter)
        executor.onSuccess = {
            reportMetric(settings, Result.Succeeded, environment.isDebug())
        }
        executor.onError = {
            reportMetric(settings, Result.Failed, environment.isDebug())
        }

        // Let run config system start the execution through the process handler
        return executor.getProcessHandler()
    }

    private fun runConfigId() = environment.executionId.toString()

    companion object {
        private const val dummyLogicalId = "Function"

        internal fun buildBuildLambdaRequest(project: Project, lambdaSettings: LocalLambdaRunSettings) = when (lambdaSettings) {
            is TemplateRunSettings ->
                buildLambdaFromTemplate(
                    project,
                    lambdaSettings
                )
            is ImageTemplateRunSettings ->
                buildLambdaFromTemplate(
                    project,
                    lambdaSettings
                )
            is HandlerRunSettings ->
                lambdaSettings.lambdaBuilder().buildFromHandler(
                    project,
                    lambdaSettings
                )
        }

        private fun buildLambdaFromTemplate(
            project: Project,
            lambdaSettings: TemplateRunSettings,
        ): BuildLambdaRequest = buildLambdaFromTemplate(
            project,
            lambdaSettings.lambdaBuilder(),
            lambdaSettings.templateFile,
            lambdaSettings.logicalId,
            lambdaSettings.samOptions
        )

        private fun buildLambdaFromTemplate(
            project: Project,
            lambdaSettings: ImageTemplateRunSettings,
        ): BuildLambdaRequest = buildLambdaFromTemplate(
            project,
            lambdaSettings.lambdaBuilder(),
            lambdaSettings.templateFile,
            lambdaSettings.logicalId,
            lambdaSettings.samOptions
        )

        private fun buildLambdaFromTemplate(
            project: Project,
            lambdaBuilder: LambdaBuilder,
            templateFile: VirtualFile,
            logicalId: String,
            samOptions: SamOptions
        ): BuildLambdaRequest {
            val templatePath = Paths.get(templateFile.path)
            val buildDir = templatePath.resolveSibling(".aws-sam").resolve("build")
            val module = ModuleUtil.findModuleForFile(templateFile, project)
            val additionalBuildEnvironmentVariables = lambdaBuilder.additionalBuildEnvironmentVariables(project, module, samOptions)

            return BuildLambdaRequest(templatePath, logicalId, buildDir, additionalBuildEnvironmentVariables, samOptions)
        }

        private fun LocalLambdaRunSettings.lambdaBuilder() = LambdaBuilder.getInstance(this.runtimeGroup)

        private fun ExecutionEnvironment.isDebug(): Boolean = (executor.id == DefaultDebugExecutor.EXECUTOR_ID)
    }
}


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

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

import com.intellij.docker.DockerCloudConfiguration
import com.intellij.docker.DockerCloudType
import com.intellij.openapi.options.SettingsEditor
import com.intellij.remoteServer.configuration.RemoteServersManager
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.core.plugins.pluginIsInstalledAndEnabled
import software.aws.toolkits.jetbrains.utils.ui.selected
import java.net.URI
import javax.swing.JComponent

/*
 * An interface implemented by run configurations that can have a Sam Settings panel
 */
interface SamSettingsRunConfiguration {
    var dockerNetwork: String?
    var buildInContainer: Boolean
    var skipPullImage: Boolean
    var debugHost: String
    var additionalLocalArgs: String?
    var additionalBuildArgs: String?
}

class SamSettingsEditor<T : SamSettingsRunConfiguration> : SettingsEditor<T>() {
    private val view = SamSettingsEditorPanel()

    override fun createEditor(): JComponent {
        val hostOptions = mutableSetOf<String>()

        hostOptions.add("localhost")
        if (pluginIsInstalledAndEnabled("Docker")) {
            val dockerCloudType = DockerCloudType.getInstance()
            RemoteServersManager.getInstance().servers.asSequence()
                .filter { it.type == dockerCloudType }
                .map { it.configuration }
                .filterIsInstance<DockerCloudConfiguration>()
                .mapNotNull { convertDockerApiToHost(it.apiUrl) }
                .forEach { hostOptions.add(it) }
        }
        hostOptions.forEach { view.debugHostChooser.addItem(it) }

        return view.panel
    }

    private fun convertDockerApiToHost(api: String) = LOGGER.tryOrNull("Failed to convert $api to host") {
        val apiUri = URI.create(api)
        if (apiUri.scheme != "unix") {
            apiUri.host
        } else {
            null
        }
    }

    override fun resetEditorFrom(configuration: T) {
        view.dockerNetwork.text = configuration.dockerNetwork
        view.debugHostChooser.selectedItem = configuration.debugHost
        view.skipPullImage.isSelected = configuration.skipPullImage
        view.buildInContainer.isSelected = configuration.buildInContainer
        view.additionalBuildArgs.text = configuration.additionalBuildArgs
        view.additionalLocalArgs.text = configuration.additionalLocalArgs
    }

    override fun applyEditorTo(configuration: T) {
        configuration.dockerNetwork = view.dockerNetwork.text.trim()
        configuration.debugHost = view.debugHostChooser.selected() ?: "localhost"
        configuration.skipPullImage = view.skipPullImage.isSelected
        configuration.buildInContainer = view.buildInContainer.isSelected
        configuration.additionalBuildArgs = view.additionalBuildArgs.text.trim()
        configuration.additionalLocalArgs = view.additionalLocalArgs.text.trim()
    }

    private companion object {
        val LOGGER = getLogger<SamSettingsEditor<*>>()
    }
}


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

import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.components.fields.ExpandableTextField
import javax.swing.JCheckBox
import javax.swing.JPanel
import javax.swing.JTextField

class SamSettingsEditorPanel {
    lateinit var buildInContainer: JCheckBox
        private set
    lateinit var dockerNetwork: JTextField
        private set
    lateinit var skipPullImage: JCheckBox
        private set
    lateinit var panel: JPanel
        private set
    lateinit var additionalBuildArgs: ExpandableTextField
        private set
    lateinit var additionalLocalArgs: ExpandableTextField
        private set
    lateinit var debugHostChooser: ComboBox<String>
        private set
}


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

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

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessListener
import software.aws.toolkits.jetbrains.utils.execution.steps.CliBasedStep
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.resources.message

class ValidateDocker : CliBasedStep() {
    override val stepName: String = "Validate Docker"

    override fun constructCommandLine(context: Context): GeneralCommandLine = GeneralCommandLine("docker", "ps")

    override fun handleErrorResult(exitCode: Int, output: String, stepEmitter: StepEmitter) {
        throw Exception(message("lambda.debug.docker.not_connected"))
    }

    // Change logger to not log std out since we dont actually want the output of docker
    override fun createProcessEmitter(stepEmitter: StepEmitter): ProcessListener = CliOutputEmitter(stepEmitter, printStdOut = false)
}


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

package software.aws.toolkits.jetbrains.services.lambda.execution.template

import com.intellij.execution.lineMarker.ExecutorAction
import com.intellij.execution.lineMarker.RunLineMarkerContributor
import com.intellij.icons.AllIcons
import com.intellij.psi.PsiElement
import org.jetbrains.yaml.psi.YAMLKeyValue
import software.aws.toolkits.jetbrains.services.cloudformation.Function
import software.aws.toolkits.jetbrains.services.cloudformation.yaml.YamlCloudFormationTemplate
import software.aws.toolkits.jetbrains.utils.isTestOrInjectedText

class YamlLambdaRunLineMarkerContributor : RunLineMarkerContributor() {

    override fun getInfo(element: PsiElement): Info? {
        // Only leaf element is allowed
        if (element.firstChild != null) {
            return null
        }

        if (element.isTestOrInjectedText()) {
            return null
        }

        val parent = element.parent as? YAMLKeyValue ?: return null

        return if (parent.key == element && YamlCloudFormationTemplate.convertPsiToResource(parent) as? Function != null) {
            Info(AllIcons.RunConfigurations.TestState.Run, ExecutorAction.getActions(1), null)
        } else {
            null
        }
    }
}


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

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

import com.intellij.debugger.DebugEnvironment
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.JavaDebugProcess
import com.intellij.debugger.engine.RemoteDebugProcessHandler
import com.intellij.execution.DefaultExecutionResult
import com.intellij.execution.ExecutionResult
import com.intellij.execution.configurations.RemoteConnection
import com.intellij.execution.impl.ConsoleViewImpl
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.lang.java.JavaLanguage
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.GlobalSearchScopes
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebugProcessStarter
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.impl.XDebugSessionImpl
import kotlinx.coroutines.withContext
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.ImageDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.RuntimeDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.SamRunningState
import software.aws.toolkits.jetbrains.utils.execution.steps.Context

class JavaRuntimeDebugSupport : RuntimeDebugSupport {
    override suspend fun createDebugProcess(
        context: Context,
        environment: ExecutionEnvironment,
        state: SamRunningState,
        debugHost: String,
        debugPorts: List<Int>
    ): XDebugProcessStarter = createDebugProcess(environment, debugHost, debugPorts)
}

abstract class JavaImageDebugSupport : ImageDebugSupport {
    override fun supportsPathMappings(): Boolean = true
    override val languageId = JavaLanguage.INSTANCE.id
    override suspend fun createDebugProcess(
        context: Context,
        environment: ExecutionEnvironment,
        state: SamRunningState,
        debugHost: String,
        debugPorts: List<Int>
    ): XDebugProcessStarter = createDebugProcess(environment, debugHost, debugPorts)
}

open class Java8ImageDebugSupport : JavaImageDebugSupport() {
    override val id: String = LambdaRuntime.JAVA8.toString()
    override fun displayName() = LambdaRuntime.JAVA8.toString().capitalize()
    override fun containerEnvVars(debugPorts: List<Int>): Map<String, String> = mapOf(
        "_JAVA_OPTIONS" to "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=${debugPorts.first()} " +
            "-XX:MaxHeapSize=2834432k -XX:MaxMetaspaceSize=163840k -XX:ReservedCodeCacheSize=81920k -XX:+UseSerialGC " +
            "-XX:-TieredCompilation -Djava.net.preferIPv4Stack=true -Xshare:off"
    )
}

class Java8Al2ImageDebugSupport : Java8ImageDebugSupport() {
    override val id: String = LambdaRuntime.JAVA8_AL2.toString()
    override fun displayName() = LambdaRuntime.JAVA8_AL2.toString().capitalize()
}

open class Java11ImageDebugSupport : JavaImageDebugSupport() {
    override val id: String = LambdaRuntime.JAVA11.toString()
    override fun displayName() = LambdaRuntime.JAVA11.toString().capitalize()
    override fun containerEnvVars(debugPorts: List<Int>): Map<String, String> = mapOf(
        "_JAVA_OPTIONS" to "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=*:${debugPorts.first()} " +
            "-XX:MaxHeapSize=2834432k -XX:MaxMetaspaceSize=163840k -XX:ReservedCodeCacheSize=81920k -XX:+UseSerialGC " +
            "-XX:-TieredCompilation -Djava.net.preferIPv4Stack=true"
    )
}

open class Java17ImageDebugSupport : JavaImageDebugSupport() {
    override val id: String = LambdaRuntime.JAVA17.toString()
    override fun displayName() = LambdaRuntime.JAVA17.toString().capitalize()
    override fun containerEnvVars(debugPorts: List<Int>): Map<String, String> = mapOf(
        "_JAVA_OPTIONS" to "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=*:${debugPorts.first()} " +
            "-XX:MaxHeapSize=2834432k -XX:MaxMetaspaceSize=163840k -XX:ReservedCodeCacheSize=81920k -XX:+UseSerialGC " +
            "-XX:-TieredCompilation -Djava.net.preferIPv4Stack=true"
    )
}

private val edtContext = getCoroutineUiContext()

private suspend fun createDebugProcess(
    environment: ExecutionEnvironment,
    debugHost: String,
    debugPorts: List<Int>
): XDebugProcessStarter {
    val connection = RemoteConnection(true, debugHost, debugPorts.first().toString(), false)
    val debugEnvironment = RemotePortDebugEnvironment(environment, connection)
    val debuggerManager = DebuggerManagerEx.getInstanceEx(environment.project)

    val debuggerSession = withContext(edtContext) {
        debuggerManager.attachVirtualMachine(debugEnvironment)
    } ?: throw IllegalStateException("Attaching to the JVM failed! $debugHost:${debugPorts.first()}")

    return object : XDebugProcessStarter() {
        override fun start(session: XDebugSession): XDebugProcess {
            if (session is XDebugSessionImpl) {
                val debugProcess = debuggerSession.process
                val executionResult = debugProcess.executionResult
                session.addExtraActions(*executionResult.actions)
                if (executionResult is DefaultExecutionResult) {
                    session.addRestartActions(*executionResult.restartActions)
                }
            }

            return JavaDebugProcess.create(session, debuggerSession)
        }
    }
}

/**
 * We are required to make our own so we do not end up in a loop. DefaultDebugEnvironment will execute the run config again
 * which make a tragic recursive loop starting the run config an infinite number of times
 */
private class RemotePortDebugEnvironment(private val environment: ExecutionEnvironment, private val connection: RemoteConnection) : DebugEnvironment {
    override fun createExecutionResult(): ExecutionResult {
        val consoleView = ConsoleViewImpl(environment.project, false)
        val process = RemoteDebugProcessHandler(environment.project)
        consoleView.attachToProcess(process)
        return DefaultExecutionResult(consoleView, process)
    }

    override fun getSearchScope(): GlobalSearchScope = GlobalSearchScopes.executionScope(environment.project, environment.runProfile)
    override fun isRemote(): Boolean = true
    override fun getRemoteConnection(): RemoteConnection = connection
    override fun getPollTimeout(): Long = DebugEnvironment.LOCAL_START_TIMEOUT.toLong()
    override fun getSessionName(): String = environment.runProfile.name
}


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

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

import com.intellij.openapi.externalSystem.ExternalSystemModulePropertyManager
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.JavaSdkType
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.psi.PsiElement
import org.jetbrains.idea.maven.project.MavenProjectsManager
import software.aws.toolkits.jetbrains.core.plugins.pluginIsInstalledAndEnabled
import software.aws.toolkits.jetbrains.services.lambda.LambdaBuilder
import software.aws.toolkits.jetbrains.services.lambda.sam.SamOptions
import software.aws.toolkits.resources.message
import java.nio.file.Path
import java.nio.file.Paths

class JavaLambdaBuilder : LambdaBuilder() {
    override fun handlerBaseDirectory(module: Module, handlerElement: PsiElement): Path {
        val buildFileDir = when {
            isGradle(module) -> getGradleProjectLocation(module)
            isMaven(module) -> getPomLocation(module)
            else -> throw IllegalStateException(message("lambda.build.java.unsupported_build_system", module.name))
        }

        return Paths.get(buildFileDir)
    }

    override fun additionalBuildEnvironmentVariables(project: Project, module: Module?, samOptions: SamOptions): Map<String, String> {
        if (samOptions.buildInContainer) {
            return emptyMap()
        }

        val sdk = module ?.let { ModuleRootManager.getInstance(it).sdk } ?: ProjectRootManager.getInstance(project).projectSdk ?: return emptyMap()
        val sdkHome = sdk.homePath ?: return emptyMap()

        return if (sdk.sdkType is JavaSdkType) {
            mapOf("JAVA_HOME" to FileUtil.toSystemDependentName(sdkHome))
        } else {
            emptyMap()
        }
    }

    private fun isGradle(module: Module): Boolean = ExternalSystemModulePropertyManager.getInstance(module).getExternalSystemId() == "GRADLE"

    private fun getGradleProjectLocation(module: Module): String = ExternalSystemApiUtil.getExternalProjectPath(module)
        ?: throw IllegalStateException(message("lambda.build.unable_to_locate_project_root", module))

    private fun isMaven(module: Module): Boolean = if (pluginIsInstalledAndEnabled("org.jetbrains.idea.maven")) {
        MavenProjectsManager.getInstance(module.project).isMavenizedModule(module)
    } else {
        false
    }

    private fun getPomLocation(module: Module): String = MavenProjectsManager.getInstance(module.project).findProject(module)?.directory
        ?: throw IllegalStateException(message("lambda.build.unable_to_locate_project_root", module))
}


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

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

import com.intellij.lang.jvm.JvmModifier
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.NavigatablePsiElement
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiIdentifier
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifierListOwner
import com.intellij.psi.PsiParameter
import com.intellij.psi.PsiType
import com.intellij.psi.search.GlobalSearchScope
import software.aws.toolkits.jetbrains.services.lambda.LambdaHandlerResolver

class JavaLambdaHandlerResolver : LambdaHandlerResolver {
    override fun findPsiElements(
        project: Project,
        handler: String,
        searchScope: GlobalSearchScope
    ): Array<NavigatablePsiElement> {
        val split = handler.split("::")
        val className = split[0]
        val methodName = if (split.size >= 2) split[1] else null

        val psiFacade = JavaPsiFacade.getInstance(project)
        return DumbService.getInstance(project).computeWithAlternativeResolveEnabled<Array<NavigatablePsiElement>, Exception> {
            ApplicationManager.getApplication().runReadAction<Array<NavigatablePsiElement>> {
                val classes = psiFacade.findClasses(className, searchScope).toList()
                return@runReadAction if (methodName.isNullOrEmpty()) {
                    classes.filterIsInstance<NavigatablePsiElement>().toTypedArray()
                } else {
                    val handlerMethod = classes.asSequence()
                        .map { psiClass ->
                            psiClass.findMethodsByName(methodName, true)
                                .filter { it.body != null } // Filter out interfaces
                                .filter {
                                    val file = it.containingFile.virtualFile

                                    return@filter if (psiClass.implementsLambdaHandlerInterface(file)) {
                                        true
                                    } else {
                                        it.isValidHandler(psiClass, file)
                                    }
                                }
                        }
                        .flatMap { it.asSequence() }
                        .pickMostSpecificHandler()
                    handlerMethod?.let {
                        arrayOf(it)
                    } ?: NavigatablePsiElement.EMPTY_NAVIGATABLE_ELEMENT_ARRAY
                }
            }
        }
    }

    override fun determineHandler(element: PsiElement): String? =
        DumbService.getInstance(element.project).computeWithAlternativeResolveEnabled<String?, Exception> {
            when (element) {
                is PsiClass -> findByClass(element)
                is PsiMethod -> findByMethod(element)
                is PsiIdentifier -> determineHandler(element.parent)
                else -> null
            }
        }

    override fun determineHandlers(element: PsiElement, file: VirtualFile): Set<String> =
        DumbService.getInstance(element.project).computeWithAlternativeResolveEnabled<Set<String>, Exception> {
            when (element) {
                is PsiClass -> findHandlersByClass(element, file)
                is PsiMethod -> findHandlersByMethod(element, file)
                is PsiIdentifier -> determineHandlers(element.parent, file)
                else -> emptySet()
            }
        }

    /**
     * Always show line marker for handler if it is a class which means it implements Lambda interface.
     */
    override fun shouldShowLineMarker(handler: String): Boolean = !handler.contains("::")

    /**
     * https://docs.aws.amazon.com/lambda/latest/dg/java-programming-model-handler-types.html
     * Handler Overload Resolution
     *
     * If your Java code contains multiple methods with same name as the handler name, then AWS Lambda uses the following rules to pick a method to invoke:
     *
     * 1. Select the method with the largest number of parameters.
     *
     * 2. If two or more methods have the same number of parameters, AWS Lambda selects the method that has the Context as the last parameter.
     *
     * If none or all of these methods have the Context parameter, then the behavior is undefined.
     */
    private fun Sequence<PsiMethod>.pickMostSpecificHandler(): NavigatablePsiElement? {
        var maxWeight = -1
        val methods = this.filter { it.parameterList.parametersCount in 1..3 }
            .groupBy {
                var weight = it.parameterList.parametersCount
                if (it.parameterList.parameters.getOrNull(weight - 1)?.isContextParameter() == true) {
                    weight++
                }
                maxWeight = maxOf(maxWeight, weight)
                weight
            }.getOrDefault(maxWeight, emptyList())

        // Empty, or undefined behavior
        if (methods.isEmpty() || methods.size > 1) {
            return null
        }

        return methods[0]
    }

    private fun findByMethod(method: PsiMethod): String? {
        val file = method.containingFile.virtualFile ?: return null
        return findByMethod(method, file)
    }

    private fun findHandlersByMethod(method: PsiMethod, file: VirtualFile): Set<String> = findByMethod(method, file)?.let { setOf(it) }.orEmpty()

    private fun findByMethod(method: PsiMethod, file: VirtualFile): String? {
        val parentClass = method.parent as? PsiClass ?: return null
        return if (method.isValidHandler(parentClass, file)) {
            "${parentClass.qualifiedName}::${method.name}"
        } else {
            null
        }
    }

    override fun handlerDisplayName(handler: String): String = handler.substringAfterLast('.').replace("::", ".")

    private fun findByClass(clz: PsiClass): String? =
        if (clz.canBeInstantiatedByLambda() &&
            clz.containingFile.virtualFile != null &&
            clz.implementsLambdaHandlerInterface(clz.containingFile.virtualFile)
        ) {
            clz.qualifiedName
        } else {
            null
        }

    private fun findHandlersByClass(clz: PsiClass, file: VirtualFile): Set<String> {
        if (!clz.canBeInstantiatedByLambda()) {
            return emptySet()
        }

        val handlers = mutableSetOf<String>()
        if (clz.implementsLambdaHandlerInterface(file)) {
            clz.qualifiedName?.let { handlers.add(it) }
        }

        handlers.addAll(
            clz.allMethods
                .asSequence()
                .filter { it.isValidHandler(clz, file) }
                .map { "${clz.qualifiedName}::${it.name}" }
                .toSet()
        )

        return handlers
    }

    private fun PsiClass.canBeInstantiatedByLambda() = this.isPublic && this.isConcrete && this.hasPublicNoArgsConstructor()

    private val PsiModifierListOwner.isPublic get() = this.hasModifier(JvmModifier.PUBLIC)

    private val PsiModifierListOwner.isStatic get() = this.hasModifier(JvmModifier.STATIC)

    private val PsiClass.isConcrete get() = !this.isInterface && !this.hasModifier(JvmModifier.ABSTRACT)

    private fun PsiClass.hasPublicNoArgsConstructor() =
        this.constructors.isEmpty() || this.constructors.any { it.hasModifier(JvmModifier.PUBLIC) && it.parameters.isEmpty() }

    private fun PsiClass.implementsLambdaHandlerInterface(file: VirtualFile): Boolean {
        val module = ModuleUtilCore.findModuleForFile(file, this.project) ?: return false
        val scope = GlobalSearchScope.moduleRuntimeScope(module, false)
        val psiFacade = JavaPsiFacade.getInstance(module.project)

        return LAMBDA_INTERFACES.any { interfaceName ->
            psiFacade.findClass(interfaceName, scope)?.let { interfacePsi ->
                this.isInheritor(interfacePsi, true)
            } == true
        }
    }

    private fun PsiMethod.isValidHandler(parentClass: PsiClass, file: VirtualFile) = this.isPublic &&
        this.hasRequiredParameters() &&
        (!this.isStatic || this.name != "main") &&
        !this.isConstructor &&
        (this.isStatic || parentClass.canBeInstantiatedByLambda()) &&
        !(parentClass.implementsLambdaHandlerInterface(file) && this.name == HANDLER_NAME)

    private fun PsiMethod.hasRequiredParameters(): Boolean = when (this.parameters.size) {
        1 -> true
        2 ->
            (this.parameterList.parameters[0].isInputStreamParameter() && this.parameterList.parameters[1].isOutputStreamParameter()) ||
                this.parameterList.parameters[1].isContextParameter()
        3 ->
            this.parameterList.parameters[0].isInputStreamParameter() &&
                this.parameterList.parameters[1].isOutputStreamParameter() &&
                this.parameterList.parameters[2].isContextParameter()
        else -> false
    }

    private fun PsiParameter.isContextParameter(): Boolean = isClass(LAMBDA_CONTEXT)
    private fun PsiParameter.isInputStreamParameter(): Boolean = isClass(INPUT_STREAM)
    private fun PsiParameter.isOutputStreamParameter(): Boolean = isClass(OUTPUT_STREAM)

    private fun PsiParameter.isClass(classFullName: String): Boolean =
        PsiType.getTypeByName(
            classFullName,
            project,
            GlobalSearchScope.allScope(project)
        ).isAssignableFrom(this.type)

    private companion object {
        val LAMBDA_INTERFACES = setOf(
            "com.amazonaws.services.lambda.runtime.RequestStreamHandler",
            "com.amazonaws.services.lambda.runtime.RequestHandler"
        )
        const val LAMBDA_CONTEXT = "com.amazonaws.services.lambda.runtime.Context"
        const val INPUT_STREAM = "java.io.InputStream"
        const val OUTPUT_STREAM = "java.io.OutputStream"
        const val HANDLER_NAME = "handleRequest"
    }
}


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

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

import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.module.JavaModuleType
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.projectRoots.JavaSdkType
import com.intellij.openapi.projectRoots.JavaSdkVersion
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkType
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.BuiltInRuntimeGroups
import software.aws.toolkits.jetbrains.services.lambda.SdkBasedRuntimeGroup

class JavaRuntimeGroup : SdkBasedRuntimeGroup() {
    override val id: String = BuiltInRuntimeGroups.Java
    override val languageIds = setOf(JavaLanguage.INSTANCE.id)
    override val supportsPathMappings: Boolean = false

    override val supportedRuntimes: List<LambdaRuntime> = listOf(
        LambdaRuntime.JAVA8,
        LambdaRuntime.JAVA8_AL2,
        LambdaRuntime.JAVA11,
        LambdaRuntime.JAVA17
    )

    override fun runtimeForSdk(sdk: Sdk): LambdaRuntime? {
        if (sdk.sdkType is JavaSdkType) {
            val javaSdkVersion = JavaSdk.getInstance().getVersion(sdk) ?: return null
            return determineRuntimeForSdk(javaSdkVersion)
        }
        return null
    }

    private fun determineRuntimeForSdk(sdk: JavaSdkVersion) = when {
        // TODO: is this actually the right logic?
        sdk <= JavaSdkVersion.JDK_1_8 -> LambdaRuntime.JAVA8_AL2
        sdk <= JavaSdkVersion.JDK_11 -> LambdaRuntime.JAVA11
        sdk <= JavaSdkVersion.JDK_17 -> LambdaRuntime.JAVA17
        else -> null
    }

    override fun getModuleType(): ModuleType<*> = JavaModuleType.getModuleType()

    override fun getIdeSdkType(): SdkType = JavaSdk.getInstance()
}


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

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

import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.idea.maven.project.MavenProjectsManager
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.logWhenNull
import software.aws.toolkits.jetbrains.services.lambda.BuiltInRuntimeGroups
import software.aws.toolkits.jetbrains.services.lambda.wizard.IntelliJSdkSelectionPanel
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamAppTemplateBased
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamNewProjectSettings
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectTemplate
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectWizard
import software.aws.toolkits.jetbrains.services.lambda.wizard.SdkSelector
import software.aws.toolkits.resources.message

class JavaSamProjectWizard : SamProjectWizard {
    override fun createSdkSelectionPanel(projectLocation: TextFieldWithBrowseButton?): SdkSelector = IntelliJSdkSelectionPanel(BuiltInRuntimeGroups.Java)

    override fun listTemplates(): Collection<SamProjectTemplate> = listOf(
        SamHelloWorldGradle(),
        SamHelloWorldMaven(),
        SamEventBridgeHelloWorldGradle(),
        SamEventBridgeHelloWorldMaven(),
        SamEventBridgeStarterAppGradle(),
        SamEventBridgeStarterAppMaven()
    )
}

abstract class JavaSamProjectTemplate : SamAppTemplateBased() {
    override fun supportedZipRuntimes() = setOf(LambdaRuntime.JAVA8, LambdaRuntime.JAVA8_AL2, LambdaRuntime.JAVA11, LambdaRuntime.JAVA17)
    override fun supportedImageRuntimes() = setOf(LambdaRuntime.JAVA8, LambdaRuntime.JAVA8_AL2, LambdaRuntime.JAVA11, LambdaRuntime.JAVA17)

    // Helper method to locate the build file, such as pom.xml in the project content root.
    protected fun locateBuildFile(contentRoot: VirtualFile, buildFileName: String): VirtualFile? {
        val contentRootFile = VfsUtil.virtualToIoFile(contentRoot)
        val baseSearchPath = contentRootFile.absolutePath

        val buildFile = LOG.logWhenNull("Failed to locate $buildFileName under $baseSearchPath") {
            FileUtil.fileTraverser(contentRootFile).bfsTraversal().first { it.name == buildFileName }
        }

        return buildFile?.let {
            LOG.logWhenNull("Failed to convert $it to VirtualFile") {
                LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it)
            }
        }
    }

    private companion object {
        val LOG = getLogger<SamHelloWorldMaven>()
    }
}

abstract class JavaMavenSamProjectTemplate : JavaSamProjectTemplate() {
    override val dependencyManager: String = "maven"

    override fun postCreationAction(
        settings: SamNewProjectSettings,
        contentRoot: VirtualFile,
        rootModel: ModifiableRootModel,
        indicator: ProgressIndicator
    ) {
        super.postCreationAction(settings, contentRoot, rootModel, indicator)
        val pomFile = locateBuildFile(contentRoot, "pom.xml") ?: return
        val projectsManager = MavenProjectsManager.getInstance(rootModel.project)
        runInEdt {
            projectsManager.addManagedFilesOrUnignore(listOf(pomFile))
        }
    }
}

abstract class JavaGradleSamProjectTemplate : JavaSamProjectTemplate() {
    override val dependencyManager: String = "gradle"

    override fun postCreationAction(
        settings: SamNewProjectSettings,
        contentRoot: VirtualFile,
        rootModel: ModifiableRootModel,
        indicator: ProgressIndicator
    ) {
        val buildFile = locateBuildFile(contentRoot, "build.gradle") ?: return

        val gradleProjectSettings = GradleProjectSettings().apply {
            withQualifiedModuleNames()
            externalProjectPath = buildFile.parent.path
        }

        val externalSystemSettings = ExternalSystemApiUtil.getSettings(rootModel.project, GradleConstants.SYSTEM_ID)
        externalSystemSettings.setLinkedProjectsSettings(setOf(gradleProjectSettings))

        super.postCreationAction(settings, contentRoot, rootModel, indicator)
    }
}

class SamHelloWorldMaven : JavaMavenSamProjectTemplate() {
    override fun displayName() = message("sam.init.template.hello_world_maven.name")

    override fun description() = message("sam.init.template.hello_world.description")

    override val appTemplateName: String = "hello-world"
}

class SamHelloWorldGradle : JavaGradleSamProjectTemplate() {
    override fun displayName() = message("sam.init.template.hello_world_gradle.name")

    override fun description() = message("sam.init.template.hello_world.description")

    override val appTemplateName: String = "hello-world"
}

class SamEventBridgeStarterAppGradle : JavaGradleSamProjectTemplate() {
    override fun displayName() = message("sam.init.template.event_bridge_starter_app_gradle.name")

    override fun description() = message("sam.init.template.event_bridge_starter_app.description")

    override fun supportedImageRuntimes() = emptySet<LambdaRuntime>()

    override val appTemplateName: String = "eventBridge-schema-app"

    override fun supportsDynamicSchemas(): Boolean = true
}

class SamEventBridgeStarterAppMaven : JavaMavenSamProjectTemplate() {
    override fun displayName() = message("sam.init.template.event_bridge_starter_app_maven.name")

    override fun description() = message("sam.init.template.event_bridge_starter_app.description")

    override fun supportedImageRuntimes() = emptySet<LambdaRuntime>()

    override val appTemplateName: String = "eventBridge-schema-app"

    override fun supportsDynamicSchemas(): Boolean = true
}

class SamEventBridgeHelloWorldGradle : JavaGradleSamProjectTemplate() {
    override fun displayName() = message("sam.init.template.event_bridge_hello_world_gradle.name")

    override fun description() = message("sam.init.template.event_bridge_hello_world.description")

    override fun supportedImageRuntimes() = emptySet<LambdaRuntime>()

    override val appTemplateName: String = "eventBridge-hello-world"
}

class SamEventBridgeHelloWorldMaven : JavaMavenSamProjectTemplate() {
    override fun displayName() = message("sam.init.template.event_bridge_hello_world_maven.name")

    override fun description() = message("sam.init.template.event_bridge_hello_world.description")

    override fun supportedImageRuntimes() = emptySet<LambdaRuntime>()

    override val appTemplateName: String = "eventBridge-hello-world"
}


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

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

import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.DocumentAdapter
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.newProject.steps.PyAddExistingSdkPanel
import com.jetbrains.python.newProject.steps.PyAddNewEnvironmentPanel
import com.jetbrains.python.sdk.PreferredSdkComparator
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.add.PyAddSdkGroupPanel
import software.aws.toolkits.jetbrains.services.lambda.wizard.SdkSelector
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.event.DocumentEvent

class PyCharmSdkSelectionPanel(private val projectLocation: TextFieldWithBrowseButton?) : SdkSelector {
    private val sdkPanel by lazy {
        sdkPanel()
    }

    override fun sdkSelectionPanel(): JComponent = sdkPanel

    override fun sdkSelectionLabel(): JLabel? = null

    private fun sdkPanel(): PyAddSdkGroupPanel {
        // Based on PyCharm's ProjectSpecificSettingsStep
        val existingSdks = getValidPythonSdks()
        val newProjectLocation = getProjectLocation()
        val newEnvironmentPanel = PyAddNewEnvironmentPanel(existingSdks, newProjectLocation, null)
        val existingSdkPanel = PyAddExistingSdkPanel(null, null, existingSdks, newProjectLocation, existingSdks.firstOrNull())

        val defaultPanel = if (PySdkSettings.instance.useNewEnvironmentForNewProject) newEnvironmentPanel else existingSdkPanel

        val interpreterPanel = createPythonSdkPanel(listOf(newEnvironmentPanel, existingSdkPanel), defaultPanel)

        projectLocation?.textField?.document?.addDocumentListener(
            object : DocumentAdapter() {
                override fun textChanged(e: DocumentEvent) {
                    interpreterPanel.newProjectPath = getProjectLocation()
                }
            }
        )

        return interpreterPanel
    }

    private fun getProjectLocation(): String? = projectLocation?.text?.trim()

    private fun getValidPythonSdks(): List<Sdk> = PyConfigurableInterpreterList.getInstance(null).allPythonSdks
        .asSequence()
        .filter { it.sdkType is PythonSdkType && !PythonSdkUtil.isInvalid(it) }
        .sortedWith(PreferredSdkComparator())
        .toList()

    override fun getSdk(): Sdk? {
        val sdk = sdkPanel.getOrCreateSdk() ?: return null
        if (sdkPanel.selectedPanel is PyAddNewEnvironmentPanel) {
            SdkConfigurationUtil.addSdk(sdk)
        }
        return sdk
    }

    override fun validateSelection(): ValidationInfo? = sdkPanel.validateAll().firstOrNull()
}


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

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

import com.intellij.util.ui.EmptyIcon
import com.jetbrains.python.sdk.add.PyAddSdkGroupPanel
import com.jetbrains.python.sdk.add.PyAddSdkPanel
import java.util.function.Supplier

fun createPythonSdkPanel(panels: List<PyAddSdkPanel>, defaultPanel: PyAddSdkPanel): PyAddSdkGroupPanel =
    // String and icon aren't used, they are just returned again in the icon and panelName val's
    PyAddSdkGroupPanel(Supplier { "Python SDK" }, EmptyIcon.ICON_16, panels, defaultPanel)


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

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

import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.xdebugger.DefaultDebugProcessHandler
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebugProcessStarter
import com.intellij.xdebugger.XDebugSession
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.PythonLanguage
import com.jetbrains.python.console.PyDebugConsoleBuilder
import com.jetbrains.python.console.PythonDebugLanguageConsoleView
import com.jetbrains.python.debugger.PyDebugProcess
import com.jetbrains.python.debugger.PyDebugRunner
import com.jetbrains.python.sdk.PythonSdkType
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.core.utils.buildList
import software.aws.toolkits.jetbrains.services.PathMapper
import software.aws.toolkits.jetbrains.services.PathMapping
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.ImageDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.RuntimeDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.SamRunningState
import software.aws.toolkits.jetbrains.utils.execution.steps.Context

class PythonRuntimeDebugSupport : RuntimeDebugSupport {
    override fun samArguments(debugPorts: List<Int>): List<String> = listOf(
        "--debugger-path",
        // Mount pydevd from PyCharm into docker
        PythonHelper.DEBUGGER.pythonPathEntry,
        "--debug-args",
        "-u $DEBUGGER_VOLUME_PATH/pydevd.py --multiprocess --port ${debugPorts.first()} --file"
    )

    override suspend fun createDebugProcess(
        context: Context,
        environment: ExecutionEnvironment,
        state: SamRunningState,
        debugHost: String,
        debugPorts: List<Int>
    ): XDebugProcessStarter = createDebugProcess(environment, state, debugHost, debugPorts)
}

abstract class PythonImageDebugSupport : ImageDebugSupport {
    override fun supportsPathMappings(): Boolean = true
    override val languageId = PythonLanguage.INSTANCE.id

    // Image debug settings are out of the SAM cli https://github.com/aws/aws-sam-cli/blob/develop/samcli/local/docker/lambda_debug_settings.py
    protected abstract val pythonPath: String
    protected abstract val bootstrapPath: String

    override suspend fun createDebugProcess(
        context: Context,
        environment: ExecutionEnvironment,
        state: SamRunningState,
        debugHost: String,
        debugPorts: List<Int>
    ): XDebugProcessStarter = createDebugProcess(environment, state, debugHost, debugPorts)

    override fun samArguments(debugPorts: List<Int>): List<String> = buildList {
        addAll(super.samArguments(debugPorts))

        val debugArgs = "$pythonPath -u $DEBUGGER_VOLUME_PATH/pydevd.py --multiprocess --port ${debugPorts.first()} --file $bootstrapPath"

        add("--debugger-path")
        // Mount pydevd from PyCharm into docker
        add(PythonHelper.DEBUGGER.pythonPathEntry)
        add("--debug-args")
        add(debugArgs)
    }
}

class Python37ImageDebugSupport : PythonImageDebugSupport() {
    override val id: String = LambdaRuntime.PYTHON3_7.toString()
    override fun displayName() = LambdaRuntime.PYTHON3_7.toString().capitalize()
    override val pythonPath: String = "/var/lang/bin/python3.7"
    override val bootstrapPath: String = "/var/runtime/bootstrap"
}

class Python38ImageDebugSupport : PythonImageDebugSupport() {
    override val id: String = LambdaRuntime.PYTHON3_8.toString()
    override fun displayName() = LambdaRuntime.PYTHON3_8.toString().capitalize()
    override val pythonPath: String = "/var/lang/bin/python3.8"
    override val bootstrapPath: String = "/var/runtime/bootstrap.py"
}

class Python39ImageDebugSupport : PythonImageDebugSupport() {
    override val id: String = LambdaRuntime.PYTHON3_9.toString()
    override fun displayName() = LambdaRuntime.PYTHON3_9.toString().capitalize()
    override val pythonPath: String = "/var/lang/bin/python3.9"
    override val bootstrapPath: String = "/var/runtime/bootstrap.py"
}

class Python310ImageDebugSupport : PythonImageDebugSupport() {
    override val id: String = LambdaRuntime.PYTHON3_10.toString()
    override fun displayName() = LambdaRuntime.PYTHON3_10.toString().capitalize()
    override val pythonPath: String = "/var/lang/bin/python3.10"
    override val bootstrapPath: String = "/var/runtime/bootstrap.py"
}

private const val DEBUGGER_VOLUME_PATH = "/tmp/lambci_debug_files"

private fun createDebugProcess(
    environment: ExecutionEnvironment,
    state: SamRunningState,
    debugHost: String,
    debugPorts: List<Int>
): XDebugProcessStarter {
    // TODO: We should allow using the module SDK, but we can't easily get the module
    val sdk = ProjectRootManager.getInstance(environment.project).projectSdk?.takeIf { it.sdkType is PythonSdkType }

    return object : XDebugProcessStarter() {
        override fun start(session: XDebugSession): XDebugProcess {
            val mappings = state.pathMappings.plus(
                listOf(
                    PathMapping(
                        PythonHelper.DEBUGGER.pythonPathEntry,
                        DEBUGGER_VOLUME_PATH
                    )
                )
            )

            val console = PyDebugConsoleBuilder(environment.project, sdk).console as PythonDebugLanguageConsoleView

            val handler = DefaultDebugProcessHandler()
            return PyDebugProcess(
                session,
                console,
                handler,
                debugHost,
                debugPorts.first()
            ).also {
                it.positionConverter = PathMapper.PositionConverter(PathMapper(mappings))
                console.attachToProcess(handler)
                PyDebugRunner.initDebugConsoleView(environment.project, it, console, it.processHandler, session)
            }
        }
    }
}


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

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

import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import software.aws.toolkits.jetbrains.services.lambda.LambdaBuilder
import java.nio.file.Path
import java.nio.file.Paths

class PythonLambdaBuilder : LambdaBuilder() {
    override fun handlerBaseDirectory(module: Module, handlerElement: PsiElement): Path {
        val handlerVirtualFile = ReadAction.compute<VirtualFile, Throwable> {
            handlerElement.containingFile?.virtualFile
                ?: throw IllegalArgumentException("Handler file must be backed by a VirtualFile")
        }

        return Paths.get(locateRequirementsTxt(handlerVirtualFile).parent.path)
    }

    companion object {
        fun locateRequirementsTxt(startLocation: VirtualFile): VirtualFile = runReadAction {
            var dir = if (startLocation.isDirectory) startLocation else startLocation.parent
            while (dir != null) {
                val requirementsFile = dir.findChild("requirements.txt")
                if (requirementsFile != null && requirementsFile.isValid) {
                    return@runReadAction requirementsFile
                }
                dir = dir.parent
            }

            throw IllegalStateException("Cannot locate requirements.txt in a parent directory of ${startLocation.path}")
        }
    }
}


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

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

import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.TestSourcesFilter
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.NavigatablePsiElement
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.text.nullize
import com.jetbrains.python.PyTokenTypes
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.psi.PyFunction
import com.jetbrains.python.psi.stubs.PyModuleNameIndex
import software.aws.toolkits.jetbrains.services.lambda.LambdaHandlerResolver

class PythonLambdaHandlerResolver : LambdaHandlerResolver {
    override fun determineHandlers(element: PsiElement, file: VirtualFile): Set<String> =
        determineHandler(element)?.let { setOf(it) }.orEmpty()

    override fun findPsiElements(
        project: Project,
        handler: String,
        searchScope: GlobalSearchScope
    ): Array<NavigatablePsiElement> {
        if (!handler.contains('.')) {
            return NavigatablePsiElement.EMPTY_NAVIGATABLE_ELEMENT_ARRAY
        }
        // [par/rent/fold/ers/][module.folders.]moduleFile.functionName
        val functionName = handler.substringAfterLast('.')
        val parentFolders = handler.substringBeforeLast('/', "").nullize(true)?.split("/") ?: emptyList()

        val fullyQualifiedModule = handler.substringAfterLast('/').substringBeforeLast('.').split('.')
        val moduleFile = fullyQualifiedModule.last()
        val moduleFolders = fullyQualifiedModule.take(fullyQualifiedModule.size - 1)

        // Find the module by the name
        PyModuleNameIndex.find(moduleFile, project, false).forEach { pyModule ->
            val lambdaFunctionCandidate = pyModule.findTopLevelFunction(functionName) ?: return@forEach
            if (validateHandlerPath(pyModule, moduleFolders, parentFolders)) {
                return arrayOf(lambdaFunctionCandidate)
            }
        }

        return NavigatablePsiElement.EMPTY_NAVIGATABLE_ELEMENT_ARRAY
    }

    private fun validateHandlerPath(
        pyModule: PyFile,
        parentModuleFolders: List<String>,
        parentFolders: List<String>
    ): Boolean {
        // Start matching to see if the parent folders align
        var directory = pyModule.containingDirectory
        val hasRequirementsTxt = { directory.virtualFile.findChild("requirements.txt") != null }

        // Go from the deepest back up until we find a requirements.txt
        // SAM CLI will strictly use requirements.txt in the directory defined by CodeUri, but we don't have that data here
        parentModuleFolders.reversed().forEach { parentModule ->
            if (parentModule != directory?.name || !directoryHasInitPy(directory)) {
                return false
            }

            if (hasRequirementsTxt()) {
                return true
            }

            directory = directory.parentDirectory
        }

        parentFolders.reversed().forEach { folder ->
            if (folder != directory?.name) {
                return false
            }

            if (hasRequirementsTxt()) {
                return true
            }

            directory = directory.parentDirectory
        }

        if (hasRequirementsTxt()) {
            return true
        } else {
            throw IllegalStateException("Failed to locate requirements.txt")
        }
    }

    private fun directoryHasInitPy(psiDirectory: PsiDirectory) = psiDirectory.findFile("__init__.py") != null

    override fun determineHandler(element: PsiElement): String? {
        if (element.node?.elementType != PyTokenTypes.IDENTIFIER) {
            return null
        }
        val project = element.project
        val function = element.parent as? PyFunction ?: return null
        val virtualFile = element.containingFile.virtualFile ?: return null

        if (function.parent is PyFile &&
            function.parameterList.parameters.size == 2 &&
            // Ignore files that are considered test sources. Ignore the IDE warning, it uses IDE extension points.
            !TestSourcesFilter.isTestSources(virtualFile, project) &&
            // ignore pytest tests: they start with test_ by convention:
            // https://pytest.readthedocs.io/en/reorganize-docs/new-docs/user/naming_conventions.html#id1
            function.name?.startsWith("test_") != true
        ) {
            val requirementsFileDir = runCatching { PythonLambdaBuilder.locateRequirementsTxt(virtualFile).parent }.getOrNull() ?: return null
            val filePath = if (virtualFile.parent != requirementsFileDir) {
                VfsUtil.getRelativePath(virtualFile.parent, requirementsFileDir)?.let { "$it/" } ?: return null
            } else {
                ""
            }
            return "$filePath${virtualFile.nameWithoutExtension}.${function.name}"
        }
        return 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.python

import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkType
import com.jetbrains.python.PythonLanguage
import com.jetbrains.python.PythonModuleTypeBase
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.PythonSdkType
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.BuiltInRuntimeGroups
import software.aws.toolkits.jetbrains.services.lambda.SdkBasedRuntimeGroup

class PythonRuntimeGroup : SdkBasedRuntimeGroup() {
    override val id: String = BuiltInRuntimeGroups.Python
    override val languageIds: Set<String> = setOf(PythonLanguage.INSTANCE.id)
    override val supportsPathMappings: Boolean = true

    override val supportedRuntimes = listOf(
        LambdaRuntime.PYTHON3_7,
        LambdaRuntime.PYTHON3_8,
        LambdaRuntime.PYTHON3_9,
        LambdaRuntime.PYTHON3_10
    )

    override fun runtimeForSdk(sdk: Sdk): LambdaRuntime? = when {
        sdk.sdkType is PythonSdkType && PythonSdkType.getLanguageLevelForSdk(sdk).isAtLeast(LanguageLevel.PYTHON310) -> LambdaRuntime.PYTHON3_10
        sdk.sdkType is PythonSdkType && PythonSdkType.getLanguageLevelForSdk(sdk).isAtLeast(LanguageLevel.PYTHON39) -> LambdaRuntime.PYTHON3_9
        sdk.sdkType is PythonSdkType && PythonSdkType.getLanguageLevelForSdk(sdk).isAtLeast(LanguageLevel.PYTHON38) -> LambdaRuntime.PYTHON3_8
        sdk.sdkType is PythonSdkType && PythonSdkType.getLanguageLevelForSdk(sdk).isAtLeast(LanguageLevel.PYTHON37) -> LambdaRuntime.PYTHON3_7
        else -> null
    }

    override fun getModuleType(): ModuleType<*> = PythonModuleTypeBase.getInstance()

    override fun getIdeSdkType(): SdkType = PythonSdkType.getInstance()
}


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

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

import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.PlatformUtils
import software.amazon.awssdk.services.lambda.model.PackageType
import software.aws.toolkits.core.lambda.LambdaArchitecture
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.BuiltInRuntimeGroups
import software.aws.toolkits.jetbrains.services.lambda.wizard.IntelliJSdkSelectionPanel
import software.aws.toolkits.jetbrains.services.lambda.wizard.LocationBasedTemplate
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamAppTemplateBased
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamNewProjectSettings
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectTemplate
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectWizard
import software.aws.toolkits.jetbrains.services.lambda.wizard.SdkSelector
import software.aws.toolkits.jetbrains.services.lambda.wizard.TemplateParameters
import software.aws.toolkits.resources.message

private val pythonTemplateRuntimes = setOf(LambdaRuntime.PYTHON3_7, LambdaRuntime.PYTHON3_8, LambdaRuntime.PYTHON3_9, LambdaRuntime.PYTHON3_10)
private val eventBridgeTemplateRuntimes = setOf(LambdaRuntime.PYTHON3_7, LambdaRuntime.PYTHON3_8, LambdaRuntime.PYTHON3_9, LambdaRuntime.PYTHON3_10)

class PythonSamProjectWizard : SamProjectWizard {
    override fun createSdkSelectionPanel(projectLocation: TextFieldWithBrowseButton?): SdkSelector = when {
        PlatformUtils.isIntelliJ() -> IntelliJSdkSelectionPanel(BuiltInRuntimeGroups.Python)
        else -> PyCharmSdkSelectionPanel(projectLocation)
    }

    override fun listTemplates(): Collection<SamProjectTemplate> = listOf(
        SamHelloWorldPython(),
        SamDynamoDBCookieCutter(),
        SamEventBridgeHelloWorld(),
        SamEventBridgeStarterApp()
    )
}

abstract class PythonSamProjectTemplate : SamAppTemplateBased() {
    override fun supportedZipRuntimes() = pythonTemplateRuntimes
    override fun supportedImageRuntimes() = pythonTemplateRuntimes

    override val dependencyManager: String = "pip"

    override fun postCreationAction(
        settings: SamNewProjectSettings,
        contentRoot: VirtualFile,
        rootModel: ModifiableRootModel,
        indicator: ProgressIndicator
    ) {
        super.postCreationAction(settings, contentRoot, rootModel, indicator)
        addSourceRoots(rootModel.project, rootModel, contentRoot)
    }
}

class SamHelloWorldPython : PythonSamProjectTemplate() {
    override fun displayName() = message("sam.init.template.hello_world.name")

    override fun description() = message("sam.init.template.hello_world.description")

    override val appTemplateName: String = "hello-world"
}

class SamDynamoDBCookieCutter : SamProjectTemplate() {
    override fun displayName() = message("sam.init.template.dynamodb_cookiecutter.name")

    override fun description() = message("sam.init.template.dynamodb_cookiecutter.description")

    override fun supportedZipRuntimes() = pythonTemplateRuntimes
    override fun supportedImageRuntimes() = emptySet<LambdaRuntime>()

    override fun postCreationAction(
        settings: SamNewProjectSettings,
        contentRoot: VirtualFile,
        rootModel: ModifiableRootModel,
        indicator: ProgressIndicator
    ) {
        super.postCreationAction(settings, contentRoot, rootModel, indicator)
        addSourceRoots(rootModel.project, rootModel, contentRoot)
    }

    override fun templateParameters(
        projectName: String,
        runtime: LambdaRuntime,
        architecture: LambdaArchitecture,
        packagingType: PackageType
    ): TemplateParameters = LocationBasedTemplate(
        "gh:aws-samples/cookiecutter-aws-sam-dynamodb-python"
    )
}

class SamEventBridgeHelloWorld : PythonSamProjectTemplate() {
    override fun supportedZipRuntimes() = eventBridgeTemplateRuntimes
    override fun supportedImageRuntimes() = emptySet<LambdaRuntime>()

    override fun displayName() = message("sam.init.template.event_bridge_hello_world.name")

    override fun description() = message("sam.init.template.event_bridge_hello_world.description")

    override val appTemplateName: String = "eventBridge-hello-world"
}

class SamEventBridgeStarterApp : PythonSamProjectTemplate() {
    override fun supportedZipRuntimes() = eventBridgeTemplateRuntimes
    override fun supportedImageRuntimes() = emptySet<LambdaRuntime>()

    override fun displayName() = message("sam.init.template.event_bridge_starter_app.name")

    override fun description() = message("sam.init.template.event_bridge_starter_app.description")

    override val appTemplateName: String = "eventBridge-schema-app"

    override fun supportsDynamicSchemas(): Boolean = true
}


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

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

import software.amazon.awssdk.services.lambda.LambdaClient
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.find

object LambdaResources {
    @JvmField
    val LIST_FUNCTIONS = ClientBackedCachedResource(LambdaClient::class, "lambda.list_functions") {
        listFunctionsPaginator().functions().filterNotNull().toList()
    }

    fun function(name: String) = LIST_FUNCTIONS.find { it.functionName() == name }
}