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

package software.aws.toolkits.jetbrains.services.ecs.exec

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.ec2.Ec2Client
import software.amazon.awssdk.services.ecs.EcsClient
import software.amazon.awssdk.services.ecs.model.DeploymentRolloutState
import software.amazon.awssdk.services.ecs.model.DescribeServicesRequest
import software.amazon.awssdk.services.ecs.model.InvalidParameterException
import software.amazon.awssdk.services.ecs.model.LaunchType
import software.amazon.awssdk.services.ecs.model.Service
import software.amazon.awssdk.services.iam.IamClient
import software.amazon.awssdk.services.iam.model.PolicyEvaluationDecisionType
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.toEnvironmentVariables
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.getConnectionSettingsOrThrow
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.core.getResourceNow
import software.aws.toolkits.jetbrains.core.tools.getOrInstallTool
import software.aws.toolkits.jetbrains.services.ecs.ContainerDetails
import software.aws.toolkits.jetbrains.services.ecs.resources.EcsResources
import software.aws.toolkits.jetbrains.services.ssm.SsmPlugin
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.jetbrains.utils.notifyWarn
import software.aws.toolkits.jetbrains.utils.runUnderProgressIfNeeded
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.EcsTelemetry
import software.aws.toolkits.telemetry.Result
import software.amazon.awssdk.services.ecs.model.Task as EcsTask

object EcsExecUtils {
    private val MAPPER = jacksonObjectMapper()
    private const val SESSION_MANAGER_CREATE_CONTROL_CHANNEL_PERMISSION = "ssmmessages:CreateControlChannel"
    private const val SESSION_MANAGER_CREATE_DATA_CHANNEL_PERMISSION = "ssmmessages:CreateDataChannel"
    private const val SESSION_MANAGER_OPEN_CONTROL_CHANNEL_PERMISSION = "ssmmessages:OpenControlChannel"
    private const val SESSION_MANAGER_OPEN_DATA_CHANNEL_PERMISSION = "ssmmessages:OpenDataChannel"

    fun updateExecuteCommandFlag(project: Project, service: Service, enabled: Boolean) {
        if (ensureServiceIsInStableState(project, service)) {
            try {
                project.awsClient<EcsClient>().updateService {
                    it.cluster(service.clusterArn())
                    it.service(service.serviceName())
                    it.enableExecuteCommand(enabled)
                    it.forceNewDeployment(true)
                }
                checkServiceState(project, service, enabled)
            } catch (e: InvalidParameterException) {
                runInEdt {
                    TaskRoleNotFoundWarningDialog(project).show()
                    EcsTelemetry.enableExecuteCommand(project, Result.Failed)
                }
            }
        } else {
            if (enabled) {
                notifyWarn(
                    title = message("ecs.execute_command_enable"),
                    content = message("ecs.execute_command_enable_in_progress", service.serviceName()),
                    project = project
                )
            } else {
                notifyWarn(
                    title = message("ecs.execute_command_disable"),
                    content = message("ecs.execute_command_disable_in_progress", service.serviceName()),
                    project = project
                )
            }
        }
    }

    private fun checkServiceState(project: Project, service: Service, enable: Boolean) {
        val title = if (enable) {
            message("ecs.execute_command_enable_progress_indicator_message", service.serviceName())
        } else {
            message("ecs.execute_command_disable_progress_indicator_message", service.serviceName())
        }

        ProgressManager.getInstance().run(
            object : Task.Backgroundable(project, title, false) {
                override fun run(indicator: ProgressIndicator) {
                    val request = DescribeServicesRequest.builder().cluster(service.clusterArn()).services(service.serviceArn()).build()
                    val client = project.awsClient<EcsClient>()
                    val waiter = client.waiter()
                    waiter.waitUntilServicesStable(request)
                }

                override fun onSuccess() {
                    val currentConnectionSettings = project.getConnectionSettingsOrThrow()
                    project.refreshAwsTree(EcsResources.describeService(service.clusterArn(), service.serviceArn()), currentConnectionSettings)

                    if (enable) {
                        notifyInfo(
                            title = message("ecs.execute_command_enable"),
                            content = message("ecs.execute_command_enable_success", service.serviceName()),
                            project = project
                        )
                        EcsTelemetry.enableExecuteCommand(project, Result.Succeeded)
                    } else {
                        notifyInfo(
                            title = message("ecs.execute_command_disable"),
                            content = message("ecs.execute_command_disable_success", service.serviceName()),
                            project = project
                        )
                        EcsTelemetry.disableExecuteCommand(project, Result.Succeeded)
                    }
                }

                override fun onThrowable(error: Throwable) {
                    if (enable) {
                        notifyError(
                            title = message("ecs.execute_command_enable"),
                            content = message("ecs.execute_command_enable_failed", service.serviceName()),
                            project = project
                        )
                        EcsTelemetry.enableExecuteCommand(project, Result.Failed)
                    } else {
                        notifyError(
                            title = message("ecs.execute_command_disable"),
                            content = message("ecs.execute_command_disable_failed", service.serviceName()),
                            project = project
                        )
                        EcsTelemetry.disableExecuteCommand(project, Result.Failed)
                    }
                }
            }
        )
    }

    private fun getEc2InstanceTaskRoleArn(project: Project, clusterArn: String, ecsClient: EcsClient, task: EcsTask): String? {
        try {
            val iamClient = project.awsClient<IamClient>()
            val containerInstanceArn = task.containerInstanceArn()
            val res = ecsClient.describeContainerInstances {
                it.cluster(clusterArn)
                it.containerInstances(containerInstanceArn)
            }
            val ec2InstanceId = res.containerInstances().first().ec2InstanceId()
            val instanceProfileArn = project.awsClient<Ec2Client>().describeInstances {
                it.instanceIds(ec2InstanceId)
            }.reservations().first().instances().first().iamInstanceProfile().arn() ?: return null
            val instanceProfileName = instanceProfileArn.substringAfter(":instance-profile/")
            return iamClient.getInstanceProfile { it.instanceProfileName(instanceProfileName) }.instanceProfile().roles().first().arn() ?: null
        } catch (e: Exception) {
            return null
        }
    }

    fun getTaskRoleArn(project: Project, clusterArn: String, taskArn: String): String? {
        val ecsClient = project.awsClient<EcsClient>()
        val task = ecsClient.describeTasks {
            it.tasks(taskArn)
            it.cluster(clusterArn)
        }.tasks().first()
        return if (task.overrides().taskRoleArn() != null) {
            task.overrides().taskRoleArn()
        } else {
            project.getResourceNow(EcsResources.describeTaskDefinition(task.taskDefinitionArn())).taskRoleArn()
                ?: when (task.launchType()) {
                    LaunchType.EC2 -> getEc2InstanceTaskRoleArn(project, clusterArn, ecsClient, task)
                    LaunchType.FARGATE -> null
                    else -> throw RuntimeException("Launch Type is not supported")
                }
        }
    }

    fun checkRequiredPermissions(project: Project, clusterArn: String, taskArn: String): Boolean {
        try {
            val iamClient = project.awsClient<IamClient>()
            val taskRoleArn = getTaskRoleArn(project, clusterArn, taskArn) ?: return false

            val permissions = listOf(
                SESSION_MANAGER_CREATE_CONTROL_CHANNEL_PERMISSION,
                SESSION_MANAGER_CREATE_DATA_CHANNEL_PERMISSION,
                SESSION_MANAGER_OPEN_CONTROL_CHANNEL_PERMISSION,
                SESSION_MANAGER_OPEN_DATA_CHANNEL_PERMISSION
            )
            val response = iamClient.simulatePrincipalPolicy {
                it.policySourceArn(taskRoleArn)
                it.actionNames(permissions)
            }

            val permissionResults = response.evaluationResults().map { it.evalDecision().name }
            for (permission in permissionResults) {
                if (permission != PolicyEvaluationDecisionType.ALLOWED.name) {
                    return false
                }
            }
        } catch (e: Exception) {
            notifyWarn(
                title = message("ecs.execute_command_permissions_required_title"),
                content = message("ecs.execute_command_permissions_not_verified"),
                project = project
            )
        }
        return true
    }

    fun ensureServiceIsInStableState(project: Project, service: Service): Boolean {
        val response = project.awsClient<EcsClient>().describeServices {
            it.cluster(service.clusterArn())
            it.services(service.serviceArn())
        }
        val deployment = response.services().first().deployments().first()
        val serviceStateChangeInProgress = deployment.rolloutState() == DeploymentRolloutState.IN_PROGRESS || deployment.status() == "ACTIVE"
        return !serviceStateChangeInProgress
    }

    /**
     * Start a session with ECS (calling ECS execute-command) and then pass the resulting
     * session information (along with some other pieces) to the SSM Session Manager Plugin.
     *
     * This replicates logic from the AWS CLI:
     * https://github.com/aws/aws-cli/blob/63f3fcf368805d14848769feae4bbf87cc359739/awscli/customizations/ecs/executecommand.py
     */
    fun createCommand(
        project: Project,
        connection: ConnectionSettings,
        container: ContainerDetails,
        task: String,
        command: String
    ): GeneralCommandLine {
        val client = connection.awsClient<EcsClient>()
        val path = SsmPlugin.getOrInstallTool(project).path.toAbsolutePath().toString()

        val session = runUnderProgressIfNeeded(project, message("ecs.execute_command_call_service"), cancelable = false) {
            client.executeCommand {
                it.cluster(container.service.clusterArn())
                it.task(task)
                it.container(container.containerDefinition.name())
                it.interactive(true)
                it.command(command)
            }
        }

        return GeneralCommandLine()
            .withExePath(path)
            .withParameters(
                MAPPER.writeValueAsString(session.session().toBuilder()),
                connection.region.id,
                "StartSession"
            )
            .withEnvironment(connection.toEnvironmentVariables())
    }
}


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

