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

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

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

class GoLambdaBuilder : LambdaBuilder() {
    override fun handlerBaseDirectory(module: Module, handlerElement: PsiElement): Path {
        val handlerVirtualFile = runReadAction {
            handlerElement.containingFile?.virtualFile
                ?: throw IllegalArgumentException("Handler file must be backed by a VirtualFile")
        }
        val source = inferSourceRoot(module.project, handlerVirtualFile) ?: throw IllegalStateException("Cannot locate go.mod for $handlerVirtualFile")
        return Paths.get(source.path)
    }
}


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

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

import com.goide.execution.testing.GoTestFinder
import com.goide.psi.GoFunctionDeclaration
import com.goide.psi.GoTokenType
import com.goide.psi.GoType
import com.goide.psi.GoTypeList
import com.goide.psi.impl.GoLightType
import com.goide.psi.impl.GoPsiUtil
import com.goide.stubs.index.GoFunctionIndex
import com.goide.stubs.index.GoIdFilter
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 com.intellij.psi.util.elementType
import software.aws.toolkits.jetbrains.services.lambda.LambdaHandlerResolver

class GoLambdaHandlerResolver : LambdaHandlerResolver {
    override fun findPsiElements(project: Project, handler: String, searchScope: GlobalSearchScope): Array<NavigatablePsiElement> =
        // GoFunctionDeclarationImpl is a NavigatablePsiElement
        GoFunctionIndex.find(handler, project, searchScope, GoIdFilter.getFilesFilter(searchScope))
            .filter { it.isValidHandlerIdentifier() }
            .filterIsInstance<NavigatablePsiElement>()
            .toTypedArray()

    override fun determineHandler(element: PsiElement): String? {
        // Go PSI is different, go function declarations are not leaf's like in some other
        // languages, they are CompositeElements
        val parent = element.parent
        if (parent !is GoFunctionDeclaration) {
            return null
        }

        // we only want to pick up the identifier otherwise we will get 4 gutter icons
        // `func` is also a GoTokenType and we don't have any way to extract
        // that information (without using the internal name for debug)
        if (element.elementType !is GoTokenType || element.text == "func") {
            return null
        }

        if (!parent.isValidHandlerIdentifier()) {
            return null
        }

        return parent.name
    }

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

    // see https://docs.aws.amazon.com/lambda/latest/dg/golang-handler.html for what is valid
    private fun GoFunctionDeclaration.isValidHandlerIdentifier(): Boolean {
        // disable on test files
        if (GoTestFinder.isTestFile(this.containingFile)) {
            return false
        }

        // make sure it's a top level function
        if (!GoPsiUtil.isTopLevelDeclaration(this)) {
            return false
        }

        val params = signature?.parameters?.parameterDeclarationList ?: listOf()

        // 0, 1 or 2 parameters
        if (params.size > 2) {
            return false
        }

        // if 2 parameters, first must be context.Context
        if (params.size == 2 && params.first().type?.textMatches("context.Context") != true) {
            return false
        }

        val returnType = signature?.resultType
        // 0, 1, or 2 returned values. 0 is always valid so check 1 and 2
        if (returnType is GoTypeList) {
            val types = returnType.typeList
            if ((types.size > 2) || (types.size == 2 && !types[1].textMatches("error"))) {
                return false
            }
        } else if (returnType is GoType && returnType !is GoLightType.LightVoidType && !returnType.textMatches("error")) {
            return false
        }
        return true
    }
}


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

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

import com.goide.GoLanguage
import com.goide.sdk.GoSdkService
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.module.WebModuleTypeBase
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.BuiltInRuntimeGroups
import software.aws.toolkits.jetbrains.services.lambda.SdkBasedRuntimeGroup

class GoRuntimeGroup : SdkBasedRuntimeGroup() {
    override val id: String = BuiltInRuntimeGroups.Go
    override val languageIds: Set<String> = setOf(GoLanguage.INSTANCE.id)
    override val supportsPathMappings: Boolean = false
    override fun getModuleType(): ModuleType<*> = WebModuleTypeBase.getInstance()
    override val supportedRuntimes = listOf(
        LambdaRuntime.GO1_X
    )

    override fun runtimeForSdk(sdk: Sdk): LambdaRuntime? = null
    override fun determineRuntime(project: Project): LambdaRuntime? = null
    override fun determineRuntime(module: Module): LambdaRuntime? {
        val goSdkService = GoSdkService.getInstance(module.project)
        return if (goSdkService.isGoModule(module)) {
            LambdaRuntime.GO1_X
        } else {
            null
        }
    }
}


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

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

import com.goide.project.GoProjectLibrariesService
import com.goide.sdk.GoSdkService
import com.goide.sdk.combobox.GoSdkChooserCombo
import com.goide.vgo.configuration.VgoProjectSettings
import com.intellij.facet.ui.ValidationResult
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.vfs.VirtualFile
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamAppTemplateBased
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamNewProjectSettings
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectTemplate
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectWizard
import software.aws.toolkits.jetbrains.services.lambda.wizard.SdkSelector
import software.aws.toolkits.jetbrains.utils.ui.validationInfo
import software.aws.toolkits.resources.message
import javax.swing.JComponent
import javax.swing.JLabel

class GoSamProjectWizard : SamProjectWizard {
    override fun createSdkSelectionPanel(projectLocation: TextFieldWithBrowseButton?): SdkSelector = GoSdkSelectionPanel()

    override fun listTemplates(): Collection<SamProjectTemplate> = listOf(
        SamHelloWorldGo(),
        SamEventBridgeHelloWorldGo(),
        SamEventBridgeStarterAppGo()
    )
}

class GoSdkSelectionPanel : SdkSelector {
    private val interpreterPanel = GoSdkChooserCombo()

    override fun sdkSelectionLabel() = JLabel(message("sam.init.go.sdk"))

    override fun sdkSelectionPanel(): JComponent = interpreterPanel

    override fun validateSelection(): ValidationInfo? = interpreterPanel.validator.validate(interpreterPanel.sdk)?.let {
        if (it == ValidationResult.OK) {
            return null
        }
        interpreterPanel.validationInfo(it.errorMessage)
    }

    override fun applySdkSettings(model: ModifiableRootModel) {
        GoSdkService.getInstance(model.project).setSdk(interpreterPanel.sdk)
    }
}

class SamHelloWorldGo : SamAppTemplateBased() {
    override fun postCreationAction(settings: SamNewProjectSettings, contentRoot: VirtualFile, rootModel: ModifiableRootModel, indicator: ProgressIndicator) {
        super.postCreationAction(settings, contentRoot, rootModel, indicator)
        // Turn off indexing entire gopath for the project since we are using go modules
        GoProjectLibrariesService.getInstance(rootModel.project).isIndexEntireGopath = false
        // Turn on vgo integration, required for it to resolve dependencies properly
        VgoProjectSettings.getInstance(rootModel.project).isIntegrationEnabled = true
    }

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

    override fun supportedZipRuntimes(): Set<LambdaRuntime> = setOf(LambdaRuntime.GO1_X)
    override fun supportedImageRuntimes(): Set<LambdaRuntime> = setOf(LambdaRuntime.GO1_X)

    override val appTemplateName: String = "hello-world"

    override val dependencyManager: String = "mod"
}

class SamEventBridgeHelloWorldGo : SamAppTemplateBased() {
    override fun postCreationAction(settings: SamNewProjectSettings, contentRoot: VirtualFile, rootModel: ModifiableRootModel, indicator: ProgressIndicator) {
        super.postCreationAction(settings, contentRoot, rootModel, indicator)
        // Turn off indexing entire gopath for the project since we are using go modules
        GoProjectLibrariesService.getInstance(rootModel.project).isIndexEntireGopath = false
        // Turn on vgo integration, required for it to resolve dependencies properly
        VgoProjectSettings.getInstance(rootModel.project).isIntegrationEnabled = true
    }

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

    override fun supportedZipRuntimes(): Set<LambdaRuntime> = setOf(LambdaRuntime.GO1_X)
    override fun supportedImageRuntimes() = emptySet<LambdaRuntime>()

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

    override val dependencyManager: String = "mod"
}

class SamEventBridgeStarterAppGo : SamAppTemplateBased() {
    override fun postCreationAction(settings: SamNewProjectSettings, contentRoot: VirtualFile, rootModel: ModifiableRootModel, indicator: ProgressIndicator) {
        super.postCreationAction(settings, contentRoot, rootModel, indicator)
        // Turn off indexing entire gopath for the project since we are using go modules
        GoProjectLibrariesService.getInstance(rootModel.project).isIndexEntireGopath = false
        // Turn on vgo integration, required for it to resolve dependencies properly
        VgoProjectSettings.getInstance(rootModel.project).isIntegrationEnabled = true
    }

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

    override fun supportedZipRuntimes(): Set<LambdaRuntime> = setOf(LambdaRuntime.GO1_X)
    override fun supportedImageRuntimes() = emptySet<LambdaRuntime>()

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

    override fun supportsDynamicSchemas(): Boolean = true

    override val dependencyManager: String = "mod"
}


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

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

import com.google.common.collect.BiMap
import com.google.common.collect.HashBiMap
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.javascript.debugger.LocalFileSystemFileFinder
import com.intellij.javascript.debugger.RemoteDebuggingFileFinder
import com.intellij.lang.javascript.JavascriptLanguage
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebugProcessStarter
import com.intellij.xdebugger.XDebugSession
import com.jetbrains.debugger.wip.WipLocalVmConnection
import com.jetbrains.nodeJs.NodeChromeDebugProcess
import org.jetbrains.io.LocalFileFinder
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.PathMapping
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.ImageDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.RuntimeDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.SamRunningState
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import java.net.InetSocketAddress

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

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

    override fun containerEnvVars(debugPorts: List<Int>): Map<String, String> = mapOf(
        "NODE_OPTIONS" to "--inspect-brk=0.0.0.0:${debugPorts.first()} --max-http-header-size 81920"
    )
}

