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

package software.aws.toolkits.jetbrains.services.ssm

import com.intellij.openapi.util.SystemInfo
import com.intellij.testFramework.ApplicationRule
import com.intellij.util.io.HttpRequests
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.util.UUID

class SsmPluginTest {
    @Rule
    @JvmField
    val application = ApplicationRule()

    @Rule
    @JvmField
    val tempFolder = TemporaryFolder()

    @Test
    fun `download URLs all work`() {
        val latest = SsmPlugin.determineLatestVersion()
        SoftAssertions.assertSoftly { softly ->
            listOf(
                SsmPlugin.windowsUrl(latest),
                SsmPlugin.linuxArm64Url(latest),
                SsmPlugin.linuxI64Url(latest),
                SsmPlugin.ubuntuArm64Url(latest),
                SsmPlugin.ubuntuI64Url(latest),
                SsmPlugin.macUrl(latest)
            ).forEach { url ->
                softly.assertThatCode { HttpRequests.head(url).tryConnect() }.doesNotThrowAnyException()
            }
        }
    }

    @Test
    fun `end to end install works`() {
        val executableName = if (SystemInfo.isWindows) {
            "session-manager-plugin.exe"
        } else {
            "session-manager-plugin"
        }

        val latest = SsmPlugin.determineLatestVersion()
        val downloadDir = tempFolder.newFolder().toPath()
        val installDir = tempFolder.newFolder()
            .resolve("nested1-${UUID.randomUUID()}")
            .resolve("nested2-${UUID.randomUUID()}")
            .toPath()

        val downloadedFile = SsmPlugin.downloadVersion(latest, downloadDir, null)
        SsmPlugin.installVersion(downloadedFile, installDir, null)
        val tool = SsmPlugin.toTool(installDir)
        assertThat(tool.path.fileName.toString()).isEqualTo(executableName)

        val reportedLatest = SsmPlugin.determineVersion(tool.path)
        assertThat(reportedLatest).isEqualTo(latest)
    }
}


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

package icons

import com.intellij.icons.AllIcons
import com.intellij.openapi.util.IconLoader
import com.intellij.ui.LayeredIcon
import javax.swing.Icon

/**
 * Lives in `icons` package due to that is how [com.intellij.openapi.util.IconLoader.getReflectiveIcon] works
 */
object AwsIcons {
    object Logos {
        @JvmField val AWS = load("/icons/logos/AWS.svg") // 13x13

        @JvmField val AWS_SMILE_SMALL = load("/icons/logos/AWS_smile.svg") // 16x16

        @JvmField val AWS_SMILE_LARGE = load("/icons/logos/AWS_smile_Large.svg") // 64x64

        @JvmField val CLOUD_FORMATION_TOOL = load("/icons/logos/CloudFormationTool.svg") // 13x13

        @JvmField val CODE_CATALYST_MEDIUM = load("/icons/logos/Amazon_CodeCatalyst_Medium.svg") // 32x32

        @JvmField val CODE_CATALYST_SMALL = load("/icons/logos/Amazon_CodeCatalyst_Small.svg") // 16x16

        @JvmField val EVENT_BRIDGE = load("/icons/logos/EventBridge.svg") // 13x13
    }

    object Misc {
        @JvmField val SMILE = load("/icons/misc/smile.svg") // 16x16

        @JvmField val SMILE_GREY = load("/icons/misc/smile_grey.svg") // 16x16

        @JvmField val FROWN = load("/icons/misc/frown.svg") // 16x16
    }

    object Resources {
        @JvmField val APPRUNNER_SERVICE = load("/icons/resources/AppRunnerService.svg") // 16x16

        @JvmField val CLOUDFORMATION_STACK = load("/icons/resources/CloudFormationStack.svg") // 16x16
        object CloudWatch {
            @JvmField val LOGS = load("/icons/resources/cloudwatchlogs/CloudWatchLogs.svg") // 16x16

            @JvmField val LOGS_TOOL_WINDOW = load("/icons/resources/cloudwatchlogs/CloudWatchLogsToolWindow.svg") // 13x13

            @JvmField val LOG_GROUP = load("/icons/resources/cloudwatchlogs/CloudWatchLogsGroup.svg") // 16x16
        }

        @JvmField val ECR_REPOSITORY = load("/icons/resources/ECRRepository.svg") // 16x16

        @JvmField val LAMBDA_FUNCTION = load("/icons/resources/LambdaFunction.svg") // 16x16

        @JvmField val SCHEMA_REGISTRY = load("/icons/resources/SchemaRegistry.svg") // 16x16

        @JvmField val SCHEMA = load("/icons/resources/Schema.svg") // 16x16

        @JvmField val SERVERLESS_APP = load("/icons/resources/ServerlessApp.svg") // 16x16

        @JvmField val S3_BUCKET = load("/icons/resources/S3Bucket.svg") // 16x16

        @JvmField val REDSHIFT = load("/icons/resources/Redshift.svg") // 16x16
        object DynamoDb {
            @JvmField val TABLE = load("/icons/resources/dynamodb/DynamoDbTable.svg")
        }
        object Ecs {
            @JvmField val ECS_CLUSTER = load("/icons/resources/ecs/EcsCluster.svg")

            @JvmField val ECS_SERVICE = load("/icons/resources/ecs/EcsService.svg")

            @JvmField val ECS_TASK_DEFINITION = load("/icons/resources/ecs/EcsTaskDefinition.svg")
        }
        object Rds {
            @JvmField val MYSQL = load("/icons/resources/rds/Mysql.svg") // 16x16

            @JvmField val POSTGRES = load("/icons/resources/rds/Postgres.svg") // 16x16
        }
        object Sqs {
            @JvmField val SQS_QUEUE = load("/icons/resources/sqs/SqsQueue.svg") // 16x16

            @JvmField val SQS_TOOL_WINDOW = load("/icons/resources/sqs/SqsToolWindow.svg") // 13x13
        }
    }

    object Actions {
        @JvmField val LAMBDA_FUNCTION_NEW: Icon = LayeredIcon.create(Resources.LAMBDA_FUNCTION, AllIcons.Actions.New)

        @JvmField val SCHEMA_VIEW: Icon = AllIcons.Actions.Preview

        @JvmField val SCHEMA_CODE_GEN: Icon = AllIcons.Actions.Download

        @JvmField val SCHEMA_SEARCH: Icon = AllIcons.Actions.Search
    }

    private fun load(path: String): Icon = IconLoader.getIcon(path, AwsIcons::class.java)
}


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

package software.aws.toolkits.jetbrains

import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.extensions.PluginDescriptor
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.util.registry.Registry
import java.nio.file.Paths

object AwsToolkit {
    const val PLUGIN_ID = "aws.toolkit"
    const val GITHUB_URL = "https://github.com/aws/aws-toolkit-jetbrains"
    const val AWS_DOCS_URL = "https://docs.aws.amazon.com/console/toolkit-for-jetbrains"

    val PLUGIN_VERSION: String by lazy {
        DESCRIPTOR?.version ?: "Unknown"
    }

    val DESCRIPTOR: PluginDescriptor? by lazy {
        PluginManagerCore.getPlugin(PluginId.getId(PLUGIN_ID))
    }

    fun pluginPath() = if (ApplicationManager.getApplication().isUnitTestMode) {
        Paths.get(System.getProperty("plugin.path"))
    } else {
        DESCRIPTOR?.pluginPath ?: throw RuntimeException("Toolkit root not available")
    }

    fun isDeveloperMode() = Registry.`is`("aws.toolkit.developerMode", false)
}


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

package software.aws.toolkits.jetbrains

object ToolkitPlaces {
    const val DEVTOOLS_TOOL_WINDOW = "DevToolsToolWindow"
    const val EXPLORER_TOOL_WINDOW = "ExplorerToolWindow"
    const val EDITOR_PSI_REFERENCE = "Editor"
    const val DEV_TOOL_WINDOW = "DeveloperToolsWindow"
    const val ADD_CONNECTION_DIALOG = "ToolkitAddConnectionDialog"
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.ex.ApplicationInfoEx
import com.intellij.openapi.components.service
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider
import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder
import software.amazon.awssdk.core.SdkClient
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
import software.amazon.awssdk.http.SdkHttpClient
import software.aws.toolkits.core.ClientConnectionSettings
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.TokenConnectionSettings
import software.aws.toolkits.core.ToolkitClientCustomizer
import software.aws.toolkits.core.ToolkitClientManager
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.core.credentials.ToolkitCredentialsChangeListener
import software.aws.toolkits.core.region.ToolkitRegionProvider
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import software.aws.toolkits.jetbrains.settings.AwsSettings

open class AwsClientManager : ToolkitClientManager(), Disposable {
    init {
        val busConnection = ApplicationManager.getApplication().messageBus.connect(this)
        busConnection.subscribe(
            CredentialManager.CREDENTIALS_CHANGED,
            object : ToolkitCredentialsChangeListener {
                override fun providerRemoved(identifier: CredentialIdentifier) {
                    invalidateSdks(identifier.id)
                }

                override fun providerRemoved(providerId: String) {
                    invalidateSdks(providerId)
                }
            }
        )
    }

    override val userAgent = AwsClientManager.userAgent

    override fun dispose() {
        shutdown()
    }

    override fun sdkHttpClient(): SdkHttpClient = AwsSdkClient.getInstance().sharedSdkClient()

    override fun getRegionProvider(): ToolkitRegionProvider = AwsRegionProvider.getInstance()

    override fun globalClientCustomizer(
        credentialProvider: AwsCredentialsProvider?,
        tokenProvider: SdkTokenProvider?,
        regionId: String,
        builder: AwsClientBuilder<*, *>,
        clientOverrideConfiguration: ClientOverrideConfiguration.Builder
    ) {
        CUSTOMIZER_EP.extensionList.forEach { it.customize(credentialProvider, tokenProvider, regionId, builder, clientOverrideConfiguration) }
    }

