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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes

import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes
import software.aws.toolkits.jetbrains.core.credentials.pinning.FeatureWithPinnedConnection
import java.awt.event.MouseEvent
import javax.swing.Icon

abstract class AbstractActionTreeNode(project: Project, value: String, private val awsIcon: Icon?) : AbstractTreeNode<String>(project, value) {
    override fun update(presentation: PresentationData) {
        val attr = if (isEnabled()) {
            SimpleTextAttributes.REGULAR_ATTRIBUTES
        } else {
            SimpleTextAttributes.GRAYED_ATTRIBUTES
        }
        presentation.addText(value, attr)
        awsIcon?.let { presentation.setIcon(it) }
    }

    abstract fun onDoubleClick(event: MouseEvent)

    open fun isEnabled(): Boolean = true
    override fun getChildren(): Collection<AbstractTreeNode<*>> = emptyList()
}

interface ActionGroupOnRightClick {
    fun actionGroupName(): String
}

interface PinnedConnectionNode {
    fun feature(): FeatureWithPinnedConnection
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes

import com.intellij.ide.DataManager
import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes
import software.aws.toolkits.jetbrains.ToolkitPlaces
import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeCatalystConnection
import software.aws.toolkits.jetbrains.core.credentials.pinning.ConnectionPinningManager
import software.aws.toolkits.jetbrains.core.credentials.sono.SonoCredentialManager
import software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.actions.OpenWorkspaceInGateway
import software.aws.toolkits.jetbrains.utils.isRunningOnRemoteBackend
import software.aws.toolkits.resources.message
import java.awt.event.MouseEvent

class CawsRootNode(private val nodeProject: Project) : AbstractTreeNode<String>(nodeProject, message("caws.devtoolPanel.title")), PinnedConnectionNode {
    override fun getChildren(): Collection<AbstractTreeNode<*>> {
        val groupId = if (SonoCredentialManager.getInstance(nodeProject).hasPreviouslyConnected()) {
            "aws.caws.devtools.actions.loggedin"
        } else {
            "aws.caws.devtools.actions.loggedout"
        }

        val actions = ActionManager.getInstance().getAction(groupId) as ActionGroup
        return actions.getChildren(null).mapNotNull {
            if (it is OpenWorkspaceInGateway && isRunningOnRemoteBackend()) {
                return@mapNotNull null
            }

            object : AbstractActionTreeNode(nodeProject, it.templatePresentation.text, it.templatePresentation.icon) {
                override fun onDoubleClick(event: MouseEvent) {
                    val e = AnActionEvent.createFromInputEvent(
                        event,
                        ToolkitPlaces.DEVTOOLS_TOOL_WINDOW,
                        it.templatePresentation.clone(),
                        DataManager.getInstance().getDataContext(event.component)
                    )

                    it.actionPerformed(e)
                }
            }
        }
    }

    override fun update(presentation: PresentationData) {
        presentation.addText(value, SimpleTextAttributes.REGULAR_ATTRIBUTES)

        with(ConnectionPinningManager.getInstance()) {
            if (isFeaturePinned(CodeCatalystConnection.getInstance())) {
                presentation.addText(message("caws.connected.builder_id"), SimpleTextAttributes.GRAY_ATTRIBUTES)
            }
        }
    }