class NodeJs14ImageDebug : NodeJsImageDebugSupport() {
    override val id: String = LambdaRuntime.NODEJS14_X.toString()
    override fun displayName() = LambdaRuntime.NODEJS14_X.toString().capitalize()
}

class NodeJs16ImageDebug : NodeJsImageDebugSupport() {
    override val id: String = LambdaRuntime.NODEJS16_X.toString()
    override fun displayName() = LambdaRuntime.NODEJS16_X.toString().capitalize()
}

class NodeJs18ImageDebug : NodeJsImageDebugSupport() {
    override val id: String = LambdaRuntime.NODEJS18_X.toString()
    override fun displayName() = LambdaRuntime.NODEJS18_X.toString().capitalize()
}

object NodeJsDebugUtils {
    private const val NODE_MODULES = "node_modules"

    fun createDebugProcess(
        state: SamRunningState,
        debugHost: String,
        debugPorts: List<Int>
    ): XDebugProcessStarter = object : XDebugProcessStarter() {
        override fun start(session: XDebugSession): XDebugProcess {
            val mappings = createBiMapMappings(state.pathMappings)
            val fileFinder = RemoteDebuggingFileFinder(mappings, LocalFileSystemFileFinder())

            val connection = WipLocalVmConnection()

            val process = NodeChromeDebugProcess(session, fileFinder, connection, null)

            val processHandler = process.processHandler
            val socketAddress = InetSocketAddress(debugHost, debugPorts.first())

            if (processHandler.isStartNotified) {
                connection.open(socketAddress)
            } else {
                processHandler.addProcessListener(
                    object : ProcessAdapter() {
                        override fun startNotified(event: ProcessEvent) {
                            connection.open(socketAddress)
                        }
                    }
                )
            }

            return process
        }
    }

    /**
     * Convert [PathMapping] to NodeJs debugger path mapping format.
     *
     * Docker uses the same project structure for dependencies in the folder node_modules. We map the source code and
     * the dependencies in node_modules folder separately as the node_modules might not exist in the local project.
     */
    private fun createBiMapMappings(pathMapping: List<PathMapping>): BiMap<String, VirtualFile> {
        val mappings = HashBiMap.create<String, VirtualFile>(pathMapping.size)

        listOf(".", NODE_MODULES).forEach { subPath ->
            pathMapping.forEach {
                val remotePath = FileUtil.toCanonicalPath("${it.remoteRoot}/$subPath")
                LocalFileFinder.findFile("${it.localRoot}/$subPath")?.let { localFile ->
                    mappings.putIfAbsent("file://$remotePath", localFile)
                }
            }
        }

        return mappings
    }
}


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

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

import com.intellij.lang.javascript.buildTools.npm.PackageJsonUtil
import com.intellij.openapi.vfs.VirtualFile

/**
 * WebStorm doesn't allow user to mark a folder as source root. This method infers a folder as source root based on
 * whether it has package.json file in it. If there is no package.json found in the path, content root will be returned.
 *
 * @param virtualFile The Node.js source code file.
 * @return The inferred source root that contains package.json file
 */
fun inferSourceRoot(virtualFile: VirtualFile): VirtualFile? = PackageJsonUtil.findUpPackageJson(virtualFile)?.parent


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

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

import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.lang.javascript.DialectDetector
import com.intellij.lang.typescript.compiler.TypeScriptService
import com.intellij.lang.typescript.tsconfig.TypeScriptConfig
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.psi.PsiElement
import software.aws.toolkits.core.utils.exists
import software.aws.toolkits.core.utils.inputStream
import software.aws.toolkits.core.utils.writeText
import software.aws.toolkits.jetbrains.services.lambda.Lambda
import software.aws.toolkits.jetbrains.services.lambda.LambdaBuilder
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.HandlerRunSettings
import software.aws.toolkits.jetbrains.services.lambda.steps.BuildLambdaRequest
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.Step
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.resources.message
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

class NodeJsLambdaBuilder : LambdaBuilder() {
    private open class JsLambdaBuilder : LambdaBuilder() {
        override fun handlerBaseDirectory(module: Module, handlerElement: PsiElement): Path =
            getSourceRoot(handlerElement)
    }

    private class TsLambdaBuilder : JsLambdaBuilder() {
        override fun handlerForDummyTemplate(settings: HandlerRunSettings, handlerElement: PsiElement): String {
            val handler = super.handlerForDummyTemplate(settings, handlerElement)

            return "$TS_BUILD_DIR/$handler"
        }

        override fun buildFromHandler(project: Project, settings: HandlerRunSettings): BuildLambdaRequest {
            val buildRequest = super.buildFromHandler(project, settings)
            val handlerElement = Lambda.findPsiElementsForHandler(project, settings.runtime, settings.handler).first()
            val sourceRoot = getSourceRoot(handlerElement)

            val buildWithTsStep = object : Step() {
                override val stepName = message("lambda.build.typescript.compiler.step")

                override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
                    // relative to source root so that SAM build can copy it over to the correct place
                    val tsOutput = sourceRoot.resolve(TS_BUILD_DIR).normalize().toAbsolutePath().toString()
                    // relative to existing tsconfig because there is no other option https://github.com/microsoft/TypeScript/issues/25430
                    val tsConfig = sourceRoot.resolve(TS_CONFIG_FILE)

                    // read base ts config into mutable map
                    fun loadBaseConfig(tsConfig: Path): MutableMap<String, Any>? {
                        if (!tsConfig.exists()) {
                            return null
                        }

                        try {
                            val tsConfigMap: MutableMap<String, Any> = MAPPER.readValue(tsConfig.inputStream())
                            stepEmitter.emitMessageLine(message("lambda.build.typescript.compiler.using_base", tsConfig), false)
                            return tsConfigMap
                        } catch (e: JsonProcessingException) {
                            stepEmitter.emitMessageLine(message("lambda.build.typescript.compiler.using_base_error", tsConfig), true)
                        }

                        return null
                    }
                    val tsConfigMap = loadBaseConfig(tsConfig)
                        ?: loadBaseConfig(sourceRoot.resolve(TS_CONFIG_INITIAL_BASE_FILE))
                        ?: mutableMapOf()

                    // will create config from scratch if no base config has been loaded
                    if (tsConfigMap.isEmpty()) {
                        stepEmitter.emitMessageLine(message("lambda.build.typescript.compiler.creating_config"), false)
                    }

                    // use initial skeleton for compilerOptions if it does not exist
                    val compilerOptions = tsConfigMap[TypeScriptConfig.COMPILER_OPTIONS_PROPERTY] as? MutableMap<String, Any>
                        ?: mutableMapOf<String, Any>(
                            TypeScriptConfig.TARGET_OPTION to TypeScriptConfig.LanguageTarget.ES6.libName,
                            TypeScriptConfig.MODULE to TypeScriptConfig.MODULE_COMMON_JS,
                            TypeScriptConfig.SOURCE_MAP to true
                        )
                    tsConfigMap[TypeScriptConfig.COMPILER_OPTIONS_PROPERTY] = compilerOptions

                    // overwrite outDir, rootDir, sourceRoot
                    compilerOptions[TypeScriptConfig.OUT_DIR] = tsOutput
                    compilerOptions[TypeScriptConfig.ROOT_DIR] = "."
                    compilerOptions["sourceRoot"] = sourceRoot.toString()

                    // ensure typeRoots has resolved path
                    val typeRoots = compilerOptions[TypeScriptConfig.TYPE_ROOTS] as? MutableList<String> ?: mutableListOf()
                    typeRoots.add(sourceRoot.resolve(TypeScriptConfig.DEFAULT_TYPES_DIRECTORY).toString())
                    compilerOptions[TypeScriptConfig.TYPE_ROOTS] = typeRoots.toSet().toList()

                    // ensure types has node
                    val types = compilerOptions[TypeScriptConfig.TYPES] as? MutableList<String> ?: mutableListOf()
                    types.add("node")
                    compilerOptions[TypeScriptConfig.TYPES] = types.toSet().toList()

                    // pretty print the merged result
                    if (!tsConfig.exists()) {
                        Files.createFile(tsConfig)
                    }
                    tsConfig.writeText(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(tsConfigMap))

                    val tsConfigVirtualFile = VfsUtil.findFile(tsConfig, true) ?: throw RuntimeException("Could not find temporary tsconfig file using VFS")
                    val tsService = TypeScriptService.getCompilerServiceForFile(project, handlerElement.containingFile.virtualFile)

                    stepEmitter.emitMessageLine(message("lambda.build.typescript.compiler.running", tsConfig), false)
                    val compilerFuture = tsService?.compileConfigProjectAndGetErrors(tsConfigVirtualFile)
                    val results = compilerFuture?.get()
                        ?: throw RuntimeException(message("lambda.build.typescript.compiler.ide_error"))

                    stepEmitter.emitMessageLine(message("lambda.build.typescript.compiler.processed_files"), false)
                    results.processedFiles.forEach {
                        stepEmitter.emitMessageLine(it, false)
                    }

                    stepEmitter.emitMessageLine(message("lambda.build.typescript.compiler.emitted_files"), false)
                    results.emittedFiles.forEach {
                        stepEmitter.emitMessageLine(it, false)
                    }

                    // errors, warnings, etc
                    stepEmitter.emitMessageLine(message("lambda.build.typescript.compiler.annotation_results"), false)
                    results.annotationResults.forEach {
                        val isError = it.severity >= HighlightSeverity.WARNING
                        val location = "${it.absoluteFilePath ?: '?'}:${it.line + 1}:"
                        stepEmitter.emitMessageLine(location + it.description, isError)
                    }
                }
            }