    companion object {
        @JvmStatic
        fun getInstance(): ToolkitClientManager = service()

        val userAgent: String by lazy {
            val platformName = tryOrNull { ApplicationNamesInfo.getInstance().fullProductNameWithEdition.replace(' ', '-') }
            val platformVersion = tryOrNull { ApplicationInfoEx.getInstanceEx().fullVersion.replace(' ', '-') }
            "AWS-Toolkit-For-JetBrains/${AwsToolkit.PLUGIN_VERSION} $platformName/$platformVersion ClientId/${AwsSettings.getInstance().clientId}"
        }

        internal val CUSTOMIZER_EP = ExtensionPointName<ToolkitClientCustomizer>("aws.toolkit.sdk.clientCustomizer")
    }
}

inline fun <reified T : SdkClient> Project.awsClient(): T {
    val accountSettingsManager = AwsConnectionManager.getInstance(this)

    return AwsClientManager
        .getInstance()
        .getClient(accountSettingsManager.activeCredentialProvider, accountSettingsManager.activeRegion)
}

inline fun <reified T : SdkClient> ConnectionSettings.awsClient(): T = AwsClientManager.getInstance().getClient(credentials, region)

inline fun <reified T : SdkClient> TokenConnectionSettings.awsClient(): T = AwsClientManager.getInstance().getClient(this)

inline fun <reified T : SdkClient> ClientConnectionSettings<*>.awsClient(): T = when (this) {
    is ConnectionSettings -> awsClient<T>()
    is TokenConnectionSettings -> awsClient<T>()
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.util.Alarm
import com.intellij.util.AlarmFactory
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting
import software.amazon.awssdk.core.SdkClient
import software.aws.toolkits.core.ClientConnectionSettings
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.TokenConnectionSettings
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.core.credentials.ToolkitBearerTokenProvider
import software.aws.toolkits.core.credentials.ToolkitCredentialsChangeListener
import software.aws.toolkits.core.credentials.ToolkitCredentialsProvider
import software.aws.toolkits.core.credentials.toEnvironmentVariables
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.credentials.getConnectionSettingsOrThrow
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance
import software.aws.toolkits.jetbrains.core.executables.ExecutableManager
import software.aws.toolkits.jetbrains.core.executables.ExecutableType
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import kotlin.reflect.KClass

// Getting resources can take a long time on a slow connection or if there are a lot of resources. This call should
// always be done in an async context so it should be OK to take multiple seconds.
private val DEFAULT_TIMEOUT = Duration.ofSeconds(30)

/**
 * Intended to prevent repeated unnecessary calls to AWS to understand resource state.
 *
 * Will cache responses from AWS by [AwsRegion]/[ToolkitCredentialsProvider] - generically applicable to any AWS call.
 */
interface AwsResourceCache {
    /**
     * Get a [resource] either by making a call or returning it from the cache if present and unexpired.
     *
     * @param[useStale] if an exception occurs attempting to refresh the resource return a cached version if it exists (even if it's expired). Default: true
     * @param[forceFetch] force the resource to refresh (and update cache) even if a valid cache version exists. Default: false
     */
    fun <T> getResource(
        resource: Resource<T>,
        region: AwsRegion,
        credentialProvider: ToolkitCredentialsProvider,
        useStale: Boolean = true,
        forceFetch: Boolean = false
    ): CompletionStage<T>

    /**
     * @see [getResource]
     */
    fun <T> getResource(
        resource: Resource<T>,
        region: AwsRegion,
        tokenProvider: ToolkitBearerTokenProvider,
        useStale: Boolean = true,
        forceFetch: Boolean = false
    ): CompletionStage<T>

    /**
     * @see [getResource]
     */
    fun <T> getResource(
        resource: Resource<T>,
        connectionSettings: ClientConnectionSettings<*>,
        useStale: Boolean = true,
        forceFetch: Boolean = false
    ): CompletionStage<T> = when (connectionSettings) {
        is ConnectionSettings -> getResource(resource, connectionSettings.region, connectionSettings.credentials, useStale, forceFetch)
        is TokenConnectionSettings -> getResource(resource, connectionSettings.region, connectionSettings.tokenProvider, useStale, forceFetch)
    }

    /**
     * Blocking version of [getResource]
     *
     * @param[region] the specific [AwsRegion] to use for this resource
     * @param[credentialProvider] the specific [ToolkitCredentialsProvider] to use for this resource
     */
    fun <T> getResourceNow(
        resource: Resource<T>,
        region: AwsRegion,
        credentialProvider: ToolkitCredentialsProvider,
        timeout: Duration = DEFAULT_TIMEOUT,
        useStale: Boolean = true,
        forceFetch: Boolean = false
    ): T = wait(timeout) { getResource(resource, region, credentialProvider, useStale, forceFetch) }

    /**
     * Blocking version of [getResource]
     */
    fun <T> getResourceNow(
        resource: Resource<T>,
        region: AwsRegion,
        tokenProvider: ToolkitBearerTokenProvider,
        timeout: Duration = DEFAULT_TIMEOUT,
        useStale: Boolean = true,
        forceFetch: Boolean = false
    ): T = wait(timeout) { getResource(resource, region, tokenProvider, useStale, forceFetch) }

    /**
     * Blocking version of [getResource]
     */
    fun <T> getResourceNow(
        resource: Resource<T>,
        connectionSettings: ClientConnectionSettings<*>,
        timeout: Duration = DEFAULT_TIMEOUT,
        useStale: Boolean = true,
        forceFetch: Boolean = false
    ): T = when (connectionSettings) {
        is ConnectionSettings -> getResourceNow(resource, connectionSettings.region, connectionSettings.credentials, timeout, useStale, forceFetch)
        is TokenConnectionSettings -> getResourceNow(resource, connectionSettings.region, connectionSettings.tokenProvider, timeout, useStale, forceFetch)
    }

    /**
     * Gets the [resource] if it exists in the cache.
     *
     * @param[region] the specific [AwsRegion] to use for this resource
     * @param[credentialProvider] the specific [ToolkitCredentialsProvider] to use for this resource
     */
    fun <T> getResourceIfPresent(resource: Resource<T>, region: AwsRegion, credentialProvider: ToolkitCredentialsProvider, useStale: Boolean = true): T?

    /**
     * Gets the [resource] if it exists in the cache.
     */
    fun <T> getResourceIfPresent(resource: Resource<T>, region: AwsRegion, tokenProvider: ToolkitBearerTokenProvider, useStale: Boolean = true): T?

    /**
     * Gets the [resource] if it exists in the cache.
     */
    fun <T> getResourceIfPresent(resource: Resource<T>, connectionSettings: ClientConnectionSettings<*>, useStale: Boolean = true): T? =
        when (connectionSettings) {
            is ConnectionSettings -> getResourceIfPresent(resource, connectionSettings.region, connectionSettings.credentials, useStale)
            is TokenConnectionSettings -> getResourceIfPresent(resource, connectionSettings.region, connectionSettings.tokenProvider, useStale)
        }

    /**
     * Clears the contents of the cache across all regions, credentials and resource types.
     */
    suspend fun clear() // TODO: ultimately all of these calls need to be made suspend - start with this one to resolve UI lock

    /**
     * Clears the contents of the cache for the specific [ClientConnectionSettings]
     */
    fun clear(connectionSettings: ClientConnectionSettings<*>)

    /**
     * Clears the contents of the cache for the specific [resource] type] & [ClientConnectionSettings]
     */
    fun clear(resource: Resource<*>, connectionSettings: ClientConnectionSettings<*>)

    companion object {
        @JvmStatic
        fun getInstance(): AwsResourceCache = service()

        private fun <T> wait(timeout: Duration, call: () -> CompletionStage<T>) = try {
            call().toCompletableFuture().get(timeout.toMillis(), TimeUnit.MILLISECONDS)
        } catch (e: ExecutionException) {
            throw e.cause ?: e
        }
    }
}

/**
 * Get a [resource] either by making a call or returning it from the cache if present and unexpired. Uses the currently [AwsRegion]
 * & [ToolkitCredentialsProvider] active in [AwsConnectionManager].
 *
 * @param[useStale] if an exception occurs attempting to refresh the resource return a cached version if it exists (even if it's expired). Default: true
 * @param[forceFetch] force the resource to refresh (and update cache) even if a valid cache version exists. Default: false
 */
fun <T> Project.getResource(resource: Resource<T>, useStale: Boolean = true, forceFetch: Boolean = false): CompletionStage<T> =
    AwsResourceCache.getInstance().getResource(resource, this.getConnectionSettingsOrThrow(), useStale, forceFetch)

/**
 * Blocking version of [getResource]
 *
 * @param[useStale] if an exception occurs attempting to refresh the resource return a cached version if it exists (even if it's expired). Default: true
 * @param[forceFetch] force the resource to refresh (and update cache) even if a valid cache version exists. Default: false
 */
fun <T> Project.getResourceNow(resource: Resource<T>, timeout: Duration = DEFAULT_TIMEOUT, useStale: Boolean = true, forceFetch: Boolean = false): T =
    AwsResourceCache.getInstance().getResourceNow(resource, this.getConnectionSettingsOrThrow(), timeout, useStale, forceFetch)

/**
 * Gets the [resource] if it exists in the cache.
 *
 * @param[useStale] return a cached version if it exists (even if it's expired). Default: true
 */
fun <T> ConnectionSettings.getResourceIfPresent(resource: Resource<T>, useStale: Boolean = true): T? =
    AwsResourceCache.getInstance().getResourceIfPresent(resource, this, useStale)

/**
 * Gets the [resource] if it exists in the cache.
 *
 * @see [ConnectionSettings.getResourceIfPresent]
 */
fun <T> Project.getResourceIfPresent(resource: Resource<T>, useStale: Boolean = true): T? =
    getConnectionSettingsOrThrow().getResourceIfPresent(resource, useStale)

/**
 * Clears the contents of the cache for the specific [resource] type, in the currently active [ConnectionSettings]
 */
fun Project.clearResourceForCurrentConnection(resource: Resource<*>) =
    AwsResourceCache.getInstance().clear(resource, this.getConnectionSettingsOrThrow())

/**
 * Clears the contents of the cache of all resource types for the currently active [ConnectionSettings]
 */
fun Project.clearResourceForCurrentConnection() =
    AwsResourceCache.getInstance().clear(this.getConnectionSettingsOrThrow())

sealed class Resource<T> {

    /**
     * A [Cached] resource is one whose fetch is potentially expensive, the result of which should be memoized for a period of time ([expiry]).
     */
    abstract class Cached<T> : Resource<T>() {
        abstract fun fetch(connectionSettings: ClientConnectionSettings<*>): T
        open fun expiry(): Duration = DEFAULT_EXPIRY
        abstract val id: String

        companion object {
            private val DEFAULT_EXPIRY = Duration.ofMinutes(10)
        }
    }

    /**
     * A [View] resource depends on some other underlying [Resource] and then performs some [transform] of the [underlying]'s result
     * in order to return the desired type [Output]. The [transform] result is not cached, [transform]s are re-applied on each fetch - thus should
     * should be relatively cheap.
     */
    class View<Input, Output>(val underlying: Resource<Input>, private val transform: (Input, AwsRegion) -> Output) : Resource<Output>() {
        @Suppress("UNCHECKED_CAST")
        fun doMap(input: Any, region: AwsRegion) = transform(input as Input, region)
    }

    companion object {
        fun <Input, Output> view(underlying: Resource<Input>, transform: Input.() -> Output): Resource<Output> =
            View(underlying) { input, _ -> transform(input) }
    }
}

fun <Input, Output> Resource<out Iterable<Input>>.map(transform: (Input) -> Output): Resource<List<Output>> = Resource.view(this) { map(transform) }

fun <T> Resource<out Iterable<T>>.filter(predicate: (T) -> Boolean): Resource<List<T>> = Resource.view(this) { filter(predicate) }

fun <T> Resource<out Iterable<T>>.find(predicate: (T) -> Boolean): Resource<T?> = Resource.view(this) { find(predicate) }

class ClientBackedCachedResource<ReturnType, ClientType : SdkClient>(
    private val sdkClientClass: KClass<ClientType>,
    override val id: String,
    private val expiry: Duration?,
    private val fetchCall: ClientType.() -> ReturnType
) : Resource.Cached<ReturnType>() {

    constructor(sdkClientClass: KClass<ClientType>, id: String, fetchCall: ClientType.() -> ReturnType) : this(sdkClientClass, id, null, fetchCall)

    override fun fetch(connectionSettings: ClientConnectionSettings<*>): ReturnType {
        val client = AwsClientManager.getInstance().getClient(sdkClientClass, connectionSettings)
        return fetchCall(client)
    }

    override fun expiry(): Duration = expiry ?: super.expiry()
    override fun toString(): String = "ClientBackedCachedResource(id='$id')"
}

class ExecutableBackedCacheResource<ReturnType, ExecType : ExecutableType<*>>(
    private val executableTypeClass: KClass<ExecType>,
    override val id: String,
    private val expiry: Duration? = null,
    private val fetchCall: GeneralCommandLine.() -> ReturnType
) : Resource.Cached<ReturnType>() {

    override fun fetch(connectionSettings: ClientConnectionSettings<*>): ReturnType {
        val executableType = ExecutableType.getExecutable(executableTypeClass.java)

        val executable = ExecutableManager.getInstance().getExecutableIfPresent(executableType).let {
            when (it) {
                is ExecutableInstance.Executable -> it
                is ExecutableInstance.InvalidExecutable, is ExecutableInstance.UnresolvedExecutable ->
                    throw IllegalStateException((it as ExecutableInstance.BadExecutable).validationError)
            }
        }

        return fetchCall(
            executable.getCommandLine()
                .withEnvironment(connectionSettings.region.toEnvironmentVariables())
                .apply {
                    if (connectionSettings is ConnectionSettings) {
                        withEnvironment(connectionSettings.credentials.resolveCredentials().toEnvironmentVariables())
                    }
                }
        )
    }

    override fun expiry(): Duration = expiry ?: super.expiry()
    override fun toString(): String = "ExecutableBackedCacheResource(id='$id')"
}

@ExperimentalCoroutinesApi
class DefaultAwsResourceCache(
    private val clock: Clock,
    private val maximumCacheEntries: Int,
    private val maintenanceInterval: Duration
) : AwsResourceCache, Disposable, ToolkitCredentialsChangeListener {
    private val coroutineScope = disposableCoroutineScope(this)

    @Suppress("unused")
    constructor() : this(Clock.systemDefaultZone(), MAXIMUM_CACHE_ENTRIES, DEFAULT_MAINTENANCE_INTERVAL)

    private val cache = ConcurrentHashMap<CacheKey, Entry<*>>()
    private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this)

    init {
        ApplicationManager.getApplication().messageBus.connect(this).apply {
            subscribe(CredentialManager.CREDENTIALS_CHANGED, this@DefaultAwsResourceCache)

            subscribe(
                BearerTokenProviderListener.TOPIC,
                object : BearerTokenProviderListener {
                    override fun onChange(providerId: String) {
                        clearByCredential(providerId)
                    }
                }
            )
        }
        scheduleCacheMaintenance()
    }

    override fun <T> getResource(
        resource: Resource<T>,
        region: AwsRegion,
        credentialProvider: ToolkitCredentialsProvider,
        useStale: Boolean,
        forceFetch: Boolean
    ): CompletionStage<T> = when (resource) {
        is Resource.View<*, T> -> getResource(
            resource.underlying,
            region,
            credentialProvider,
            useStale,
            forceFetch
        ).thenApply { resource.doMap(it as Any, region) }
        is Resource.Cached<T> -> Context(resource, region, ConnectionSettings(credentialProvider, region), useStale, forceFetch)
            .also { getCachedResource(it) }
            .future
    }

    override fun <T> getResource(
        resource: Resource<T>,
        region: AwsRegion,
        tokenProvider: ToolkitBearerTokenProvider,
        useStale: Boolean,
        forceFetch: Boolean
    ): CompletionStage<T> = when (resource) {
        is Resource.View<*, T> -> getResource(
            resource.underlying,
            region,
            tokenProvider,
            useStale,
            forceFetch
        ).thenApply { resource.doMap(it as Any, region) }
        is Resource.Cached<T> -> Context(resource, region, TokenConnectionSettings(tokenProvider, region), useStale, forceFetch)
            .also { getCachedResource(it) }
            .future
    }

    private fun <T> getCachedResource(context: Context<T>) {
        ApplicationManager.getApplication().executeOnPooledThread {
            var currentValue: Entry<T>? = null
            try {
                @Suppress("UNCHECKED_CAST")
                val result = cache.compute(context.cacheKey) { _, value ->
                    currentValue = value as Entry<T>?
                    fetchIfNeeded(context, currentValue)
                } as Entry<T>

                coroutineScope.launch {
                    try {
                        context.future.complete(result.value.await())
                    } catch (e: Throwable) {
                        val previousValue = currentValue
                        if (context.useStale && previousValue != null && previousValue.value.isCompleted && !previousValue.value.isCompletedExceptionally) {
                            context.future.complete(previousValue.value.getCompleted())
                        } else {
                            context.future.completeExceptionally(e)
                        }
                    }
                }
            } catch (e: Throwable) {
                context.future.completeExceptionally(e)
            }
        }
    }

    private fun runCacheMaintenance() {
        try {
            doRunCacheMaintenance()
        } finally {
            scheduleCacheMaintenance()
        }
    }

    @VisibleForTesting
    internal fun doRunCacheMaintenance() {
        var totalWeight = 0
        cache.entries.removeIf { it.value.value.isCompletedExceptionally }
        val entries = cache.entries.asSequence().filter { it.value.value.isCompleted }.onEach { totalWeight += it.value.weight }.toList()
        var exceededWeight = totalWeight - maximumCacheEntries
        if (exceededWeight <= 0) return
        entries.sortedBy { it.value.expiry }.forEach { (key, value) ->
            if (exceededWeight <= 0) return@doRunCacheMaintenance
            if (cache.computeRemoveIf(key) { it === value }) {
                exceededWeight -= value.weight
            }
        }
    }

    private fun scheduleCacheMaintenance() {
        if (!alarm.isDisposed) {
            alarm.addRequest(this::runCacheMaintenance, maintenanceInterval.toMillis())
        }
    }

    override fun <T> getResourceIfPresent(resource: Resource<T>, region: AwsRegion, credentialProvider: ToolkitCredentialsProvider, useStale: Boolean): T? =
        when (resource) {
            is Resource.Cached<T> -> {
                val key = CacheKey(resource.id, region.id, credentialProvider.id)
                val entry = cache.getTyped<T>(key)
                when {
                    entry != null && (useStale || entry.notExpired) &&
                        entry.value.isCompleted && entry.value.getCompletionExceptionOrNull() == null -> entry.value.getCompleted()
                    else -> null
                }
            }
            is Resource.View<*, T> -> getResourceIfPresent(resource.underlying, region, credentialProvider, useStale)?.let {
                resource.doMap(
                    it,
                    region
                )
            }
        }

    override fun <T> getResourceIfPresent(resource: Resource<T>, region: AwsRegion, tokenProvider: ToolkitBearerTokenProvider, useStale: Boolean): T? =
        when (resource) {
            is Resource.Cached<T> -> {
                val key = CacheKey(resource.id, region.id, tokenProvider.id)
                val entry = cache.getTyped<T>(key)
                when {
                    entry != null && (useStale || entry.notExpired) &&
                        entry.value.isCompleted && entry.value.getCompletionExceptionOrNull() == null -> entry.value.getCompleted()
                    else -> null
                }
            }
            is Resource.View<*, T> -> getResourceIfPresent(resource.underlying, region, tokenProvider, useStale)?.let {
                resource.doMap(
                    it,
                    region
                )
            }
        }

    override fun clear(resource: Resource<*>, connectionSettings: ClientConnectionSettings<*>) {
        when (resource) {
            is Resource.Cached<*> -> cache.remove(CacheKey(resource.id, connectionSettings.region.id, connectionSettings.providerId))
            is Resource.View<*, *> -> clear(resource.underlying, connectionSettings)
        }
    }

    override suspend fun clear() {
        coroutineScope { launch { cache.clear() } }
    }

    override fun clear(connectionSettings: ClientConnectionSettings<*>) {
        cache.keys.removeIf { it.providerId == connectionSettings.providerId && it.regionId == connectionSettings.region.id }
    }

    override fun dispose() {
        coroutineScope.launch { clear() }
    }

    override fun providerRemoved(identifier: CredentialIdentifier) = clearByCredential(identifier.id)

    override fun providerModified(identifier: CredentialIdentifier) = clearByCredential(identifier.id)

    private fun clearByCredential(providerId: String) {
        cache.keys.removeIf { it.providerId == providerId }
    }

    private fun <T> fetchIfNeeded(context: Context<T>, currentEntry: Entry<T>?) = when {
        currentEntry == null -> fetch(context)
        currentEntry.value.isCompletedExceptionally -> fetch(context)
        currentEntry.notExpired && !context.forceFetch -> currentEntry
        context.useStale -> fetchWithFallback(context, currentEntry)
        else -> fetch(context)
    }

    private val Deferred<*>.isCompletedExceptionally get() = isCompleted && getCompletionExceptionOrNull() != null

    private fun <T> fetchWithFallback(context: Context<T>, currentEntry: Entry<T>) = try {
        fetch(context)
    } catch (e: Exception) {
        LOG.warn(e) { "Failed to fetch resource using ${context.cacheKey}, falling back to expired entry" }
        currentEntry
    }

    private fun <T> fetch(context: Context<T>): Entry<T> {
        val value = coroutineScope.async {
            context.resource.fetch(context.connectionSettings)
        }

        return Entry(clock.instant().plus(context.resource.expiry()), value)
    }

    private val Entry<*>.notExpired get() = value.isActive || clock.instant().isBefore(expiry)

    @VisibleForTesting
    internal fun hasCacheEntry(resourceId: String): Boolean = cache.filterKeys { it.resourceId == resourceId }.isNotEmpty()

    companion object {
        private val LOG = getLogger<DefaultAwsResourceCache>()
        private const val MAXIMUM_CACHE_ENTRIES = 1000
        private val DEFAULT_MAINTENANCE_INTERVAL: Duration = Duration.ofMinutes(5)

        private data class CacheKey(val resourceId: String, val regionId: String, val providerId: String)

        private class Context<T>(
            val resource: Resource.Cached<T>,
            val region: AwsRegion,
            val connectionSettings: ClientConnectionSettings<*>,
            val useStale: Boolean,
            val forceFetch: Boolean
        ) {
            val cacheKey = CacheKey(resource.id, region.id, connectionSettings.providerId)
            val future = CompletableFuture<T>()
        }

        private class Entry<T>(val expiry: Instant, val value: Deferred<T>) {
            val weight: Int
                get() = if (value.isCompleted && value.getCompletionExceptionOrNull() == null) {
                    when (val underlying = value.getCompleted()) {
                        is Collection<*> -> underlying.size
                        else -> 1
                    }
                } else {
                    1
                }
        }

        private fun <T> ConcurrentMap<CacheKey, Entry<*>>.getTyped(key: CacheKey) = this[key]?.let {
            @Suppress("UNCHECKED_CAST")
            it as Entry<T>
        }

        /**
         * Atomically apply a [predicate] to the value at [key] (if it exists) and remove if matched.
         *
         * @return - true if removal occurred else false
         */
        private fun <K, V> ConcurrentHashMap<K, V>.computeRemoveIf(key: K, predicate: (V) -> Boolean): Boolean {
            var removed = false
            computeIfPresent(key) { _, v ->
                if (predicate(v)) {
                    removed = true
                    null
                } else {
                    v
                }
            }
            return removed
        }
    }
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.util.net.ssl.CertificateManager
import com.intellij.util.proxy.CommonProxy
import org.apache.http.impl.client.SystemDefaultCredentialsProvider
import org.apache.http.impl.conn.SystemDefaultRoutePlanner
import software.amazon.awssdk.http.ExecutableHttpRequest
import software.amazon.awssdk.http.HttpExecuteRequest
import software.amazon.awssdk.http.SdkHttpClient
import software.amazon.awssdk.http.apache.ApacheHttpClient
import software.amazon.awssdk.http.apache.ProxyConfiguration
import software.aws.toolkits.core.clients.SdkClientProvider
import software.aws.toolkits.core.utils.assertTrue
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info

class AwsSdkClient : SdkClientProvider, Disposable {
    private val sdkHttpClient: SdkHttpClient by lazy {
        LOG.info { "Create new Apache client" }
        val httpClientBuilder = ApacheHttpClient.builder()
            .proxyConfiguration(ProxyConfiguration.builder().useSystemPropertyValues(false).build())
            .httpRoutePlanner(SystemDefaultRoutePlanner(CommonProxy.getInstance()))
            .credentialsProvider(SystemDefaultCredentialsProvider())
            .tlsTrustManagersProvider { arrayOf(CertificateManager.getInstance().trustManager) }

        ValidateCorrectThreadClient(httpClientBuilder.build())
    }

    override fun sharedSdkClient(): SdkHttpClient = sdkHttpClient

    override fun dispose() {
        sdkHttpClient.close()
    }

    private class ValidateCorrectThreadClient(private val base: SdkHttpClient) : SdkHttpClient by base {
        override fun prepareRequest(request: HttpExecuteRequest?): ExecutableHttpRequest {
            val application = ApplicationManager.getApplication()

            val isValidThread = !application.isWriteAccessAllowed && !application.isReadAccessAllowed
            LOG.assertTrue(isValidThread) { WRONG_THREAD }
            if (!isValidThread && application.isUnitTestMode) {
                throw AssertionError(WRONG_THREAD)
            }

            return base.prepareRequest(request)
        }
    }

    companion object {
        private val LOG = getLogger<AwsSdkClient>()
        private const val WRONG_THREAD = "Network calls can't be made inside read/write action"

        fun getInstance(): SdkClientProvider = service()
    }
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
import software.aws.toolkits.jetbrains.settings.AwsSettings
import software.aws.toolkits.jetbrains.settings.AwsSettingsConfigurable
import software.aws.toolkits.resources.message

class AwsTelemetryPrompter : StartupActivity.Background {
    override fun runActivity(project: Project) {
        if (AwsSettings.getInstance().promptedForTelemetry || System.getProperty("aws.telemetry.skip_prompt", null)?.toBoolean() == true) {
            return
        }

        val notification = Notification(
            "aws.toolkit_telemetry",
            message("aws.settings.telemetry.prompt.title"),
            message("aws.settings.telemetry.prompt.message"),
            NotificationType.INFORMATION
        ).also {
            it.setListener { notification, _ ->
                ShowSettingsUtil.getInstance().showSettingsDialog(project, AwsSettingsConfigurable::class.java)
                notification.expire()
            }
        }

        notification.notify(project)

        AwsSettings.getInstance().promptedForTelemetry = true
    }
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.lang.Language
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiManager
import com.intellij.psi.impl.light.LightElement

class DummyPsiElement(project: Project) : LightElement(PsiManager.getInstance(project), Language.ANY) {
    override fun toString(): String = "DummyPsiElement"
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.util.io.HttpRequests
import org.apache.http.entity.ContentType
import java.nio.file.Path

fun saveFileFromUrl(url: String, path: Path, indicator: ProgressIndicator? = null) =
    HttpRequests.request(url).userAgent(AwsClientManager.userAgent).saveToFile(path.toFile(), indicator)

fun readBytesFromUrl(url: String, indicator: ProgressIndicator? = null) =
    HttpRequests.request(url).userAgent(AwsClientManager.userAgent).readBytes(indicator)

fun getTextFromUrl(url: String): String =
    HttpRequests.request(url).userAgent(AwsClientManager.userAgent).readString()

fun writeJsonToUrl(url: String, jsonString: String, indicator: ProgressIndicator? = null): String =
    HttpRequests.post(url, ContentType.APPLICATION_JSON.toString())
        .userAgent(AwsClientManager.userAgent)
        .connect { request ->
            request.write(jsonString)
            request.readString(indicator)
        }


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

import com.intellij.openapi.extensions.CustomLoadingExtensionPointBean
import com.intellij.util.KeyedLazyInstance
import com.intellij.util.xmlb.annotations.Attribute

/**
 * Extension point that is used to tie multiple extension points together by a common ID.
 *
 * For example, if you have a "parent" extension point that defines a new ID, i.e Lambda runtime.
 * "Children" extension points that support it can then use the same ID to correlate each other,
 * i.e Lambda building, Lambda handler can be looked up using the same ID as defined by the Runtime parent EP.
 *
 * Additional attributes can be defined on the EP by extending this class.
 */
open class IdBasedExtensionPoint<T> : CustomLoadingExtensionPointBean<T>(), KeyedLazyInstance<T> {
    @Attribute("id")
    lateinit var id: String

    @Attribute("implementationClass")
    lateinit var implementationClass: String

    override fun getImplementationClassName(): String = implementationClass

    override fun getKey(): String = id
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.components.service
import com.intellij.util.io.createDirectories
import software.aws.toolkits.core.utils.DefaultRemoteResourceResolver
import software.aws.toolkits.core.utils.RemoteResourceResolver
import software.aws.toolkits.core.utils.UrlFetcher
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.CompletableFuture

interface RemoteResourceResolverProvider {
    fun get(): RemoteResourceResolver

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

class DefaultRemoteResourceResolverProvider : RemoteResourceResolverProvider {
    override fun get() = RESOLVER_INSTANCE

    companion object {
        private val RESOLVER_INSTANCE by lazy {
            val cachePath = Paths.get(PathManager.getSystemPath(), "aws-static-resources").createDirectories()

            DefaultRemoteResourceResolver(HttpRequestUrlFetcher, cachePath) {
                val future = CompletableFuture<Path>()
                ApplicationManager.getApplication().executeOnPooledThread {
                    try {
                        future.complete(it.call())
                    } catch (e: Exception) {
                        future.completeExceptionally(e)
                    }
                }
                future
            }
        }

        object HttpRequestUrlFetcher : UrlFetcher {
            override fun fetch(url: String, file: Path) {
                saveFileFromUrl(url, file)
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.coroutines

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.util.concurrency.AppExecutorUtil
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext

// https://github.com/JetBrains/intellij-community/blob/375a130e119e5550972f65fafea2335dfffa28e3/platform/core-api/src/com/intellij/openapi/application/coroutines.kt#L134-L151
fun ModalityState.asContextElement(): CoroutineContext.Element = ModalityStateElement(this)

private class ModalityStateElement(val modalityState: ModalityState) : AbstractCoroutineContextElement(ModalityStateElementKey)

private object ModalityStateElementKey : CoroutineContext.Key<ModalityStateElement>

fun getCoroutineUiContext(): CoroutineContext = EdtCoroutineDispatcher
private object EdtCoroutineDispatcher : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        val state = context[ModalityStateElementKey]?.modalityState ?: ModalityState.any()
        ApplicationManager.getApplication().invokeLater(block, state)
    }
}

fun getCoroutineBgContext(): CoroutineContext = AppExecutorUtil.getAppExecutorService().asCoroutineDispatcher()


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

package software.aws.toolkits.jetbrains.core.coroutines

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.Application
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import java.util.concurrent.CancellationException

/**
 * Coroutine scope that is tied to the full Application closing, or plugin unloading. Default to dispatching to background thread pool.
 *
 * Use this if the coroutine needs to live past a project being closed or across projects such as an Application Service
 */
fun applicationCoroutineScope(coroutineName: String): CoroutineScope =
    PluginCoroutineScopeTracker.getInstance().applicationThreadPoolScope(coroutineName)

/**
 * Coroutine scope that is tied to a project closing, or plugin unloading. Default to dispatching to background thread pool.
 *
 * Use this if the coroutine needs to live past a UI being closed, or tied to a project's life cycle such as a Project Service.
 */
fun projectCoroutineScope(project: Project, coroutineName: String): CoroutineScope =
    PluginCoroutineScopeTracker.getInstance(project).applicationThreadPoolScope(coroutineName)

/**
 * Coroutine scope that is tied to a disposable or a plugin unloading. Default to dispatching to background thread pool.
 *
 * Use this if the coroutine is tied to a UI such as a tool window, or a run configuration
 *
 * **Note: If a call lives past the closing of a UI such as kicking off a resource creation, use [projectCoroutineScope].
 * Otherwise, the coroutine will be canceled when the UI is closed!**
 */
fun disposableCoroutineScope(disposable: Disposable, coroutineName: String): CoroutineScope {
    check(disposable !is Project && disposable !is Application) { "disposable should not be a project or application" }
    return PluginCoroutineScopeTracker.getInstance().applicationThreadPoolScope(coroutineName).also {
        Disposer.register(disposable) {
            it.cancel(CancellationException("Parent disposable was disposed"))
        }
    }
}

/**
 * Version of [applicationCoroutineScope] the class name as the coroutine name.
 */
inline fun <reified T : Any> T.applicationCoroutineScope(): CoroutineScope =
    applicationCoroutineScope(T::class.java.name)

/**
 * Version of [projectCoroutineScope] the class name as the coroutine name.
 */
inline fun <reified T : Any> T.projectCoroutineScope(project: Project): CoroutineScope =
    projectCoroutineScope(project, T::class.java.name)

/**
 * Version of [disposableCoroutineScope] the class name as the coroutine name.
 */
inline fun <reified T : Any> T.disposableCoroutineScope(disposable: Disposable): CoroutineScope =
    disposableCoroutineScope(disposable, T::class.java.name)

class PluginCoroutineScopeTracker : Disposable {
    @PublishedApi
    internal fun applicationThreadPoolScope(coroutineName: String): CoroutineScope = BackgroundThreadPoolScope(coroutineName, this)

    override fun dispose() {}

    companion object {
        fun getInstance() = service<PluginCoroutineScopeTracker>()
        fun getInstance(project: Project) = project.service<PluginCoroutineScopeTracker>()
    }
}

private class BackgroundThreadPoolScope(coroutineName: String, disposable: Disposable) : CoroutineScope {
    override val coroutineContext = SupervisorJob() + CoroutineName(coroutineName) + getCoroutineBgContext()

    init {
        Disposer.register(disposable) {
            coroutineContext.cancel(CancellationException("Parent disposable was disposed"))
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.credentials

import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.application.AppUIExecutor
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.SimpleModificationTracker
import com.intellij.util.ExceptionUtil
import com.intellij.util.messages.Topic
import org.jetbrains.concurrency.AsyncPromise
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.core.credentials.CredentialProviderNotFoundException
import software.aws.toolkits.core.credentials.ToolkitCredentialsChangeListener
import software.aws.toolkits.core.credentials.ToolkitCredentialsProvider
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import software.aws.toolkits.jetbrains.services.sts.StsResources
import software.aws.toolkits.jetbrains.utils.MRUList
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AwsTelemetry
import java.util.concurrent.atomic.AtomicReference

abstract class AwsConnectionManager(private val project: Project) : SimpleModificationTracker(), Disposable {
    private val resourceCache = AwsResourceCache.getInstance()
    private val regionProvider = AwsRegionProvider.getInstance()
    private val credentialsRegionHandler = CredentialsRegionHandler.getInstance(project)

    private val validationJob = AtomicReference<AsyncPromise<ConnectionState>>()

    @Volatile
    var connectionState: ConnectionState = ConnectionState.InitializingToolkit
        internal set(value) {
            field = value
            incModificationCount()

            AppUIExecutor.onWriteThread(ModalityState.any()).expireWith(this).execute {
                project.messageBus.syncPublisher(CONNECTION_SETTINGS_STATE_CHANGED).settingsStateChanged(value)
            }
        }

    protected val recentlyUsedProfiles = MRUList<String>(MAX_HISTORY)
    protected val recentlyUsedRegions = MRUList<String>(MAX_HISTORY)

    // Internal state is visible for AwsSettingsPanel and ChangeAccountSettingsActionGroup
    internal var selectedCredentialIdentifier: CredentialIdentifier? = null
    internal var selectedRegion: AwsRegion? = null

    init {
        @Suppress("LeakingThis")
        ApplicationManager.getApplication().messageBus.connect(this)
            .subscribe(
                CredentialManager.CREDENTIALS_CHANGED,
                object : ToolkitCredentialsChangeListener {
                    override fun providerRemoved(identifier: CredentialIdentifier) {
                        if (selectedCredentialIdentifier == identifier) {
                            changeConnectionSettings(null, selectedRegion)
                        }
                    }

                    override fun providerModified(identifier: CredentialIdentifier) {
                        if (selectedCredentialIdentifier == identifier) {
                            refreshConnectionState()
                        }
                    }
                }
            )
    }

    fun isValidConnectionSettings(): Boolean = connectionState is ConnectionState.ValidConnection

    fun connectionSettings(): ConnectionSettings? = when (val state = connectionState) {
        is ConnectionState.ValidConnection -> ConnectionSettings(state.credentials, state.region)
        else -> null
    }

    /**
     * Re-trigger validation of the current connection
     */
    fun refreshConnectionState() {
        changeFieldsAndNotify { }
    }

    /**
     * Internal setter that allows for null values and is intended to set the internal state and still notify
     */
    protected fun changeConnectionSettings(identifier: CredentialIdentifier?, region: AwsRegion?) {
        changeFieldsAndNotify {
            identifier?.let {
                recentlyUsedProfiles.add(it.id)
            }

            region?.let {
                recentlyUsedRegions.add(it.id)
            }

            selectedCredentialIdentifier = identifier
            selectedRegion = region
        }
    }

    /**
     * Changes the credentials and then validates them. Notifies listeners of results
     */
    fun changeCredentialProvider(identifier: CredentialIdentifier, passive: Boolean = false) {
        changeFieldsAndNotify {
            recentlyUsedProfiles.add(identifier.id)

            selectedCredentialIdentifier = identifier

            selectedRegion = credentialsRegionHandler.determineSelectedRegion(identifier, selectedRegion)
        }

        AwsTelemetry.setCredentials(project = project, credentialType = identifier.credentialType.toTelemetryType(), passive = passive)
    }

    /**
     * Changes the region and then validates them. Notifies listeners of results
     */
    fun changeRegion(region: AwsRegion, passive: Boolean = false) {
        val oldRegion = selectedRegion
        changeFieldsAndNotify {
            recentlyUsedRegions.add(region.id)
            selectedRegion = region
        }

        if (oldRegion?.partitionId != region.partitionId) {
            AwsTelemetry.setPartition(project = project, partitionId = region.partitionId, passive = passive)
        }

        AwsTelemetry.setRegion(project = project, passive = passive)
    }

    @Synchronized
    private fun changeFieldsAndNotify(fieldUpdateBlock: () -> Unit) {
        val isInitial = connectionState is ConnectionState.InitializingToolkit
        connectionState = ConnectionState.ValidatingConnection

        // Grab the current state stamp
        val modificationStamp = this.modificationCount

        fieldUpdateBlock()

        val validateCredentialsResult = validateCredentials(selectedCredentialIdentifier, selectedRegion, isInitial)
        validationJob.getAndSet(validateCredentialsResult)?.cancel()

        validateCredentialsResult.onSuccess {
            // Validate we are still operating in the latest view of the world
            if (modificationStamp == this.modificationCount) {
                connectionState = it
            } else {
                LOGGER.warn { "validateCredentials returned but the account manager state has been manipulated before results were back, ignoring" }
            }
        }
    }

    private fun validateCredentials(credentialsIdentifier: CredentialIdentifier?, region: AwsRegion?, isInitial: Boolean): AsyncPromise<ConnectionState> {
        val promise = AsyncPromise<ConnectionState>()
        ApplicationManager.getApplication().executeOnPooledThread {
            if (credentialsIdentifier == null || region == null) {
                promise.setResult(ConnectionState.IncompleteConfiguration(credentialsIdentifier, region))
                return@executeOnPooledThread
            }

            if (isInitial && credentialsIdentifier is InteractiveCredential && credentialsIdentifier.userActionRequired()) {
                promise.setResult(ConnectionState.RequiresUserAction(credentialsIdentifier))
                return@executeOnPooledThread
            }

            var success = true
            try {
                val credentialsProvider = CredentialManager.getInstance().getAwsCredentialProvider(credentialsIdentifier, region)

                validate(credentialsProvider, region)

                promise.setResult(ConnectionState.ValidConnection(credentialsProvider, region))
            } catch (e: Exception) {
                LOGGER.warn(e) { message("credentials.profile.validation_error", credentialsIdentifier.displayName) }
                success = false
                promise.setResult(ConnectionState.InvalidConnection(e))
            } finally {
                AwsTelemetry.validateCredentials(
                    project,
                    success = success,
                    credentialType = credentialsIdentifier.credentialType.toTelemetryType()
                )
            }
        }

        return promise
    }

    /**
     * Legacy method, should be considered deprecated and avoided since it loads defaults out of band
     */
    val activeRegion: AwsRegion
        get() = selectedRegion ?: AwsRegionProvider.getInstance().defaultRegion().also {
            LOGGER.warn(IllegalStateException()) { "Using activeRegion when region is null, calling code needs to be migrated to handle null" }
        }

    /**
     * Legacy method, should be considered deprecated and avoided since it loads defaults out of band
     */
    val activeCredentialProvider: ToolkitCredentialsProvider
        @Throws(CredentialProviderNotFoundException::class)
        get() {
            val state = connectionState
            return if (state is ConnectionState.ValidConnection) {
                state.credentials
            } else {
                if (selectedCredentialIdentifier == null) {
                    LOGGER.warn(IllegalStateException()) {
                        "Using activeCredentialProvider when credentials is null, calling code needs to be migrated to handle null"
                    }
                }

                throw CredentialProviderNotFoundException(message("credentials.profile.not_configured"))
            }
        }

    /**
     * Returns the list of recently used [AwsRegion]
     */
    fun recentlyUsedRegions(): List<AwsRegion> = recentlyUsedRegions.elements().mapNotNull { regionProvider.allRegions()[it] }

    /**
     * Returns the list of recently used [CredentialIdentifier]
     */
    fun recentlyUsedCredentials(): List<CredentialIdentifier> {
        val credentialManager = CredentialManager.getInstance()
        return recentlyUsedProfiles.elements().mapNotNull { credentialManager.getCredentialIdentifierById(it) }
    }

    /**
     * Internal method that executes the actual validation of credentials
     */
    protected open fun validate(credentialsProvider: ToolkitCredentialsProvider, region: AwsRegion) {
        resourceCache.getResource(
            StsResources.ACCOUNT,
            region = region,
            credentialProvider = credentialsProvider,
            useStale = false,
            forceFetch = true
        ).toCompletableFuture().get()
    }

    override fun dispose() {
    }

    companion object {
        /***
         * MessageBus topic for when the active credential profile or region is changed
         */
        val CONNECTION_SETTINGS_STATE_CHANGED: Topic<ConnectionSettingsStateChangeNotifier> = Topic.create(
            "AWS Account setting changed",
            ConnectionSettingsStateChangeNotifier::class.java
        )

        @JvmStatic
        fun getInstance(project: Project): AwsConnectionManager = project.service()

        private val LOGGER = getLogger<AwsConnectionManager>()
        private const val MAX_HISTORY = 5
        internal val AwsConnectionManager.selectedPartition get() = selectedRegion?.let { AwsRegionProvider.getInstance().partitions()[it.partitionId] }
    }
}

fun Project.getConnectionSettingsOrThrow(): ConnectionSettings = getConnectionSettings()
    ?: throw IllegalStateException("Bug: Attempting to retrieve connection settings with invalid connection state")

fun Project.getConnectionSettings(): ConnectionSettings? = AwsConnectionManager.getInstance(this).connectionSettings()

fun <T> Project.withAwsConnection(block: (ConnectionSettings) -> T): T {
    val connectionSettings = AwsConnectionManager.getInstance(this).connectionSettings()
        ?: throw IllegalStateException("Connection settings are not configured")
    return block(connectionSettings)
}

/**
 * A state machine around the connection validation steps the toolkit goes through. Attempts to encapsulate both state, data available at each state and
 * a consistent place to determine how to display state information (e.g. [displayMessage]). Exposes an [isTerminal] property that indicates if this
 * state is temporary in the 'connection validation' workflow or if this is a terminal state.
 */
sealed class ConnectionState(val displayMessage: String, val isTerminal: Boolean) {
    protected val editCredsAction: AnAction = ActionManager.getInstance().getAction("aws.settings.upsertCredentials")

    /**
     * An optional short message to display in places where space is at a premium
     */
    open val shortMessage: String = displayMessage

    open val actions: List<AnAction> = emptyList()

    object InitializingToolkit : ConnectionState(message("settings.states.initializing"), isTerminal = false)

    object ValidatingConnection : ConnectionState(message("settings.states.validating"), isTerminal = false) {
        override val shortMessage: String = message("settings.states.validating.short")
    }

    class ValidConnection(internal val credentials: ToolkitCredentialsProvider, internal val region: AwsRegion) :
        ConnectionState("${credentials.displayName}@${region.displayName}", isTerminal = true) {
        override val shortMessage: String = "${credentials.shortName}@${region.id}"
        val connection by lazy { ConnectionSettings(credentials, region) }
    }

    class IncompleteConfiguration(credentials: CredentialIdentifier?, region: AwsRegion?) : ConnectionState(
        when {
            region == null && credentials == null -> message("settings.none_selected")
            region == null -> message("settings.regions.none_selected")
            credentials == null -> message("settings.credentials.none_selected")
            else -> throw IllegalArgumentException("At least one of regionId ($region) or toolkitCredentialsIdentifier ($credentials) must be null")
        },
        isTerminal = true
    ) {
        override val actions: List<AnAction> = listOf(editCredsAction)
    }

    class InvalidConnection(private val cause: Exception) :
        ConnectionState(message("settings.states.invalid", ExceptionUtil.getMessage(cause) ?: ExceptionUtil.getThrowableText(cause)), isTerminal = true) {
        override val shortMessage = message("settings.states.invalid.short")

        override val actions: List<AnAction> = listOf(RefreshConnectionAction(message("settings.retry")), editCredsAction)
    }

    class RequiresUserAction(interactiveCredentials: InteractiveCredential) :
        ConnectionState(interactiveCredentials.userActionDisplayMessage, isTerminal = true) {
        override val shortMessage = interactiveCredentials.userActionShortDisplayMessage

        override val actions = listOf(interactiveCredentials.userAction)
    }
}

interface ConnectionSettingsStateChangeNotifier {
    fun settingsStateChanged(newState: ConnectionState)
}

/**
 * Legacy method, should be considered deprecated and avoided since it loads defaults out of band
 */
fun Project.activeRegion(): AwsRegion = AwsConnectionManager.getInstance(this).activeRegion

/**
 * Legacy method, should be considered deprecated and avoided since it loads defaults out of band
 */
fun Project.activeCredentialProvider(): ToolkitCredentialsProvider = AwsConnectionManager.getInstance(this).activeCredentialProvider


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

package software.aws.toolkits.jetbrains.core.credentials

import com.intellij.openapi.project.Project
import software.aws.toolkits.core.ConnectionSettings

class AwsConnectionManagerConnection(private val project: Project) : AwsCredentialConnection {
    override val id: String = "AwsConnectionManagerConnection"
    override val label: String
        get() = AwsConnectionManager.getInstance(project).connectionState.displayMessage

    override fun getConnectionSettings(): ConnectionSettings = error("Use AwsConnectionManager for connection")
}


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

package software.aws.toolkits.jetbrains.core.credentials

import com.intellij.icons.AllIcons
import com.intellij.ide.DataManager
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.ListPopup
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.wm.StatusBar
import com.intellij.openapi.wm.StatusBarWidget
import com.intellij.openapi.wm.StatusBarWidgetFactory
import com.intellij.util.Consumer
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
import software.aws.toolkits.resources.message
import java.awt.event.MouseEvent
import javax.swing.Icon

private const val WIDGET_ID = "AwsSettingsPanel"

class AwsSettingsPanelInstaller : StatusBarWidgetFactory {
    override fun getId(): String = WIDGET_ID

    override fun getDisplayName(): String = message("settings.title")

    override fun isAvailable(project: Project): Boolean = true

    override fun createWidget(project: Project): StatusBarWidget = AwsSettingsPanel(project)

    override fun disposeWidget(widget: StatusBarWidget) {
        Disposer.dispose(widget)
    }

    override fun canBeEnabledOn(statusBar: StatusBar): Boolean = true
}

private class AwsSettingsPanel(private val project: Project) :
    StatusBarWidget,
    StatusBarWidget.MultipleTextValuesPresentation,
    ConnectionSettingsStateChangeNotifier,
    ToolkitConnectionManagerListener {
    private val settingsSelector = ProjectLevelSettingSelector(project, ChangeSettingsMode.BOTH)
    private val accountSettingsManager = AwsConnectionManager.getInstance(project)
    private val connectionManager = ToolkitConnectionManager.getInstance(project)
    private lateinit var statusBar: StatusBar

    override fun ID(): String = WIDGET_ID

    override fun getPresentation(): StatusBarWidget.WidgetPresentation = this

    override fun getTooltipText(): String {
        val displayMessage = when (val connection = connectionManager.activeConnection()) {
            null, is AwsConnectionManagerConnection -> accountSettingsManager.connectionState.displayMessage
            else -> connection.label
        }

        return "${message("settings.title")} [$displayMessage]"
    }

    override fun getSelectedValue(): String {
        if (!accountSettingsManager.connectionState.isTerminal) {
            return accountSettingsManager.connectionState.shortMessage
        }

        val currentProfileInvalid = accountSettingsManager.connectionState.let { it.isTerminal && it !is ConnectionState.ValidConnection }
        val invalidBearerConnections = lazyGetUnauthedBearerConnections()

        if (currentProfileInvalid || invalidBearerConnections.isNotEmpty()) {
            val numInvalid = invalidBearerConnections.size + if (currentProfileInvalid) 1 else 0
            if (numInvalid == 1) {
                invalidBearerConnections.firstOrNull()?.let {
                    return message("settings.statusbar.widget.format", message("settings.statusbar.widget.expired.1", it.label))
                }

                return message("settings.statusbar.widget.format", accountSettingsManager.connectionState.shortMessage)
            }

            return message("settings.statusbar.widget.format", message("settings.statusbar.widget.expired.n", numInvalid))
        }

        val totalConnections = ToolkitAuthManager.getInstance().listConnections().size + CredentialManager.getInstance().getCredentialIdentifiers().size
        if (totalConnections == 1) {
            val displayText = when (val connection = connectionManager.activeConnection()) {
                null -> message("settings.credentials.none_selected")
                is AwsConnectionManagerConnection -> accountSettingsManager.connectionState.shortMessage
                else -> connection.label
            }

            return message("settings.statusbar.widget.format", displayText)
        }

        return message("settings.statusbar.widget.format", message("settings.statusbar.widget.connections.n", totalConnections))
    }

    override fun getPopupStep(): ListPopup = settingsSelector.createPopup(DataManager.getInstance().getDataContext(statusBar.component))

    override fun getClickConsumer(): Consumer<MouseEvent>? = null

    override fun install(statusBar: StatusBar) {
        this.statusBar = statusBar
        project.messageBus.connect(this).subscribe(AwsConnectionManager.CONNECTION_SETTINGS_STATE_CHANGED, this)
        ApplicationManager.getApplication().messageBus.connect(this).subscribe(ToolkitConnectionManagerListener.TOPIC, this)
        ApplicationManager.getApplication().messageBus.connect(this).subscribe(
            BearerTokenProviderListener.TOPIC,
            object : BearerTokenProviderListener {
                override fun onChange(providerId: String) {
                    updateWidget()
                }

                override fun invalidate(providerId: String) {
                    updateWidget()
                }
            }
        )

        updateWidget()

        // ideally should be through notification bus. instead we simulate the update() method used by the actions
        disposableCoroutineScope(this, "AwsSettingsPanel icon update loop").launch {
            while (isActive) {
                updateWidget()
                delay(10000)
            }
        }
    }

    override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
        updateWidget()
    }

    override fun settingsStateChanged(newState: ConnectionState) {
        updateWidget()
    }

    private fun updateWidget() {
        statusBar.updateWidget(ID())
    }

    override fun getIcon(): Icon? =
        if (lazyGetUnauthedBearerConnections().isNotEmpty()) {
            AllIcons.General.Warning
        } else {
            null
        }

    override fun dispose() {}
}


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

package software.aws.toolkits.jetbrains.core.credentials

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.project.DumbAwareAction
import software.aws.toolkits.resources.message

class ConfigureAwsConnectionAction(private val mode: ChangeSettingsMode = ChangeSettingsMode.BOTH) : DumbAwareAction(message("configure.toolkit")) {
    override fun actionPerformed(e: AnActionEvent) {
        val project = e.getRequiredData(PlatformDataKeys.PROJECT)
        val selector = ProjectLevelSettingSelector(project, mode)
        selector.createPopup(e.dataContext).showCenteredInCurrentWindow(project)
    }
}


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

package software.aws.toolkits.jetbrains.core.credentials

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.DefaultActionGroup
import com.intellij.openapi.actionSystem.Separator
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import software.aws.toolkits.jetbrains.core.utils.buildList
import software.aws.toolkits.resources.message

class ConnectionSettingsMenuBuilder private constructor() {
    private data class RegionSelectionSettings(val currentSelection: AwsRegion?, val onChange: (AwsRegion) -> Unit)
    private data class ProfileSelectionSettings(val currentSelection: CredentialIdentifier?, val onChange: (CredentialIdentifier) -> Unit)

    private sealed interface IdentitySelectionSettings
    private data class SelectableIdentitySelectionSettings(
        val currentSelection: AwsBearerTokenConnection?,
        val onChange: (AwsBearerTokenConnection) -> Unit
    ) : IdentitySelectionSettings
    private data class ActionsIdentitySelectionSettings(val project: Project?) : IdentitySelectionSettings

    private var regionSelectionSettings: RegionSelectionSettings? = null
    private var profileSelectionSettings: ProfileSelectionSettings? = null
    private var identitySelectionSettings: IdentitySelectionSettings? = null
    private var accountSettingsManager: AwsConnectionManager? = null

    fun withRegions(currentSelection: AwsRegion?, onChange: (AwsRegion) -> Unit): ConnectionSettingsMenuBuilder = apply {
        regionSelectionSettings = RegionSelectionSettings(currentSelection, onChange)
    }

    fun withCredentials(currentSelection: CredentialIdentifier?, onChange: (CredentialIdentifier) -> Unit): ConnectionSettingsMenuBuilder = apply {
        profileSelectionSettings = ProfileSelectionSettings(currentSelection, onChange)
    }

    fun withRecentChoices(project: Project): ConnectionSettingsMenuBuilder = apply {
        accountSettingsManager = AwsConnectionManager.getInstance(project)
    }

    fun withIndividualIdentitySettings(project: Project) {
        identitySelectionSettings = SelectableIdentitySelectionSettings(
            currentSelection = ToolkitConnectionManager.getInstance(project).activeConnection() as? AwsBearerTokenConnection,
            onChange = ToolkitConnectionManager.getInstance(project)::switchConnection
        )
    }

    fun withIndividualIdentityActions(project: Project?) {
        identitySelectionSettings = ActionsIdentitySelectionSettings(project)
    }

    fun build(): DefaultActionGroup {
        val topLevelGroup = DefaultActionGroup()

        identitySelectionSettings?.let { settings ->
            val connections = ToolkitAuthManager.getInstance().listConnections().filterIsInstance<AwsBearerTokenConnection>()
            if (connections.isEmpty()) {
                return@let
            }

            topLevelGroup.add(Separator.create(message("settings.credentials.individual_identity_sub_menu")))
            val actions = when (settings) {
                is SelectableIdentitySelectionSettings -> {
                    connections.map {
                        object : DumbAwareToggleAction<AwsBearerTokenConnection>(
                            title = it.label,
                            value = it,
                            selected = it == settings.currentSelection,
                            onSelect = settings.onChange
                        ) {
                            override fun update(e: AnActionEvent) {
                                super.update(e)
                                if (value.lazyIsUnauthedBearerConnection()) {
                                    e.presentation.icon = AllIcons.General.Warning
                                }
                            }
                        }
                    }
                }

                is ActionsIdentitySelectionSettings -> {
                    connections.map {
                        IndividualIdentityActionGroup(it)
                    }
                }
            }

            topLevelGroup.addAll(actions)

            topLevelGroup.add(Separator.create())
        }

        val profileActions = createProfileActions()
        val regionActions = createRegionActions()

        // no header if only regions
        if (profileActions.isNotEmpty() && regionActions.isNotEmpty()) {
            // both profiles & regions
            topLevelGroup.add(Separator.create(message("settings.credentials.iam_and_regions")))
        } else if (profileActions.isNotEmpty() && regionActions.isEmpty()) {
            // only profiles
            topLevelGroup.add(Separator.create(message("settings.credentials.iam")))
        }

        val regionSettings = regionSelectionSettings
        val recentRegions = accountSettingsManager?.recentlyUsedRegions()
        if (recentRegions?.isNotEmpty() == true && regionSettings != null) {
            recentRegions.forEach {
                topLevelGroup.add(SwitchRegionAction(it, it == regionSettings.currentSelection, regionSettings.onChange))
            }

            val allRegionsGroup = DefaultActionGroup.createPopupGroup { message("settings.regions.region_sub_menu") }
            allRegionsGroup.addAll(regionActions)
            topLevelGroup.add(allRegionsGroup)
        } else {
            topLevelGroup.addAll(regionActions)
        }

        topLevelGroup.add(Separator.create())

        val credentialsSettings = profileSelectionSettings
        val recentCredentials = accountSettingsManager?.recentlyUsedCredentials()
        if (recentCredentials?.isNotEmpty() == true && credentialsSettings != null) {
            recentCredentials.forEach {
                topLevelGroup.add(SwitchCredentialsAction(it, it == credentialsSettings.currentSelection, credentialsSettings.onChange))
            }

            val allCredentialsGroup = DefaultActionGroup.createPopupGroup { message("settings.credentials.profile_sub_menu") }
            allCredentialsGroup.addAll(profileActions)
            topLevelGroup.add(allCredentialsGroup)
        } else {
            topLevelGroup.addAll(profileActions)
        }

        return topLevelGroup
    }

    private fun createRegionActions(): List<AnAction> = buildList {
        val (currentSelection, onChange) = regionSelectionSettings ?: return@buildList

        val regionProvider = AwsRegionProvider.getInstance()

        val primaryRegions = currentSelection?.partitionId?.let {
            regionProvider.regions(it).values
        } ?: regionProvider.allRegions().values

        addAll(createRegionGroupActions(primaryRegions, currentSelection, onChange))

        if (currentSelection != null && regionProvider.partitions().size > 1) {
            val otherPartitionActionGroup = DefaultActionGroup.createPopupGroup { message("settings.partitions") }
            val otherPartitions = regionProvider.partitions().values.filterNot { it.id == currentSelection.partitionId }.sortedBy { it.displayName }
            otherPartitions.forEach {
                val partitionGroup = DefaultActionGroup.createPopupGroup { it.displayName }
                partitionGroup.addAll(createRegionGroupActions(it.regions, currentSelection = null, onChange))

                otherPartitionActionGroup.add(partitionGroup)
            }

            add(Separator.create())
            add(otherPartitionActionGroup)
        }
    }

    private fun createRegionGroupActions(regions: Collection<AwsRegion>, currentSelection: AwsRegion?, onChange: (AwsRegion) -> Unit) = buildList<AnAction> {
        regions.groupBy { it.category }
            .forEach { (category, categoryRegions) ->
                add(Separator.create(category))
                categoryRegions.sortedBy { it.displayName }
                    .forEach { add(SwitchRegionAction(it, it == currentSelection, onChange)) }
            }
    }

    private fun createProfileActions(): List<AnAction> = buildList {
        val (currentSelection, onChange) = profileSelectionSettings ?: return@buildList

        add(Separator.create(message("settings.credentials")))

        val credentialManager = CredentialManager.getInstance()
        credentialManager.getCredentialIdentifiers().forEach {
            add(SwitchCredentialsAction(it, it == currentSelection, onChange))
        }

        add(Separator.create())
        add(ActionManager.getInstance().getAction("aws.settings.upsertCredentials"))
    }

    // Helper actions, note: these are public to help make tests easier by leveraging instanceOf checks

    abstract inner class DumbAwareToggleAction<T>(
        title: String,
        val value: T,
        private val selected: Boolean,
        private val onSelect: (T) -> Unit
    ) : ToggleAction(title), DumbAware {
        override fun isSelected(e: AnActionEvent): Boolean = selected

        override fun setSelected(e: AnActionEvent, state: Boolean) {
            if (!isSelected(e)) {
                onSelect.invoke(value)
            }
        }
    }

    inner class SwitchRegionAction(
        value: AwsRegion,
        selected: Boolean,
        onSelect: (AwsRegion) -> Unit
    ) : DumbAwareToggleAction<AwsRegion>(value.displayName, value, selected, onSelect)

    inner class SwitchCredentialsAction(
        value: CredentialIdentifier,
        selected: Boolean,
        onSelect: (CredentialIdentifier) -> Unit
    ) : DumbAwareToggleAction<CredentialIdentifier>(value.displayName, value, selected, onSelect)

    inner class IndividualIdentityActionGroup(private val value: AwsBearerTokenConnection) :
        DefaultActionGroup(
            {
                val suffix = if (value.lazyIsUnauthedBearerConnection()) {
                    message("credentials.individual_identity.expired")
                } else {
                    message("credentials.individual_identity.connected")
                }

                "${value.label} $suffix"
            },
            true
        ) {
        init {
            templatePresentation.icon = if (value.lazyIsUnauthedBearerConnection()) AllIcons.General.Warning else null

            addAll(
                object : DumbAwareAction(message("credentials.individual_identity.reconnect")) {
                    override fun actionPerformed(e: AnActionEvent) {
                        reauthProviderIfNeeded(e.project, value)
                    }
                },

                object : DumbAwareAction(message("credentials.individual_identity.signout")) {
                    override fun actionPerformed(e: AnActionEvent) {
                        val settings = identitySelectionSettings as? ActionsIdentitySelectionSettings
                        logoutFromSsoConnection(settings?.project, value)
                    }
                }
            )
        }
    }

    companion object {
        fun connectionSettingsMenuBuilder(): ConnectionSettingsMenuBuilder = ConnectionSettingsMenuBuilder()
    }
}


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

package software.aws.toolkits.jetbrains.core.credentials

import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.ex.ComboBoxAction
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.popup.ListPopup
import com.intellij.util.EventDispatcher
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.credentials.ChangeSettingsMode.BOTH
import software.aws.toolkits.jetbrains.core.credentials.ChangeSettingsMode.CREDENTIALS
import software.aws.toolkits.jetbrains.core.credentials.ChangeSettingsMode.NONE
import software.aws.toolkits.jetbrains.core.credentials.ChangeSettingsMode.REGIONS
import software.aws.toolkits.jetbrains.core.credentials.ConnectionSettingsMenuBuilder.Companion.connectionSettingsMenuBuilder
import software.aws.toolkits.jetbrains.ui.ActionPopupComboLogic
import software.aws.toolkits.resources.message
import javax.swing.JComponent
import javax.swing.event.ChangeEvent
import javax.swing.event.ChangeListener

/**
 * Determine what settings the settings selector is capable of changing
 */
enum class ChangeSettingsMode(val showRegions: Boolean, val showCredentials: Boolean) {
    NONE(false, false),
    CREDENTIALS(false, true),
    REGIONS(true, false),
    BOTH(true, true)
}

/**
 * Base logic for different ways to present connection settings in a consistent manner across the IDE
 *
 * @see ProjectLevelSettingSelector
 * @see SettingsSelectorComboBoxAction
 */
abstract class SettingsSelectorLogicBase(private val menuMode: ChangeSettingsMode) : ActionPopupComboLogic {
    private val listeners by lazy {
        EventDispatcher.create(ChangeListener::class.java)
    }

    override fun displayValue(): String = when (menuMode) {
        CREDENTIALS -> credentialsDisplay()
        REGIONS -> regionDisplay()
        BOTH -> "${credentialsDisplay()}@${regionDisplay()}"
        NONE -> ""
    }

    override fun tooltip(): String? = when (menuMode) {
        CREDENTIALS -> credentialsTooltip()
        REGIONS -> regionTooltip()
        NONE, BOTH -> null
    }

    private fun regionDisplay() = currentRegion()?.id ?: message("settings.regions.none_selected")
    private fun regionTooltip() = currentRegion()?.displayName

    protected abstract fun currentRegion(): AwsRegion?
    protected open fun onRegionChange(region: AwsRegion) {}

    private fun credentialsDisplay() = currentCredentials()?.shortName ?: message("settings.credentials.none_selected")
    private fun credentialsTooltip() = currentCredentials()?.displayName

    protected abstract fun currentCredentials(): CredentialIdentifier?
    protected open fun onCredentialChange(identifier: CredentialIdentifier) {}

    fun selectionMenuActions(): DefaultActionGroup = connectionSettingsMenuBuilder().apply {
        if (menuMode.showRegions) {
            withRegions(currentRegion()) {
                onRegionChange(it)

                listeners.multicaster.stateChanged(ChangeEvent(this))
            }
        }

        if (menuMode.showCredentials) {
            withCredentials(currentCredentials()) {
                onCredentialChange(it)

                listeners.multicaster.stateChanged(ChangeEvent(this))
            }
        }

        customizeSelectionMenu(this)
    }.build()

    fun createPopup(context: DataContext): ListPopup = JBPopupFactory.getInstance().createActionGroupPopup(
        message("settings.title"),
        selectionMenuActions(),
        context,
        JBPopupFactory.ActionSelectionAid.SPEEDSEARCH,
        true
    )

    protected open fun customizeSelectionMenu(builder: ConnectionSettingsMenuBuilder) {}

    override fun addChangeListener(changeListener: ChangeListener) {
        listeners.addListener(changeListener)
    }

    override fun showPopup(sourceComponent: JComponent) {
        createPopup(DataManager.getInstance().getDataContext(sourceComponent)).showUnderneathOf(sourceComponent)
    }
}

/**
 * Version of a [SettingsSelectorLogicBase] that stores the settings locally to the instance of the selector. Typically, this would be used if the settings
 * differ from the Project settings such as a UI panel.
 */
class LocalSettingsSelector(initialRegion: AwsRegion? = null, initialCredentialIdentifier: CredentialIdentifier? = null, settingsMode: ChangeSettingsMode) :
    SettingsSelectorLogicBase(settingsMode) {
    var currentRegion: AwsRegion? = initialRegion
        set(value) {
            if (field == value) return
            field = value
            value?.let { onRegionChange(it) }
        }
    var currentCredentials: CredentialIdentifier? = initialCredentialIdentifier
        set(value) {
            if (field == value) return
            field = value
            value?.let { onCredentialChange(it) }
        }

    override fun currentRegion(): AwsRegion? = currentRegion

    override fun onRegionChange(region: AwsRegion) {
        currentRegion = region
    }

    override fun currentCredentials(): CredentialIdentifier? = currentCredentials

    override fun onCredentialChange(identifier: CredentialIdentifier) {
        currentCredentials = identifier
    }
}

/**
 * Version of a [SettingsSelectorLogicBase] that stores the settings at the project level.
 */
open class ProjectLevelSettingSelector(private val project: Project, settingsMode: ChangeSettingsMode) : SettingsSelectorLogicBase(settingsMode) {
    override fun currentRegion(): AwsRegion? = AwsConnectionManager.getInstance(project).selectedRegion

    override fun onRegionChange(region: AwsRegion) {
        AwsConnectionManager.getInstance(project).changeRegion(region)
    }

    override fun currentCredentials(): CredentialIdentifier? = AwsConnectionManager.getInstance(project).selectedCredentialIdentifier

    override fun onCredentialChange(identifier: CredentialIdentifier) {
        AwsConnectionManager.getInstance(project).changeCredentialProvider(identifier)
    }

    override fun customizeSelectionMenu(builder: ConnectionSettingsMenuBuilder) {
        builder.withRecentChoices(project)
        builder.withIndividualIdentityActions(project)
    }
}

class ToolkitConnectionComboBoxAction(private val project: Project) : ComboBoxAction(), DumbAware {
    private val logic = object : ProjectLevelSettingSelector(project, CREDENTIALS) {
        override fun currentCredentials(): CredentialIdentifier? {
            val active = ToolkitConnectionManager.getInstance(project).activeConnection()
            if (active is AwsConnectionManagerConnection) {
                return super.currentCredentials()
            }

            return null
        }

        override fun onCredentialChange(identifier: CredentialIdentifier) {
            super.onCredentialChange(identifier)
            val connectionManager = ToolkitConnectionManager.getInstance(project)
            connectionManager.switchConnection(AwsConnectionManagerConnection(project))
        }

        override fun customizeSelectionMenu(builder: ConnectionSettingsMenuBuilder) {
            super.customizeSelectionMenu(builder)
            builder.withIndividualIdentitySettings(project)
        }
    }

    override fun createPopupActionGroup(button: JComponent?) = logic.selectionMenuActions()

    override fun update(e: AnActionEvent) {
        val active = ToolkitConnectionManager.getInstance(project).activeConnection()
        if (active is AwsConnectionManagerConnection) {
            e.presentation.text = logic.displayValue()
            e.presentation.description = logic.tooltip()
        } else {
            e.presentation.text = active?.label?.let {
                "Connected with $it"
            } ?: message("settings.credentials.none_selected")

            e.presentation.description = null
        }
    }
}

class SettingsSelectorComboBoxAction(private val selectorLogic: SettingsSelectorLogicBase) : ComboBoxAction(), DumbAware {
    override fun createPopupActionGroup(button: JComponent?) = selectorLogic.selectionMenuActions()

    override fun update(e: AnActionEvent) {
        updatePresentation(e.presentation)
    }

    override fun displayTextInToolbar(): Boolean = true

    private fun updatePresentation(presentation: Presentation) {
        presentation.text = selectorLogic.displayValue()
        presentation.description = selectorLogic.tooltip()
    }
}

class CredsComboBoxActionGroup(private val project: Project) : DefaultActionGroup() {
    private val toolkitConnectionAction = ToolkitConnectionComboBoxAction(project)
    private val profileRegionSelectorGroup: Array<AnAction> by lazy {
        arrayOf(
            toolkitConnectionAction,
            SettingsSelectorComboBoxAction(ProjectLevelSettingSelector(project, ChangeSettingsMode.REGIONS))
        )
    }

    private val ssoSelectorGroup: Array<AnAction> by lazy {
        arrayOf(
            toolkitConnectionAction
        )
    }

    override fun getChildren(e: AnActionEvent?): Array<AnAction> {
        val activeConnection = ToolkitConnectionManager.getInstance(project).activeConnection()

        return if (activeConnection is AwsBearerTokenConnection) {
            ssoSelectorGroup
        } else if (activeConnection == null) {
            arrayOf(
                ActionManager.getInstance().getAction("aws.toolkit.toolwindow.explorer.newConnection")
            )
        } else {
            profileRegionSelectorGroup
        }
    }
}