package software.aws.toolkits.jetbrains.services.ecs.exec

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.ui.components.JBLabel
import com.intellij.ui.layout.panel
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.settings.EcsExecCommandSettings
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.EcsTelemetry
import software.aws.toolkits.telemetry.Result
import javax.swing.JComponent

class EnableDisableExecuteCommandWarning(private val project: Project, private val enable: Boolean, private val serviceName: String) : DialogWrapper(project) {
    private val warningIcon = JBLabel(Messages.getWarningIcon())
    var dontDisplayWarning = false
    var confirmNonProduction = false
    private val settings = EcsExecCommandSettings.getInstance()
    private val component by lazy {
        panel {
            row {
                warningIcon(grow)
                label(message("ecs.execute_command_enable_warning")).constraints(grow).visible(enable)
                label(message("ecs.execute_command_disable_warning")).constraints(grow).visible(!enable)
            }

            row {
                checkBox(
                    message("ecs.execute_command.production_warning.checkbox_label", serviceName),
                    { confirmNonProduction },
                    { confirmNonProduction = it }
                ).withErrorOnApplyIf(message("general.confirm_proceed")) { !it.isSelected }
                    .constraints(grow)
            }

            row {
                checkBox(message("general.notification.action.hide_forever"), { dontDisplayWarning }, { dontDisplayWarning = it })
            }
        }
    }

    init {
        super.init()
        title = if (enable) {
            message("ecs.execute_command_enable_warning_title")
        } else {
            message("ecs.execute_command_disable_warning_title")
        }
    }

    override fun doOKAction() {
        super.doOKAction()
        if (dontDisplayWarning) {
            settings.showExecuteCommandWarning = false
        }
    }

    override fun doCancelAction() {
        super.doCancelAction()
        if (enable) {
            EcsTelemetry.enableExecuteCommand(project, Result.Cancelled)
        } else {
            EcsTelemetry.disableExecuteCommand(project, Result.Cancelled)
        }
    }

    override fun createCenterPanel(): JComponent = component

    override fun getHelpId(): String = HelpIds.ECS_EXEC_PERMISSIONS_REQUIRED.id
}


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

package software.aws.toolkits.jetbrains.services.ecs.exec

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.Project
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.ecs.model.Service
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.experiments.isEnabled
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.ecs.EcsExecExperiment
import software.aws.toolkits.jetbrains.services.ecs.EcsServiceNode
import software.aws.toolkits.jetbrains.settings.EcsExecCommandSettings
import software.aws.toolkits.resources.message

class EnableEcsExecuteCommand :
    SingleResourceNodeAction<EcsServiceNode>(message("ecs.execute_command_enable"), null) {
    private val settings = EcsExecCommandSettings.getInstance()

    override fun actionPerformed(selected: EcsServiceNode, e: AnActionEvent) {
        if (!settings.showExecuteCommandWarning ||
            EnableDisableExecuteCommandWarning(selected.nodeProject, enable = true, selected.value.serviceName()).showAndGet()
        ) {
            val coroutineScope = projectCoroutineScope(selected.nodeProject)
            coroutineScope.launch {
                enableExecuteCommand(selected.nodeProject, selected.value)
            }
        }
    }

    override fun update(selected: EcsServiceNode, e: AnActionEvent) {
        e.presentation.isVisible = !selected.executeCommandEnabled() && EcsExecExperiment.isEnabled()
    }

    private fun enableExecuteCommand(project: Project, service: Service) {
        EcsExecUtils.updateExecuteCommandFlag(project, service, enabled = true)
    }
}


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

package software.aws.toolkits.jetbrains.services.ecs.exec

import com.intellij.execution.configurations.PtyCommandLine
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.layout.GrowPolicy
import com.intellij.ui.layout.applyToComponent
import com.intellij.ui.layout.panel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.plugins.terminal.TerminalTabState
import org.jetbrains.plugins.terminal.TerminalView
import org.jetbrains.plugins.terminal.cloud.CloudTerminalProcess
import org.jetbrains.plugins.terminal.cloud.CloudTerminalRunner
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.ecs.ContainerDetails
import software.aws.toolkits.jetbrains.services.ecs.resources.EcsResources
import software.aws.toolkits.jetbrains.ui.resourceSelector
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.EcsExecuteCommandType
import software.aws.toolkits.telemetry.EcsTelemetry
import software.aws.toolkits.telemetry.Result
import javax.swing.JComponent

class OpenShellInContainerDialog(
    private val project: Project,
    private val container: ContainerDetails,
    private val connectionSettings: ConnectionSettings
) : DialogWrapper(project) {
    private val coroutineScope = projectCoroutineScope(project)
    private val shellList = listOf("/bin/bash", "/bin/sh", "/bin/zsh")
    private val shellOption = CollectionComboBoxModel(shellList)

    var task: String? = null
    var shell: String = ""

    init {
        super.init()
        title = message("ecs.execute_command_run_command_in_shell")
        setOKButtonText(message("general.execute_button"))
    }

    override fun createCenterPanel(): JComponent = panel {
        row(message("ecs.execute_command_task.label")) {
            resourceSelector(
                EcsResources.listTasks(container.service.clusterArn(), container.service.serviceArn()),
                connectionSettings,
                ::task
            ).constraints(growX, pushX)
                .growPolicy(GrowPolicy.MEDIUM_TEXT)
                .withErrorOnApplyIf(message("ecs.execute_command_task_comboBox_empty")) { it.selected().isNullOrEmpty() }
        }
        row(message("ecs.execute_command_shell.label")) {
            comboBox(shellOption, ::shell).constraints(grow)
                .withErrorOnApplyIf(message("ecs.execute_command_shell_comboBox_empty")) { it.editor.item.toString().isBlank() }
                .applyToComponent { isEditable = true }
        }
    }

    override fun doOKAction() {
        super.doOKAction()
        val task = task ?: throw IllegalStateException("Task not Selected")
        coroutineScope.launch {
            val taskRoleFound: Boolean = EcsExecUtils.checkRequiredPermissions(project, container.service.clusterArn(), task)
            if (taskRoleFound) {
                runExecCommand(task)
            } else {
                withContext(getCoroutineUiContext()) {
                    TaskRoleNotFoundWarningDialog(project).show()
                }
            }
        }
    }

    override fun doCancelAction() {
        super.doCancelAction()
        EcsTelemetry.runExecuteCommand(project, Result.Cancelled, EcsExecuteCommandType.Shell)
    }

    private fun runExecCommand(task: String) {
        try {
            val commandLine = EcsExecUtils.createCommand(project, connectionSettings, container, task, shell)
            val ptyProcess = PtyCommandLine(commandLine).createProcess()
            val process = CloudTerminalProcess(ptyProcess.outputStream, ptyProcess.inputStream)
            val runner = CloudTerminalRunner(project, container.containerDefinition.name(), process)

            runInEdt(ModalityState.any()) {
                TerminalView.getInstance(project).createNewSession(runner, TerminalTabState().also { it.myTabName = container.containerDefinition.name() })
            }
            EcsTelemetry.runExecuteCommand(project, Result.Succeeded, EcsExecuteCommandType.Shell)
        } catch (e: Exception) {
            e.notifyError(message("ecs.execute_command_failed"))
            LOG.warn(e) { "Failed to start interactive shell" }
            EcsTelemetry.runExecuteCommand(project, Result.Failed, EcsExecuteCommandType.Shell)
        }
    }
    companion object {
        private val LOG = getLogger<OpenShellInContainerDialog>()
    }
}


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

package software.aws.toolkits.jetbrains.services.ecs.exec

import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.execution.runners.ExecutionEnvironmentBuilder
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.components.JBTextField
import com.intellij.ui.layout.GrowPolicy
import com.intellij.ui.layout.applyToComponent
import com.intellij.ui.layout.panel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.ecs.ContainerDetails
import software.aws.toolkits.jetbrains.services.ecs.resources.EcsResources
import software.aws.toolkits.jetbrains.ui.resourceSelector
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.EcsExecuteCommandType
import software.aws.toolkits.telemetry.EcsTelemetry
import software.aws.toolkits.telemetry.Result
import javax.swing.JComponent
import javax.swing.plaf.basic.BasicComboBoxEditor