            return buildRequest.copy(
                preBuildSteps = buildRequest.preBuildSteps + listOf(buildWithTsStep)
            )
        }
    }

    private val jsLambdaBuilder = JsLambdaBuilder()
    private val tsLambdaBuilder = TsLambdaBuilder()

    private fun determineBuilder(handlerElement: PsiElement) =
        runReadAction {
            if (DialectDetector.isTypeScript(handlerElement)) {
                tsLambdaBuilder
            } else {
                jsLambdaBuilder
            }
        }

    override fun handlerBaseDirectory(module: Module, handlerElement: PsiElement) =
        determineBuilder(handlerElement).handlerBaseDirectory(module, handlerElement)

    override fun handlerForDummyTemplate(settings: HandlerRunSettings, handlerElement: PsiElement) =
        determineBuilder(handlerElement).handlerForDummyTemplate(settings, handlerElement)

    override fun buildFromHandler(project: Project, settings: HandlerRunSettings): BuildLambdaRequest {
        val handlerElement = Lambda.findPsiElementsForHandler(project, settings.runtime, settings.handler).first()

        return determineBuilder(handlerElement).buildFromHandler(project, settings)
    }

    companion object {
        private const val TS_BUILD_DIR = "aws-toolkit-ts-output"
        private const val TS_CONFIG_FILE = "aws-toolkit-tsconfig.json"

        // use project tsconfig.json as initial base - if unable to parse existing config
        private const val TS_CONFIG_INITIAL_BASE_FILE = "tsconfig.json"

        private val MAPPER = jacksonObjectMapper()

        private fun getSourceRoot(handlerElement: PsiElement): Path {
            val handlerVirtualFile = runReadAction { handlerElement.containingFile?.virtualFile }
                ?: throw IllegalArgumentException("Handler file must be backed by a VirtualFile")
            val sourceRootVirtualFile = inferSourceRoot(handlerVirtualFile)
                ?: throw IllegalStateException("Cannot locate package.json for $handlerVirtualFile")
            return Paths.get(sourceRootVirtualFile.path)
        }
    }
}


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

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

import com.intellij.lang.javascript.JSTokenTypes
import com.intellij.lang.javascript.psi.JSAssignmentExpression
import com.intellij.lang.javascript.psi.JSDefinitionExpression
import com.intellij.lang.javascript.psi.JSFunction
import com.intellij.lang.javascript.psi.JSReferenceExpression
import com.intellij.lang.javascript.psi.ecma6.TypeScriptFunction
import com.intellij.lang.javascript.psi.ecma6.TypeScriptVariable
import com.intellij.lang.javascript.psi.ecmal4.JSAttributeList
import com.intellij.lang.javascript.psi.ecmal4.JSQualifiedNamedElement
import com.intellij.lang.javascript.psi.resolve.JSClassResolver
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.NavigatablePsiElement
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import software.aws.toolkits.jetbrains.services.lambda.LambdaHandlerResolver

class NodeJsLambdaHandlerResolver : LambdaHandlerResolver {
    override fun findPsiElements(
        project: Project,
        handler: String,
        searchScope: GlobalSearchScope
    ): Array<NavigatablePsiElement> {
        val lastDotIndex = handler.lastIndexOf(".")
        if (lastDotIndex < 0) {
            return NavigatablePsiElement.EMPTY_NAVIGATABLE_ELEMENT_ARRAY
        }
        val fileName = handler.substring(0, lastDotIndex)
        val elementName = handler.substring(lastDotIndex + 1)

        return JSClassResolver.findElementsByNameIncludingImplicit(elementName, searchScope, false)
            .filter { it.isValidHandlerElement(fileName) }
            .toTypedArray()
    }

    /**
     * Whether the element is a valid Lambda handler found by [JSClassResolver] through the handler name.
     */
    private fun PsiElement.isValidHandlerElement(fileName: String): Boolean {
        val virtualFile = this.containingFile.virtualFile ?: return false
        val sourceRoot = inferSourceRoot(virtualFile) ?: return false

        val relativePath = VfsUtilCore.findRelativePath(sourceRoot, virtualFile, '/') ?: return false
        return this is NavigatablePsiElement &&
            (this.parent?.isValidJsLambdaHandler() == true || this.isValidTypeScriptLambdaHandler()) &&
            FileUtilRt.getNameWithoutExtension(relativePath) == fileName
    }

    // NodeJs lambda handler string format should be: parent/folders/file.handler and handler element should follow
    // https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html
    override fun determineHandler(element: PsiElement): String? {
        if (!element.isValidHandlerIdentifier()) {
            return null
        }

        val virtualFile = element.containingFile.virtualFile ?: return null

        val sourceRoot = inferSourceRoot(virtualFile) ?: return null
        val relativePath = VfsUtilCore.findRelativePath(sourceRoot, virtualFile, '/') ?: return null
        val prefix = FileUtilRt.getNameWithoutExtension(relativePath)
        val handlerName = element.text

        return "$prefix.$handlerName"
    }

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

    /**
     * Whether the element is a valid lambda handler identifier.
     */
    private fun PsiElement.isValidHandlerIdentifier(): Boolean {
        if (this.node?.elementType != JSTokenTypes.IDENTIFIER) {
            return false
        }

        if (this.parent is TypeScriptVariable || this.parent is TypeScriptFunction) {
            // in TS, exports are modifiers rather than the target of an assignment expression
            return this.parent.isValidTypeScriptLambdaHandler()
        }

        val exportsDefinition = this.parent?.parent ?: return false

        if (!exportsDefinition.isExportsDefinition()) {
            return false
        }

        val lambdaHandlerAssignment = exportsDefinition.parent as? JSAssignmentExpression ?: return false

        if (lambdaHandlerAssignment.rOperand?.isLambdaFunctionExpression() != true) {
            return false
        }
        return true
    }

    /**
     * Whether the element is top level PSI element for a valid Lambda handler. It must be in the format as:
     * export const lambdaHandler = functionExpression
     * or
     * export function lambdaHandler(...) { ... }
     */
    private fun PsiElement.isValidTypeScriptLambdaHandler(): Boolean {
        if ((this as? JSQualifiedNamedElement)?.isExported != true) {
            return false
        }

        return when (this) {
            is TypeScriptVariable -> this.lastChild.isLambdaFunctionExpression()
            is TypeScriptFunction -> this.isLambdaFunctionExpression()
            else -> false
        }
    }

    /**
     * Whether the element is top level PSI element for a valid Lambda handler. It must be in the format as:
     * exports.lambdaHandler = functionExpression
     */
    private fun PsiElement.isValidJsLambdaHandler(): Boolean =
        this is JSAssignmentExpression &&
            this.lOperand?.isExportsDefinition() == true &&
            this.rOperand?.isLambdaFunctionExpression() == true

    /**
     * Whether the element is a left-hand operand for a valid Lambda handler assignment. It should be in the format as:
     * exports.lambdaHandler
     */
    private fun PsiElement.isExportsDefinition(): Boolean =
        this is JSDefinitionExpression &&
            this.children.size == 1 &&
            this.children[0].isExportsReference()

    // Whether the element is function element that follows AWS Lambda function format. It allows non-async functions to
    // have at most 3 parameters while async functions 2.
    private fun PsiElement.isLambdaFunctionExpression(): Boolean {
        if (this !is JSFunction) return false
        val parameterSize = this.parameters.size
        val isAsyncFunction = this.attributeList?.hasModifier(JSAttributeList.ModifierType.ASYNC) == true
        return (isAsyncFunction && parameterSize <= 2) || (!isAsyncFunction && parameterSize <= 3)
    }

    // Whether the element is exports reference element in the format of exports.lambdaHandler
    private fun PsiElement.isExportsReference(): Boolean =
        this is JSReferenceExpression &&
            this.children.size == 3 &&
            this.children[0].isExports() &&
            this.children[1].node?.elementType == JSTokenTypes.DOT &&
            this.children[2].node?.elementType == JSTokenTypes.IDENTIFIER

    // Whether the element is exports reference
    private fun PsiElement.isExports(): Boolean =
        this is JSReferenceExpression &&
            this.children.size == 1 &&
            this.children[0].node?.elementType == JSTokenTypes.IDENTIFIER &&
            this.children[0].text == "exports"
}


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

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

import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager
import com.intellij.lang.javascript.JavaScriptSupportLoader
import com.intellij.lang.javascript.JavascriptLanguage
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.module.WebModuleTypeBase
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.BuiltInRuntimeGroups
import software.aws.toolkits.jetbrains.services.lambda.SdkBasedRuntimeGroup

class NodeJsRuntimeGroup : SdkBasedRuntimeGroup() {
    override val id: String = BuiltInRuntimeGroups.NodeJs
    override val languageIds: Set<String> = setOf(
        JavascriptLanguage.INSTANCE.id,
        JavaScriptSupportLoader.ECMA_SCRIPT_6.id
    )
    override val supportsPathMappings: Boolean = true

    override val supportedRuntimes = listOf(
        LambdaRuntime.NODEJS14_X,
        LambdaRuntime.NODEJS16_X,
        LambdaRuntime.NODEJS18_X
    )

    override fun determineRuntime(module: Module): LambdaRuntime? = determineRuntime(module.project)

    override fun determineRuntime(project: Project): LambdaRuntime? =
        NodeJsInterpreterManager.getInstance(project).interpreter?.cachedVersion?.get()?.let {
            when {
                it.major <= 14 -> LambdaRuntime.NODEJS14_X
                it.major <= 16 -> LambdaRuntime.NODEJS16_X
                it.major <= 18 -> LambdaRuntime.NODEJS18_X
                else -> null
            }
        }

    /**
     * JavaScript does not define SDK. We override [determineRuntime] for fetching the correct Runtime.
     */
    override fun runtimeForSdk(sdk: Sdk): LambdaRuntime? = null

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


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

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

import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterField
import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager
import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterRef
import com.intellij.lang.javascript.dialects.JSLanguageLevel
import com.intellij.lang.javascript.settings.JSRootConfiguration
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamAppTemplateBased
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectTemplate
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectWizard
import software.aws.toolkits.jetbrains.services.lambda.wizard.SdkSelector
import software.aws.toolkits.jetbrains.utils.ui.validationInfo
import software.aws.toolkits.resources.message
import javax.swing.JComponent
import javax.swing.JLabel

class NodeJsSamProjectWizard : SamProjectWizard {
    override fun createSdkSelectionPanel(projectLocation: TextFieldWithBrowseButton?): SdkSelector? = NodeJsSdkSelectionPanel()