    override fun feature() = CodeCatalystConnection.getInstance()
}

class CawsServiceNode : DevToolsServiceNode {
    override val serviceId: String = "aws.toolkit.caws.service"
    override fun buildServiceRootNode(nodeProject: Project): AbstractTreeNode<*> = CawsRootNode(nodeProject)
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.codewhisperer.CodeWhispererClient
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererServiceNode
import software.aws.toolkits.resources.message

class CodeWhispererExplorerRootNode : DevToolsServiceNode {
    override val serviceId = CodeWhispererClient.SERVICE_NAME
    override fun buildServiceRootNode(nodeProject: Project) = CodeWhispererServiceNode(nodeProject, message("explorer.node.codewhisperer"))
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes

import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.openapi.project.Project

interface DevToolsServiceNode {
    val serviceId: String
    fun buildServiceRootNode(nodeProject: Project): AbstractTreeNode<*>
    fun enabled(): Boolean = true
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.actions

import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import software.aws.toolkits.jetbrains.services.caws.CawsEndpoints

class CawsLearnMore : DumbAwareAction(AllIcons.Actions.Help) {
    override fun actionPerformed(e: AnActionEvent) {
        BrowserUtil.browse(CawsEndpoints.ConsoleFactory.marketing())
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.vcs.ProjectLevelVcsManager
import com.intellij.util.ui.cloneDialog.VcsCloneDialog
import software.aws.toolkits.jetbrains.core.credentials.sono.lazilyGetUserId
import software.aws.toolkits.jetbrains.services.caws.CawsCloneDialogExtension
import software.aws.toolkits.telemetry.CodecatalystTelemetry
import software.aws.toolkits.telemetry.Result as TelemetryResult

class CloneCawsRepository : DumbAwareAction(AllIcons.Vcs.Clone) {
    override fun actionPerformed(e: AnActionEvent) {
        val project = e.getRequiredData(CommonDataKeys.PROJECT)
        // TODO: can we simplify further and reuse all the logic from the real action somehow?
        val cloneDialog = VcsCloneDialog.Builder(project).forExtension(CawsCloneDialogExtension::class.java)
        if (cloneDialog.showAndGet()) {
            cloneDialog.doClone(ProjectLevelVcsManager.getInstance(project).compositeCheckoutListener)
        } else {
            CodecatalystTelemetry.localClone(project = null, userId = lazilyGetUserId(), result = TelemetryResult.Cancelled)
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.ComputableActionGroup
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopupFactory
import kotlinx.coroutines.future.await
import kotlinx.coroutines.runBlocking
import software.aws.toolkits.core.ClientConnectionSettings
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.credentials.sono.SonoCredentialManager
import software.aws.toolkits.jetbrains.services.caws.CawsCodeRepository
import software.aws.toolkits.jetbrains.services.caws.CawsResources
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import java.awt.datatransfer.StringSelection

class CopyCawsRepositoryUrl : DumbAwareAction(AllIcons.Actions.Copy) {
    override fun actionPerformed(e: AnActionEvent) {
        val project = e.getRequiredData(CommonDataKeys.PROJECT)
        val cawsConnectionSettings = SonoCredentialManager.getInstance(project).getConnectionSettings() ?: return

        JBPopupFactory.getInstance().createActionGroupPopup(
            message("caws.copy.url.select_repository"),
            object : ComputableActionGroup.Simple() {
                override fun computeChildren(manager: ActionManager): Array<AnAction> {
                    val cache = AwsResourceCache.getInstance()
                    return runBlocking {
                        val projects = cache.getResource(CawsResources.ALL_PROJECTS, cawsConnectionSettings).await()

                        projects.flatMap { cawsProject ->
                            cache.getResource(CawsResources.codeRepositories(cawsProject), cawsConnectionSettings).await()
                        }.map {
                            object : DumbAwareAction(it.presentableString) {
                                override fun actionPerformed(e: AnActionEvent) {
                                    copyUrl(project, cawsConnectionSettings, it)
                                }
                            }
                        }.toTypedArray()
                    }
                }
            },
            e.dataContext,
            false,
            null,
            5
        )
            .showInBestPositionFor(e.dataContext)
    }

    private fun copyUrl(project: Project, cawsConnectionSettings: ClientConnectionSettings<*>, repository: CawsCodeRepository) {
        object : Task.Backgroundable(project, message("caws.devtoolPanel.fetch.git.url", repository.presentableString)) {
            override fun run(indicator: ProgressIndicator) {
                val url = AwsResourceCache.getInstance()
                    .getResource(CawsResources.cloneUrls(repository), cawsConnectionSettings)
                    .toCompletableFuture()
                    .get()
                CopyPasteManager.getInstance().setContents(StringSelection(url))

                notifyInfo(
                    title = message("action.aws.caws.devtools.actions.copyCloneUrl.text"),
                    content = message("caws.devtoolPanel.git_url_copied"),
                    project = project
                )
            }
        }.queue()
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.actions

import com.intellij.ide.ui.ProductIcons
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction

class OpenWorkspaceInGateway : DumbAwareAction(ProductIcons.getInstance().productIcon) {
    override fun actionPerformed(e: AnActionEvent) {
        ActionManager.getInstance().getAction("OpenRemoteDevelopment").actionPerformed(e)
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.DumbAwareAction
import software.aws.toolkits.jetbrains.core.credentials.sono.SonoCredentialManager

class SonoLogin : DumbAwareAction(AllIcons.Actions.Execute) {
    override fun actionPerformed(e: AnActionEvent) {
        val project = e.project ?: return
        ApplicationManager.getApplication().executeOnPooledThread {
            SonoCredentialManager.loginSono(project)
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.actions

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.UpdateInBackground
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.credentials.pinning.ConnectionPinningManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.FeatureWithPinnedConnection
import software.aws.toolkits.jetbrains.core.explorer.devToolsTab.DevToolsToolWindow
import software.aws.toolkits.jetbrains.core.explorer.devToolsTab.DevToolsToolWindowDataKeys
import software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.PinnedConnectionNode
import software.aws.toolkits.resources.message

class UnpinConnectionAction : AnAction(), DumbAware, UpdateInBackground {
    override fun update(e: AnActionEvent) {
        val project = e.project
        val feature = feature(e)

        e.presentation.isEnabledAndVisible = project != null && feature != null && ConnectionPinningManager.getInstance().isFeaturePinned(feature)

        feature?.featureName?.let { e.presentation.text = message("connection.pinning.unlink", it) }
    }

    override fun actionPerformed(e: AnActionEvent) {
        val project = e.project ?: return
        val feature = feature(e) ?: return
        ConnectionPinningManager.getInstance().setPinnedConnection(feature, null)

        DevToolsToolWindow.getInstance(project).redrawContent()
    }

    private fun feature(e: AnActionEvent): FeatureWithPinnedConnection? {
        val nodes = e.getData(DevToolsToolWindowDataKeys.SELECTED_NODES)
        if (nodes == null || nodes.size != 1) {
            return null
        }

        val node = nodes.firstOrNull() as? PinnedConnectionNode ?: return null
        return node.feature()
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.apprunner.AppRunnerClient
import software.aws.toolkits.jetbrains.services.apprunner.AppRunnerNode

class AppRunnerExplorerRootNode : AwsExplorerServiceNode {
    override val serviceId: String = AppRunnerClient.SERVICE_NAME

    override fun buildServiceRootNode(project: Project) = AppRunnerNode(project, this)
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.ide.projectView.PresentationData
import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes
import software.aws.toolkits.resources.message

/**
 * Used when a parent node has no children. This allows us to show a message indication that there are resources for that
 * type
 */
class AwsExplorerEmptyNode(project: Project, value: String = message("explorer.empty_node")) :
    AwsExplorerNode<String>(project, value, awsIcon = null) {

    override fun getChildren(): List<AwsExplorerNode<*>> = emptyList()

    override fun update(presentation: PresentationData) {
        presentation.addText(displayName(), SimpleTextAttributes.GRAYED_ATTRIBUTES)
    }

    override fun isAlwaysLeaf() = true
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.ide.projectView.PresentationData
import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes
import software.amazon.awssdk.awscore.exception.AwsServiceException
import software.aws.toolkits.resources.message

/**
 * Used to represent an error loading a node, such as an AccessDenied when loading resources
 */
class AwsExplorerErrorNode(project: Project, exception: Throwable) :
    AwsExplorerNode<Throwable>(project, exception, null) {

    override fun getChildren(): List<AwsExplorerNode<*>> = emptyList()

    override fun update(presentation: PresentationData) {
        presentation.apply {
            // If we don't have a message, at least give them the error type
            tooltip = value.message ?: value.javaClass.simpleName

            val exception = value
            val errorDetails = if (exception is AwsServiceException) {
                val awsErrorDetails = exception.awsErrorDetails()
                "${awsErrorDetails.serviceName()}: ${awsErrorDetails.errorCode()}"
            } else {
                message("explorer.error_loading_resources_default_details")
            }

            addText(
                nodeText(errorDetails),
                SimpleTextAttributes.ERROR_ATTRIBUTES
            )
        }
    }

    override fun isAlwaysLeaf() = true

    private companion object {
        fun nodeText(details: String): String = message("explorer.error_loading_resources", details)
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes
import software.aws.toolkits.jetbrains.core.credentials.activeCredentialProvider
import software.aws.toolkits.jetbrains.core.credentials.activeRegion
import software.aws.toolkits.jetbrains.core.explorer.AwsExplorerNodeProcessor
import javax.swing.Icon

/**
 * Top level class for any node in the AWS explorer tree
 */
abstract class AwsExplorerNode<T : Any>(val nodeProject: Project, value: T, private val awsIcon: Icon?) :
    AbstractTreeNode<T>(nodeProject, value) {

    protected val region by lazy { nodeProject.activeRegion() }

    protected val credentialProvider by lazy { nodeProject.activeCredentialProvider() }

    override fun update(presentation: PresentationData) {
        presentation.let {
            it.setIcon(awsIcon)
            it.addText(displayName(), SimpleTextAttributes.REGULAR_ATTRIBUTES)
            statusText()?.let { status ->
                it.addText(" [$status]", SimpleTextAttributes.GRAY_ATTRIBUTES)
            }
        }
    }

    final override fun postprocess(presentation: PresentationData) {
        AwsExplorerNodeProcessor.EP_NAME.extensionList.forEach {
            it.postProcessPresentation(
                this,
                presentation
            )
        }
    }

    open fun displayName() = value.toString()

    open fun statusText(): String? = null

    /**
     * Called when the node is double clicked on
     */
    open fun onDoubleClick() {}

    override fun toString(): String = displayName()
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import javax.swing.Icon

/**
 * Top level class for a node that represents a resource such as an AWS Lambda.
 */
abstract class AwsExplorerResourceNode<T : Any>(
    project: Project,
    val serviceId: String,
    value: T,
    awsIcon: Icon? = null
) : AwsExplorerNode<T>(project, value, awsIcon),
    ResourceActionNode {
    override fun actionGroupName() = "aws.toolkit.explorer.$serviceId.${resourceType()}"

    override fun isAlwaysLeaf(): Boolean = true

    override fun getChildren(): List<AwsExplorerNode<*>> = emptyList()

    abstract fun resourceType(): String

    abstract fun resourceArn(): String
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager

/**
 * The root node of the AWS explorer tree.
 */
class AwsExplorerRootNode(private val nodeProject: Project) : AbstractTreeNode<Any>(nodeProject, Object()) {
    override fun getChildren(): List<AwsExplorerNode<*>> {
        val settings = AwsConnectionManager.getInstance(nodeProject)
        val region = settings.selectedRegion ?: return emptyList()

        return EP_NAME.extensionList
            .filter { it.enabled(region) }
            .map { it.buildServiceRootNode(nodeProject) }
    }

    override fun update(presentation: PresentationData) {}

    companion object {
        private val EP_NAME = ExtensionPointName<AwsExplorerServiceNode>("aws.toolkit.explorer.serviceNode")
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider

interface AwsExplorerServiceNode {
    val serviceId: String
    fun buildServiceRootNode(project: Project): AwsExplorerNode<*>
    fun enabled(region: AwsRegion): Boolean = AwsRegionProvider.getInstance().isServiceSupported(region, serviceId)
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.core.Resource
import software.aws.toolkits.jetbrains.core.getResourceNow

/**
 * Top level node for any AWS service node
 */
abstract class AwsExplorerServiceRootNode(project: Project, service: AwsExplorerServiceNode) :
    AwsExplorerNode<AwsExplorerServiceNode>(project, service, null),
    ResourceActionNode,
    ResourceParentNode {

    private val serviceId = service.serviceId

    abstract override fun displayName(): String

    override fun getChildren(): List<AwsExplorerNode<*>> = super.getChildren()
    override fun isAlwaysShowPlus(): Boolean = true
    override fun actionGroupName() = "aws.toolkit.explorer.$serviceId"
}

abstract class CacheBackedAwsExplorerServiceRootNode<T>(project: Project, service: AwsExplorerServiceNode, private val resource: Resource<out Collection<T>>) :
    AwsExplorerServiceRootNode(project, service) {

    final override fun getChildrenInternal(): List<AwsExplorerNode<*>> = nodeProject.getResourceNow(resource).map(this::toNode)

    abstract fun toNode(child: T): AwsExplorerNode<*>
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.aws.toolkits.jetbrains.services.cloudformation.CloudFormationServiceNode

class CloudFormationExplorerRootNode : AwsExplorerServiceNode {
    override val serviceId: String = CloudFormationClient.SERVICE_NAME
    override fun buildServiceRootNode(project: Project): AwsExplorerNode<*> = CloudFormationServiceNode(project, this)
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogsServiceNode

class CloudWatchRootNode : AwsExplorerServiceNode {
    override val serviceId: String = CloudWatchLogsClient.SERVICE_NAME
    override fun buildServiceRootNode(project: Project): AwsExplorerNode<*> = CloudWatchLogsServiceNode(project, this)
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import software.aws.toolkits.jetbrains.services.dynamodb.explorer.DynamoDbServiceNode

class DynamoDbExplorerRootNode : AwsExplorerServiceNode {
    override val serviceId: String = DynamoDbClient.SERVICE_NAME
    override fun buildServiceRootNode(project: Project) = DynamoDbServiceNode(project, this)
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.ecr.EcrClient
import software.aws.toolkits.jetbrains.services.ecr.EcrServiceNode

class EcrExplorerRootNode : AwsExplorerServiceNode {
    override val serviceId: String = EcrClient.SERVICE_NAME
    override fun buildServiceRootNode(project: Project): AwsExplorerNode<*> = EcrServiceNode(project, this)
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.ecs.EcsClient
import software.aws.toolkits.jetbrains.services.ecs.EcsParentNode

class EcsExplorerRootNode : AwsExplorerServiceNode {
    override val serviceId: String = EcsClient.SERVICE_NAME
    override fun buildServiceRootNode(project: Project): AwsExplorerNode<*> = EcsParentNode(project, this)
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.lambda.LambdaClient
import software.aws.toolkits.jetbrains.services.lambda.LambdaServiceNode

class LambdaExplorerRootNode : AwsExplorerServiceNode {
    override val serviceId: String = LambdaClient.SERVICE_NAME
    override fun buildServiceRootNode(project: Project): AwsExplorerNode<*> = LambdaServiceNode(project, this)
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.aws.toolkits.jetbrains.services.dynamic.explorer.OtherResourcesNode

class OtherResourcesRootNode : AwsExplorerServiceNode {
    override val serviceId: String = CloudFormationClient.SERVICE_NAME
    override fun buildServiceRootNode(project: Project): AwsExplorerNode<*> = OtherResourcesNode(project, this)
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

/**
 * Implemented by explorer resources that have associated actions.
 *
 * @see AwsExplorerServiceRootNode for actions associated with top-level service nodes
 * @see AwsExplorerResourceNode for actions associated with an ARN-aware AWS resource
 */
interface ResourceActionNode {
    /**
     * @returns The name of the action group declared in plugin.xml that is associated with this explorer node
     */
    fun actionGroupName(): String
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.execution.Location

/**
 * Implemented by explorer resources that have an associated PSI location
 */
interface ResourceLocationNode {
    fun location(): Location<*>? = null
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.execution.ExecutionException
import com.intellij.openapi.project.Project
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info

/**
 * Implemented by explorer resources that have children
 */
interface ResourceParentNode {
    val nodeProject: Project

    fun isAlwaysShowPlus(): Boolean = true

    fun getChildren(): List<AwsExplorerNode<*>> = try {
        val children = getChildrenInternal()
        if (children.isEmpty()) {
            listOf(emptyChildrenNode())
        } else {
            children
        }
    } catch (e: ExecutionException) {
        getLogger(this::class).info(e) { "Failed to get children" }
        listOf(AwsExplorerErrorNode(nodeProject, e.cause ?: e))
    } catch (e: Exception) {
        getLogger(this::class).info(e) { "Failed to get children" }
        listOf(AwsExplorerErrorNode(nodeProject, e))
    }

    fun emptyChildrenNode(): AwsExplorerEmptyNode = AwsExplorerEmptyNode(nodeProject)

    fun getChildrenInternal(): List<AwsExplorerNode<*>>
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.s3.S3Client
import software.aws.toolkits.jetbrains.services.s3.S3ServiceNode

class S3ExplorerRootNode : AwsExplorerServiceNode {
    override val serviceId: String = S3Client.SERVICE_NAME
    override fun buildServiceRootNode(project: Project): AwsExplorerNode<*> = S3ServiceNode(project, this)
}


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

package software.aws.toolkits.jetbrains.core.explorer.nodes

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.schemas.SchemasClient
import software.aws.toolkits.jetbrains.services.schemas.SchemasServiceNode

class SchemasExplorerRootNode : AwsExplorerServiceNode {
    override val serviceId: String = SchemasClient.SERVICE_NAME
    override fun buildServiceRootNode(project: Project): AwsExplorerNode<*> = SchemasServiceNode(project, this)
}


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

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.sqs.SqsClient
import software.aws.toolkits.jetbrains.services.sqs.SqsServiceNode

class SqsExplorerRootNode : AwsExplorerServiceNode {
    override val serviceId: String = SqsClient.SERVICE_NAME
    override fun buildServiceRootNode(project: Project): AwsExplorerNode<*> = SqsServiceNode(project, this)
}


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

package software.aws.toolkits.jetbrains.core.help

import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants

enum class HelpIds(shortId: String, val url: String) {
    // App Runner
    APPRUNNER_PAUSE_RESUME(
        "appRunnerPauseResume",
        "https://docs.aws.amazon.com/console/apprunner/manage-pause"
    ),
    APPRUNNER_CODE_CONFIG(
        "appRunnerCodeConfig",
        "https://docs.aws.amazon.com/console/apprunner/config-file"
    ),
    APPRUNNER_CONNECTIONS(
        "appRunnnerServiceConnections",
        "https://docs.aws.amazon.com/console/apprunner/manage-connections"
    ),

    // Explorer
    EXPLORER_WINDOW(
        "explorerWindow",
        "https://docs.aws.amazon.com/console/toolkit-for-jetbrains/aws-explorer"
    ),
    EXPLORER_CREDS_HELP(
        "explorerCredsHelp",
        "https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/setup-credentials.html"
    ),

    // Lambda
    CREATE_FUNCTION_DIALOG(
        "createFunctionDialog",
        "https://docs.aws.amazon.com/console/toolkit-for-jetbrains/create-function-dialog"
    ),
    UPDATE_FUNCTION_CONFIGURATION_DIALOG(
        "updateFunctionConfigurationDialog",
        "https://docs.aws.amazon.com/console/toolkit-for-jetbrains/update-configuration-dialog"
    ),
    UPDATE_FUNCTION_CODE_DIALOG(
        "updateFunctionCodeDialog",
        "https://docs.aws.amazon.com/console/toolkit-for-jetbrains/update-code-dialog"
    ),

    // Serverless
    NEW_SERVERLESS_PROJECT_DIALOG(
        "newServerlessProjectDialog",
        "https://docs.aws.amazon.com/console/toolkit-for-jetbrains/new-project-dialog"
    ),
    DEPLOY_SERVERLESS_APPLICATION_DIALOG(
        "deployServerlessApplicationDialog",
        "https://docs.aws.amazon.com/console/toolkit-for-jetbrains/deploy-serverless-application-dialog"
    ),

    // Schema code download
    DOWNLOAD_CODE_FOR_SCHEMA_DIALOG(
        "downloadCodeForSchemaDialog",
        "https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/eventbridge-schemas.html"
    ),

    // Schema search
    SCHEMA_SEARCH_DIALOG(
        "schemaSearchDialog",
        "https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/eventbridge-schemas.html"
    ),

    // Others
    RUN_DEBUG_CONFIGURATIONS_DIALOG(
        "runDebugConfigurationsDialog",
        "https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/run-debug-configurations-dialog.html"
    ),
    SETUP_CREDENTIALS(
        "setupCredentials",
        "https://docs.aws.amazon.com/console/toolkit-for-jetbrains/credentials"
    ),
    SAM_CLI_INSTALL(
        "sam.install",
        "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html"
    ),

    // RDS
    RDS_SETUP_IAM_AUTH(
        "rdsIamAuth",
        "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html"
    ),

    // AWS CLI
    AWS_CLI_INSTALL(
        "awsCli.install",
        "https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html"
    ),

    // Ecs Exec
    ECS_EXEC_PERMISSIONS_REQUIRED(
        "ecsExecPermissions",
        "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html#ecs-exec-enabling-and-using"
    ),

    // What is AWS Toolkit?
    AWS_TOOLKIT_GETTING_STARTED(
        "awsToolkitGettingStarted",
        "https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html"
    ),

    // CodeWhisperer
    CODEWHISPERER_TOKEN(
        "CodeWhispererToken",
        CodeWhispererConstants.CODEWHISPERER_LEARN_MORE_URI
    ),

    // TODO: update this
    CODEWHISPERER_LOGIN_YES_NO(
        "CodeWhispererLoginYesNoDialog",
        CodeWhispererConstants.CODEWHISPERER_LOGIN_HELP_URI
    ),

    // TODO: update this
    CODEWHISPERER_LOGIN_DIALOG(
        "CodeWhispererLoginDialog",
        CodeWhispererConstants.CODEWHISPERER_LOGIN_HELP_URI
    ),

    // TODO: update this
    TOOLKIT_ADD_CONNECTIONS_DIALOG(
        "ToolkitAddConnectionsDialog",
        CodeWhispererConstants.CODEWHISPERER_LOGIN_HELP_URI
    )
    ;

    val id = "aws.toolkit.$shortId"
}


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

package software.aws.toolkits.jetbrains.core.help

import com.intellij.openapi.help.WebHelpProvider
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn

class HelpIdTranslator : WebHelpProvider() {
    override fun getHelpPageUrl(helpTopicId: String) = HELP_REGISTRY.getOrElse(helpTopicId) {
        LOGGER.warn { "Missing id $helpTopicId" }
        DEFAULT_LOCATION
    }

    private companion object {
        const val DEFAULT_LOCATION = "https://docs.aws.amazon.com/console/toolkit-for-jetbrains"
        val LOGGER = getLogger<HelpIdTranslator>()
        private val HELP_REGISTRY by lazy {
            HelpIds.values().asSequence().map { it.id to it.url }.toMap()
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.notification

import com.intellij.ide.util.PropertiesComponent
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
import software.aws.toolkits.resources.message

class MinimumVersionChange @JvmOverloads constructor(isUnderTest: Boolean = false) : StartupActivity.DumbAware {
    init {
        if (ApplicationManager.getApplication().isUnitTestMode && !isUnderTest) {
            throw ExtensionNotApplicableException.INSTANCE
        }
    }

    override fun runActivity(project: Project) {
        if (System.getProperty(SKIP_PROMPT)?.toBoolean() == true) {
            return
        }

        // Setting is stored application wide
        if (PropertiesComponent.getInstance().getBoolean(IGNORE_PROMPT)) {
            return
        }

        if (ApplicationInfo.getInstance().build.baselineVersion >= MIN_VERSION) {
            return
        }

        val title = message("aws.toolkit_deprecation.title")
        val message = message(
            "aws.toolkit_deprecation.message",
            ApplicationNamesInfo.getInstance().fullProductName,
            ApplicationInfo.getInstance().fullVersion,
            MIN_VERSION_HUMAN
        )

        val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("aws.toolkit_deprecation")
        notificationGroup.createNotification(title, message, NotificationType.WARNING)
            .addAction(
                NotificationAction.createSimpleExpiring(message("general.notification.action.hide_forever")) {
                    PropertiesComponent.getInstance().setValue(IGNORE_PROMPT, true)
                }
            )
            .notify(project)
    }

    companion object {
        const val MIN_VERSION = 222
        const val MIN_VERSION_HUMAN = "2022.2"

        // Used by tests to make sure the prompt never shows up
        const val SKIP_PROMPT = "aws.suppress_deprecation_prompt"
        const val IGNORE_PROMPT = "aws.ignore_deprecation_prompt"
    }
}


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

package software.aws.toolkits.jetbrains.core.plugins

import com.intellij.ide.plugins.PluginManagerCore.getPlugin
import com.intellij.openapi.extensions.PluginId

// TODO: all usages should probably be leveraging EPs
fun pluginIsInstalledAndEnabled(pluginId: String): Boolean = getPlugin(PluginId.findId(pluginId))?.isEnabled == true


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

package software.aws.toolkits.jetbrains.core.region

import com.intellij.openapi.components.service
import org.slf4j.event.Level
import software.amazon.awssdk.regions.providers.AwsProfileRegionProvider
import software.amazon.awssdk.regions.providers.AwsRegionProviderChain
import software.amazon.awssdk.regions.providers.SystemSettingsRegionProvider
import software.aws.toolkits.core.region.AwsPartition
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.region.PartitionParser
import software.aws.toolkits.core.region.ServiceEndpointResource
import software.aws.toolkits.core.region.ToolkitRegionProvider
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.inputStream
import software.aws.toolkits.core.utils.logWhenNull
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.core.RemoteResourceResolverProvider
import software.aws.toolkits.resources.BundledResources

class AwsRegionProvider : ToolkitRegionProvider() {
    private val regionChain by lazy {
        // Querying the instance metadata is expensive due to high timeouts and retries
        AwsRegionProviderChain(SystemSettingsRegionProvider(), AwsProfileRegionProvider())
    }
    private val partitions: Map<String, PartitionData> by lazy {
        val inputStream = RemoteResourceResolverProvider.getInstance().get().resolve(ServiceEndpointResource).toCompletableFuture().get()?.inputStream()
        val partitions = inputStream?.use { PartitionParser.parse(it) }?.partitions
            ?: BundledResources.ENDPOINTS_FILE.use { PartitionParser.parse(BundledResources.ENDPOINTS_FILE) }?.partitions
            ?: throw Exception("Failed to retrieve partitions.")

        partitions.asSequence().associateBy { it.partition }.mapValues {
            PartitionData(
                it.value.partitionName,
                it.value.services,
                it.value.regions.asSequence().associate { region -> region.key to AwsRegion(region.key, region.value.description, it.key) }
            )
        }
    }

    override fun partitionData(): Map<String, PartitionData> = partitions

    override fun defaultPartition(): AwsPartition = partitions().getValue(defaultRegion().partitionId)

    override fun defaultRegion(): AwsRegion {
        val regionIdFromChain = LOG.tryOrNull("Failed to find default region in chain", level = Level.WARN) {
            regionChain.region.id()
        }

        val regionFromChain = regionIdFromChain?.let { regionId ->
            LOG.logWhenNull("Could not find $regionId in endpoint data") {
                this[regionId]
            }
        }

        return regionFromChain
            ?: this[DEFAULT_REGION]
            ?: allRegions().values.firstOrNull()
            ?: throw IllegalStateException("Region provider data is missing default data")
    }

    companion object {
        private const val DEFAULT_REGION = "us-east-1"
        private val LOG = getLogger<AwsRegionProvider>()

        @JvmStatic
        fun getInstance(): ToolkitRegionProvider = service()
    }
}


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

package software.aws.toolkits.jetbrains.core.terminal

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.util.ExceptionUtil
import icons.TerminalIcons
import org.jetbrains.plugins.terminal.TerminalTabState
import org.jetbrains.plugins.terminal.TerminalView
import software.amazon.awssdk.profiles.ProfileFileSystemSetting
import software.aws.toolkits.core.credentials.mergeWithExistingEnvironmentVariables
import software.aws.toolkits.core.region.mergeWithExistingEnvironmentVariables
import software.aws.toolkits.core.shortName
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.ConnectionState
import software.aws.toolkits.jetbrains.core.credentials.profiles.ProfileCredentialsIdentifier
import software.aws.toolkits.jetbrains.core.experiments.ToolkitExperiment
import software.aws.toolkits.jetbrains.core.experiments.isEnabled
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AwsTelemetry
import software.aws.toolkits.telemetry.Result

class OpenAwsLocalTerminal : DumbAwareAction(
    { message("aws.terminal.action") },
    { message("aws.terminal.action.tooltip") },
    TerminalIcons.OpenTerminal_13x13
) {

    override fun update(e: AnActionEvent) {
        if (AwsLocalTerminalExperiment.isEnabled()) {
            e.presentation.isEnabled = e.project?.let { AwsConnectionManager.getInstance(it) }?.isValidConnectionSettings() == true
        } else {
            e.presentation.isEnabledAndVisible = false
        }
    }

    override fun actionPerformed(e: AnActionEvent) {
        val project = e.getRequiredData(LangDataKeys.PROJECT)
        when (val state = AwsConnectionManager.getInstance(project).connectionState) {
            is ConnectionState.ValidConnection -> {
                val connection = state.connection
                ApplicationManager.getApplication().executeOnPooledThread {
                    val credentials = try {
                        connection.credentials.resolveCredentials()
                    } catch (e: Exception) {
                        LOG.error(e) { message("aws.terminal.exception.failed_to_resolve_credentials", ExceptionUtil.getThrowableText(e)) }
                        AwsTelemetry.openLocalTerminal(project, result = Result.Failed)
                        return@executeOnPooledThread
                    }
                    runInEdt {
                        val runner = AwsLocalTerminalRunner(project, connection.shortName) { envs ->
                            connection.region.mergeWithExistingEnvironmentVariables(envs, replace = true)
                            when (val identifier = connection.credentials.identifier) {
                                is ProfileCredentialsIdentifier -> envs[ProfileFileSystemSetting.AWS_PROFILE.environmentVariable()] = identifier.profileName
                                else -> credentials.mergeWithExistingEnvironmentVariables(envs, replace = true)
                            }
                        }
                        TerminalView.getInstance(project).createNewSession(runner, TerminalTabState().apply { this.myTabName = connection.shortName })
                        AwsTelemetry.openLocalTerminal(project, result = Result.Succeeded)
                    }
                }
            }
            else -> {
                LOG.error { message("aws.terminal.exception.invalid_credentials", state.displayMessage) }
                AwsTelemetry.openLocalTerminal(project, result = Result.Failed)
            }
        }
    }

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

object AwsLocalTerminalExperiment :
    ToolkitExperiment("connectedLocalTerminal", { message("aws.terminal.action") }, { message("aws.terminal.action.tooltip") }, default = true)


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

package software.aws.toolkits.jetbrains.core.tools

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.progress.PerformInBackgroundOption
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.util.ProgressIndicatorUtils
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.ThrowableComputable
import com.intellij.openapi.util.io.FileUtil
import com.intellij.serviceContainer.NonInjectable
import com.intellij.util.ExceptionUtil
import com.intellij.util.io.delete
import com.intellij.util.io.readText
import com.intellij.util.io.write
import org.jetbrains.annotations.VisibleForTesting
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.tools.ToolManager.Companion.MANAGED_TOOL_INSTALL_ROOT
import software.aws.toolkits.jetbrains.utils.assertIsNonDispatchThread
import software.aws.toolkits.jetbrains.utils.runUnderProgressIfNeeded
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AwsTelemetry
import software.aws.toolkits.telemetry.Result
import java.io.FileNotFoundException
import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Clock
import java.time.Instant
import java.time.Period
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.io.path.exists

class DefaultToolManager @NonInjectable internal constructor(private val clock: Clock) : ToolManager {
    constructor() : this(Clock.systemUTC())

    private val versionCache = ToolVersionCache()
    private val updateCheckCache = ConcurrentHashMap<ManagedToolType<*>, Instant>()
    private val managedToolLock = ReentrantLock()

    override fun <V : Version> getTool(type: ToolType<V>): Tool<ToolType<V>>? {
        // Check if user gave a custom path
        ToolSettings.getInstance().getExecutablePath(type)?.let {
            return Tool(type, Paths.get(it))
        }

        if (type is ManagedToolType<V>) {
            val managedTool = checkForInstalledTool(type)
            if (managedTool != null) {
                ApplicationManager.getApplication().executeOnPooledThread {
                    checkForUpdates(type)
                }
                return managedTool
            }
        }

        return detectTool(type)?.let {
            Tool(type, it)
        }
    }

    override fun <V : Version> getOrInstallTool(type: ManagedToolType<V>, project: Project?): Tool<ToolType<V>> = getTool(type) ?: runUnderProgressIfNeeded(
        project,
        message("executableCommon.installing", type.displayName),
        cancelable = false
    ) {
        installTool(project, type, ProgressManager.getInstance().progressIndicator)
    }

    /**
     * Returns a reference to the requested tool located at the specified path.
     *
     * Note: The tool may not have been checked for [Validity] yet. Caller must make the check before using the tool.
     */
    override fun <V : Version> getToolForPath(type: ToolType<V>, toolExecutablePath: Path): Tool<ToolType<V>> =
        Tool(type, toolExecutablePath)

    /**
     * Attempts to detect the requested Tool on the local file system.
     *
     * @return Either the path to the tool if found, or null if the tool can't be found or not auto-detectable
     */
    override fun <V : Version> detectTool(type: ToolType<V>): Path? {
        if (type is AutoDetectableToolType<*>) {
            return type.resolve()
        }
        return null
    }

    /**
     * Checks the [Validity] of the specified tool instance. An optional stricter version can be specified to override the minimum version.
     *
     * If called on a UI thread, a modal dialog is shown while the validation is in progress to avoid UI lock-ups
     */
    override fun <T : Version> validateCompatability(
        tool: Tool<ToolType<T>>?,
        stricterMinVersion: T?,
        project: Project?
    ): Validity {
        tool ?: return Validity.NotInstalled

        return runUnderProgressIfNeeded(project, message("executableCommon.validating", tool.type.displayName), false) {
            determineCompatability(tool, stricterMinVersion)
        }
    }

    private fun <T : Version> determineCompatability(tool: Tool<ToolType<T>>, stricterMinVersion: T?): Validity {
        assertIsNonDispatchThread()

        val version = when (val cacheResult = versionCache.getValue(tool)) {
            is ToolVersionCache.Result.Failure -> return Validity.ValidationFailed(detailedMessage(cacheResult.reason, tool))
            is ToolVersionCache.Result.Success -> cacheResult.version
        }

        val baseVersionCompatability = tool.type.supportedVersions()?.let {
            version.isValid(it)
        } ?: Validity.Valid(version)

        if (baseVersionCompatability !is Validity.Valid) {
            return baseVersionCompatability
        }

        stricterMinVersion?.let {
            if (stricterMinVersion > version) {
                return Validity.VersionTooOld(version, stricterMinVersion)
            }
        }

        return Validity.Valid(version)
    }

    private fun detailedMessage(exception: Exception, tool: Tool<ToolType<*>>) = when (exception) {
        is FileNotFoundException, is NoSuchFileException -> message("general.file_not_found", exception.message ?: tool.path)
        else -> ExceptionUtil.getMessage(exception)
    } ?: message("general.unknown_error")

    @VisibleForTesting
    internal fun <V : Version> checkForUpdates(type: ManagedToolType<V>, project: Project? = null) {
        assertIsNonDispatchThread()

        val now = Instant.now(clock)
        val lastCheck = updateCheckCache.getOrDefault(type, Instant.MIN)
        val needCheck = lastCheck.plus(UPDATE_CHECK_INTERVAL).isBefore(now)
        if (!needCheck) {
            LOG.debug { "Checked for newer versions of ${type.id} recently, nothing to do" }
            return
        }

        updateCheckCache[type] = now

        val latestVersion = type.determineLatestVersion()
        type.supportedVersions()?.let {
            val latestVersionCompatibility = latestVersion.isValid(it)
            if (latestVersionCompatibility !is Validity.Valid) {
                LOG.warn { "Latest version of ${type.id} (${latestVersion.displayValue()} is not compatible with the toolkit: ${type.supportedVersions()}" }
                return
            }
        }

        val currentInstall = checkForInstalledTool(type)
        val currentVersion = currentInstall?.let { type.determineVersion(it.path) }
        if (currentVersion != null && currentVersion >= latestVersion) {
            LOG.debug { "Current version of ${type.id} is greater than or equal to latest version, nothing to do" }
            return
        }

        ProgressManager.getInstance().run(
            object : Task.Backgroundable(
                project,
                message("executableCommon.updating", type.displayName),
                /* canBeCanceled */
                false,
                PerformInBackgroundOption.ALWAYS_BACKGROUND
            ) {
                override fun run(indicator: ProgressIndicator) {
                    installTool(project, type, indicator)
                }
            }
        )
    }

    private fun <V : Version> installTool(project: Project?, type: ManagedToolType<V>, indicator: ProgressIndicator?): Tool<ToolType<V>> {
        assertIsNonDispatchThread()

        try {
            val latestVersion = type.determineLatestVersion()

            type.supportedVersions()?.let {
                val latestVersionCompatibility = latestVersion.isValid(it)
                if (latestVersionCompatibility !is Validity.Valid) {
                    throw IllegalStateException(
                        message(
                            "executableCommon.latest_not_compatible",
                            type.displayName,
                            it.displayValue()
                        )
                    )
                }
            }

            return performInstall(type, latestVersion, indicator).also {
                AwsTelemetry.toolInstallation(project, type.telemetryId, Result.Succeeded)
            }
        } catch (e: Exception) {
            AwsTelemetry.toolInstallation(project, type.telemetryId, Result.Failed)
            throw IllegalStateException(message("executableCommon.failed_install", type.displayName), e)
        }
    }

    private fun <V : Version> performInstall(type: ManagedToolType<V>, version: V, indicator: ProgressIndicator?): Tool<ToolType<V>> {
        assertIsNonDispatchThread()

        val downloadDir = Files.createTempDirectory(type.id)
        try {
            val downloadFile = try {
                indicator?.text2 = message("executableCommon.downloading", type.displayName)
                type.downloadVersion(version, downloadDir, indicator)
            } finally {
                indicator?.text2 = ""
            }

            val versionString = version.displayValue()
            val installLocation = managedToolInstallDir(type.id, versionString)

            return ProgressIndicatorUtils.computeWithLockAndCheckingCanceled(
                managedToolLock,
                50,
                TimeUnit.MILLISECONDS,
                ThrowableComputable {
                    // Default the indicator to be indeterminate, a tool type can change it back if it can track status better
                    indicator?.isIndeterminate = true

                    if (installLocation.exists()) {
                        installLocation.delete(recursively = true)
                    }
                    type.installVersion(downloadFile, installLocation, indicator)

                    // Check install before updating marker
                    val tool = type.toTool(installLocation)

                    managedToolMarkerFile(type.id).write(versionString)

                    return@ThrowableComputable tool
                }
            )
        } finally {
            FileUtil.delete(downloadDir)
        }
    }

    private fun <V : Version> checkForInstalledTool(type: ManagedToolType<V>): Tool<ToolType<V>>? {
        val markerVersion = readMarkerVersion(type) ?: return null
        val installLocation = managedToolInstallDir(type.id, markerVersion).takeIf { it.exists() } ?: return null
        return type.toTool(installLocation)
    }

    private fun readMarkerVersion(type: ManagedToolType<*>): String? {
        val markerFile = managedToolMarkerFile(type.id).takeIf { it.exists() } ?: return null
        val markerVersion = managedToolLock.withLock {
            markerFile.readText()
        }
        // Avoid dir traversal
        if (markerVersion.contains("/") || markerVersion.contains("\\")) {
            return null
        }

        return markerVersion
    }

    companion object {
        private val LOG = getLogger<ToolManager>()
        private val UPDATE_CHECK_INTERVAL = Period.ofDays(1)
        private const val VERSION_MARKER_FILENAME = "VERSION"

        internal fun managedToolMarkerFile(toolId: String) = MANAGED_TOOL_INSTALL_ROOT.resolve(toolId).resolve(VERSION_MARKER_FILENAME)
        internal fun managedToolInstallDir(toolId: String, version: String) = MANAGED_TOOL_INSTALL_ROOT.resolve(toolId).resolve(version)
    }
}


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

package software.aws.toolkits.jetbrains.core.tools

data class FourPartVersion(val major: Int, val minor: Int, val patch: Int, val build: Int) : Version {
    override fun displayValue(): String = "$major.$minor.$patch.$build"

    override fun compareTo(other: Version): Int = COMPARATOR.compare(this, other as FourPartVersion)

    companion object {
        private val COMPARATOR = compareBy<FourPartVersion> { it.major }
            .thenBy { it.minor }
            .thenBy { it.patch }
            .thenBy { it.build }

        fun parse(version: String): FourPartVersion {
            val parts = version.split(".")
            if (parts.size != 4) {
                throw IllegalArgumentException("[$version] not in the format of MAJOR.MINOR.PATCH.BUILD")
            }

            try {
                return FourPartVersion(parts[0].toInt(), parts[1].toInt(), parts[2].toInt(), parts[3].toInt())
            } catch (e: Exception) {
                throw IllegalArgumentException("[$version] could not be parsed", e)
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.tools

data class SemanticVersion(val major: Int, val minor: Int, val patch: Int) : Version {
    override fun displayValue(): String = "$major.$minor.$patch"

    override fun compareTo(other: Version): Int = COMPARATOR.compare(this, other as SemanticVersion)

    companion object {
        // TODO: Support pre-release
        private val COMPARATOR = compareBy<SemanticVersion> { it.major }
            .thenBy { it.minor }
            .thenBy { it.patch }

        fun parse(version: String): SemanticVersion {
            val parts = version.split(".")
            if (parts.size != 3) {
                throw IllegalArgumentException("[$version] not in the format of MAJOR.MINOR.PATCH")
            }

            try {
                val preReleaseStart = parts[2].indexOfFirst { it == '+' || it == '-' }
                val patchStr = if (preReleaseStart >= 0) {
                    parts[2].substring(0, preReleaseStart)
                } else {
                    parts[2]
                }

                return SemanticVersion(parts[0].toInt(), parts[1].toInt(), patchStr.toInt())
            } catch (e: Exception) {
                throw IllegalArgumentException("[$version] could not be parsed", e)
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.tools

import java.nio.file.Path

data class Tool<out T : ToolType<*>>(val type: T, val path: Path) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Tool<*>

        if (type.id != other.type.id) return false
        if (path != other.path) return false

        return true
    }

    override fun hashCode(): Int {
        var result = type.id.hashCode()
        result = 31 * result + path.hashCode()
        return result
    }
}


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

package software.aws.toolkits.jetbrains.core.tools

import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.util.ClearableLazyValue
import com.intellij.ui.components.JBTextField
import com.intellij.ui.layout.applyToComponent
import com.intellij.ui.layout.panel
import software.aws.toolkits.jetbrains.ui.ValidatingPanel
import software.aws.toolkits.resources.message
import java.nio.file.Path

class ToolConfigurable : BoundConfigurable(message("executableCommon.configurable.title")), SearchableConfigurable {
    private val settings = ToolSettings.getInstance()
    private val manager = ToolManager.getInstance()
    private val panel = ClearableLazyValue.create {
        ValidatingPanel(
            disposable ?: throw RuntimeException("Should never happen as `createPanel` is called after disposable is assigned"),
            checkContinuously = false,
            panel {
                ToolType.EP_NAME.extensionList.forEach { toolType ->
                    row(toolType.displayName) {
                        textFieldWithBrowseButton(
                            getter = { settings.getExecutablePath(toolType) ?: "" },
                            setter = { settings.setExecutablePath(toolType, it.takeIf { v -> v.isNotBlank() }) },
                            fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileDescriptor()
                        ).withValidationOnApply {
                            it.textField.text.takeIf { t -> t.isNotBlank() }?.let { path ->
                                manager.validateCompatability(Path.of(path), toolType).toValidationInfo(toolType, component)
                            }
                        }.applyToComponent {
                            setEmptyText(toolType, textField as JBTextField)
                        }.constraints(
                            growX,
                            pushX
                        )

                        browserLink(message("aws.settings.learn_more"), toolType.documentationUrl())
                    }
                }
            },
            emptyMap()
        )
    }

    override fun createPanel() = panel.value.contentPanel

    override fun apply() {
        panel.value.apply()
    }

    override fun disposeUIResources() {
        // TODO: why are we overriding and not allowing the disposable to be disposed?
        panel.drop()
    }

    override fun getId(): String = "aws.tools"

    private fun setEmptyText(toolType: ToolType<Version>, field: JBTextField) {
        val resolved = (toolType as? AutoDetectableToolType<*>)?.resolve()
        field.emptyText.text = when {
            resolved != null && toolType.getTool()?.path == resolved -> message("executableCommon.auto_resolved", resolved)
            toolType is ManagedToolType<*> -> message("executableCommon.auto_managed")
            else -> message("common.none")
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.tools

import com.intellij.openapi.application.PathManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.util.io.createDirectories
import java.nio.file.Path
import java.nio.file.Paths

/**
 * Service for managing external tools such as CLIs.
 */
interface ToolManager {

    /**
     * Returns a reference to the requested tool.
     *
     * The returned Tool will first be checked if the user has set a path for it explicitly. If they have not, we will attempt to detect it for them if
     * supported.
     * Note: The tool may not have been checked for [Validity] yet. Caller must make the check before using the tool.
     */
    fun <V : Version> getTool(type: ToolType<V>): Tool<ToolType<V>>?

    fun <V : Version> getOrInstallTool(type: ManagedToolType<V>, project: Project? = null): Tool<ToolType<V>>

    /**
     * Returns a reference to the requested tool located at the specified path.
     *
     * Note: The tool may not have been checked for [Validity] yet. Caller must make the check before using the tool.
     */
    fun <V : Version> getToolForPath(type: ToolType<V>, toolExecutablePath: Path): Tool<ToolType<V>>

    /**
     * Attempts to detect the requested Tool on the local file system.
     *
     * @return Either the path to the tool if found, or null if the tool can't be found or not auto-detectable
     */
    fun <V : Version> detectTool(type: ToolType<V>): Path?

    /**
     * Checks the [Validity] of the specified tool instance. An optional stricter version can be specified to override the minimum version.
     *
     * If called on a UI thread, a modal dialog is shown while the validation is in progress to avoid UI lock-ups
     */
    fun <T : Version> validateCompatability(
        tool: Tool<ToolType<T>>?,
        stricterMinVersion: T? = null,
        project: Project? = null
    ): Validity

    companion object {
        fun getInstance(): ToolManager = service()

        internal val MANAGED_TOOL_INSTALL_ROOT by lazy {
            Paths.get(PathManager.getSystemPath(), "aws-static-resources").resolve("tools").createDirectories()
        }
    }
}

/**
 * Checks the [Validity] of the specified tool at the specified path. An optional stricter version can be specified to override the minimum version.
 *
 * If called on a UI thread, a modal dialog is shown while the validation is in progress to avoid UI lock-ups
 */
fun <T : Version> ToolManager.validateCompatability(
    path: Path,
    type: ToolType<T>,
    stricterMinVersion: T? = null,
    project: Project? = null
): Validity = validateCompatability(getToolForPath(type, path), stricterMinVersion, project)

fun <V : Version> ToolType<V>.getTool(): Tool<ToolType<V>>? = ToolManager.getInstance().getTool(this)

fun <V : Version> ManagedToolType<V>.getOrInstallTool(project: Project? = null): Tool<ToolType<V>> = ToolManager.getInstance().getOrInstallTool(this, project)


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

package software.aws.toolkits.jetbrains.core.tools

import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.RoamingType
import com.intellij.openapi.components.SimplePersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.util.xmlb.annotations.Attribute
import com.intellij.util.xmlb.annotations.Property
import com.intellij.util.xmlb.annotations.Tag

@State(name = "tools", storages = [Storage("aws.xml", roamingType = RoamingType.DISABLED)])
class ToolSettings : SimplePersistentStateComponent<ExecutableOptions>(ExecutableOptions()) {
    fun getExecutablePath(executable: ToolType<*>) = state.value[executable.id]?.executablePath

    fun setExecutablePath(executable: ToolType<*>, value: String?) {
        if (value == null) {
            state.value.remove(executable.id)
        } else {
            val original = state.value[executable.id] ?: ExecutableState2()
            state.value[executable.id] = original.copy(executablePath = value)
        }
    }

    companion object {
        fun getInstance(): ToolSettings = service()
    }
}

class ExecutableOptions : BaseState() {
    @get:Property
    val value by map<String, ExecutableState2>()
}

@Tag("ExecutableState")
data class ExecutableState2(
    @Attribute(value = "path")
    val executablePath: String? = null,
)


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

package software.aws.toolkits.jetbrains.core.tools

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.util.ExecUtil
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.progress.ProgressIndicator
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.services.ssm.SsmPlugin
import software.aws.toolkits.jetbrains.utils.checkSuccess
import software.aws.toolkits.telemetry.ToolId
import java.nio.file.Path

/**
 * Represents an executable external tool such as a CLI
 *
 * Note: It is recommended that all implementations of this interface are stateless and are an `object`
 */
interface ToolType<VersionScheme : Version> {
    /**
     * ID used to represent the executable in caches and settings. Must be globally unique
     */
    val id: String

    /**
     * Name of the executable for users, e.g. the marketing name of the executable
     */
    val displayName: String

    /**
     * List of supported [VersionRange]. An empty list means any version is supported
     */
    fun supportedVersions(): VersionRange<VersionScheme>?

    /**
     * Returns the [Version] for the executable of this type located at the specified location
     */
    fun determineVersion(path: Path): VersionScheme

    companion object {
        internal val EP_NAME = ExtensionPointName.create<DocumentedToolType<Version>>("aws.toolkit.tool")
    }
}

/**
 * Used to power the 'Learn more' link in the configurable
 */
interface DocumentedToolType<VersionScheme : Version> : ToolType<VersionScheme> {
    fun documentationUrl(): String
}

/**
 * Indicates that a [ToolType] can be auto-detected for the user on their system
 */
interface AutoDetectableToolType<VersionScheme : Version> : ToolType<VersionScheme> {
    /**
     * Attempt to automatically detect the tool's binary file
     *
     * @return the resolved path or null if not found
     * @throws Exception if an exception occurred attempting to resolve the path
     */
    fun resolve(): Path?
}

interface ManagedToolType<VersionScheme : Version> : ToolType<VersionScheme> {
    override val id: String
        get() = telemetryId.toString()

    /**
     * Used to identify this tool with the telemetry stack
     */
    val telemetryId: ToolId
    fun determineLatestVersion(): VersionScheme
    fun downloadVersion(version: VersionScheme, destinationDir: Path, indicator: ProgressIndicator?): Path
    fun installVersion(downloadArtifact: Path, destinationDir: Path, indicator: ProgressIndicator?)
    fun toTool(installDir: Path): Tool<ToolType<VersionScheme>>
}

abstract class BaseToolType<VersionScheme : Version> : ToolType<VersionScheme> {
    final override fun determineVersion(path: Path): VersionScheme {
        val processOutput = ExecUtil.execAndGetOutput(GeneralCommandLine(path.toString()).apply(::versionCommand), VERSION_TIMEOUT_MS)

        if (!processOutput.checkSuccess(LOGGER)) {
            throw IllegalStateException("Failed to determine version of ${SsmPlugin.displayName}")
        }
        return parseVersion(processOutput.stdout.trim())
    }

    /**
     * Used as a hook to call executable with parameters to determine version.
     *
     * [baseCmd] is a [GeneralCommandLine] with the [Tool.path] as the base
     */
    open fun versionCommand(baseCmd: GeneralCommandLine) {
        baseCmd.withParameters("--version")
    }

    /**
     * Parse the version from standard out
     */
    abstract fun parseVersion(output: String): VersionScheme

    companion object {
        private const val VERSION_TIMEOUT_MS = 5000
        private val LOGGER = getLogger<BaseToolType<*>>()
    }
}


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

package software.aws.toolkits.jetbrains.core.tools

import com.intellij.openapi.progress.util.ProgressIndicatorUtils
import com.intellij.openapi.util.ThrowableComputable
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.lastModified
import software.aws.toolkits.core.utils.warn
import java.nio.file.Files
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock

/**
 * Stores data related to a file path. Cache is invalidated when the cache entry is detected as stale.  Errors are
 * cached until the underlying path is detected as stale. Stale is defined as the cache entries (file modification time)[Files.getLastModifiedTime]
 * is older than the path's current modification time.
 */
class ToolVersionCache {
    private val cache = ConcurrentHashMap<Tool<*>, Result<*>>()
    private val lock = ReentrantLock()

    @Suppress("UNCHECKED_CAST")
    fun <V : Version> getValue(tool: Tool<ToolType<V>>): Result<V> = ProgressIndicatorUtils.computeWithLockAndCheckingCanceled(
        lock,
        50,
        TimeUnit.MILLISECONDS,
        ThrowableComputable {
            val lastResult = cache[tool]
            var lastModifiedTime = 0L
            try {
                lastModifiedTime = tool.path.lastModified().toMillis()

                if (lastResult == null || lastResult.lastModifiedTime < lastModifiedTime) {
                    Result.Success(getVersion(tool), lastModifiedTime).also {
                        cache[tool] = it
                    } as Result<V>
                } else {
                    lastResult as Result<V>
                }
            } catch (e: Exception) {
                LOG.warn(e) { "Unable to get tool version for $tool" }
                Result.Failure(e, lastModifiedTime).also {
                    cache[tool] = it
                } as Result<V>
            }
        }
    )

    private fun <T : Version> getVersion(tool: Tool<ToolType<T>>): Version = tool.type.determineVersion(tool.path)

    sealed class Result<T : Version>(open val lastModifiedTime: Long) {
        data class Failure(val reason: Exception, override val lastModifiedTime: Long) : Result<Nothing>(lastModifiedTime)
        data class Success<V : Version>(val version: V, override val lastModifiedTime: Long) : Result<V>(lastModifiedTime)
    }

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


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

package software.aws.toolkits.jetbrains.core.tools

import com.intellij.openapi.ui.ValidationInfo
import software.aws.toolkits.core.utils.htmlWrap
import software.aws.toolkits.resources.message
import javax.swing.JComponent

/**
 * Represents if the [Tool] is compatible with the Toolkit
 */
sealed class Validity {

    /**
     * The Tool is installed and compatible
     */
    data class Valid(val version: Version) : Validity()

    /**
     * The tool is not valid for some reason
     */
    sealed class Invalid : Validity()

    /**
     * Represents that the tool was not found
     */
    object NotInstalled : Invalid()

    /**
     * The Tool is not installed on the system, or it is unsuitable to be used due to some issue such as corruption or permissions.
     */
    data class ValidationFailed(val detailedMessage: String) : Invalid()

    /**
     * The Tool is installed, but its version is too low to be compatible with the Toolkit / feature
     */
    data class VersionTooOld(val actualVersion: Version, val minVersion: Version) : Invalid()

    /**
     * The Tool is installed, but its version is too new to be compatible with the Toolkit / feature
     */
    data class VersionTooNew(val actualVersion: Version, val maxVersion: Version) : Invalid()
}

/**
 * @return Convert [Validity] to a human-readable error message if one is applicable
 */
fun Validity.Invalid.toErrorMessage(executableType: ToolType<*>): String = when (this) {
    is Validity.ValidationFailed -> message("executableCommon.missing_executable", executableType.displayName, this.detailedMessage)
    is Validity.NotInstalled -> message("executableCommon.not_installed")
    is Validity.VersionTooNew -> message("executableCommon.version_too_high")
    is Validity.VersionTooOld -> message("executableCommon.version_too_low2", executableType.displayName, this.minVersion)
}

/**
 * @return Convert [Validity] to a human-readable error message for UI validation if one is applicable
 */
fun Validity.toValidationInfo(executableType: ToolType<*>, component: JComponent? = null) = when (this) {
    is Validity.Valid -> null
    is Validity.Invalid -> ValidationInfo(toErrorMessage(executableType).htmlWrap(), component)
}


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

package software.aws.toolkits.jetbrains.core.tools

/**
 * Top level interface for different versioning schemes such as semantic version
 */
interface Version : Comparable<Version> {
    /**
     * @return Human-readable representation of the version
     */
    fun displayValue(): String
}

/**
 * @return true if the specified version is compatible with the specified version ranges. Always returns true if no range is specified.
 */
fun <T : Version> T.isValid(range: VersionRange<T>?): Validity = when {
    range == null -> Validity.Valid(this)
    this < range.minVersion -> Validity.VersionTooOld(this, range.minVersion)
    range.maxVersion <= this -> Validity.VersionTooNew(this, range.maxVersion)
    else -> Validity.Valid(this)
}

/**
 * Represents a range of versions.
 *
 * @property minVersion The minimum version supported, inclusive.
 * @property maxVersion The maximum version supported, exclusive.
 */
data class VersionRange<T : Version>(val minVersion: T, val maxVersion: T) {
    fun displayValue() = "${minVersion.displayValue()} â‰¤ X < ${maxVersion.displayValue()}"
}

infix fun <T : Version> T.until(that: T): VersionRange<T> = VersionRange(this, that)


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

package software.aws.toolkits.jetbrains.core.toolwindow

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.ui.content.Content
import javax.swing.JComponent

interface ToolkitToolWindow {
    val project: Project
    val toolWindowId: String

    fun toolWindow() = ToolWindowManager.getInstance(project).getToolWindow(toolWindowId)
        ?: throw IllegalStateException("Can't find tool window $toolWindowId")

    /**
     * Adds a new tab to the tool window
     *
     * @param title Title of the tab
     * @param component The JComponent of the tab's content. If [Disposable] will be auto disposed on close
     * @param activate Show the tab upon adding it
     * @param id Unique ID to identify the tab
     * @param additionalDisposable An additional [Disposable] to dispose when the tab is closed
     */
    fun addTab(
        title: String,
        component: JComponent,
        activate: Boolean = false,
        id: String = title,
        additionalDisposable: Disposable? = null
    ): Content {
        val toolWindow = toolWindow()
        val contentManager = toolWindow.contentManager
        val content = contentManager.factory.createContent(component, title, false).also {
            it.isCloseable = true
            it.isPinnable = true
            it.putUserData(AWS_TOOLKIT_TAB_ID_KEY, id)

            if (additionalDisposable != null) {
                it.setDisposer(additionalDisposable)
            }
        }

        contentManager.addContent(content)
        if (activate) {
            show(content)
        }

        return content
    }

    fun removeContent(content: Content) = runInEdt {
        val toolWindow = toolWindow()
        toolWindow.contentManager.removeContent(content, true)
    }

    fun show(content: Content) {
        val toolWindow = toolWindow()
        toolWindow.activate(null, true)
        toolWindow.contentManager.setSelectedContent(content)
    }

    fun showExistingContent(id: String): Boolean {
        val toolWindow = toolWindow()

        val content = find(id)
        if (content != null) {
            runInEdt {
                toolWindow.activate(null, true)
                toolWindow.contentManager.setSelectedContent(content)
            }

            return true
        }

        return false
    }

    fun find(id: String): Content? =
        toolWindow().contentManager.contents.find { id == it.getUserData(AWS_TOOLKIT_TAB_ID_KEY) }

    // prefix is prefix of the id. Assumes the window is using id composed of paths, like: "loggroup/logstream"
    fun findPrefix(prefix: String): List<Content> {
        val toolWindow = toolWindow()

        return toolWindow.contentManager.contents.filter {
            val key = it.getUserData(AWS_TOOLKIT_TAB_ID_KEY) ?: ""
            key.startsWith("$prefix/") || key == prefix
        }
    }

    companion object {
        private val AWS_TOOLKIT_TAB_ID_KEY = Key.create<String>("awsToolkitTabId")
    }
}


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

package software.aws.toolkits.jetbrains.core.utils

/*
 * Replace with [kotlin.collections.buildList] when experimental is removed
 */
inline fun <E> buildList(builderAction: MutableList<E>.() -> Unit): List<E> = ArrayList<E>().apply(builderAction)
inline fun <E> buildList(capacity: Int, builderAction: MutableList<E>.() -> Unit): List<E> = ArrayList<E>(capacity).apply(builderAction)

/*
 * Replace with [kotlin.collections.buildMap] when experimental is removed
 */
inline fun <T, U> buildMap(builderAction: MutableMap<T, U>.() -> Unit): Map<T, U> = mutableMapOf<T, U>().apply(builderAction)


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

package software.aws.toolkits.jetbrains.core.utils

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DataKey

/**
 * Returns the specified DataKey from the DataContext. If not found, it will throw an exception
 */
fun <T> DataContext.getRequiredData(dataId: DataKey<T>): T = this.getData(dataId) ?: throw IllegalStateException("Required dataId '${dataId.name}` was missing")


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

package software.aws.toolkits.jetbrains.services

import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import com.intellij.util.io.isFile
import com.intellij.xdebugger.XSourcePosition
import com.jetbrains.python.debugger.PyLocalPositionConverter
import com.jetbrains.python.debugger.PySourcePosition
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.services.PathMapper.Companion.normalizeLocal
import java.nio.file.Files
import java.nio.file.Paths
import java.util.Objects

/**
 * Maps a local path to a remote path. The order of the list indicates the order of priority where first possible
 * candidate wins out. When mapping back to the local file system, the file must exist for it to count as a match.
 */
class PathMapper(private val mappings: List<PathMapping>) {
    fun convertToLocal(remotePath: String): String? {
        val normalizedRemote = FileUtil.normalize(remotePath)
        for (mapping in mappings) {
            if (normalizedRemote.startsWith(mapping.remoteRoot)) {
                val localPath = normalizedRemote.replaceFirst(mapping.remoteRoot, mapping.localRoot)
                if (Files.exists(Paths.get(FileUtil.toSystemDependentName(localPath)))) {
                    return localPath
                }
            }
        }
        LOG.debug { "Failed to map $remotePath to local file system: $mappings" }
        return null
    }

    fun convertToRemote(localPath: String): String? {
        val (normalizedPath, ignoreCase) = if (SystemInfo.isWindows) {
            FileUtil.normalize(localPath) to true
        } else {
            localPath to false
        }
        for (mapping in mappings) {
            if (normalizedPath.startsWith(mapping.localRoot, ignoreCase)) {
                return FileUtil.normalize(normalizedPath.replaceFirst(mapping.localRoot, mapping.remoteRoot, ignoreCase))
            }
        }
        LOG.debug { "Failed to map $localPath to remote file system: $mappings" }
        return null
    }

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

        fun normalizeLocal(localPath: String): String {
            val updatedPath = if (SystemInfo.isWindows) {
                localPath.toLowerCase()
            } else {
                localPath
            }

            return FileUtil.toCanonicalPath(updatedPath, true)
        }
    }

    /**
     * Converts the IDE's view of the world into the Docker image's view allowing for breakpoints and frames to work
     */
    internal class PositionConverter(private val pathMapper: PathMapper) : PyLocalPositionConverter() {
        class PyLocalSourcePosition(file: String, line: Int) : PySourcePosition(file, line) {

            override fun normalize(file: String?): String? = file?.let {
                super.normalize(file)
            }
        }

        override fun convertToPython(filePath: String, line: Int): PySourcePosition {
            val localSource = PyLocalSourcePosition(filePath, line)
            val remoteFile = pathMapper.convertToRemote(localSource.file) ?: localSource.file
            return PyRemoteSourcePosition(remoteFile, localSource.line)
        }

        override fun convertFromPython(position: PySourcePosition, frameName: String?): XSourcePosition? {
            val localFile = pathMapper.convertToLocal(position.file) ?: position.file
            return createXSourcePosition(getVirtualFile(localFile), position.line)
        }
    }
}

class PathMapping(localPath: String, remoteDirectory: String) {
    private val directory = Paths.get(localPath).let {
        if (it.isFile()) {
            it.parent.toString()
        } else {
            localPath
        }
    }
    val localRoot = normalizeLocal(directory) + "/"
    val remoteRoot = FileUtil.normalize("$remoteDirectory/")

    override fun toString() = "PathMapping(localRoot='$localRoot', remoteRoot='$remoteRoot')"

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as PathMapping

        if (localRoot != other.localRoot) return false
        if (remoteRoot != other.remoteRoot) return false

        return true
    }

    override fun hashCode(): Int = Objects.hash(localRoot, remoteRoot)
}


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

package software.aws.toolkits.jetbrains.services.apprunner

import com.intellij.openapi.project.Project
import icons.AwsIcons
import software.amazon.awssdk.services.apprunner.AppRunnerClient
import software.amazon.awssdk.services.apprunner.model.ServiceSummary
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerResourceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerServiceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.CacheBackedAwsExplorerServiceRootNode
import software.aws.toolkits.jetbrains.services.apprunner.resources.AppRunnerResources
import software.aws.toolkits.jetbrains.utils.toHumanReadable
import software.aws.toolkits.resources.message

class AppRunnerNode(project: Project, service: AwsExplorerServiceNode) :
    CacheBackedAwsExplorerServiceRootNode<ServiceSummary>(project, service, AppRunnerResources.LIST_SERVICES) {
    override fun displayName() = message("explorer.node.apprunner")
    override fun toNode(child: ServiceSummary): AwsExplorerNode<*> = AppRunnerServiceNode(nodeProject, child)
}

class AppRunnerServiceNode(
    project: Project,
    val service: ServiceSummary
) : AwsExplorerResourceNode<String>(
    project,
    AppRunnerClient.SERVICE_NAME,
    service.serviceName(),
    AwsIcons.Resources.APPRUNNER_SERVICE
) {
    override fun resourceType(): String = "service"
    override fun resourceArn(): String = service.serviceArn()
    override fun statusText(): String = service.status().toString().toHumanReadable()

    override fun isAlwaysShowPlus(): Boolean = false
    override fun isAlwaysLeaf(): Boolean = true
}


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.project.DumbAwareAction
import software.aws.toolkits.jetbrains.services.apprunner.ui.CreationDialog
import software.aws.toolkits.resources.message

class CreateServiceAction : DumbAwareAction(message("apprunner.action.create.service")) {
    override fun actionPerformed(e: AnActionEvent) {
        CreationDialog(e.getRequiredData(PlatformDataKeys.PROJECT)).show()
    }
}


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

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

import software.amazon.awssdk.services.apprunner.AppRunnerClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.explorer.actions.DeleteResourceAction
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.services.apprunner.AppRunnerServiceNode
import software.aws.toolkits.jetbrains.services.apprunner.resources.AppRunnerResources
import software.aws.toolkits.resources.message

class DeleteServiceAction : DeleteResourceAction<AppRunnerServiceNode>(message("apprunner.action.delete.service")) {
    override fun performDelete(selected: AppRunnerServiceNode) {
        val client = selected.nodeProject.awsClient<AppRunnerClient>()
        client.deleteService { it.serviceArn(selected.resourceArn()) }
        selected.nodeProject.refreshAwsTree(AppRunnerResources.LIST_SERVICES)
    }
}


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.DumbAware
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import software.amazon.awssdk.services.apprunner.AppRunnerClient
import software.amazon.awssdk.services.apprunner.model.AppRunnerException
import software.amazon.awssdk.services.apprunner.model.ServiceStatus
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.apprunner.AppRunnerServiceNode
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.checkIfLogStreamExists
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.ApprunnerTelemetry
import software.aws.toolkits.telemetry.Result

class DeployAction : SingleResourceNodeAction<AppRunnerServiceNode>(message("apprunner.action.deploy")), DumbAware {
    override fun update(selected: AppRunnerServiceNode, e: AnActionEvent) {
        e.presentation.isVisible = selected.service.status() == ServiceStatus.RUNNING
    }

    override fun actionPerformed(selected: AppRunnerServiceNode, e: AnActionEvent) {
        val project = selected.nodeProject
        ProgressManager.getInstance().run(
            object : Task.Backgroundable(project, message("apprunner.action.deploy.starting"), false, ALWAYS_BACKGROUND) {
                override fun run(indicator: ProgressIndicator) = runBlocking {
                    deploy(selected, project.awsClient(), project.awsClient())
                }
            }
        )
    }

    internal suspend fun deploy(selected: AppRunnerServiceNode, client: AppRunnerClient, cloudwatchClient: CloudWatchLogsClient) {
        val project = selected.nodeProject
        val deployment = try {
            client.startDeployment { it.serviceArn(selected.resourceArn()) }
        } catch (e: Exception) {
            notifyError(
                project = project,
                title = message("apprunner.action.deploy.failed"),
                content = if (e is AppRunnerException) e.awsErrorDetails().errorMessage() else message("apprunner.action.deploy.failed")
            )
            LOG.error(e) { "Failed to deploy new AppRunner service revision" }
            ApprunnerTelemetry.startDeployment(project, Result.Failed)
            return
        }
        ApprunnerTelemetry.startDeployment(project, Result.Succeeded)
        val logGroup = "/aws/apprunner/${selected.service.serviceName()}/${selected.service.serviceId()}/service"
        val logStream = "deployment/${deployment.operationId()}"
        val logWindow = CloudWatchLogWindow.getInstance(project)
        // Try 15 times. If it's not made by 15 seconds, there is probably another issue, so show an error message
        repeat(15) { _ ->
            if (runCatching { cloudwatchClient.checkIfLogStreamExists(logGroup, logStream) }.getOrNull() == true) {
                LOG.info { "Found log stream for deployment $logStream" }
                logWindow.showLogStream(logGroup, logStream, streamLogs = true)
                return
            }
            delay(1000)
        }
        notifyError(
            project = project,
            content = message("apprunner.action.deploy.unableToFindLogStream", logStream)
        )
        LOG.error { "Unable to find log stream for deployment $logStream" }
    }

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