class RunCommandDialog(private val project: Project, private val container: ContainerDetails, private val connectionSettings: ConnectionSettings) :
    DialogWrapper(project) {
    private val coroutineScope = projectCoroutineScope(project)
    private val commandList = CollectionComboBoxModel(commandsEnteredPreviously.toMutableList())

    var command = ""
    var task: String? = null

    init {
        super.init()
        title = message("ecs.execute_command_run")
        setOKButtonText(message("general.execute_button"))
    }

    override fun createCenterPanel(): JComponent = panel {
        row(message("ecs.execute_command_task.label")) {
            resourceSelector(
                EcsResources.listTasks(container.service.clusterArn(), container.service.serviceArn()),
                connectionSettings,
                ::task
            ).constraints(growX, pushX)
                .growPolicy(GrowPolicy.MEDIUM_TEXT)
                .withErrorOnApplyIf(message("ecs.execute_command_task_comboBox_empty")) { it.selected().isNullOrEmpty() }
        }
        row(message("ecs.execute_command.label")) {
            comboBox(commandList, ::command)
                .constraints(grow)
                .withErrorOnApplyIf(message("ecs.execute_command_no_command")) { it.item.isNullOrEmpty() }
                .applyToComponent {
                    isEditable = true
                    selectedIndex = -1
                    editor = object : BasicComboBoxEditor.UIResource() {
                        override fun createEditorComponent() =
                            JBTextField().also { it.emptyText.text = message("ecs.execute_command_run_command_default_text") }
                    }
                }
        }
    }

    override fun doOKAction() {
        super.doOKAction()
        commandsEnteredPreviously.add(command)
        val task = task ?: throw IllegalStateException("Task not Selected")
        coroutineScope.launch {
            val taskRoleFound: Boolean = EcsExecUtils.checkRequiredPermissions(project, container.service.clusterArn(), task)
            if (taskRoleFound) {
                runCommand(task)
            } else {
                withContext(getCoroutineUiContext()) {
                    TaskRoleNotFoundWarningDialog(project).show()
                }
            }
        }
    }

    override fun doCancelAction() {
        super.doCancelAction()
        EcsTelemetry.runExecuteCommand(project, Result.Cancelled, EcsExecuteCommandType.Command)
    }

    private suspend fun runCommand(task: String) {
        try {
            val environment = ExecutionEnvironmentBuilder.create(
                project,
                DefaultRunExecutor.getRunExecutorInstance(),
                RunCommandRunProfile(project, connectionSettings, container, task, command)
            ).build()

            withContext(getCoroutineUiContext()) {
                environment.runner.execute(environment)
            }

            EcsTelemetry.runExecuteCommand(project, Result.Succeeded, EcsExecuteCommandType.Command)
        } catch (e: Exception) {
            e.notifyError(message("ecs.execute_command_failed"))
            LOG.warn(e) { "Run command failed" }
            EcsTelemetry.runExecuteCommand(project, Result.Failed, EcsExecuteCommandType.Command)
        }
    }

    companion object {
        private val commandsEnteredPreviously = mutableSetOf<String>()
        private val LOG = getLogger<RunCommandDialog>()
    }
}


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

package software.aws.toolkits.jetbrains.services.ecs.exec

import com.intellij.execution.Executor
import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.configurations.RunProfile
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.filters.TextConsoleBuilderFactory
import com.intellij.execution.process.ColoredProcessHandler
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.ProgramRunner
import com.intellij.execution.ui.RunContentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.services.ecs.ContainerDetails
import javax.swing.Icon

class RunCommandRunProfile(
    private val project: Project,
    private val connection: ConnectionSettings,
    private val container: ContainerDetails,
    private val task: String,
    private val command: String
) : RunProfile {

    override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState = RunCommandRunProfileState(environment)

    override fun getName(): String = container.containerDefinition.name()

    override fun getIcon(): Icon? = null

    inner class RunCommandRunProfileState(environment: ExecutionEnvironment) : CommandLineState(environment) {
        init {
            consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project)
        }

        override fun startProcess(): ProcessHandler {
            val processHandler = ColoredProcessHandler(EcsExecUtils.createCommand(project, connection, container, task, command))
            ProcessTerminatedListener.attach(processHandler)
            return processHandler
        }

        override fun execute(executor: Executor, runner: ProgramRunner<*>) = super.execute(executor, runner).apply {
            processHandler?.addProcessListener(object : ProcessAdapter() {
                override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
                    if (outputType === ProcessOutputTypes.STDOUT ||
                        outputType === ProcessOutputTypes.STDERR
                    ) {
                        RunContentManager.getInstance(project).toFrontRunContent(executor, processHandler)
                    }
                }
            })
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.ecs.exec

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.ui.components.JBLabel
import com.intellij.ui.layout.panel
import software.aws.toolkits.resources.message
import javax.swing.Action
import javax.swing.JComponent

class TaskRoleNotFoundWarningDialog(project: Project) : DialogWrapper(project) {
    private val warningIcon = JBLabel(Messages.getWarningIcon())
    private val warningMessage = JBLabel(message("ecs.execute_command_task_role_invalid_warning"))
    private val component by lazy {
        panel {
            row {
                warningIcon(grow)
                warningMessage(grow).also { it.component.setCopyable(true) }
            }
        }
    }

    init {
        super.init()
        title = message("ecs.execute_command_task_role_invalid_warning_title")
    }

    // Overriden to remove the Cancel button
    override fun createActions(): Array<Action> = arrayOf(okAction)

    override fun createCenterPanel(): JComponent? = component
}


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

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

import software.amazon.awssdk.services.ecs.EcsClient
import software.amazon.awssdk.services.ecs.model.ContainerDefinition
import software.amazon.awssdk.services.ecs.model.Service
import software.amazon.awssdk.services.ecs.model.ServiceNotFoundException
import software.amazon.awssdk.services.ecs.model.TaskDefinition
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.Resource
import software.aws.toolkits.jetbrains.core.map
import software.aws.toolkits.resources.message

object EcsResources {
    val LIST_CLUSTER_ARNS: Resource.Cached<List<String>> =
        ClientBackedCachedResource(EcsClient::class, "ecs.list_clusters") {
            listClustersPaginator().clusterArns().toList()
        }

    fun listServiceArns(clusterArn: String): Resource.Cached<List<String>> =
        ClientBackedCachedResource(EcsClient::class, "ecs.list_services.$clusterArn") {
            listServicesPaginator { it.cluster(clusterArn) }.serviceArns().toList()
        }

    fun describeService(clusterArn: String, serviceArn: String): Resource.Cached<Service> =
        ClientBackedCachedResource(EcsClient::class, "ecs.describe_service.$clusterArn.$serviceArn") {
            describeServices { it.cluster(clusterArn).services(serviceArn) }.services().firstOrNull()
                ?: throw ServiceNotFoundException.builder().message(message("ecs.service.not_found", serviceArn, clusterArn)).build()
        }

    fun describeTaskDefinition(familyName: String): Resource.Cached<TaskDefinition> =
        ClientBackedCachedResource(EcsClient::class, "ecs.task_definition.$familyName") {
            describeTaskDefinition { it.taskDefinition(familyName) }.taskDefinition()
        }

    fun listTasks(clusterArn: String, serviceArn: String): Resource.Cached<List<String>> =
        ClientBackedCachedResource(EcsClient::class, "ecs.task_list.$clusterArn.$serviceArn") {
            listTasksPaginator { it.cluster(clusterArn).serviceName(serviceArn) }.taskArns().toList()
        }

    fun listTaskIds(clusterArn: String, serviceArn: String) = listTasks(clusterArn, serviceArn).map { it.substringAfterLast("/") }

    fun listContainers(taskDefinitionArn: String): Resource<List<ContainerDefinition>> =
        Resource.view(describeTaskDefinition(taskDefinitionArn)) { containerDefinitions() }
}


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

package software.aws.toolkits.jetbrains.services.federation

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.client.methods.HttpPost
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.message.BasicNameValuePair
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.amazon.awssdk.services.sts.StsClient
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.credentials.getConnectionSettings
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyNoActiveCredentialsError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DeeplinkTelemetry
import software.aws.toolkits.telemetry.Result
import java.net.URLEncoder
import java.time.Duration

object AwsConsoleUrlFactory {
    private val defaultHttpClientBuilder: HttpClientBuilder by lazy { HttpClientBuilder.create() }

    fun federationUrl(region: AwsRegion): String {
        // https://docs.aws.amazon.com/general/latest/gr/signin-service.html
        // https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-Beijing.html
        // TODO: pull this into our endpoints generator somehow

        val signinTld = when (region.partitionId) {
            // special case cn since signin.amazonaws.com.cn does not resolve
            "aws-cn" -> "signin.amazonaws.cn"
            else -> "signin.${consoleTld(region)}"
        }

        val subdomain = when (region.id) {
            "us-east-1", "us-gov-west-1", "cn-north-1" -> ""
            else -> "${region.id}."
        }

        return "https://$subdomain$signinTld/federation"
    }

    fun consoleTld(region: AwsRegion) = when (region.partitionId) {
        // needs to be these; for example, redirecting to "amazonaws.com" is not allowed
        "aws" -> {
            "aws.amazon.com"
        }
        // TODO: gov is not supported for POST-based federation yet
//        "aws-us-gov" -> {
//            "amazonaws-us-gov.com"
//        }
        "aws-cn" -> {
            "amazonaws.com.cn"
        }
        else -> throw IllegalStateException("Partition '${region.partitionId}' is not supported")
    }

    fun consoleUrl(fragment: String? = null, region: AwsRegion): String {
        val consoleHome = "https://${region.id}.console.${consoleTld(region)}"

        return "$consoleHome${fragment ?: "/"}"
    }

    fun getSigninUrl(connectionSettings: ConnectionSettings, destination: String?, httpClientBuilder: HttpClientBuilder = defaultHttpClientBuilder): String =
        getSigninUrl(getSigninToken(connectionSettings, httpClientBuilder), destination, connectionSettings.region)

    fun getSigninToken(connectionSettings: ConnectionSettings, httpClientBuilder: HttpClientBuilder = defaultHttpClientBuilder): String {
        val resolvedCreds = connectionSettings.credentials.resolveCredentials()
        val sessionCredentials = if (resolvedCreds !is AwsSessionCredentials) {
            val stsClient = AwsClientManager.getInstance().getClient<StsClient>(connectionSettings)

            val tokenResponse = stsClient.use { client ->
                client.getFederationToken {
                    it.durationSeconds(Duration.ofMinutes(15).toSeconds().toInt())
                    it.name("FederationViaAWSJetBrainsToolkit")
                    // policy is required otherwise resulting session has no permissions
                    // session will have the intersection of role permissions and this policy
                    it.policyArns({ builder ->
                        builder.arn("arn:aws:iam::aws:policy/AdministratorAccess")
                    })
                }
            }

            tokenResponse.credentials().let { AwsSessionCredentials.create(it.accessKeyId(), it.secretAccessKey(), it.sessionToken()) }
        } else {
            resolvedCreds
        }

        val sessionJson = mapper.writeValueAsString(
            GetSigninTokenRequest(
                sessionId = sessionCredentials.accessKeyId(),
                sessionKey = sessionCredentials.secretAccessKey(),
                sessionToken = sessionCredentials.sessionToken()
            )
        )

        val params = mapOf(
            "Action" to "getSigninToken",
            "SessionType" to "json",
            "Session" to sessionJson
        ).map { BasicNameValuePair(it.key, it.value) }

        val request = HttpPost(federationUrl(connectionSettings.region))
            .apply {
                entity = UrlEncodedFormEntity(params)
            }

        val result = httpClientBuilder
            .setUserAgent(AwsClientManager.userAgent)
            .build().use { c ->
                c.execute(
                    request
                ).use { resp ->
                    if (resp.statusLine.statusCode !in 200..399) {
                        throw RuntimeException("getSigninToken request to AWS Signin endpoint failed: ${resp.statusLine}")
                    }
                    resp.entity.content.readAllBytes().decodeToString()
                }
            }

        return mapper.readValue<GetSigninTokenResponse>(result).signinToken
    }

    private fun getSigninUrl(token: String, destination: String? = null, region: AwsRegion): String {
        val params = mapOf(
            "Action" to "login",
            "SigninToken" to token,
            "Destination" to consoleUrl(fragment = destination, region = region)
        ).map { BasicNameValuePair(it.key, it.value) }

        return "${federationUrl(region)}?${UrlEncodedFormEntity(params).toUrlEncodedString()}"
    }

    fun openArnInConsole(project: Project, place: String, arn: String) {
        val connectionSettings = project.getConnectionSettings()

        if (connectionSettings == null) {
            notifyNoActiveCredentialsError(project)
            return
        }

        ApplicationManager.getApplication().executeOnPooledThread {
            try {
                val encodedArn = URLEncoder.encode(arn, Charsets.UTF_8)
                val encodedUa = URLEncoder.encode(AwsClientManager.userAgent, Charsets.UTF_8)
                val url = AwsConsoleUrlFactory.getSigninUrl(
                    connectionSettings,
                    "/go/view?arn=$encodedArn&source=$encodedUa"
                )
                BrowserUtil.browse(url)
                DeeplinkTelemetry.open(project, source = place, passive = false, result = Result.Succeeded)
            } catch (e: Exception) {
                val message = message("general.open_in_aws_console.error")
                notifyError(content = message, project = project)
                LOG.error(e) { message }
                DeeplinkTelemetry.open(project, source = place, passive = false, result = Result.Failed)
            }
        }
    }

    private val mapper = jacksonObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    private val LOG = getLogger<AwsConsoleUrlFactory>()
}

private data class GetSigninTokenRequest(
    @JsonProperty("sessionId")
    val sessionId: String,
    @JsonProperty("sessionKey")
    val sessionKey: String,
    @JsonProperty("sessionToken")
    val sessionToken: String
)

private data class GetSigninTokenResponse(
    @JsonProperty("SigninToken")
    val signinToken: String
)

private fun UrlEncodedFormEntity.toUrlEncodedString() = this.content.bufferedReader().readText()


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

package software.aws.toolkits.jetbrains.services.federation

import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.project.DumbAwareAction

class OpenArnInConsoleEditorPopupAction : DumbAwareAction() {
    override fun actionPerformed(e: AnActionEvent) {
        val editor = e.getData(CommonDataKeys.EDITOR)
        val selection = editor.selection() ?: return
        val project = e.getData(CommonDataKeys.PROJECT) ?: return

        AwsConsoleUrlFactory.openArnInConsole(project, ActionPlaces.EDITOR_POPUP, selection)
    }

    override fun update(e: AnActionEvent) {
        val editor = e.getData(CommonDataKeys.EDITOR)
        val isAvailable = if (editor == null || !EditorUtil.isRealFileEditor(editor)) {
            false
        } else {
            editor.selection()?.startsWith("arn:", ignoreCase = true) == true
        }

        e.presentation.isEnabledAndVisible = isAvailable
    }

    private fun Editor?.selection() = this?.selectionModel?.selectedText
}


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

package software.aws.toolkits.jetbrains.services.federation.psireferences

import com.intellij.patterns.PsiElementPattern
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiLiteralValue
import com.intellij.psi.PsiReferenceContributor
import com.intellij.psi.PsiReferenceRegistrar
import com.intellij.util.ProcessingContext

class ArnPsiReferenceContributor : PsiReferenceContributor() {
    override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
        registrar.registerReferenceProvider(
            object : PsiElementPattern.Capture<PsiElement>(
                PsiElement::class.java
            ) {
                override fun accepts(o: Any?, context: ProcessingContext): Boolean =
                    // this is lower fidelity than we'd like but avoids resolving the entire PSI tree
                    o is PsiLiteralValue && o.value is String
            },
            ArnPsiReferenceProvider(),
            PsiReferenceRegistrar.LOWER_PRIORITY
        )
    }
}


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