    override fun listTemplates(): Collection<SamProjectTemplate> = listOf(
        SamHelloWorldNodeJs(),
        SamHelloWorldNodeJsTypeScript()
    )
}

class NodeJsSdkSelectionPanel : SdkSelector {
    private val interpreterPanel = createInterpreterField()

    private fun createInterpreterField(): NodeJsInterpreterField {
        val project = ProjectManager.getInstance().defaultProject
        return object : NodeJsInterpreterField(project, false) {
            override fun isDefaultProjectInterpreterField(): Boolean = true
        }
    }

    override fun sdkSelectionLabel() = JLabel(message("sam.init.node_interpreter.label"))

    override fun sdkSelectionPanel(): JComponent = interpreterPanel

    override fun applySdkSettings(model: ModifiableRootModel) {
        NodeJsInterpreterManager.getInstance(model.project).setInterpreterRef(NodeJsInterpreterRef.create(interpreterPanel.interpreter))
        JSRootConfiguration.getInstance(model.project).storeLanguageLevelAndUpdateCaches(JSLanguageLevel.ES6)
    }

    override fun validateSelection(): ValidationInfo? = interpreterPanel.interpreter?.validate(null)?.let {
        interpreterPanel.validationInfo(it)
    }
}

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

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

    override fun supportedZipRuntimes(): Set<LambdaRuntime> = setOf(
        LambdaRuntime.NODEJS14_X,
        LambdaRuntime.NODEJS16_X,
        LambdaRuntime.NODEJS18_X
    )

    override fun supportedImageRuntimes(): Set<LambdaRuntime> = setOf(
        LambdaRuntime.NODEJS14_X,
        LambdaRuntime.NODEJS16_X,
        LambdaRuntime.NODEJS18_X
    )

    override val appTemplateName: String = "hello-world"

    override val dependencyManager: String = "npm"
}

class SamHelloWorldNodeJsTypeScript : SamAppTemplateBased() {
    override fun displayName() = message("sam.init.template.hello_world_typescript.name")

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

    override fun supportedZipRuntimes(): Set<LambdaRuntime> = setOf(
        LambdaRuntime.NODEJS14_X,
        LambdaRuntime.NODEJS16_X,
        LambdaRuntime.NODEJS18_X
    )

    override fun supportedImageRuntimes(): Set<LambdaRuntime> = setOf(
        LambdaRuntime.NODEJS14_X,
        LambdaRuntime.NODEJS16_X,
        LambdaRuntime.NODEJS18_X
    )

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

    override val dependencyManager: String = "npm"
}


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

package software.aws.toolkits.jetbrains.services.rds

data class RdsDatasourceConfiguration(
    val regionId: String,
    val credentialId: String,
    val database: RdsDatabase,
    val username: String
)


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

package software.aws.toolkits.jetbrains.services.rds

import com.intellij.database.dataSource.DataSourceSslConfiguration
import com.intellij.icons.AllIcons
import icons.AwsIcons
import software.aws.toolkits.jetbrains.datagrip.RequireSsl
import software.aws.toolkits.resources.message
import javax.swing.Icon

/**
 * The [additionalInfo] shows as [software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode.statusText] if present
 * TODO: This can probably ultimately become [DbEngine] and encompass Redshift functionality too
 */
sealed class RdsEngine(val engines: Set<String>, val icon: Icon, val additionalInfo: String?) {
    /**
     * Create a connection string, using the passed hostname/port [endpoint] parameter
     */
    abstract fun connectionStringUrl(endpoint: String): String

    /**
     * Some engines need to manipulate the [username] (e.g. Postgres should be lower-cased), this hook allows that
     */
    open fun iamUsername(username: String) = username

    /**
     * SSL is not turned on by default by DataGrip. We would like it to be turned on, and Aurora MySQL requires it turned on, so
     * we have to patch it into the configuration. Postgres breaks by default with ssl (due to requring adding a trust store manually),
     * so we have to configure it on a per database basis.
     */
    open fun sslConfig(): DataSourceSslConfiguration? = null

    companion object {
        fun values(): Set<RdsEngine> = setOf(Maria, MySql, AuroraMySql, Postgres, AuroraPostgres)
        fun fromEngine(engine: String) = values().find { it.engines.contains(engine) } ?: throw IllegalArgumentException("Unknown RDS engine $engine")
    }
}

abstract class MySqlBase(engine: Set<String>, additionalInfo: String? = null) : RdsEngine(engine, AwsIcons.Resources.Rds.MYSQL, additionalInfo) {
    override fun sslConfig(): DataSourceSslConfiguration = RequireSsl
}

abstract class PostgresBase(engine: Set<String>, additionalInfo: String? = null) : RdsEngine(engine, AwsIcons.Resources.Rds.POSTGRES, additionalInfo) {
    override fun connectionStringUrl(endpoint: String) = "jdbc:$JDBC_POSTGRES://$endpoint/"

    /**
     * In postgres this is case sensitive as lower case. If you add a db user for
     * IAM role "Admin", it is inserted to the db as "admin"
     */
    override fun iamUsername(username: String) = username.lowercase()
}

object Maria : RdsEngine(setOf("mariadb"), AllIcons.Providers.Mariadb, null) {
    override fun sslConfig(): DataSourceSslConfiguration = RequireSsl
    override fun connectionStringUrl(endpoint: String) = "jdbc:$JDBC_MARIADB://$endpoint/"
}

object MySql : MySqlBase(setOf("mysql")) {
    override fun connectionStringUrl(endpoint: String) = "jdbc:$JDBC_MYSQL://$endpoint/"
}

// aurora-mysql == 5.7
object AuroraMySql : MySqlBase(setOf("aurora-mysql"), additionalInfo = message("rds.aurora")) {
    // The docs recommend using MariaDB instead of MySQL to connect to MySQL Aurora DBs:
    // https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Connecting.html#Aurora.Connecting.AuroraMySQL
    override fun connectionStringUrl(endpoint: String) = "jdbc:$JDBC_MYSQL_AURORA://$endpoint/"
}

object Postgres : PostgresBase(setOf("postgres"))

object AuroraPostgres : PostgresBase(setOf("aurora-postgresql"), additionalInfo = message("rds.aurora"))


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

package software.aws.toolkits.jetbrains.services.rds

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.rds.RdsClient
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.AwsExplorerServiceRootNode
import software.aws.toolkits.jetbrains.core.getResourceNow
import software.aws.toolkits.jetbrains.core.utils.buildMap
import software.aws.toolkits.jetbrains.services.rds.resources.LIST_SUPPORTED_CLUSTERS
import software.aws.toolkits.jetbrains.services.rds.resources.LIST_SUPPORTED_INSTANCES
import software.aws.toolkits.resources.message

class RdsExplorerParentNode(project: Project, service: AwsExplorerServiceNode) : AwsExplorerServiceRootNode(project, service) {
    override fun displayName(): String = message("explorer.node.rds")

    override fun getChildrenInternal(): List<AwsExplorerNode<*>> = buildMap<String, RdsDatabase> {
        // De-dupe by db identifier
        nodeProject.getResourceNow(LIST_SUPPORTED_CLUSTERS).forEach { putIfAbsent(it.identifier, it) }
        nodeProject.getResourceNow(LIST_SUPPORTED_INSTANCES).forEach { putIfAbsent(it.identifier, it) }
    }.values.map {
        RdsNode(nodeProject, it)
    }
}

class RdsNode(project: Project, val database: RdsDatabase, private val rdsEngine: RdsEngine = database.rdsEngine()) : AwsExplorerResourceNode<String>(
    project,
    RdsClient.SERVICE_NAME,
    database.arn,
    rdsEngine.icon
) {
    override fun displayName(): String = database.identifier
    override fun resourceArn(): String = database.arn
    override fun resourceType(): String = "instance"
    override fun statusText(): String? = rdsEngine.additionalInfo
}


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

package software.aws.toolkits.jetbrains.services.rds

// These are the member engine in DBInstance, but it is a string
const val MYSQL_ENGINE_TYPE = "mysql"
const val POSTGRES_ENGINE_TYPE = "postgres"

const val JDBC_MYSQL = "mysql"
const val JDBC_MYSQL_AURORA = "mysql:aurora"
const val JDBC_MARIADB = "mariadb"
const val JDBC_POSTGRES = "postgresql"

object RdsResources {
    private val RDS_REGION_REGEX = """.*\.(.+).rds\.""".toRegex()

    fun extractRegionFromUrl(url: String?): String? = url?.let { RDS_REGION_REGEX.find(url)?.groupValues?.get(1) }
}

data class RdsDatabase(
    val identifier: String,
    val arn: String,
    val engine: String,
    val iamDatabaseAuthenticationEnabled: Boolean,
    val endpoint: Endpoint,
    val masterUsername: String
) {
    fun rdsEngine() = RdsEngine.fromEngine(engine)
}

data class Endpoint(val host: String, val port: Int)


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

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

import com.intellij.database.DatabaseBundle
import com.intellij.database.autoconfig.DataSourceRegistry
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.progress.PerformInBackgroundOption
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.credentials.activeCredentialProvider
import software.aws.toolkits.jetbrains.core.credentials.activeRegion
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleExplorerNodeAction
import software.aws.toolkits.jetbrains.core.getResourceNow
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.datagrip.CREDENTIAL_ID_PROPERTY
import software.aws.toolkits.jetbrains.datagrip.REGION_ID_PROPERTY
import software.aws.toolkits.jetbrains.services.rds.RdsDatasourceConfiguration
import software.aws.toolkits.jetbrains.services.rds.RdsNode
import software.aws.toolkits.jetbrains.services.rds.auth.IamAuth
import software.aws.toolkits.jetbrains.services.rds.auth.RDS_SIGNING_HOST_PROPERTY
import software.aws.toolkits.jetbrains.services.rds.auth.RDS_SIGNING_PORT_PROPERTY
import software.aws.toolkits.jetbrains.services.sts.StsResources
import software.aws.toolkits.jetbrains.utils.actions.OpenBrowserAction
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DatabaseCredentials
import software.aws.toolkits.telemetry.RdsTelemetry
import software.aws.toolkits.telemetry.Result

class CreateIamDataSourceAction : SingleExplorerNodeAction<RdsNode>(message("rds.iam_config")), DumbAware {
    override fun actionPerformed(selected: RdsNode, e: AnActionEvent) {
        if (!checkPrerequisites(selected)) {
            return
        }
        object : Task.Backgroundable(
            selected.nodeProject,
            DatabaseBundle.message("message.text.refreshing.data.source"),
            true,
            PerformInBackgroundOption.ALWAYS_BACKGROUND
        ) {
            override fun run(indicator: ProgressIndicator) {
                val registry = DataSourceRegistry(selected.nodeProject)
                createDatasource(selected, registry)
                // Asynchronously show the user the configuration dialog to let them save/edit/test the profile
                runInEdt {
                    registry.showDialog()
                }
            }

            override fun onCancel() = recordTelemetry(Result.Cancelled)
            override fun onThrowable(error: Throwable) = recordTelemetry(Result.Failed)
            override fun onSuccess() = recordTelemetry(Result.Succeeded)

            private fun recordTelemetry(result: Result) = RdsTelemetry.createConnectionConfiguration(
                selected.nodeProject,
                result,
                DatabaseCredentials.IAM,
                selected.database.engine
            )
        }.queue()
    }

    internal fun checkPrerequisites(node: RdsNode): Boolean {
        // Assert IAM auth enabled
        if (!node.database.iamDatabaseAuthenticationEnabled) {
            notifyError(
                project = node.nodeProject,
                title = message("aws.notification.title"),
                content = message("rds.validation.no_iam_auth", node.database.identifier),
                action = OpenBrowserAction(message("rds.validation.setup_guide"), null, HelpIds.RDS_SETUP_IAM_AUTH.url)
            )
            return false
        }
        return true
    }

    internal fun createDatasource(node: RdsNode, registry: DataSourceRegistry) {
        val username = try {
            // use current STS user as username. Split on : because it comes back id:username
            node.nodeProject.getResourceNow(StsResources.USER).substringAfter(':')
        } catch (e: Exception) {
            LOG.warn(e) { "Getting username from STS failed, falling back to master username" }
            node.database.masterUsername
        }
        registry.createRdsDatasource(
            RdsDatasourceConfiguration(
                regionId = node.nodeProject.activeRegion().id,
                credentialId = node.nodeProject.activeCredentialProvider().id,
                database = node.database,
                username = username
            )
        )
    }

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

fun DataSourceRegistry.createRdsDatasource(config: RdsDatasourceConfiguration) {
    val engine = config.database.rdsEngine()
    val port = config.database.endpoint.port
    val host = config.database.endpoint.host
    val endpoint = "$host:$port"

    builder
        .withJdbcAdditionalProperty(CREDENTIAL_ID_PROPERTY, config.credentialId)
        .withJdbcAdditionalProperty(REGION_ID_PROPERTY, config.regionId)
        .withJdbcAdditionalProperty(RDS_SIGNING_HOST_PROPERTY, host)
        .withJdbcAdditionalProperty(RDS_SIGNING_PORT_PROPERTY, port.toString())
        .withUrl(engine.connectionStringUrl(endpoint))
        .withUser(engine.iamUsername(config.username))
        .commit()
    // TODO FIX_WHEN_MIN_IS_203 set auth provider ID in builder. It's in 202 but doesn't work
    newDataSources.firstOrNull()?.let {
        it.authProviderId = IamAuth.providerId
        it.sslCfg = engine.sslConfig()
    } ?: throw IllegalStateException("Newly inserted data source is not in the data source registry!")
}


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

package software.aws.toolkits.jetbrains.services.rds.auth

import com.intellij.credentialStore.Credentials
import com.intellij.database.access.DatabaseCredentials
import com.intellij.database.dataSource.DatabaseAuthProvider.AuthWidget
import com.intellij.database.dataSource.DatabaseConnectionInterceptor.ProtoConnection
import com.intellij.database.dataSource.DatabaseCredentialsAuthProvider
import com.intellij.database.dataSource.LocalDataSource
import com.intellij.openapi.project.Project
import kotlinx.coroutines.future.future
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.rds.RdsUtilities
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.datagrip.auth.compatability.DatabaseAuthProviderCompatabilityAdapter
import software.aws.toolkits.jetbrains.datagrip.auth.compatability.project
import software.aws.toolkits.jetbrains.datagrip.getAwsConnectionSettings
import software.aws.toolkits.jetbrains.datagrip.getDatabaseEngine
import software.aws.toolkits.jetbrains.datagrip.hostFromJdbcString
import software.aws.toolkits.jetbrains.datagrip.iamIsApplicable
import software.aws.toolkits.jetbrains.datagrip.portFromJdbcString
import software.aws.toolkits.jetbrains.datagrip.validateIamConfiguration
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DatabaseCredentials.IAM
import software.aws.toolkits.telemetry.RdsTelemetry
import software.aws.toolkits.telemetry.Result
import java.util.concurrent.CompletionStage

data class RdsAuth(
    val address: String,
    val port: Int,
    val user: String,
    val connectionSettings: ConnectionSettings
)

// [DatabaseAuthProvider] is marked as internal, but JetBrains advised this was a correct usage
class IamAuth : DatabaseAuthProviderCompatabilityAdapter {
    private val rdsUtilities = RdsUtilities.builder().build()

    override fun getId(): String = providerId
    override fun getDisplayName(): String = message("rds.iam_connection_display_name")

    override fun isApplicable(dataSource: LocalDataSource): Boolean = iamIsApplicable(dataSource)

    override fun createWidget(project: Project?, credentials: DatabaseCredentials, dataSource: LocalDataSource): AuthWidget? = IamAuthWidget()

    override fun intercept(
        connection: ProtoConnection,
        silent: Boolean
    ): CompletionStage<ProtoConnection>? {
        LOG.info { "Intercepting db connection [$connection]" }
        val project = connection.project()
        val scope = projectCoroutineScope(project)
        return scope.future {
            var result = Result.Succeeded
            try {
                val credentials = getCredentials(connection)
                DatabaseCredentialsAuthProvider.applyCredentials(connection, credentials, true)
            } catch (e: Throwable) {
                result = Result.Failed
                throw e
            } finally {
                RdsTelemetry.getCredentials(project, result, IAM, connection.getDatabaseEngine())
            }
        }
    }

    internal fun getAuthInformation(connection: ProtoConnection): RdsAuth {
        validateIamConfiguration(connection)
        val signingUrl = connection.connectionPoint.additionalProperties[RDS_SIGNING_HOST_PROPERTY]
            ?: connection.connectionPoint.url.hostFromJdbcString()
            ?: throw IllegalArgumentException(message("rds.validation.no_instance_host"))
        val signingPort = connection.connectionPoint.additionalProperties[RDS_SIGNING_PORT_PROPERTY]?.toIntOrNull()
            ?: connection.connectionPoint.url.portFromJdbcString()?.toIntOrNull()
            ?: throw IllegalArgumentException(message("rds.validation.no_instance_port"))
        val user = connection.connectionPoint.dataSource.username

        if (user.isBlank()) {
            throw IllegalArgumentException(message("rds.validation.username"))
        }

        return RdsAuth(
            signingUrl,
            signingPort,
            user,
            connection.getAwsConnectionSettings()
        )
    }

    internal fun generateAuthToken(auth: RdsAuth): String = rdsUtilities.generateAuthenticationToken {
        it.credentialsProvider(auth.connectionSettings.credentials)
        it.region(Region.of(auth.connectionSettings.region.id))
        it.hostname(auth.address)
        it.port(auth.port)
        it.username(auth.user)
    }

    private fun getCredentials(connection: ProtoConnection): Credentials {
        val authInformation = getAuthInformation(connection)
        val authToken = generateAuthToken(authInformation)
        return Credentials(authInformation.user, authToken)
    }

    companion object {
        const val providerId = "aws.rds.iam"
        private val LOG = getLogger<IamAuth>()
    }
}


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

package software.aws.toolkits.jetbrains.services.rds.auth

import com.intellij.database.dataSource.DataSourceUiUtil
import com.intellij.database.dataSource.LocalDataSource
import com.intellij.database.dataSource.url.template.ParametersHolder
import com.intellij.database.dataSource.url.ui.UrlPropertiesPanel
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import software.amazon.awssdk.services.rds.RdsClient
import software.aws.toolkits.jetbrains.services.rds.RdsResources
import software.aws.toolkits.jetbrains.ui.AwsAuthWidget
import software.aws.toolkits.resources.message
import javax.swing.JPanel

const val RDS_SIGNING_HOST_PROPERTY = "AWS.RdsSigningHost"
const val RDS_SIGNING_PORT_PROPERTY = "AWS.RdsSigningPort"

class IamAuthWidget : AwsAuthWidget() {
    private val rdsSigningHostField = JBTextField()
    private val rdsSigningPortField = JBTextField()

    override val rowCount = 4
    override fun getRegionFromUrl(url: String?): String? = RdsResources.extractRegionFromUrl(url)
    override val serviceId: String = RdsClient.SERVICE_NAME

    override fun createPanel(): JPanel {
        val panel = super.createPanel()
        val rdsSigningHostLabel = JBLabel(message("rds.url")).apply { toolTipText = message("rds.iam_help") }
        val rdsSigningPortLabel = JBLabel(message("rds.port")).apply { toolTipText = message("rds.iam_help") }
        panel.add(rdsSigningHostLabel, UrlPropertiesPanel.createLabelConstraints(3, 0, rdsSigningHostLabel.preferredSize.getWidth()))
        panel.add(rdsSigningHostField, UrlPropertiesPanel.createSimpleConstraints(3, 1, 3))
        panel.add(rdsSigningPortLabel, UrlPropertiesPanel.createLabelConstraints(3, 4, rdsSigningPortLabel.preferredSize.getWidth()))
        panel.add(rdsSigningPortField, UrlPropertiesPanel.createSimpleConstraints(3, 5, 1))
        return panel
    }