package software.aws.toolkits.jetbrains.services.federation.psireferences

import com.intellij.openapi.util.TextRange
import com.intellij.psi.ElementManipulators
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.intellij.psi.PsiReferenceProvider
import com.intellij.util.ProcessingContext

class ArnPsiReferenceProvider : PsiReferenceProvider() {
    // these results should probably be cached with [CachedValuesManager]
    override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
        val manipulator = ElementManipulators.getManipulator(element)
        if (manipulator != null) {
            val range = manipulator.getRangeInElement(element)
            // TODO: we can definitely make this more robust
            // assume this was a string that was quoted
            if (range.length == element.textRange.length - 2) {
                val substring = range.substring(element.text)
                if (substring.startsWith("arn:")) {
                    // don't do anything fancy and just treat it as a full match
                    return arrayOf(ArnReference(element, range, substring))
                }
            }
        }

        val matches = ARN_REGEX.findAll(element.text)
        return matches.map {
            ArnReference(
                element,
                TextRange.from(it.range.start, it.value.length),
                it.value
            )
        }.toList().toTypedArray()
    }

    companion object {
        //            partition  service   region  account     (optional)
        //                   v         v      v     v      resource-type         resource
        val ARN_REGEX = "arn:aws[^/:]*:[^/:]*:[^:]*:[^/:]*:(?:[^:\\s\\/]*[:\\/])?(?:[^\\s'\\\"\\\\]*)".toRegex()
    }
}


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

package software.aws.toolkits.jetbrains.services.federation.psireferences

import com.intellij.openapi.paths.WebReference
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.SyntheticElement
import com.intellij.psi.impl.FakePsiElement
import software.aws.toolkits.jetbrains.ToolkitPlaces
import software.aws.toolkits.jetbrains.services.federation.AwsConsoleUrlFactory

class ArnReference(element: PsiElement, textRange: TextRange, private val arn: String) : WebReference(element, textRange) {
    inner class MyFakePsiElement : FakePsiElement(), SyntheticElement {
        override fun getName() = arn
        override fun getParent() = element
        override fun getPresentableText() = arn

        override fun navigate(requestFocus: Boolean) {
            val project = element.project

            AwsConsoleUrlFactory.openArnInConsole(project, ToolkitPlaces.EDITOR_PSI_REFERENCE, arn)
        }
    }
    override fun resolve() = MyFakePsiElement()
}


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

package software.aws.toolkits.jetbrains.services.federation.psireferences

import com.intellij.lang.documentation.AbstractDocumentationProvider
import com.intellij.psi.PsiElement
import software.aws.toolkits.resources.message

class ArnReferenceDocumentationProvider : AbstractDocumentationProvider() {
    override fun getQuickNavigateInfo(element: PsiElement?, originalElement: PsiElement?): String? {
        if (element is ArnReference.MyFakePsiElement) {
            return message("general.open_in_aws_console")
        }

        return null
    }
}


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

package software.aws.toolkits.jetbrains.services.federation.psireferences

import com.intellij.json.psi.JsonStringLiteral
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiReferenceContributor
import com.intellij.psi.PsiReferenceRegistrar

class JsonArnPsiReferenceContributor : PsiReferenceContributor() {
    override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
        registrar.registerReferenceProvider(
            PlatformPatterns.psiElement(JsonStringLiteral::class.java),
            ArnPsiReferenceProvider(),
            PsiReferenceRegistrar.LOWER_PRIORITY
        )
    }
}


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

package software.aws.toolkits.jetbrains.services.iam

import com.intellij.json.JsonLanguage
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import org.intellij.lang.annotations.Language
import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.services.iam.IamClient
import software.amazon.awssdk.services.iam.model.Role
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.services.iam.Iam.createRoleWithPolicy
import software.aws.toolkits.jetbrains.utils.ui.formatAndSet
import software.aws.toolkits.resources.message
import java.awt.Component
import javax.swing.JComponent