    override fun save(dataSource: LocalDataSource, copyCredentials: Boolean) {
        super.save(dataSource, copyCredentials)

        // If the user has not specified a signing host/port we will try to use the URL in the connection
        val host = if (rdsSigningHostField.text.isNullOrBlank()) {
            null
        } else {
            rdsSigningHostField.text
        }
        DataSourceUiUtil.putOrRemove(
            dataSource.additionalProperties,
            RDS_SIGNING_HOST_PROPERTY,
            host
        )

        val port = if (rdsSigningPortField.text.isNullOrBlank()) {
            null
        } else {
            rdsSigningPortField.text
        }
        DataSourceUiUtil.putOrRemove(
            dataSource.additionalProperties,
            RDS_SIGNING_PORT_PROPERTY,
            port
        )
    }

    override fun reset(dataSource: LocalDataSource, resetCredentials: Boolean) {
        super.reset(dataSource, resetCredentials)
        rdsSigningHostField.text = dataSource.additionalProperties[RDS_SIGNING_HOST_PROPERTY]
        rdsSigningPortField.text = dataSource.additionalProperties[RDS_SIGNING_PORT_PROPERTY]
    }

    override fun updateFromUrl(holder: ParametersHolder) {
        super.updateFromUrl(holder)
        holder.getParameter(hostParameter)?.let {
            rdsSigningHostField.emptyText.text = it
        }
        holder.getParameter(portParameter)?.let {
            rdsSigningPortField.emptyText.text = it
        }
    }
}


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

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

import software.amazon.awssdk.services.rds.RdsClient
import software.amazon.awssdk.services.rds.model.Filter
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.Resource
import software.aws.toolkits.jetbrains.services.rds.Endpoint
import software.aws.toolkits.jetbrains.services.rds.RdsDatabase
import software.aws.toolkits.jetbrains.services.rds.RdsEngine

// Filters are also just a string
private const val ENGINE_FILTER = "engine"

val LIST_SUPPORTED_INSTANCES: Resource.Cached<List<RdsDatabase>> = ClientBackedCachedResource(RdsClient::class, "rds.list_supported_instances") {
    describeDBInstancesPaginator {
        it.filters(
            Filter.builder()
                .name(ENGINE_FILTER)
                .values(
                    RdsEngine
                        .values()
                        .flatMap { e -> e.engines }
                ).build()
        )
    }.dbInstances()
        .map {
            RdsDatabase(
                // if it has a cluster (aurora), use that as the identifier so it will de-dupe
                identifier = it.dbClusterIdentifier() ?: it.dbInstanceIdentifier(),
                arn = it.dbInstanceArn(),
                engine = it.engine(),
                iamDatabaseAuthenticationEnabled = it.iamDatabaseAuthenticationEnabled(),
                masterUsername = it.masterUsername(),
                endpoint = Endpoint(
                    host = it.endpoint().address(),
                    port = it.endpoint().port()
                )
            )
        }.toList()
}

val LIST_SUPPORTED_CLUSTERS: Resource.Cached<List<RdsDatabase>> = ClientBackedCachedResource(RdsClient::class, "rds.list_supported_cluster") {
    describeDBClustersPaginator {
        it.filters(
            Filter.builder()
                .name(ENGINE_FILTER)
                .values(
                    RdsEngine
                        .values()
                        .flatMap { e -> e.engines }
                ).build()
        )
    }.dbClusters()
        .map {
            RdsDatabase(
                identifier = it.dbClusterIdentifier(),
                arn = it.dbClusterArn(),
                engine = it.engine(),
                iamDatabaseAuthenticationEnabled = it.iamDatabaseAuthenticationEnabled(),
                masterUsername = it.masterUsername(),
                endpoint = Endpoint(
                    host = it.endpoint(),
                    port = it.port()
                )
            )
        }.toList()
}


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

package software.aws.toolkits.jetbrains.services.redshift

import com.intellij.openapi.project.Project
import icons.AwsIcons
import software.amazon.awssdk.services.redshift.RedshiftClient
import software.amazon.awssdk.services.redshift.model.Cluster
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.resources.message

class RedshiftExplorerParentNode(project: Project, service: AwsExplorerServiceNode) :
    CacheBackedAwsExplorerServiceRootNode<Cluster>(project, service, RedshiftResources.LIST_CLUSTERS) {
    override fun displayName(): String = message("explorer.node.redshift")
    override fun toNode(child: Cluster): AwsExplorerNode<*> = RedshiftExplorerNode(nodeProject, child)
}

class RedshiftExplorerNode(project: Project, val cluster: Cluster) : AwsExplorerResourceNode<Cluster>(
    project,
    RedshiftClient.SERVICE_NAME,
    cluster,
    AwsIcons.Resources.REDSHIFT
) {
    override fun displayName(): String = cluster.clusterIdentifier()
    override fun resourceType(): String = "cluster"
    override fun resourceArn(): String = nodeProject.clusterArn(cluster, region)
}


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

package software.aws.toolkits.jetbrains.services.redshift

import software.amazon.awssdk.services.redshift.RedshiftClient
import software.amazon.awssdk.services.redshift.model.Cluster
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.Resource

object RedshiftResources {
    val LIST_CLUSTERS: Resource.Cached<List<Cluster>> = ClientBackedCachedResource(RedshiftClient::class, "redshift.list_instances") {
        describeClustersPaginator().clusters().toList()
    }

    const val JDBC_REDSHIFT = "redshift"
    const val REDSHIFT_ENGINE_TYPE = "redshift"
}


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

package software.aws.toolkits.jetbrains.services.redshift

import com.intellij.database.autoconfig.DataSourceRegistry
import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.redshift.model.Cluster
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.getResourceIfPresent
import software.aws.toolkits.jetbrains.datagrip.CREDENTIAL_ID_PROPERTY
import software.aws.toolkits.jetbrains.datagrip.REGION_ID_PROPERTY
import software.aws.toolkits.jetbrains.datagrip.RequireSsl
import software.aws.toolkits.jetbrains.services.redshift.auth.CLUSTER_ID_PROPERTY
import software.aws.toolkits.jetbrains.services.redshift.auth.IamAuth
import software.aws.toolkits.jetbrains.services.sts.StsResources

object RedshiftUtils {
    private val REDSHIFT_REGION_REGEX =
        """.*\..*\.(.+).redshift\.""".toRegex()
    private val REDSHIFT_IDENTIFIER_REGEX =
        """.*//(.+)\..*\..*.redshift\..""".toRegex()

    fun extractRegionFromUrl(url: String?): String? = url?.let { REDSHIFT_REGION_REGEX.find(url)?.groupValues?.get(1) }
    fun extractClusterIdFromUrl(url: String?): String? = url?.let { REDSHIFT_IDENTIFIER_REGEX.find(url)?.groupValues?.get(1) }
}

fun Project.clusterArn(cluster: Cluster, region: AwsRegion): String {
    // Attempt to get account out of the cache. If not, it's empty so, it is still a valid arn
    val account = tryOrNull { this.getResourceIfPresent(StsResources.ACCOUNT) } ?: ""
    return "arn:${region.partitionId}:redshift:${region.id}:$account:cluster:${cluster.clusterIdentifier()}"
}

fun DataSourceRegistry.createDatasource(project: Project, cluster: Cluster) {
    val connectionSettings = AwsConnectionManager.getInstance(project).connectionSettings()
    builder
        .withJdbcAdditionalProperty(CREDENTIAL_ID_PROPERTY, connectionSettings?.credentials?.id)
        .withJdbcAdditionalProperty(REGION_ID_PROPERTY, connectionSettings?.region?.id)
        .withJdbcAdditionalProperty(CLUSTER_ID_PROPERTY, cluster.clusterIdentifier())
        .withUser(cluster.masterUsername())
        .withUrl("jdbc:redshift://${cluster.endpoint().address()}:${cluster.endpoint().port()}/${cluster.dbName()}")
        .commit()
    // TODO FIX_WHEN_MIN_IS_203 set auth provider ID in builder. It's in 202 but doesn't work
    newDataSources.firstOrNull()?.let {
        it.authProviderId = IamAuth.providerId
        // Force SSL on
        it.sslCfg = RequireSsl
    } ?: throw IllegalStateException("Newly inserted data source is not in the data source registry!")
}


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

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

import com.intellij.database.DatabaseBundle
import com.intellij.database.autoconfig.DataSourceRegistry
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.progress.PerformInBackgroundOption
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task.Backgroundable
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleExplorerNodeAction
import software.aws.toolkits.jetbrains.services.redshift.RedshiftExplorerNode
import software.aws.toolkits.jetbrains.services.redshift.createDatasource
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DatabaseCredentials.IAM
import software.aws.toolkits.telemetry.RedshiftTelemetry
import software.aws.toolkits.telemetry.Result

class CreateDataSourceAction : SingleExplorerNodeAction<RedshiftExplorerNode>(message("redshift.connect_aws_credentials")), DumbAware {
    override fun actionPerformed(selected: RedshiftExplorerNode, e: AnActionEvent) {
        object : Backgroundable(
            selected.nodeProject,
            DatabaseBundle.message("message.text.refreshing.data.source"),
            true,
            PerformInBackgroundOption.ALWAYS_BACKGROUND
        ) {
            override fun run(indicator: ProgressIndicator) {
                val registry = DataSourceRegistry(selected.nodeProject)
                registry.createDatasource(selected.nodeProject, selected.cluster)
                // Asynchronously show the user the configuration dialog to let them save/edit/test the profile
                runInEdt {
                    registry.showDialog()
                }
            }

            override fun onCancel() = recordTelemetry(Result.Cancelled)
            override fun onThrowable(error: Throwable) = recordTelemetry(Result.Failed)
            override fun onSuccess() = recordTelemetry(Result.Succeeded)

            private fun recordTelemetry(result: Result) = RedshiftTelemetry.createConnectionConfiguration(
                selected.nodeProject,
                result,
                IAM
            )
        }.queue()
    }
}


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

package software.aws.toolkits.jetbrains.services.redshift.auth

import com.intellij.credentialStore.Credentials
import com.intellij.database.access.DatabaseCredentials
import com.intellij.database.dataSource.DatabaseAuthProvider.AuthWidget
import com.intellij.database.dataSource.DatabaseConnectionInterceptor.ProtoConnection
import com.intellij.database.dataSource.DatabaseCredentialsAuthProvider
import com.intellij.database.dataSource.LocalDataSource
import com.intellij.openapi.project.Project
import kotlinx.coroutines.future.future
import software.amazon.awssdk.services.redshift.RedshiftClient
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.datagrip.auth.compatability.DatabaseAuthProviderCompatabilityAdapter
import software.aws.toolkits.jetbrains.datagrip.auth.compatability.project
import software.aws.toolkits.jetbrains.datagrip.getAwsConnectionSettings
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DatabaseCredentials.IAM
import software.aws.toolkits.telemetry.RedshiftTelemetry
import software.aws.toolkits.telemetry.Result
import java.util.concurrent.CompletionStage

data class RedshiftSettings(
    val clusterId: String,
    val username: String,
    val connectionSettings: ConnectionSettings
)

class IamAuth : DatabaseAuthProviderCompatabilityAdapter {
    override fun getId(): String = providerId
    override fun isApplicable(dataSource: LocalDataSource): Boolean = dataSource.dbms.isRedshift
    override fun getDisplayName(): String = message("redshift.auth.aws")

    override fun createWidget(project: Project?, creds: DatabaseCredentials, source: LocalDataSource): AuthWidget? = IamAuthWidget()

    override fun intercept(connection: ProtoConnection, silent: Boolean): CompletionStage<ProtoConnection>? {
        LOG.info { "Intercepting db connection [$connection]" }
        val project = connection.project()
        val scope = projectCoroutineScope(project)
        return scope.future {
            var result = Result.Succeeded
            try {
                val auth = validateConnection(connection)
                val client = AwsClientManager.getInstance().getClient<RedshiftClient>(
                    auth.connectionSettings.credentials,
                    auth.connectionSettings.region
                )
                val credentials = getCredentials(auth, client)
                DatabaseCredentialsAuthProvider.applyCredentials(connection, credentials, true)
            } catch (e: Throwable) {
                result = Result.Failed
                throw e
            } finally {
                RedshiftTelemetry.getCredentials(project, result, IAM)
            }
        }
    }

    internal fun validateConnection(connection: ProtoConnection): RedshiftSettings {
        val auth = connection.getAwsConnectionSettings()
        val clusterIdentifier = connection.connectionPoint.additionalProperties[CLUSTER_ID_PROPERTY]
            ?: throw IllegalArgumentException(message("redshift.validation.no_cluster_id"))
        val username = connection.connectionPoint.dataSource.username
        if (username.isEmpty()) {
            throw IllegalArgumentException(message("redshift.validation.username"))
        }
        return RedshiftSettings(
            clusterIdentifier,
            username,
            auth
        )
    }

    internal fun getCredentials(settings: RedshiftSettings, client: RedshiftClient): Credentials {
        if (client.describeClusters { it.clusterIdentifier(settings.clusterId).build() }.clusters().isEmpty()) {
            throw IllegalArgumentException(message("redshift.validation.cluster_does_not_exist", settings.clusterId, settings.connectionSettings.region.id))
        }
        val creds = client.getClusterCredentials {
            it.clusterIdentifier(settings.clusterId)
            it.dbUser(settings.username)
            // By default it auto-creates the user if it doesn't exist, which we don't want?
            it.autoCreate(false)
        }
        return Credentials(creds.dbUser(), creds.dbPassword())
    }

    companion object {
        const val providerId = "aws.redshift.iam"
        private val LOG = getLogger<IamAuth>()
    }
}


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

package software.aws.toolkits.jetbrains.services.redshift.auth

import com.intellij.database.dataSource.DataSourceUiUtil
import com.intellij.database.dataSource.LocalDataSource
import com.intellij.database.dataSource.url.template.ParametersHolder
import com.intellij.database.dataSource.url.template.UrlEditorModel
import com.intellij.database.dataSource.url.ui.UrlPropertiesPanel
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.services.redshift.RedshiftClient
import software.aws.toolkits.jetbrains.services.redshift.RedshiftUtils
import software.aws.toolkits.jetbrains.ui.AwsAuthWidget
import software.aws.toolkits.resources.message
import javax.swing.JPanel

const val CLUSTER_ID_PROPERTY = "AWS.RedshiftClusterId"

class IamAuthWidget : AwsAuthWidget() {
    private val clusterIdSelector = JBTextField()

    override val rowCount = 4
    override fun getRegionFromUrl(url: String?): String? = RedshiftUtils.extractRegionFromUrl(url)
    override val serviceId: String = RedshiftClient.SERVICE_NAME

    override fun createPanel(): JPanel {
        val panel = super.createPanel()
        val regionLabel = JBLabel(message("redshift.cluster_id"))
        panel.add(regionLabel, UrlPropertiesPanel.createLabelConstraints(3, 0, regionLabel.preferredSize.getWidth()))
        panel.add(clusterIdSelector, UrlPropertiesPanel.createSimpleConstraints(3, 1, 3))
        return panel
    }

    override fun save(dataSource: LocalDataSource, copyCredentials: Boolean) {
        super.save(dataSource, copyCredentials)

        DataSourceUiUtil.putOrRemove(
            dataSource.additionalProperties,
            CLUSTER_ID_PROPERTY,
            clusterIdSelector.text
        )
    }

    override fun reset(dataSource: LocalDataSource, resetCredentials: Boolean) {
        super.reset(dataSource, resetCredentials)
        clusterIdSelector.text = dataSource.additionalProperties[CLUSTER_ID_PROPERTY]
    }

    override fun updateFromUrl(holder: ParametersHolder) {
        super.updateFromUrl(holder)
        val url = (holder as? UrlEditorModel)?.url
        val clusterId = RedshiftUtils.extractClusterIdFromUrl(url)
        clusterId?.let { clusterIdSelector.text = it }
    }

    @TestOnly
    internal fun getClusterId() = clusterIdSelector.text
}


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

package software.aws.toolkits.jetbrains.ui

import com.intellij.database.dataSource.DatabaseConnectionConfig
import com.intellij.database.dataSource.DatabaseConnectionPoint

abstract class AwsAuthWidget(userFieldEnabled: Boolean = true) : @Suppress("DEPRECATION") AwsAuthWidgetBase(userFieldEnabled) {
    override fun save(config: DatabaseConnectionConfig, copyCredentials: Boolean) {
        super.save(config, copyCredentials)
        save(config.dataSource, copyCredentials)
    }

    override fun reset(point: DatabaseConnectionPoint, resetCredentials: Boolean) {
        super.reset(point, resetCredentials)
        reset(point.dataSource, resetCredentials)
    }
}


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

package software.aws.toolkits.jetbrains.ui

import com.intellij.database.dataSource.DataSourceUiUtil
import com.intellij.database.dataSource.DatabaseCredentialsAuthProviderUi
import com.intellij.database.dataSource.LocalDataSource
import com.intellij.database.dataSource.url.template.ParametersHolder
import com.intellij.database.dataSource.url.template.UrlEditorModel
import com.intellij.database.dataSource.url.ui.UrlPropertiesPanel
import com.intellij.ui.components.JBLabel
import com.intellij.uiDesigner.core.GridLayoutManager
import com.intellij.util.text.nullize
import org.jetbrains.annotations.TestOnly
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import software.aws.toolkits.jetbrains.datagrip.CREDENTIAL_ID_PROPERTY
import software.aws.toolkits.jetbrains.datagrip.REGION_ID_PROPERTY
import software.aws.toolkits.jetbrains.utils.ui.selected
import software.aws.toolkits.resources.message
import javax.swing.JPanel

@Deprecated("Use AwsAuthWidget shim instead") // FIX_WHEN_MIN_IS_221
abstract class AwsAuthWidgetBase(private val userFieldEnabled: Boolean) : DatabaseCredentialsAuthProviderUi.UserWidget() {
    private val credentialSelector = CredentialProviderSelector()
    private val regionSelector = RegionSelector()

    // These are defined in DataGrip and used in the url holder which is called in `updateFromUrl`
    protected val hostParameter = "host"
    protected val portParameter = "port"

    abstract fun getRegionFromUrl(url: String?): String?
    abstract val serviceId: String

    open val rowCount: Int = 3
    open val columnCount: Int = 6

    override fun createPanel(): JPanel {
        val panel = JPanel(GridLayoutManager(rowCount, columnCount))
        addUserField(panel, 0)

        // Disable the user field if we treat it as immutable
        myUserField.isEnabled = userFieldEnabled

        val credsLabel = JBLabel(message("aws_connection.credentials.label"))
        val regionLabel = JBLabel(message("aws_connection.region.label"))
        panel.add(credsLabel, UrlPropertiesPanel.createLabelConstraints(1, 0, credsLabel.preferredSize.getWidth()))
        panel.add(credentialSelector, UrlPropertiesPanel.createSimpleConstraints(1, 1, 3))
        panel.add(regionLabel, UrlPropertiesPanel.createLabelConstraints(2, 0, regionLabel.preferredSize.getWidth()))
        panel.add(regionSelector, UrlPropertiesPanel.createSimpleConstraints(2, 1, 3))

        return panel
    }

    override fun save(dataSource: LocalDataSource, copyCredentials: Boolean) {
        super.save(dataSource, copyCredentials)

        DataSourceUiUtil.putOrRemove(
            dataSource.additionalProperties,
            CREDENTIAL_ID_PROPERTY,
            credentialSelector.getSelectedCredentialsProvider()
        )
        DataSourceUiUtil.putOrRemove(
            dataSource.additionalProperties,
            REGION_ID_PROPERTY,
            regionSelector.selectedRegion?.id
        )
    }