class CreateIamRoleDialog(
    project: Project,
    private val iamClient: IamClient,
    parent: Component? = null,
    @Language("JSON") defaultPolicyDocument: String,
    @Language("JSON") defaultAssumeRolePolicyDocument: String
) : DialogWrapper(project, parent, false, IdeModalityType.PROJECT) {

    private val view = CreateRolePanel(project)

    var iamRole: Role? = null

    init {
        title = message("iam.create.role.title")
        setOKButtonText(message("general.create_button"))

        view.policyDocument.formatAndSet(defaultPolicyDocument, JsonLanguage.INSTANCE)
        view.assumeRolePolicyDocument.formatAndSet(defaultAssumeRolePolicyDocument, JsonLanguage.INSTANCE)

        init()
    }

    override fun createCenterPanel(): JComponent? = view.component

    override fun getPreferredFocusedComponent(): JComponent? = view.roleName

    override fun doValidate(): ValidationInfo? {
        if (roleName().isEmpty()) {
            return ValidationInfo(message("iam.create.role.missing.role.name"), view.roleName)
        }

        return null
    }

    override fun doOKAction() {
        if (okAction.isEnabled) {
            setOKButtonText(message("general.create_in_progress"))
            isOKActionEnabled = false

            ApplicationManager.getApplication().executeOnPooledThread {
                try {
                    createIamRole()
                    ApplicationManager.getApplication().invokeLater(
                        {
                            close(OK_EXIT_CODE)
                        },
                        ModalityState.stateForComponent(view.component)
                    )
                } catch (e: Exception) {
                    LOG.warn(e) { "Failed to create IAM role '${roleName()}'" }
                    setErrorText(e.message)
                    setOKButtonText(message("general.create_button"))
                    isOKActionEnabled = true
                }
            }
        }
    }

    private fun roleName() = view.roleName.text.trim()

    private fun policyDocument() = view.policyDocument.text.trim()

    private fun assumeRolePolicy() = view.assumeRolePolicyDocument.text.trim()

    private fun createIamRole() {
        iamRole = iamClient.createRoleWithPolicy(roleName(), assumeRolePolicy(), policyDocument())
    }

    @TestOnly
    internal fun createIamRoleForTesting() {
        createIamRole()
    }

    @TestOnly
    internal fun getViewForTesting(): CreateRolePanel = view

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


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

package software.aws.toolkits.jetbrains.services.iam

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.layout.panel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import software.amazon.awssdk.services.iam.IamClient
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.resources.message
import java.awt.Component
import javax.swing.JComponent
import kotlin.math.max

class CreateIamServiceRoleDialog(
    project: Project,
    private val iamClient: IamClient,
    private val serviceUri: String,
    private val managedPolicyName: String,
    name: String = "",
    parent: Component? = null,
) : DialogWrapper(project, parent, false, IdeModalityType.PROJECT) {
    private val coroutineScope = projectCoroutineScope(project)
    var name: String = name
        private set
    internal val view = panel {
        // make the width the widest string. Columns don't map entirely to text width (since text is variable width) but it looks better
        val size = max(serviceUri.length, managedPolicyName.length)
        row(message("iam.create.role.name.label")) {
            textField(::name, size).withErrorOnApplyIf(message("iam.create.role.missing.role.name")) { it.text.isNullOrBlank() }
        }
        row(message("iam.create.role.managed_policies")) {
            textField({ managedPolicyName }, {}, size).apply { component.isEditable = false }
        }
        row(message("iam.create.role.trust.editor.name")) {
            textField({ serviceUri }, {}, size).apply { component.isEditable = false }
        }
    }

    init {
        title = message("iam.create.role.title")
        setOKButtonText(message("general.create_button"))

        init()
    }

    override fun createCenterPanel(): JComponent = view

    override fun doOKAction() {
        if (!okAction.isEnabled) {
            return
        }
        setOKButtonText(message("general.create_in_progress"))
        isOKActionEnabled = false
        view.apply()

        coroutineScope.launch {
            try {
                createIamRole()
                runBlocking(getCoroutineUiContext()) {
                    close(OK_EXIT_CODE)
                }
            } catch (e: Exception) {
                LOG.warn(e) { "Failed to create IAM role '$name'" }
                setErrorText(e.message)
                setOKButtonText(message("general.create_button"))
                isOKActionEnabled = true
            }
        }
    }

    internal fun createIamRole() {
        val role = iamClient.createRole { it.roleName(name).assumeRolePolicyDocument(assumeRolePolicy(serviceUri)) }.role()
        try {
            iamClient.attachRolePolicy { it.roleName(role.roleName()).policyArn(managedPolicyNameToArn(managedPolicyName)) }
        } catch (exception: Exception) {
            try {
                iamClient.deleteRole {
                    it.roleName(role.roleName())
                }
            } catch (deleteException: Exception) {
                LOG.warn(deleteException) { "Failed to delete IAM role ${role.roleName()}" }
            }
            throw exception
        }
    }

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


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

package software.aws.toolkits.jetbrains.services.iam

import com.intellij.json.JsonLanguage
import com.intellij.openapi.project.Project
import com.intellij.ui.EditorTextField
import com.intellij.ui.EditorTextFieldProvider
import com.intellij.ui.IdeBorderFactory
import com.intellij.util.ui.JBUI
import software.aws.toolkits.resources.message
import javax.swing.JPanel
import javax.swing.JTextField

class CreateRolePanel(private val project: Project) {
    lateinit var component: JPanel
        private set
    lateinit var roleName: JTextField
        private set
    lateinit var policyDocument: EditorTextField
        private set
    lateinit var assumeRolePolicyDocument: EditorTextField
        private set

    private fun createUIComponents() {
        val textFieldProvider = EditorTextFieldProvider.getInstance()
        val insets = JBUI.emptyInsets()
        policyDocument = textFieldProvider.getEditorField(JsonLanguage.INSTANCE, project, emptyList())
        policyDocument.border = IdeBorderFactory.createTitledBorder(message("iam.create.role.policy.editor.name"), false, insets)
        assumeRolePolicyDocument = textFieldProvider.getEditorField(JsonLanguage.INSTANCE, project, emptyList())
        assumeRolePolicyDocument.border = IdeBorderFactory.createTitledBorder(message("iam.create.role.trust.editor.name"), false, insets)
    }
}


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

package software.aws.toolkits.jetbrains.services.iam

import org.intellij.lang.annotations.Language
import software.amazon.awssdk.services.iam.IamClient
import software.amazon.awssdk.services.iam.model.Role
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.Resource
import software.aws.toolkits.jetbrains.core.filter
import software.aws.toolkits.jetbrains.core.map
import software.aws.toolkits.jetbrains.services.lambda.upload.LAMBDA_PRINCIPAL

data class IamRole(val arn: String) {
    override fun toString(): String = name ?: arn

    val name: String? by lazy {
        ARN_REGEX.matchEntire(arn)?.groups?.elementAtOrNull(1)?.value
    }

    companion object {
        private val ARN_REGEX = "arn:.+:iam::.+:role/(.+)".toRegex()
    }
}

object IamResources {

    val LIST_RAW_ROLES = ClientBackedCachedResource(IamClient::class, "iam.list_roles") {
        listRolesPaginator().roles().toList()
    }

    @JvmField
    val LIST_ALL: Resource<List<IamRole>> = Resource.view(LIST_RAW_ROLES) {
        map { IamRole(it.arn()) }.toList()
    }

    @JvmField
    val LIST_LAMBDA_ROLES: Resource<List<IamRole>> = Resource.view(LIST_RAW_ROLES) {
        filter { it.assumeRolePolicyDocument().contains(LAMBDA_PRINCIPAL) }
            .map { IamRole(it.arn()) }
            .toList()
    }
}

fun managedPolicyNameToArn(policyName: String) = "arn:aws:iam::aws:policy/$policyName"

@Language("JSON")
fun assumeRolePolicy(servicePrincipal: String) =
    """
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": "$servicePrincipal"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        }
    """.trimIndent()

object Iam {
    private val LOG = getLogger<Iam>()

    fun IamClient.createRoleWithPolicy(roleName: String, assumeRolePolicy: String, policy: String? = null): Role {
        val role = this.createRole {
            it.roleName(roleName)
            it.assumeRolePolicyDocument(assumeRolePolicy)
        }.role()

        policy?.let {
            try {
                this.putRolePolicy {
                    it.roleName(roleName)
                        .policyName(roleName)
                        .policyDocument(policy)
                }
            } catch (exception: Exception) {
                try {
                    this.deleteRole {
                        it.roleName(role.roleName())
                    }
                } catch (deleteException: Exception) {
                    LOG.warn(deleteException) { "Failed to delete IAM role $roleName" }
                }
                throw exception
            }
        }

        return role
    }
}


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

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

import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.NavigatablePsiElement
import com.intellij.psi.search.GlobalSearchScope
import software.amazon.awssdk.services.lambda.model.FunctionConfiguration
import software.amazon.awssdk.services.lambda.model.PackageType
import software.amazon.awssdk.services.lambda.model.Runtime
import software.amazon.awssdk.services.lambda.model.TracingMode
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.services.iam.IamRole
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.DEFAULT_MEMORY_SIZE
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.DEFAULT_TIMEOUT
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.MAX_MEMORY
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.MAX_TIMEOUT
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.MEMORY_INCREMENT
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.MIN_MEMORY
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.MIN_TIMEOUT
import software.aws.toolkits.jetbrains.services.lambda.sam.SamCommon
import software.aws.toolkits.jetbrains.ui.SliderPanel

object Lambda {
    private val LOG = getLogger<Lambda>()

    fun findPsiElementsForHandler(project: Project, runtime: LambdaRuntime, handler: String): Array<NavigatablePsiElement> {
        runtime.toSdkRuntime()?.let { return findPsiElementsForHandler(project, it, handler) } ?: return emptyArray()
    }

    fun findPsiElementsForHandler(project: Project, runtime: Runtime, handler: String): Array<NavigatablePsiElement> {
        val resolver = runtime.runtimeGroup?.let { LambdaHandlerResolver.getInstanceOrNull(it) } ?: return emptyArray()

        // Don't search through ".aws-sam" folders
        val samBuildFileScopes = GlobalSearchScope.filesScope(project, findSamBuildContents(project))
        val excludeSamBuildFileScopes = GlobalSearchScope.notScope(samBuildFileScopes)
        // only search within project content roots
        val scope = GlobalSearchScope.projectScope(project).intersectWith(excludeSamBuildFileScopes)

        val elements = resolver.findPsiElements(project, handler, scope)

        LOG.debug {
            elements.joinToString(
                prefix = "Found ${elements.size} PsiElements for Handler: $handler\n",
                separator = "\n"
            ) { it.containingFile.virtualFile.path }
        }

        return elements
    }

    fun isHandlerValid(project: Project, runtime: Runtime, handler: String): Boolean = ReadAction.compute<Boolean, Throwable> {
        runtime.runtimeGroup?.let {
            LambdaHandlerResolver.getInstanceOrNull(it)
        }?.isHandlerValid(project, handler) == true
    }

    @Suppress("MissingRecentApi")
    private fun findSamBuildContents(project: Project): Collection<VirtualFile> =
        ModuleManager.getInstance(project).modules.flatMap { findSamBuildContents(it) }

    private fun findSamBuildContents(module: Module): Collection<VirtualFile> =
        ModuleRootManager.getInstance(module).contentRoots.mapNotNull {
            it.findChild(SamCommon.SAM_BUILD_DIR)
        }.flatMap {
            VfsUtil.collectChildrenRecursively(it)
        }
}

object LambdaWidgets {
    @JvmStatic
    fun lambdaTimeout(): SliderPanel =
        SliderPanel(MIN_TIMEOUT, MAX_TIMEOUT, DEFAULT_TIMEOUT, 0, MAX_TIMEOUT, 10, 100, false)

    @JvmStatic
    fun lambdaMemory(): SliderPanel =
        SliderPanel(MIN_MEMORY, MAX_MEMORY, DEFAULT_MEMORY_SIZE, MIN_MEMORY, MAX_MEMORY, MEMORY_INCREMENT, MEMORY_INCREMENT * 15, true)
}

data class LambdaFunction(
    val name: String,
    val description: String?,
    val arn: String,
    val packageType: PackageType,
    val lastModified: String,
    val handler: String?,
    val runtime: Runtime?,
    val envVariables: Map<String, String>?,
    val timeout: Int,
    val memorySize: Int,
    val xrayEnabled: Boolean,
    val role: IamRole
)

fun FunctionConfiguration.toDataClass() = LambdaFunction(
    name = this.functionName(),
    description = this.description(),
    packageType = this.packageType() ?: PackageType.ZIP,
    arn = this.functionArn(),
    lastModified = this.lastModified(),
    handler = this.handler(),
    runtime = this.runtime(),
    envVariables = this.environment()?.variables(),
    timeout = this.timeout(),
    memorySize = this.memorySize(),
    // TODO: make non-nullable when available in all partitions
    xrayEnabled = this.tracingConfig()?.mode() == TracingMode.ACTIVE,
    role = IamRole(this.role())
)


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

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

import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.rootManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import software.aws.toolkits.jetbrains.core.utils.buildList
import software.aws.toolkits.jetbrains.services.PathMapping
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.HandlerRunSettings
import software.aws.toolkits.jetbrains.services.lambda.sam.SamCommon
import software.aws.toolkits.jetbrains.services.lambda.sam.SamOptions
import software.aws.toolkits.jetbrains.services.lambda.sam.SamTemplateUtils
import software.aws.toolkits.jetbrains.services.lambda.steps.BuildLambdaRequest
import software.aws.toolkits.resources.message
import java.nio.file.Path
import java.nio.file.Paths

abstract class LambdaBuilder {

    /**
     * Returns the base directory of the Lambda handler
     *
     * @throws IllegalStateException if we cant determine a valid base directory for the handler element
     */
    abstract fun handlerBaseDirectory(module: Module, handlerElement: PsiElement): Path

    open fun handlerForDummyTemplate(settings: HandlerRunSettings, handlerElement: PsiElement): String = settings.handler

    open fun buildFromHandler(project: Project, settings: HandlerRunSettings): BuildLambdaRequest {
        val dummyLogicalId = "Function"
        val samOptions = settings.samOptions
        val runtime = settings.runtime
        val handler = settings.handler

        val element = Lambda.findPsiElementsForHandler(project, runtime, handler).first()
        val module = getModule(element.containingFile)

        val buildDirectory = getBuildDirectory(module)
        val dummyTemplate = buildDirectory.parent.resolve("temp-template.yaml")

        SamTemplateUtils.writeDummySamTemplate(
            tempFile = dummyTemplate,
            logicalId = dummyLogicalId,
            runtime = runtime.toSdkRuntime() ?: throw IllegalStateException("Cannot map runtime $runtime to SDK runtime."),
            architecture = settings.architecture.toSdkArchitecture(),
            handler = handlerForDummyTemplate(settings, element),
            timeout = settings.timeout,
            memorySize = settings.memorySize,
            codeUri = handlerBaseDirectory(module, element).toAbsolutePath().toString(),
            envVars = settings.environmentVariables
        )

        return BuildLambdaRequest(
            dummyTemplate,
            dummyLogicalId,
            buildDirectory,
            additionalBuildEnvironmentVariables(project, module, samOptions),
            samOptions
        )
    }

    /**
     * Returns the build directory of the project. Create this if it doesn't exist yet.
     */
    open fun getBuildDirectory(module: Module): Path {
        val contentRoot = module.rootManager.contentRoots.firstOrNull()
            ?: throw IllegalStateException(message("lambda.build.module_with_no_content_root", module.name))
        return Paths.get(contentRoot.path, SamCommon.SAM_BUILD_DIR, "build")
    }

    /**
     * Returns a set of default path mappings for the specified built function
     *
     * @param sourceTemplate The original template file
     * @param logicalId The logical ID of the function
     * @param buildDir The root directory where SAM built the function into
     */
    open fun defaultPathMappings(sourceTemplate: Path, logicalId: String, buildDir: Path): List<PathMapping> = buildList {
        val codeLocation = SamTemplateUtils.getCodeLocation(sourceTemplate, logicalId)
        // First one wins, so code needs to go before build
        add(PathMapping(sourceTemplate.resolveSibling(codeLocation).normalize().toString(), TASK_PATH))
        add(PathMapping(buildDir.resolve(logicalId).normalize().toString(), TASK_PATH))
    }

    /**
     * Returns a set of additional environment variables that should be passed to SAM build
     */
    open fun additionalBuildEnvironmentVariables(project: Project, module: Module?, samOptions: SamOptions): Map<String, String> = emptyMap()

    companion object : RuntimeGroupExtensionPointObject<LambdaBuilder>(ExtensionPointName("aws.toolkit.lambda.builder")) {
        /*
         * The default path to the task. The default is consistent across both Zip and Image based functions.
         */
        const val TASK_PATH = "/var/task"

        fun getModule(psiFile: PsiFile): Module = ModuleUtil.findModuleForFile(psiFile)
            ?: throw IllegalStateException("Failed to locate module for ${psiFile.virtualFile}")
    }
}


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

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

import com.intellij.openapi.project.Project
import com.intellij.psi.NavigatablePsiElement
import icons.AwsIcons
import software.amazon.awssdk.services.lambda.LambdaClient
import software.amazon.awssdk.services.lambda.model.FunctionConfiguration
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerResourceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerServiceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.CacheBackedAwsExplorerServiceRootNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.ResourceLocationNode
import software.aws.toolkits.jetbrains.services.lambda.execution.remote.RemoteLambdaLocation
import software.aws.toolkits.jetbrains.services.lambda.resources.LambdaResources
import software.aws.toolkits.resources.message

class LambdaServiceNode(project: Project, service: AwsExplorerServiceNode) :
    CacheBackedAwsExplorerServiceRootNode<FunctionConfiguration>(project, service, LambdaResources.LIST_FUNCTIONS) {
    override fun displayName(): String = message("explorer.node.lambda")
    override fun toNode(child: FunctionConfiguration): AwsExplorerNode<*> = LambdaFunctionNode(nodeProject, child.toDataClass())
}

open class LambdaFunctionNode(
    project: Project,
    function: LambdaFunction
) : AwsExplorerResourceNode<LambdaFunction>(
    project,
    LambdaClient.SERVICE_NAME,
    function,
    AwsIcons.Resources.LAMBDA_FUNCTION
),
    ResourceLocationNode {

    override fun resourceType() = "function"

    override fun resourceArn() = value.arn

    override fun toString(): String = functionName()

    override fun displayName() = functionName()

    override fun location() = RemoteLambdaLocation(nodeProject, value)

    fun functionName(): String = value.name

    fun handlerPsi(): Array<NavigatablePsiElement> {
        val runtime = value.runtime ?: return emptyArray()
        val handler = value.handler ?: return emptyArray()
        return Lambda.findPsiElementsForHandler(nodeProject, runtime, handler)
    }
}


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

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

import com.intellij.lang.Language
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.NavigatablePsiElement
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import software.amazon.awssdk.services.lambda.model.Runtime

/**
 * Used to expose Lambda handler information for different [Language]s / [Runtime]s
 */
interface LambdaHandlerResolver {
    /**
     * Converts the handler string into PSI elements that represent it. I.e. if the Handler points to a file, return the
     * class, or if a method return the method.
     *
     * @return Matching PSI elements or empty array if unable to locate one
     */
    fun findPsiElements(project: Project, handler: String, searchScope: GlobalSearchScope): Array<NavigatablePsiElement>

    /**
     * For a given [PsiElement] determine if it represents a lambda handler.
     *
     * If the [element] matches the AWS Lambda definition for a handler in the given language,
     * return the handler string representation, else null.
     *
     * Should be done at the lowest possible level (i.e. [com.intellij.psi.PsiIdentifier]
     * for Java implementations).
     *
     * @see com.intellij.codeInsight.daemon.LineMarkerProvider.getLineMarkerInfo
     */
    fun determineHandler(element: PsiElement): String?

    /**
     * Return a set of valid handlers from the given [PsiElement]. Different from [determineHandler], it returns all the
     * valid handler strings contained by the [element].
     *
     * For Java implementation, if the element is a Java class, it returns all the valid methods either defined by this
     * class or its super class(es).
     *
     * @param element The [PsiElement] that containing all the handlers
     * @param file The underlying [VirtualFile] for the [PsiElement]. In JetBrains indexing, the [VirtualFile] is not
     * attached to the target [PsiElement], you need to explicitly pass in this parameter.
     *
     * @see LambdaHandlerIndex.getIndexer
     */
    fun determineHandlers(element: PsiElement, file: VirtualFile): Set<String>

    /**
     * Given a [handler] create a shorter name used for display.
     */
    fun handlerDisplayName(handler: String): String = handler

    /**
     * Given a handler, return whether to show this handler all the time
     */
    fun shouldShowLineMarker(handler: String): Boolean = false

    /**
     * Given a handler string, return whether specified structure exists in PSI or not
     *
     * @param handler - handler string value
     */
    fun isHandlerValid(project: Project, handler: String): Boolean =
        findPsiElements(project, handler, GlobalSearchScope.allScope(project)).isNotEmpty()

    companion object : RuntimeGroupExtensionPointObject<LambdaHandlerResolver>(ExtensionPointName.create("aws.toolkit.lambda.handlerResolver"))
}


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

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

import java.util.concurrent.TimeUnit

// @see https://docs.aws.amazon.com/lambda/latest/dg/limits.html
object LambdaLimits {
    const val MIN_MEMORY = 128
    const val MAX_MEMORY = 10240
    const val MAX_FUNCTION_NAME_LENGTH = 64
    val FUNCTION_NAME_PATTERN = "[a-zA-Z0-9-_]+".toRegex()
    const val MEMORY_INCREMENT = 64
    const val DEFAULT_MEMORY_SIZE = 128
    const val MIN_TIMEOUT = 1

    @JvmField
    val MAX_TIMEOUT = TimeUnit.MINUTES.toSeconds(15).toInt()

    @JvmField
    val DEFAULT_TIMEOUT = TimeUnit.MINUTES.toSeconds(5).toInt()
}


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

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

import com.intellij.util.text.SemVer
import software.amazon.awssdk.services.lambda.LambdaClient
import software.aws.toolkits.core.lambda.LambdaArchitecture
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.jetbrains.utils.assertIsNonDispatchThread

fun LambdaRuntime.minSamDebuggingVersion(): SemVer =
    minSamDebugging?.let { SemVer.parseFromText(it) ?: throw IllegalStateException("$this has bad minSamDebuggingVersion! It should be a semver string!") }
        ?: SamExecutable.minVersion

fun LambdaRuntime.minSamInitVersion(): SemVer =
    minSamInit?.let { SemVer.parseFromText(it) ?: throw IllegalStateException("$this has bad minSamInitVersion! It should be a semver string!") }
        ?: SamExecutable.minVersion

fun LambdaArchitecture.minSamVersion(): SemVer =
    minSam?.let { SemVer.parseFromText(it) ?: throw IllegalStateException("$this has bad minSamInitVersion! It should be a semver string!") }
        ?: SamExecutable.minVersion

fun LambdaClient.waitForUpdatableState(functionName: String) {
    assertIsNonDispatchThread()
    // wait until function is both active and not being updated
    waiter().waitUntilFunctionActive { it.functionName(functionName) }
    waiter().waitUntilFunctionUpdated { it.functionName(functionName) }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
@file:JvmName("RuntimeGroupUtil")

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

import com.intellij.lang.Language
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkType
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.KeyedExtensionCollector
import com.intellij.util.text.SemVer
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.core.IdBasedExtensionPoint
import software.aws.toolkits.resources.message

/**
 * IDs for built-in runtime groups that ship with toolkit
 */
object BuiltInRuntimeGroups {
    const val Python = "PYTHON"
    const val Dotnet = "DOTNET"
    const val Java = "JAVA"
    const val NodeJs = "NODEJS"
    const val Go = "GO"
}

/**
 * Grouping of Lambda [Runtime] by parent language.
 *
 * A Lambda [Runtime] belongs to a single [RuntimeGroup], a [RuntimeGroup] may have several Lambda [Runtime]s, [Language]s or [Sdk]s.
 */
abstract class RuntimeGroup {
    abstract val id: String
    abstract val languageIds: Set<String>
    abstract val supportsPathMappings: Boolean

    val supportedSdkRuntimes: List<Runtime> by lazy {
        supportedRuntimes.mapNotNull { it.toSdkRuntime() }
    }

    abstract val supportedRuntimes: List<LambdaRuntime>

    open fun determineRuntime(project: Project): LambdaRuntime? = null
    open fun determineRuntime(module: Module): LambdaRuntime? = null
    open fun getModuleType(): ModuleType<*>? = null
    open fun getIdeSdkType(): SdkType? = null

    // This only works with Zip and is only called on that path since image is based on what debugger EPs we have
    fun validateSamVersionForZipDebugging(runtime: LambdaRuntime, samVersion: SemVer) {
        val minVersion = supportedRuntimes.first { it == runtime }.minSamDebuggingVersion()
        if (samVersion < minVersion) {
            throw RuntimeException(message("sam.executable.minimum_too_low_runtime", runtime, minVersion))
        }
    }

    companion object {
        private val EP_NAM = ExtensionPointName.create<RuntimeGroup>("aws.toolkit.lambda.runtimeGroup")

        @JvmStatic
        fun find(predicate: (RuntimeGroup) -> Boolean): RuntimeGroup? = registeredRuntimeGroups().firstOrNull(predicate)

        fun getById(id: String?): RuntimeGroup = id?.let { find { it.id == id } } ?: throw IllegalStateException("No RuntimeGroup with id '$id' is registered")

        fun determineRuntime(project: Project?): LambdaRuntime? = project?.let { _ ->
            registeredRuntimeGroups().asSequence().mapNotNull { it.determineRuntime(project) }.firstOrNull()
        }

        fun determineRuntime(module: Module?): LambdaRuntime? = module?.let { _ ->
            registeredRuntimeGroups().asSequence().mapNotNull { it.determineRuntime(module) }.firstOrNull()
        }

        fun determineRuntimeGroup(project: Project?): RuntimeGroup? = project?.let { _ ->
            registeredRuntimeGroups().find { it.determineRuntime(project) != null }
        }

        fun registeredRuntimeGroups(): List<RuntimeGroup> = EP_NAM.extensionList
    }
}

abstract class SdkBasedRuntimeGroup : RuntimeGroup() {
    protected abstract fun runtimeForSdk(sdk: Sdk): LambdaRuntime?

    override fun determineRuntime(project: Project): LambdaRuntime? = ProjectRootManager.getInstance(project).projectSdk?.let { runtimeForSdk(it) }

    override fun determineRuntime(module: Module): LambdaRuntime? = ModuleRootManager.getInstance(module).sdk?.let { runtimeForSdk(it) }
}

val Runtime.runtimeGroup: RuntimeGroup? get() = RuntimeGroup.find { this in it.supportedSdkRuntimes }
val LambdaRuntime.runtimeGroup: RuntimeGroup? get() = RuntimeGroup.find { this in it.supportedRuntimes }

/**
 * For a given [com.intellij.lang.Language] determine the corresponding Lambda [RuntimeGroup]
 */
val Language.runtimeGroup: RuntimeGroup? get() = RuntimeGroup.find { this.id in it.languageIds }

/**
 * Given [AnActionEvent] attempt to determine the [Runtime]
 */
fun AnActionEvent.runtime(): LambdaRuntime? {
    val runtimeGroup = getData(LangDataKeys.LANGUAGE)?.runtimeGroup ?: return null
    return getData(LangDataKeys.MODULE)?.let { runtimeGroup.determineRuntime(it) } ?: getData(LangDataKeys.PROJECT)?.let { runtimeGroup.determineRuntime(it) }
}

/**
 * To be implemented on a companion object of the extension point object to expose factory methods.
 * See [software.aws.toolkits.jetbrains.services.lambda.LambdaBuilder]
 */
abstract class RuntimeGroupExtensionPointObject<T>(val extensionPointName: ExtensionPointName<IdBasedExtensionPoint<T>>) {
    private val collector = KeyedExtensionCollector<T, String>(extensionPointName.name)

    fun getInstanceOrNull(runtimeGroup: RuntimeGroup): T? = collector.findSingle(runtimeGroup.id)
    fun getInstance(runtimeGroup: RuntimeGroup): T = getInstanceOrNull(runtimeGroup)
        ?: throw IllegalStateException("Attempted to retrieve feature for unsupported runtime group $runtimeGroup")

    fun supportedRuntimeGroups(): Set<RuntimeGroup> {
        val alRuntimeGroups = RuntimeGroup.registeredRuntimeGroups()
        val supportedIds = extensionPointName.extensions.map { it.id }
        return alRuntimeGroups.filter { supportedIds.contains(it.id) }.toSet()
    }

    fun supportedLanguages(): Set<Language> {
        val supportedRuntimeGroups = supportedRuntimeGroups()
        return supportedRuntimeGroups.asSequence().flatMap { it.languageIds.asSequence() }.mapNotNull { Language.findLanguageByID(it) }.toSet()
    }
}


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

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

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.dsl.builder.panel
import software.aws.toolkits.jetbrains.settings.SamDisplayDevModeWarningSettings
import software.aws.toolkits.resources.message
import javax.swing.JComponent

class SyncServerlessAppWarningDialog(private val project: Project) : DialogWrapper(project) {
    private val settings = SamDisplayDevModeWarningSettings.getInstance()
    private val dontDisplayWarning = JBCheckBox(message("general.notification.action.hide_forever")).also {
        it.isSelected = false
    }
    private val component by lazy {
        panel {
            row {
                label(
                    message("serverless.application.sync.dev.mode.warning.text")
                )
            }
            row {
                cell(dontDisplayWarning)
            }
        }
    }

    init {
        super.init()
        title = message("serverless.application.sync.confirm.dev.stack.title")
        setOKButtonText(message("general.confirm"))
    }

    override fun createCenterPanel(): JComponent? = component

    override fun doOKAction() {
        super.doOKAction()
        if (dontDisplayWarning.isSelected) {
            settings.showDevModeWarning = 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.actions

import software.amazon.awssdk.services.lambda.LambdaClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.explorer.actions.DeleteResourceAction
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.services.lambda.LambdaFunctionNode
import software.aws.toolkits.jetbrains.services.lambda.resources.LambdaResources
import software.aws.toolkits.resources.message

class DeleteFunctionAction : DeleteResourceAction<LambdaFunctionNode>(message("lambda.function.delete.action")) {
    override fun performDelete(selected: LambdaFunctionNode) {
        val project = selected.nodeProject

        val client: LambdaClient = project.awsClient()
        client.deleteFunction { it.functionName(selected.functionName()) }
        project.refreshAwsTree(LambdaResources.LIST_FUNCTIONS)
    }
}


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

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

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import icons.AwsIcons
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance
import software.aws.toolkits.jetbrains.core.executables.ExecutableManager
import software.aws.toolkits.jetbrains.core.executables.getExecutable
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.services.cloudformation.describeStack
import software.aws.toolkits.jetbrains.services.cloudformation.executeChangeSetAndWait
import software.aws.toolkits.jetbrains.services.cloudformation.stack.StackWindowManager
import software.aws.toolkits.jetbrains.services.lambda.deploy.DeployServerlessApplicationDialog
import software.aws.toolkits.jetbrains.services.lambda.deploy.DeployServerlessApplicationSettings
import software.aws.toolkits.jetbrains.services.lambda.sam.SamCommon
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.jetbrains.services.lambda.sam.SamTemplateFileUtils
import software.aws.toolkits.jetbrains.services.lambda.sam.SamTemplateFileUtils.validateTemplateFile
import software.aws.toolkits.jetbrains.services.lambda.steps.DeployLambda
import software.aws.toolkits.jetbrains.services.lambda.steps.createDeployWorkflow
import software.aws.toolkits.jetbrains.services.lambda.upload.UploadFunctionContinueDialog
import software.aws.toolkits.jetbrains.settings.DeploySettings
import software.aws.toolkits.jetbrains.settings.relativeSamPath
import software.aws.toolkits.jetbrains.utils.execution.steps.BuildViewWorkflowEmitter
import software.aws.toolkits.jetbrains.utils.execution.steps.StepExecutor
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.jetbrains.utils.notifyNoActiveCredentialsError
import software.aws.toolkits.jetbrains.utils.notifySamCliNotValidError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.SamTelemetry

class DeployServerlessApplicationAction : AnAction(
    message("serverless.application.deploy"),
    null,
    AwsIcons.Resources.SERVERLESS_APP
) {
    private val edtContext = getCoroutineUiContext()

    override fun actionPerformed(e: AnActionEvent) {
        val project = e.getRequiredData(PlatformDataKeys.PROJECT)

        if (!AwsConnectionManager.getInstance(project).isValidConnectionSettings()) {
            notifyNoActiveCredentialsError(project = project)
            return
        }

        ExecutableManager.getInstance().getExecutable<SamExecutable>().thenAccept { samExecutable ->
            if (samExecutable is ExecutableInstance.InvalidExecutable || samExecutable is ExecutableInstance.UnresolvedExecutable) {
                notifySamCliNotValidError(
                    project = project,
                    content = (samExecutable as ExecutableInstance.BadExecutable).validationError
                )
                return@thenAccept
            }

            val templateFile = SamTemplateFileUtils.retrieveSamTemplate(e, project) ?: return@thenAccept

            validateTemplateFile(project, templateFile)?.let {
                notifyError(content = it, project = project)
                return@thenAccept
            }

            runInEdt {
                // Force save before we deploy
                FileDocumentManager.getInstance().saveAllDocuments()

                val stackDialog = DeployServerlessApplicationDialog(project, templateFile)
                if (!stackDialog.showAndGet()) {
                    SamTelemetry.deploy(
                        project = project,
                        version = SamCommon.getVersionString(),
                        result = Result.Cancelled
                    )
                    return@runInEdt
                }

                val settings = stackDialog.settings()
                saveSettings(project, templateFile, settings)

                continueDeployment(project, templateFile, settings)
            }
        }
    }

    private fun continueDeployment(project: Project, templateFile: VirtualFile, settings: DeployServerlessApplicationSettings) {
        val stackName = settings.stackName
        val emitter = BuildViewWorkflowEmitter.createEmitter(
            project,
            message("serverless.application.deploy_in_progress.title", stackName),
            stackName
        )
        val workflow = StepExecutor(
            project,
            createDeployWorkflow(
                project,
                templateFile,
                settings
            ),
            emitter
        )

        workflow.onSuccess = {
            runBlocking {
                val changeSetArn = it.getRequiredAttribute(DeployLambda.CHANGE_SET_ARN)

                if (!settings.autoExecute) {
                    val response = withContext(edtContext) { UploadFunctionContinueDialog(project, changeSetArn).showAndGet() }
                    if (!response) {
                        // TODO this telemetry needs to be improved. The user can finish the deployment later so we do not know if
                        // it is actually cancelled or not
                        SamTelemetry.deploy(project = project, version = SamCommon.getVersionString(), result = Result.Cancelled)
                        return@runBlocking
                    }
                }

                val cfnClient = project.awsClient<CloudFormationClient>()

                cfnClient.describeStack(stackName) {
                    it?.run {
                        runInEdt {
                            StackWindowManager.getInstance(project).openStack(stackName(), stackId())
                        }
                    }
                }
                ApplicationManager.getApplication().executeOnPooledThread {
                    try {
                        cfnClient.executeChangeSetAndWait(stackName, changeSetArn)
                        notifyInfo(
                            message("cloudformation.service_name"),
                            message("cloudformation.execute_change_set.success", stackName),
                            project
                        )
                        SamTelemetry.deploy(
                            project = project,
                            version = SamCommon.getVersionString(),
                            result = Result.Succeeded
                        )
                        // Since we could update anything, do a full refresh of the resource cache and explorer
                        project.refreshAwsTree()
                    } catch (e: Exception) {
                        e.notifyError(message("cloudformation.execute_change_set.failed", stackName), project)
                        SamTelemetry.deploy(
                            project = project,
                            version = SamCommon.getVersionString(),
                            result = Result.Failed
                        )
                    }
                }
            }
        }

        workflow.onError = {
            it.notifyError(project = project, title = message("cloudformation.service_name"))
            SamTelemetry.deploy(
                project = project,
                version = SamCommon.getVersionString(),
                result = Result.Failed
            )
        }

        workflow.startExecution()
    }

    override fun update(e: AnActionEvent) {
        super.update(e)

        e.presentation.isVisible = false
    }

    private fun saveSettings(project: Project, templateFile: VirtualFile, settings: DeployServerlessApplicationSettings) {
        ModuleUtil.findModuleForFile(templateFile, project)?.let { module ->
            relativeSamPath(module, templateFile)?.let { samPath ->
                DeploySettings.getInstance(module)?.apply {
                    setSamStackName(samPath, settings.stackName)
                    setSamBucketName(samPath, settings.bucket)
                    setSamEcrRepoUri(samPath, settings.ecrRepo)
                    setSamAutoExecute(samPath, settings.autoExecute)
                    setSamUseContainer(samPath, settings.useContainer)
                    setEnabledCapabilities(samPath, settings.capabilities)
                }
            }
        }
    }
}


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

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

import com.intellij.icons.AllIcons
import com.intellij.idea.ActionsBundle
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.psi.NavigatablePsiElement
import com.intellij.util.OpenSourceUtil
import software.aws.toolkits.jetbrains.core.explorer.actions.ExplorerNodeAction
import software.aws.toolkits.jetbrains.services.lambda.LambdaFunctionNode
import software.aws.toolkits.telemetry.LambdaTelemetry

class GoToHandlerAction : ExplorerNodeAction<LambdaFunctionNode>(ActionsBundle.actionText("EditSource"), description = ActionsBundle.actionText("EditSource")) {
    override fun update(selected: List<LambdaFunctionNode>, e: AnActionEvent) {
        super.update(selected, e)

        val presentation = e.presentation
        presentation.icon = AllIcons.Actions.EditSource
        if (selected.size == 1) {
            presentation.isEnabled = getHandler(selected.first())?.isNotEmpty() ?: false
        } else {
            presentation.isEnabled = false
        }
        presentation.isVisible = true
    }

    override fun actionPerformed(selected: List<LambdaFunctionNode>, e: AnActionEvent) {
        val handlers = getHandler(selected.first())
        if (handlers != null) {
            OpenSourceUtil.navigate(true, *handlers)
            LambdaTelemetry.goToHandler(e.project, true)
        } else {
            LambdaTelemetry.goToHandler(e.project, false)
        }
    }

    private fun getHandler(node: LambdaFunctionNode): Array<NavigatablePsiElement>? = node.handlerPsi()
}


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import icons.AwsIcons
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleExplorerNodeAction
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.checkIfLogGroupExists
import software.aws.toolkits.jetbrains.services.lambda.LambdaFunctionNode
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message

class LambdaLogGroupAction :
    SingleExplorerNodeAction<LambdaFunctionNode>(message("cloudwatch.logs.view_log_streams"), null, AwsIcons.Resources.CloudWatch.LOGS),
    DumbAware {
    override fun actionPerformed(selected: LambdaFunctionNode, e: AnActionEvent) {
        val project = selected.nodeProject
        val client = project.awsClient<CloudWatchLogsClient>()
        val logGroup = "/aws/lambda/${selected.functionName()}"
        val scope = projectCoroutineScope(project)
        scope.launch {
            if (client.checkIfLogGroupExists(logGroup)) {
                val window = CloudWatchLogWindow.getInstance(project)
                window.showLogGroup(logGroup)
            } else {
                notifyError(message("lambda.logs.does_not_exist", selected.functionName()))
            }
        }
    }
}