    override fun reset(dataSource: LocalDataSource, resetCredentials: Boolean) {
        super.reset(dataSource, resetCredentials)

        val regionProvider = AwsRegionProvider.getInstance()
        val allRegions = regionProvider.allRegionsForService(serviceId)
        regionSelector.setRegions(allRegions.values.toMutableList())
        val regionId = dataSource.additionalProperties[REGION_ID_PROPERTY]?.nullize()
        regionId?.let {
            allRegions[regionId]?.let { region ->
                regionSelector.selectedRegion = region
            }
        }

        val credentialManager = CredentialManager.getInstance()
        credentialSelector.setCredentialsProviders(credentialManager.getCredentialIdentifiers())
        val credentialId = dataSource.additionalProperties[CREDENTIAL_ID_PROPERTY]?.nullize()
        if (credentialId != null) {
            val credentialIdentifierById = credentialManager.getCredentialIdentifierById(credentialId)
            if (credentialIdentifierById != null) {
                credentialSelector.setSelectedCredentialsProvider(credentialIdentifierById)
            } else {
                credentialSelector.setSelectedInvalidCredentialsProvider(credentialId)
            }
        } else {
            credentialSelector.model.selectedItem = null
        }
    }

    override fun isPasswordChanged(): Boolean = false

    override fun onChanged(r: Runnable) {
        // Tries to set username so if we don't have one, don't set
        if (userFieldEnabled) {
            super.onChanged(r)
        }
    }

    override fun updateFromUrl(holder: ParametersHolder) {
        // Try to get region from url and set the region box on a best effort basis
        val url = (holder as? UrlEditorModel)?.url
        val regionId = getRegionFromUrl(url)
        val region = AwsRegionProvider.getInstance().allRegions()[regionId]
        region?.let {
            regionSelector.selectedRegion = it
        }
        super.updateFromUrl(holder)
    }

    @TestOnly
    internal fun getSelectedCredential() = credentialSelector.getSelectedCredentialsProvider()

    @TestOnly
    internal fun getSelectedRegion() = regionSelector.selected()
}


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

package software.aws.toolkits.jetbrains.services.caws

const val REBUILD_ACTION_ACTION_GROUP_NAME = "UnattendedHostDropdownGroup"


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

package software.aws.toolkits.jetbrains.services.codewhisperer.importadder

import com.intellij.lang.ecmascript6.psi.impl.ES6CreateImportUtil
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile

object CodeWhispererJSImportUtil {
    fun insert(psiFile: PsiFile, element: PsiElement) {
        ES6CreateImportUtil.findPlaceAndInsertAnyImport(psiFile, element, null)
    }
}


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

package software.aws.toolkits.jetbrains.services.caws

import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.jetbrains.rdserver.unattendedHost.customization.DefaultGatewayExitCustomizationProvider
import com.jetbrains.rdserver.unattendedHost.customization.GatewayClientCustomizationProvider
import com.jetbrains.rdserver.unattendedHost.customization.GatewayExitCustomizationProvider
import com.jetbrains.rdserver.unattendedHost.customization.controlCenter.GatewayControlCenterProvider
import com.jetbrains.rdserver.unattendedHost.customization.controlCenter.GatewayHostnameDisplayKind
import icons.AwsIcons
import software.aws.toolkits.resources.message

class CodeCatalystGatewayClientCustomizer : GatewayClientCustomizationProvider {
    init {
        if (System.getenv(CawsConstants.CAWS_ENV_ID_VAR) == null) {
            throw ExtensionNotApplicableException.create()
        }
    }

    override val controlCenter: GatewayControlCenterProvider = object : GatewayControlCenterProvider {
        override fun getHostnameDisplayKind() = GatewayHostnameDisplayKind.ShowHostnameOnNavbar
        override fun getHostnameLong() = title
        override fun getHostnameShort() = title
    }

    override val icon = AwsIcons.Logos.CODE_CATALYST_SMALL
    override val title = message("caws.workspace.backend.title")

    override val exitCustomization: GatewayExitCustomizationProvider = object : GatewayExitCustomizationProvider by DefaultGatewayExitCustomizationProvider() {
        override val isEnabled: Boolean = false
    }
}


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

package software.aws.toolkits.jetbrains.services.caws

import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent

class RebuildAction : AnAction() {
    override fun actionPerformed(e: AnActionEvent) {
        val a = ActionManager.getInstance().getAction("aws.caws.updateDevfile")
        a.actionPerformed(e)
    }

    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = DevfileWatcher.getInstance().hasDevfileChanged()
    }

    override fun displayTextInToolbar() = true
}


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

package software.aws.toolkits.jetbrains.services.caws

import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.Constraints
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.impl.DynamicActionConfigurationCustomizer

class RebuildActionConfigurationCustomizer : DynamicActionConfigurationCustomizer {
    private var cawsAction: Pair<DefaultActionGroup, AnAction>? = null

    override fun registerActions(actionManager: ActionManager) {
        val actionGroup = (
            actionManager.getAction(REBUILD_ACTION_ACTION_GROUP_NAME)
                // TODO: should only need the first one
                ?: actionManager.getAction("UnattendedHostDropdownGroup")
            ) as? DefaultActionGroup

        actionGroup?.let {
            val rebuildAction = actionManager.getAction("aws.caws.rebuildAction") ?: return
            it.addAction(rebuildAction, Constraints.FIRST, actionManager)
            cawsAction = actionGroup to rebuildAction
        }
    }

    override fun unregisterActions(actionManager: ActionManager) {
        cawsAction?.let { (group, action) ->
            group.remove(action, actionManager)
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.caws

import com.jetbrains.rd.platform.codeWithMe.unattendedHost.metrics.Metric
import com.jetbrains.rd.platform.codeWithMe.unattendedHost.metrics.MetricType
import com.jetbrains.rd.platform.codeWithMe.unattendedHost.metrics.MetricsStatus
import com.jetbrains.rd.platform.codeWithMe.unattendedHost.metrics.providers.MetricProvider
import software.aws.toolkits.resources.message

class RebuildDevfileRequiredNotification : MetricProvider {
    override val id: String
        get() = "devfileRebuildRequired"

    override fun getMetrics(): Map<String, Metric> =
        if (DevfileWatcher.getInstance().hasDevfileChanged()) {
            mapOf(Pair("devfileRebuild", RebuildDevfileMetric))
        } else {
            mapOf()
        }

    // Adding MetricStatus as Danger instead of Warning, cause Warning is overriden by other notifications provided by the client
    object RebuildDevfileMetric : Metric(MetricType.OTHER, MetricsStatus.DANGER, true) {
        override fun toString(): String = message("caws.rebuild.workspace.notification")
    }
}


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

package software.aws.toolkits.jetbrains.services.caws

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.runWriteActionAndWait
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.credentials.sono.lazilyGetUserId
import software.aws.toolkits.jetbrains.services.caws.envclient.CawsEnvironmentClient
import software.aws.toolkits.jetbrains.services.caws.envclient.models.StartDevfileRequest
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodecatalystTelemetry
import java.nio.file.Path
import java.nio.file.Paths
import software.aws.toolkits.telemetry.Result as TelemetryResult

class UpdateDevfileAction : AnAction() {
    private val devfileYamlRegex = CawsConstants.DEVFILE_YAML_NAME

    override fun actionPerformed(e: AnActionEvent) {
        if (MessageDialogBuilder.yesNo(
                message("caws.update_devfile_title"),
                message("caws.update_devfile")
            ).ask(e.project)
        ) {
            ProgressManager.getInstance().run(
                object : Task.Modal(
                    e.project,
                    message("caws.updating_devfile"),
                    false
                ) {
                    override fun run(indicator: ProgressIndicator) {
                        val projectsDir = Paths.get(CawsConstants.CAWS_ENV_PROJECT_DIR)

                        val filePath = runReadAction {
                            getFilePathForDevfile()
                        }

                        runWriteActionAndWait {
                            FileDocumentManager.getInstance().saveAllDocuments()
                        }

                        // MDE Devfile start API currently expects a relative path to the Devfile
                        val relativePath: String = projectsDir.relativize(filePath).toString()

                        val request = StartDevfileRequest(
                            location = relativePath,
                            recreateHomeVolumes = true
                        )
                        try {
                            CawsEnvironmentClient.getInstance().startDevfile(request)
                            // no metric here because the environment restarts if this succeeds
                        } catch (e: Exception) {
                            LOG.error(e) { "Exception thrown while trying to update Devfile from $filePath" }
                            notifyError(
                                project = project,
                                content = message("caws.update_devfile.failed")
                            )
                            CodecatalystTelemetry.updateDevfile(
                                project = null,
                                userId = lazilyGetUserId(),
                                result = TelemetryResult.Failed
                            )
                        }
                    }

                    override fun onSuccess() {
                        DevfileWatcher.getInstance().updatedDevfile(hasFileChanged = false)
                    }

                    override fun onThrowable(error: Throwable) {
                        throw IllegalStateException(error.message)
                    }
                }
            )
        }
    }

    override fun update(e: AnActionEvent) {
        val envVar = System.getenv(CawsConstants.CAWS_ENV_ID_VAR)
        val virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE)
        e.presentation.isVisible = virtualFile?.name == devfileYamlRegex && envVar != null
    }

    override fun displayTextInToolbar() = true

    // TODO: What if there are multiple devfile.yaml files in the project?
    private fun getFilePathForDevfile(): Path = getFile()?.toNioPath() ?: error("Could not locate devfile")

    private fun getFile(): VirtualFile? = try {
        val devfileLocation = CawsEnvironmentClient.getInstance().getStatus().location ?: "devfile.yaml"
        // The path returned by getStatus() is relative to /projects
        val devfilePath = VirtualFileManager.getInstance().findFileByNioPath(
            Path.of("${CawsConstants.CAWS_ENV_PROJECT_DIR}/$devfileLocation")
        )
        devfilePath
    } catch (e: Exception) {
        throw IllegalStateException(e.message)
    }

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