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

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

import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider
import software.amazon.awssdk.profiles.Profile
import software.amazon.awssdk.profiles.ProfileProperty
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.core.credentials.CredentialIdentifierBase
import software.aws.toolkits.core.credentials.CredentialProviderFactory
import software.aws.toolkits.core.credentials.CredentialSourceId
import software.aws.toolkits.core.credentials.CredentialType
import software.aws.toolkits.core.credentials.CredentialsChangeEvent
import software.aws.toolkits.core.credentials.CredentialsChangeListener
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.credentials.MfaRequiredInteractiveCredentials
import software.aws.toolkits.jetbrains.core.credentials.SsoRequiredInteractiveCredentials
import software.aws.toolkits.jetbrains.core.credentials.ToolkitCredentialProcessProvider
import software.aws.toolkits.jetbrains.core.credentials.diskCache
import software.aws.toolkits.jetbrains.core.credentials.profiles.Ec2MetadataConfigProvider.getEc2MedataEndpoint
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoCache
import software.aws.toolkits.jetbrains.settings.AwsSettings
import software.aws.toolkits.jetbrains.settings.ProfilesNotification
import software.aws.toolkits.jetbrains.utils.createNotificationExpiringAction
import software.aws.toolkits.jetbrains.utils.createShowMoreInfoDialogAction
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message

const val DEFAULT_PROFILE_NAME = "default"
const val DEFAULT_PROFILE_ID = "profile:default"

private const val PROFILE_FACTORY_ID = "ProfileCredentialProviderFactory"

open class ProfileCredentialsIdentifier internal constructor(val profileName: String, override val defaultRegionId: String?, credentialType: CredentialType?) :
    CredentialIdentifierBase(credentialType) {
    override val id = "profile:$profileName"
    override val displayName = message("credentials.profile.name", profileName)
    override val factoryId = PROFILE_FACTORY_ID
    override val shortName: String = profileName
}

private class ProfileCredentialsIdentifierMfa(profileName: String, defaultRegionId: String?, credentialType: CredentialType?) :
    ProfileCredentialsIdentifier(profileName, defaultRegionId, credentialType), MfaRequiredInteractiveCredentials

private class ProfileCredentialsIdentifierSso(
    profileName: String,
    defaultRegionId: String?,
    override val ssoCache: SsoCache,
    override val ssoUrl: String,
    credentialType: CredentialType?
) : ProfileCredentialsIdentifier(profileName, defaultRegionId, credentialType),
    SsoRequiredInteractiveCredentials

private class NeverShowAgain : DumbAwareAction(message("settings.never_show_again")) {
    override fun actionPerformed(e: AnActionEvent) {
        AwsSettings.getInstance().profilesNotification = ProfilesNotification.Never
    }
}

class ProfileCredentialProviderFactory(private val ssoCache: SsoCache = diskCache) : CredentialProviderFactory {
    private val profileHolder = ProfileHolder()

    override val id = PROFILE_FACTORY_ID
    override val credentialSourceId: CredentialSourceId = CredentialSourceId.SharedCredentials

    override fun setUp(credentialLoadCallback: CredentialsChangeListener) {
        // Load the initial data, then start the background watcher
        loadProfiles(credentialLoadCallback, true)

        ProfileWatcher.getInstance().addListener {
            loadProfiles(credentialLoadCallback, false)
        }
    }

    private fun loadProfiles(credentialLoadCallback: CredentialsChangeListener, initialLoad: Boolean) {
        val profilesAdded = mutableListOf<ProfileCredentialsIdentifier>()
        val profilesModified = mutableListOf<ProfileCredentialsIdentifier>()

        val previousProfilesSnapshot = profileHolder.snapshot()
        val existingProfiles = profileHolder.snapshot()

        val newProfiles = try {
            validateAndGetProfiles()
        } catch (e: Exception) {
            notifyUserOfLoadFailure(e)

            return
        }

        newProfiles.validProfiles.forEach {
            val previousProfile = existingProfiles.remove(it.key)
            if (previousProfile == null) {
                // It was not in the snapshot, so it must be new
                profilesAdded.add(it.value.asId(newProfiles.validProfiles))
            } else {
                // If the profile was modified, notify listeners, else do nothing
                if (previousProfile != it.value) {
                    profilesModified.add(it.value.asId(newProfiles.validProfiles))
                }
            }
        }

        // any profiles with a modified 'source_profile' need to be marked as well
        newProfiles.validProfiles.forEach { (_, profile) ->
            val profileId = profile.asId(newProfiles.validProfiles)
            if (profileId in profilesModified) {
                // already marked; skip
                return@forEach
            }
            for (source in profile.traverseCredentialChain(newProfiles.validProfiles)) {
                if (source != profile && source.asId(newProfiles.validProfiles) in profilesModified) {
                    profilesModified.add(profileId)
                    break
                }
            }
        }

        // Any remaining profiles must have either become invalid or removed from the cred/config files
        val profilesRemoved = existingProfiles.values.map { it.asId(previousProfilesSnapshot) }

        profileHolder.update(newProfiles.validProfiles)
        credentialLoadCallback(CredentialsChangeEvent(profilesAdded, profilesModified, profilesRemoved))

        notifyUserOfResult(newProfiles, initialLoad)
    }

    private fun notifyUserOfLoadFailure(e: Exception) {
        val loadingFailureMessage = message("credentials.profile.failed_load")

        val detail = e.message?.let {
            ": $it"
        } ?: ""

        LOG.warn(e) { loadingFailureMessage }

        if (AwsSettings.getInstance().profilesNotification != ProfilesNotification.Never) {
            notifyError(
                title = message("credentials.profile.refresh_ok_title"),
                content = "$loadingFailureMessage$detail",
                notificationActions = listOf(
                    createNotificationExpiringAction(ActionManager.getInstance().getAction("aws.settings.upsertCredentials")),
                    createNotificationExpiringAction(NeverShowAgain())
                )
            )
        }
    }

    private fun notifyUserOfResult(newProfiles: Profiles, initialLoad: Boolean) {
        val refreshTitle = message("credentials.profile.refresh_ok_title")
        val totalProfiles = newProfiles.validProfiles.size + newProfiles.invalidProfiles.size
        val refreshBaseMessage = message("credentials.profile.refresh_ok_message", totalProfiles)

        // All provides were valid
        if (newProfiles.invalidProfiles.isEmpty()) {
            // Don't report we load creds on start to avoid spam
            if (!initialLoad && AwsSettings.getInstance().profilesNotification == ProfilesNotification.Always) {
                notifyInfo(
                    title = message("credentials.profile.refresh_ok_title"),
                    content = refreshBaseMessage,
                    notificationActions = listOf(
                        createNotificationExpiringAction(NeverShowAgain())
                    )
                )

                return
            }
        }

        // Some profiles failed to load
        if (newProfiles.invalidProfiles.isNotEmpty()) {
            val message = newProfiles.invalidProfiles.values.joinToString("\n")

            val errorDialogTitle = message("credentials.profile.failed_load")
            val numErrorMessage = message("credentials.profile.refresh_errors", newProfiles.invalidProfiles.size)

            if (AwsSettings.getInstance().profilesNotification != ProfilesNotification.Never) {
                notifyInfo(
                    title = refreshTitle,
                    content = "$refreshBaseMessage $numErrorMessage",
                    notificationActions = listOf(
                        createNotificationExpiringAction(ActionManager.getInstance().getAction("aws.settings.upsertCredentials")),
                        createNotificationExpiringAction(NeverShowAgain()),
                        createShowMoreInfoDialogAction(message("credentials.invalid.more_info"), errorDialogTitle, numErrorMessage, message)
                    )
                )
            }
        }
    }

    override fun createAwsCredentialProvider(providerId: CredentialIdentifier, region: AwsRegion): AwsCredentialsProvider {
        val profileProviderId = providerId as? ProfileCredentialsIdentifier
            ?: throw IllegalStateException("ProfileCredentialProviderFactory can only handle ProfileCredentialsIdentifier, but got ${providerId::class}")

        val profile = profileHolder.getProfile(profileProviderId.profileName)
            ?: throw IllegalStateException("Profile ${profileProviderId.profileName} looks to have been removed")

        return createAwsCredentialProvider(profile, region)
    }

    private fun createAwsCredentialProvider(profile: Profile, region: AwsRegion) = when {
        profile.propertyExists(ProfileProperty.SSO_START_URL) -> createSsoProvider(profile)
        profile.propertyExists(ProfileProperty.ROLE_ARN) -> createAssumeRoleProvider(profile, region)
        profile.propertyExists(ProfileProperty.AWS_SESSION_TOKEN) -> createStaticSessionProvider(profile)
        profile.propertyExists(ProfileProperty.AWS_ACCESS_KEY_ID) -> createBasicProvider(profile)
        profile.propertyExists(ProfileProperty.CREDENTIAL_PROCESS) -> createCredentialProcessProvider(profile)
        else -> {
            throw IllegalArgumentException(message("credentials.profile.unsupported", profile.name()))
        }
    }

    private fun createSsoProvider(profile: Profile): AwsCredentialsProvider = ProfileSsoProvider(profile)

    private fun createAssumeRoleProvider(profile: Profile, region: AwsRegion): AwsCredentialsProvider {
        val sourceProfileName = profile.property(ProfileProperty.SOURCE_PROFILE)
        val credentialSource = profile.property(ProfileProperty.CREDENTIAL_SOURCE)

        val parentCredentialProvider = when {
            sourceProfileName.isPresent -> {
                val sourceProfile = profileHolder.getProfile(sourceProfileName.get())
                    ?: throw IllegalStateException("Profile $sourceProfileName looks to have been removed")
                createAwsCredentialProvider(sourceProfile, region)
            }
            credentialSource.isPresent -> {
                // Can we parse the credential_source
                credentialSourceCredentialProvider(CredentialSourceType.parse(credentialSource.get()), profile)
            }
            else -> {
                throw IllegalArgumentException(message("credentials.profile.assume_role.missing_source", profile.name()))
            }
        }

        return ProfileAssumeRoleProvider(parentCredentialProvider, region, profile)
    }

    private fun credentialSourceCredentialProvider(credentialSource: CredentialSourceType, profile: Profile): AwsCredentialsProvider =
        when (credentialSource) {
            CredentialSourceType.ECS_CONTAINER -> ContainerCredentialsProvider.builder().build()
            CredentialSourceType.EC2_INSTANCE_METADATA -> {
                // The IMDS credentials provider should source the endpoint config properties from the currently active profile
                InstanceProfileCredentialsProvider.builder()
                    .endpoint(profile.getEc2MedataEndpoint())
                    .build()
            }
            CredentialSourceType.ENVIRONMENT -> AwsCredentialsProviderChain.builder()
                .addCredentialsProvider(SystemPropertyCredentialsProvider.create())
                .addCredentialsProvider(EnvironmentVariableCredentialsProvider.create())
                .build()
        }

    private fun createBasicProvider(profile: Profile) = StaticCredentialsProvider.create(
        AwsBasicCredentials.create(
            profile.requiredProperty(ProfileProperty.AWS_ACCESS_KEY_ID),
            profile.requiredProperty(ProfileProperty.AWS_SECRET_ACCESS_KEY)
        )
    )

    private fun createStaticSessionProvider(profile: Profile) = StaticCredentialsProvider.create(
        AwsSessionCredentials.create(
            profile.requiredProperty(ProfileProperty.AWS_ACCESS_KEY_ID),
            profile.requiredProperty(ProfileProperty.AWS_SECRET_ACCESS_KEY),
            profile.requiredProperty(ProfileProperty.AWS_SESSION_TOKEN)
        )
    )

    private fun createCredentialProcessProvider(profile: Profile) =
        ToolkitCredentialProcessProvider(profile.requiredProperty(ProfileProperty.CREDENTIAL_PROCESS))

    private fun Profile.asId(profiles: Map<String, Profile>): ProfileCredentialsIdentifier {
        val name = this.name()
        val defaultRegion = this.properties()[ProfileProperty.REGION]
        val requestedProfileType = this.toCredentialType()

        return when {
            this.requiresMfa(profiles) -> ProfileCredentialsIdentifierMfa(name, defaultRegion, requestedProfileType)
            this.requiresSso(profiles) -> ProfileCredentialsIdentifierSso(
                name,
                defaultRegion,
                ssoCache,
                this.traverseCredentialChain(profiles).map { it.property(ProfileProperty.SSO_START_URL) }.first { it.isPresent }.get(),
                requestedProfileType
            )
            else -> ProfileCredentialsIdentifier(name, defaultRegion, requestedProfileType)
        }
    }

    private fun Profile.requiresMfa(profiles: Map<String, Profile>) = this.traverseCredentialChain(profiles)
        .any { it.propertyExists(ProfileProperty.MFA_SERIAL) }

    private fun Profile.requiresSso(profiles: Map<String, Profile>) = this.traverseCredentialChain(profiles)
        .any { it.propertyExists(ProfileProperty.SSO_START_URL) }

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

private fun Profile.toCredentialType(): CredentialType? = when {
    this.propertyExists(ProfileProperty.SSO_START_URL) -> CredentialType.SsoProfile
    this.propertyExists(ProfileProperty.ROLE_ARN) -> {
        if (this.propertyExists(ProfileProperty.MFA_SERIAL)) {
            CredentialType.AssumeMfaRoleProfile
        } else {
            CredentialType.AssumeRoleProfile
        }
    }
    this.propertyExists(ProfileProperty.AWS_SESSION_TOKEN) -> CredentialType.StaticSessionProfile
    this.propertyExists(ProfileProperty.AWS_ACCESS_KEY_ID) -> CredentialType.StaticProfile
    this.propertyExists(ProfileProperty.CREDENTIAL_PROCESS) -> CredentialType.CredentialProcessProfile
    else -> null
}

private class ProfileHolder {
    private val profiles = mutableMapOf<String, Profile>()

    fun snapshot() = profiles.toMutableMap()

    fun update(validProfiles: Map<String, Profile>) {
        profiles.clear()
        profiles.putAll(validProfiles)
    }

    fun getProfile(profileName: String): Profile? = profiles[profileName]
}


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

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

import software.amazon.awssdk.profiles.Profile
import software.amazon.awssdk.profiles.ProfileFile
import software.amazon.awssdk.profiles.ProfileProperty
import software.aws.toolkits.resources.message

data class Profiles(val validProfiles: Map<String, Profile>, val invalidProfiles: Map<String, Exception>)

/**
 * Reads the AWS shared credentials files and produces what profiles are valid and if not why it is not
 */
fun validateAndGetProfiles(): Profiles {
    val allProfiles: Map<String, Profile> = ProfileFile.defaultProfileFile().profiles().orEmpty()

    val validProfiles = mutableMapOf<String, Profile>()
    val invalidProfiles = mutableMapOf<String, Exception>()

    allProfiles.values.forEach {
        try {
            validateProfile(it, allProfiles)
            validProfiles[it.name()] = it
        } catch (e: Exception) {
            invalidProfiles[it.name()] = e
        }
    }

    return Profiles(validProfiles, invalidProfiles)
}

private fun validateProfile(profile: Profile, allProfiles: Map<String, Profile>) {
    when {
        profile.propertyExists(ProfileProperty.SSO_START_URL) -> validateSsoProfile(profile)
        profile.propertyExists(ProfileProperty.ROLE_ARN) -> validateAssumeRoleProfile(profile, allProfiles)
        profile.propertyExists(ProfileProperty.AWS_SESSION_TOKEN) -> validateStaticSessionProfile(profile)
        profile.propertyExists(ProfileProperty.AWS_ACCESS_KEY_ID) -> validateBasicProfile(profile)
        profile.propertyExists(ProfileProperty.CREDENTIAL_PROCESS) -> {
            // NO-OP Always valid
        }
        else -> {
            throw IllegalArgumentException(message("credentials.profile.unsupported", profile.name()))
        }
    }
}

fun validateSsoProfile(profile: Profile) {
    profile.requiredProperty(ProfileProperty.SSO_ACCOUNT_ID)
    profile.requiredProperty(ProfileProperty.SSO_REGION)
    profile.requiredProperty(ProfileProperty.SSO_ROLE_NAME)
}

private fun validateAssumeRoleProfile(profile: Profile, allProfiles: Map<String, Profile>) {
    val rootProfile = profile.traverseCredentialChain(allProfiles).last()
    val credentialSource = rootProfile.property(ProfileProperty.CREDENTIAL_SOURCE)

    if (credentialSource.isPresent) {
        try {
            CredentialSourceType.parse(credentialSource.get())
        } catch (e: Exception) {
            throw IllegalArgumentException(message("credentials.profile.assume_role.invalid_credential_source", rootProfile.name()))
        }
    } else {
        validateProfile(rootProfile, allProfiles)
    }
}

private fun validateStaticSessionProfile(profile: Profile) {
    profile.requiredProperty(ProfileProperty.AWS_ACCESS_KEY_ID)
    profile.requiredProperty(ProfileProperty.AWS_SECRET_ACCESS_KEY)
    profile.requiredProperty(ProfileProperty.AWS_SESSION_TOKEN)
}

private fun validateBasicProfile(profile: Profile) {
    profile.requiredProperty(ProfileProperty.AWS_ACCESS_KEY_ID)
    profile.requiredProperty(ProfileProperty.AWS_SECRET_ACCESS_KEY)
}


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

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

import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider
import software.amazon.awssdk.auth.credentials.AwsCredentials
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.profiles.Profile
import software.amazon.awssdk.profiles.ProfileProperty
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.sso.SsoClient
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.amazon.awssdk.utils.SdkAutoCloseable
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.credentials.diskCache
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoAccessTokenProvider
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoCredentialProvider

class ProfileSsoProvider(profile: Profile) : AwsCredentialsProvider, SdkAutoCloseable {
    private val ssoClient: SsoClient
    private val ssoOidcClient: SsoOidcClient
    private val credentialsProvider: SsoCredentialProvider

    init {
        val ssoRegion = profile.requiredProperty(ProfileProperty.SSO_REGION)
        val clientManager = AwsClientManager.getInstance()

        ssoClient = clientManager.createUnmanagedClient(AnonymousCredentialsProvider.create(), Region.of(ssoRegion))
        ssoOidcClient = clientManager.createUnmanagedClient(AnonymousCredentialsProvider.create(), Region.of(ssoRegion))

        val ssoAccessTokenProvider = SsoAccessTokenProvider(
            profile.requiredProperty(ProfileProperty.SSO_START_URL),
            ssoRegion,
            diskCache,
            ssoOidcClient
        )

        credentialsProvider = SsoCredentialProvider(
            profile.requiredProperty(ProfileProperty.SSO_ACCOUNT_ID),
            profile.requiredProperty(ProfileProperty.SSO_ROLE_NAME),
            ssoClient,
            ssoAccessTokenProvider
        )
    }

    override fun resolveCredentials(): AwsCredentials = credentialsProvider.resolveCredentials()

    override fun close() {
        credentialsProvider.close()
        ssoClient.close()
        ssoOidcClient.close()
    }
}


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

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

import com.intellij.util.text.nullize
import software.amazon.awssdk.profiles.Profile
import software.amazon.awssdk.profiles.ProfileProperty
import software.aws.toolkits.resources.message

fun Profile.traverseCredentialChain(profiles: Map<String, Profile>): Sequence<Profile> = sequence {
    val profileChain = linkedSetOf<String>()
    var currentProfile = this@traverseCredentialChain

    yield(currentProfile)

    while (currentProfile.propertyExists(ProfileProperty.ROLE_ARN)) {
        val currentProfileName = currentProfile.name()
        if (!profileChain.add(currentProfileName)) {
            val chain = profileChain.joinToString("->", postfix = "->$currentProfileName")
            throw IllegalArgumentException(message("credentials.profile.circular_profiles", name(), chain))
        }

        val sourceProfile = currentProfile.property(ProfileProperty.SOURCE_PROFILE)
        val credentialSource = currentProfile.property(ProfileProperty.CREDENTIAL_SOURCE)

        if (sourceProfile.isPresent && credentialSource.isPresent) {
            throw IllegalArgumentException(message("credentials.profile.assume_role.duplicate_source", currentProfileName))
        }

        if (sourceProfile.isPresent) {
            val sourceProfileName = sourceProfile.get()
            currentProfile = profiles[sourceProfileName]
                ?: throw IllegalArgumentException(
                    message(
                        "credentials.profile.source_profile_not_found",
                        currentProfileName,
                        sourceProfileName
                    )
                )

            yield(currentProfile)
        } else if (credentialSource.isPresent) {
            return@sequence
        } else {
            throw IllegalArgumentException(message("credentials.profile.assume_role.missing_source", currentProfileName))
        }
    }
}

fun Profile.propertyExists(propertyName: String): Boolean = this.property(propertyName).isPresent

fun Profile.requiredProperty(propertyName: String): String = this.property(propertyName)
    .filter { it.nullize() != null }
    .orElseThrow {
        IllegalArgumentException(
            message(
                "credentials.profile.missing_property",
                this.name(),
                propertyName
            )
        )
    }


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

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

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.AsyncFileListener
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.openapi.vfs.pointers.VirtualFilePointer
import com.intellij.util.containers.ContainerUtil
import software.amazon.awssdk.profiles.ProfileFileLocation
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import java.nio.file.Paths

interface ProfileWatcher {
    fun addListener(listener: () -> Unit)
    fun forceRefresh() {}

    companion object {
        fun getInstance() = service<ProfileWatcher>()
    }
}

class DefaultProfileWatcher : AsyncFileListener, Disposable, ProfileWatcher {
    private val listeners = ContainerUtil.createLockFreeCopyOnWriteList<() -> Unit>()
    private val watchRoots = mutableSetOf<LocalFileSystem.WatchRequest>()
    private val watchPointers = mutableMapOf<String, VirtualFilePointer>()

    private val watchLocationsStrings = setOf(
        FileUtil.normalize(ProfileFileLocation.configurationFilePath().toAbsolutePath().toString()),
        FileUtil.normalize(ProfileFileLocation.credentialsFilePath().toAbsolutePath().toString())
    )

    init {
        LOG.info { "Starting profile watcher, profile locations: $watchLocationsStrings" }

        val localFileSystem = LocalFileSystem.getInstance()

        val watchLocationParents = watchLocationsStrings.map {
            val path = Paths.get(FileUtil.toSystemDependentName(it))

            // Make VFS aware of it
            localFileSystem.refreshAndFindFileByIoFile(path.toFile())

            // Use the parent as the watch root in case file does not exist yet
            // Note: This system requires that the parent folder already exists
            FileUtil.normalize(path.parent.toString())
        }.toSet()

        watchRoots.addAll(localFileSystem.addRootsToWatch(watchLocationParents, true))

        LOG.info { "Added watch roots: $watchRoots" }

        VirtualFileManager.getInstance().addAsyncFileListener(this, this)
    }

    override fun prepareChange(events: List<VFileEvent>): AsyncFileListener.ChangeApplier? {
        LOG.debug { "Received events: $events" }
        val isRelevant = events.any { watchLocationsStrings.contains(it.path) }

        return if (isRelevant) {
            LOG.info { "Profile file change detected, scheduling refresh" }
            object : AsyncFileListener.ChangeApplier {
                override fun afterVfsChange() {
                    // Off load this, since this is called under a write lock
                    ApplicationManager.getApplication().executeOnPooledThread {
                        listeners.forEach { it() }
                    }
                }
            }
        } else {
            null
        }
    }

    override fun forceRefresh() {
        listeners.forEach { it() }
    }

    override fun addListener(listener: () -> Unit) {
        listeners.add(listener)
    }

    override fun dispose() {
        LOG.info { "Stopping profile watcher, removing roots $watchRoots" }
        LocalFileSystem.getInstance().removeWatchedRoots(watchRoots)
        listeners.clear()
        watchPointers.clear()
    }

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


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

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

import software.aws.toolkits.jetbrains.core.credentials.ManagedBearerSsoConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection

const val SONO_REGION = "us-east-1"
const val SONO_URL = "https://view.awsapps.com/start"
val CODEWHISPERER_SCOPES = listOf(
    "codewhisperer:completions",
    "codewhisperer:analysis",
)
val CODECATALYST_SCOPES = listOf(
    "codecatalyst:read_write"
)

fun ToolkitConnection?.isSono() = if (this == null) {
    false
} else {
    this is ManagedBearerSsoConnection && this.startUrl == SONO_URL
}


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

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

import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import software.aws.toolkits.core.TokenConnectionSettings
import software.aws.toolkits.core.credentials.ToolkitBearerTokenProvider
import software.aws.toolkits.core.telemetry.DefaultMetricEvent
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.loginSso
import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeCatalystConnection
import software.aws.toolkits.jetbrains.core.credentials.reauthProviderIfNeeded
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import software.aws.toolkits.jetbrains.services.caws.CawsResources
import software.aws.toolkits.jetbrains.utils.runUnderProgressIfNeeded
import software.aws.toolkits.resources.message

class SonoCredentialManager {
    private val project: Project?
    constructor(project: Project) {
        this.project = project
    }

    constructor() {
        this.project = null
    }

    internal fun provider() = (
        ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(CodeCatalystConnection.getInstance())
            as? AwsBearerTokenConnection
        )
        ?.getConnectionSettings()
        ?.tokenProvider?.delegate as? BearerTokenProvider

    fun getConnectionSettings(passiveOnly: Boolean = false): TokenConnectionSettings? {
        val provider = provider()
        if (provider == null) {
            if (passiveOnly) {
                return null
            }
            return getSettingsAndPromptAuth()
        }

        return when (provider.state()) {
            BearerTokenAuthState.NOT_AUTHENTICATED -> null
            BearerTokenAuthState.AUTHORIZED -> TokenConnectionSettings(ToolkitBearerTokenProvider(provider), SSO_REGION)
            else -> {
                if (passiveOnly) {
                    null
                } else {
                    tryOrNull {
                        getSettingsAndPromptAuth()
                    }
                }
            }
        }
    }

    fun getSettingsAndPromptAuth() = getProviderAndPromptAuth().asConnectionSettings()

    fun getProviderAndPromptAuth(): BearerTokenProvider {
        val provider = provider()
        return when (provider?.state()) {
            null -> runUnderProgressIfNeeded(null, message("credentials.sono.login.pending"), true) {
                loginSso(project, SONO_URL, SONO_REGION, CODECATALYST_SCOPES)
            }

            else -> reauthProviderIfNeeded(project, provider)
        }
    }

    fun hasPreviouslyConnected() = provider()?.state()?.let { it != BearerTokenAuthState.NOT_AUTHENTICATED } ?: false

    private fun BearerTokenProvider.asConnectionSettings() = TokenConnectionSettings(ToolkitBearerTokenProvider(this), SSO_REGION)

    companion object {
        fun getInstance(project: Project? = null) = project?.let { it.service<SonoCredentialManager>() } ?: service()

        fun loginSono(project: Project?) {
            SonoCredentialManager.getInstance(project).getProviderAndPromptAuth()
        }

        private val SSO_REGION by lazy {
            with(AwsRegionProvider.getInstance()) {
                get(SONO_REGION) ?: throw RuntimeException("AwsRegionProvider was unable to provide SSO_REGION for AWS Builder ID")
            }
        }
    }
}

fun lazilyGetUserId() = tryOrNull {
    SonoCredentialManager.getInstance().getConnectionSettings(passiveOnly = true)?.let {
        AwsResourceCache.getInstance().getResourceNow(CawsResources.ID, it)
    }
} ?: DefaultMetricEvent.METADATA_NA


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

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

import software.aws.toolkits.jetbrains.core.credentials.DiskSsoSessionConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitStartupAuthFactory
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
import software.aws.toolkits.resources.message

class SonoDiskProfileAuthFactory : ToolkitStartupAuthFactory {
    override fun buildConnections(): List<ToolkitConnection> =
        listOf(
            DiskSsoSessionConnection("codecatalyst", SONO_REGION, message("aws_builder_id.service_name"))
        ).filter { (it.getConnectionSettings().tokenProvider.delegate as BearerTokenProvider).state() != BearerTokenAuthState.NOT_AUTHENTICATED }
}


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.DumbAwareAction
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener

class SonoLogoutAction : DumbAwareAction() {
    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = provider(e)?.supportsLogout() ?: false
    }

    override fun actionPerformed(e: AnActionEvent) {
        val provider = provider(e) ?: return

        ApplicationManager.getApplication().messageBus.syncPublisher(BearerTokenProviderListener.TOPIC).invalidate(provider.id)
    }

    private fun provider(e: AnActionEvent) = SonoCredentialManager.getInstance(e.project).provider()
}


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

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

import com.fasterxml.jackson.annotation.JsonInclude
import software.amazon.awssdk.auth.token.credentials.SdkToken
import software.amazon.awssdk.services.sso.SsoClient
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.aws.toolkits.core.utils.SensitiveField
import software.aws.toolkits.core.utils.redactedString
import java.time.Instant
import java.util.Optional

/**
 * Access token returned from [SsoOidcClient.createToken] used to retrieve AWS Credentials from [SsoClient.getRoleCredentials].
 */
data class AccessToken(
    val startUrl: String,
    val region: String,
    @SensitiveField
    val accessToken: String,
    @SensitiveField
    @JsonInclude(JsonInclude.Include.NON_NULL)
    val refreshToken: String? = null,
    val expiresAt: Instant,
    val createdAt: Instant = Instant.EPOCH
) : SdkToken {
    override fun token() = accessToken

    override fun expirationTime() = Optional.of(expiresAt)

    override fun toString() = redactedString(this)
}

// diverging from SDK/CLI impl here since they do: sha1sum(sessionName ?: startUrl)
// which isn't good enough for us
// only used in scoped case
data class AccessTokenCacheKey(
    val connectionId: String,
    val startUrl: String,
    val scopes: List<String>
)


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

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

import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.aws.toolkits.core.utils.SensitiveField
import software.aws.toolkits.core.utils.redactedString
import java.time.Instant

/**
 * Returned by [SsoOidcClient.startDeviceAuthorization] that contains the required data to construct the user visible SSO login flow.
 */
data class Authorization(
    @SensitiveField
    val deviceCode: String,
    val userCode: String,
    val verificationUri: String,
    val verificationUriComplete: String,
    val expiresAt: Instant,
    val pollInterval: Long,
    val createdAt: Instant
) {
    override fun toString(): String = redactedString(this)
}


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

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

import com.fasterxml.jackson.annotation.JsonInclude
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.aws.toolkits.core.utils.SensitiveField
import software.aws.toolkits.core.utils.redactedString
import java.time.Instant

/**
 * Client registration that represents the toolkit returned from [SsoOidcClient.registerClient].
 *
 * It should be persisted for reuse through many authentication requests.
 */
data class ClientRegistration(
    @SensitiveField
    val clientId: String,
    @SensitiveField
    val clientSecret: String,
    val expiresAt: Instant,
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    val scopes: List<String> = emptyList()
) {
    override fun toString(): String = redactedString(this)
}

// only applicable in scoped registration path
// based on internal development branch @da780a4,L2574-2586
data class ClientRegistrationCacheKey(
    val startUrl: String,
    val scopes: List<String>,
    val region: String,
)


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

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

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.util.StdDateFormat
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import software.aws.toolkits.core.utils.createParentDirectories
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.deleteIfExists
import software.aws.toolkits.core.utils.exists
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.inputStreamIfExists
import software.aws.toolkits.core.utils.outputStream
import software.aws.toolkits.core.utils.toHexString
import software.aws.toolkits.core.utils.touch
import software.aws.toolkits.core.utils.tryDirOp
import software.aws.toolkits.core.utils.tryFileOp
import software.aws.toolkits.core.utils.tryOrNull
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Path
import java.nio.file.Paths
import java.security.MessageDigest
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter.ISO_INSTANT
import java.util.TimeZone

/**
 * Caches the [AccessToken] to disk to allow it to be re-used with other tools such as the CLI.
 */
class DiskCache(
    private val cacheDir: Path = Paths.get(System.getProperty("user.home"), ".aws", "sso", "cache"),
    private val clock: Clock = Clock.systemUTC()
) : SsoCache {
    private val objectMapper = jacksonObjectMapper().also {
        it.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)

        it.registerModule(JavaTimeModule())
        val customDateModule = SimpleModule()
        customDateModule.addDeserializer(Instant::class.java, CliCompatibleInstantDeserializer())
        it.registerModule(customDateModule) // Override the Instant deserializer with custom one
        it.dateFormat = StdDateFormat().withTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC))
    }

    // only used for computing cache key names
    private val cacheNameMapper = jacksonObjectMapper()
        .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)

    override fun loadClientRegistration(ssoRegion: String): ClientRegistration? {
        LOG.debug { "loadClientRegistration for $ssoRegion" }
        val inputStream = clientRegistrationCache(ssoRegion).tryInputStreamIfExists() ?: return null
        return loadClientRegistration(inputStream)
    }

    override fun saveClientRegistration(ssoRegion: String, registration: ClientRegistration) {
        LOG.debug { "saveClientRegistration for $ssoRegion" }
        val registrationCache = clientRegistrationCache(ssoRegion)
        writeKey(registrationCache) {
            objectMapper.writeValue(it, registration)
        }
    }

    override fun invalidateClientRegistration(ssoRegion: String) {
        LOG.debug { "invalidateClientRegistration for $ssoRegion" }
        clientRegistrationCache(ssoRegion).tryDeleteIfExists()
    }

    override fun loadClientRegistration(cacheKey: ClientRegistrationCacheKey): ClientRegistration? {
        LOG.debug { "loadClientRegistration for $cacheKey" }
        val inputStream = clientRegistrationCache(cacheKey).tryInputStreamIfExists()
            ?: clientRegistrationCacheBackwardCompatible(cacheKey).tryInputStreamIfExists()
            ?: return null

        return loadClientRegistration(inputStream)
    }

    override fun saveClientRegistration(cacheKey: ClientRegistrationCacheKey, registration: ClientRegistration) {
        LOG.debug { "saveClientRegistration for $cacheKey" }
        val registrationCache = clientRegistrationCache(cacheKey)
        writeKey(registrationCache) {
            objectMapper.writeValue(it, registration)
        }
    }

    override fun invalidateClientRegistration(cacheKey: ClientRegistrationCacheKey) {
        if (clientRegistrationCache(cacheKey).exists()) {
            LOG.debug { "invalidateClientRegistration for $cacheKey" }
            clientRegistrationCache(cacheKey).tryDeleteIfExists()
        } else {
            LOG.debug { "invalidateClientRegistration (backwards compat) for $cacheKey" }
            clientRegistrationCacheBackwardCompatible(cacheKey).tryDeleteIfExists()
        }
    }

    override fun loadAccessToken(ssoUrl: String): AccessToken? {
        LOG.debug { "loadAccessToken for $ssoUrl" }
        val cacheFile = accessTokenCache(ssoUrl)
        val inputStream = cacheFile.tryInputStreamIfExists() ?: return null

        return loadAccessToken(inputStream)
    }

    override fun saveAccessToken(ssoUrl: String, accessToken: AccessToken) {
        LOG.debug { "saveAccessToken for $ssoUrl" }
        val accessTokenCache = accessTokenCache(ssoUrl)
        writeKey(accessTokenCache) {
            objectMapper.writeValue(it, accessToken)
        }
    }

    override fun invalidateAccessToken(ssoUrl: String) {
        LOG.debug { "invalidateAccessToken for $ssoUrl" }
        accessTokenCache(ssoUrl).tryDeleteIfExists()
    }

    override fun loadAccessToken(cacheKey: AccessTokenCacheKey): AccessToken? {
        LOG.debug { "loadAccessToken for $cacheKey" }
        val cacheFile = accessTokenCache(cacheKey)
        val inputStream = cacheFile.tryInputStreamIfExists() ?: return null

        return loadAccessToken(inputStream)
    }

    override fun saveAccessToken(cacheKey: AccessTokenCacheKey, accessToken: AccessToken) {
        LOG.debug { "saveAccessToken for $cacheKey" }
        val accessTokenCache = accessTokenCache(cacheKey)
        writeKey(accessTokenCache) {
            objectMapper.writeValue(it, accessToken)
        }
    }

    override fun invalidateAccessToken(cacheKey: AccessTokenCacheKey) {
        LOG.debug { "invalidateAccessToken for $cacheKey" }
        accessTokenCache(cacheKey).tryDeleteIfExists()
    }

    private fun clientRegistrationCache(ssoRegion: String): Path = cacheDir.resolve("aws-toolkit-jetbrains-client-id-$ssoRegion.json")

    private fun clientRegistrationCache(cacheKey: ClientRegistrationCacheKey): Path =
        cacheNameMapper.valueToTree<ObjectNode>(cacheKey).apply {
            // session is omitted to keep the key deterministic since we attach an epoch
            put("tool", "aws-toolkit-jetbrains")
        }.let {
            val sha = sha1(cacheNameMapper.writeValueAsString(it))

            cacheDir.resolve("$sha.json")
        }

    // Can be removed when 2022.3 is no longer supported
    private fun clientRegistrationCacheBackwardCompatible(cacheKey: ClientRegistrationCacheKey): Path =
        cacheNameMapper.valueToTree<ObjectNode>(cacheKey).apply {
            // session is omitted to keep the key deterministic since we attach an epoch
            put("tool", "aws-toolkit-jetbrains")

            // remove the region field to generate a key for the old key schema
            remove("region")
        }.let {
            val sha = sha1(cacheNameMapper.writeValueAsString(it))
            cacheDir.resolve("$sha.json")
        }

    private fun accessTokenCache(ssoUrl: String): Path {
        val fileName = "${sha1(ssoUrl)}.json"
        return cacheDir.resolve(fileName)
    }

    private fun accessTokenCache(cacheKey: AccessTokenCacheKey): Path {
        val fileName = "${sha1(cacheNameMapper.writeValueAsString(cacheKey.copy(scopes = cacheKey.scopes.sorted())))}.json"
        return cacheDir.resolve(fileName)
    }

    private fun loadClientRegistration(inputStream: InputStream) =
        tryOrNull {
            val clientRegistration = objectMapper.readValue<ClientRegistration>(inputStream)
            if (clientRegistration.expiresAt.isNotExpired()) {
                clientRegistration
            } else {
                null
            }
        }

    private fun loadAccessToken(inputStream: InputStream) = tryOrNull {
        val accessToken = objectMapper.readValue<AccessToken>(inputStream)
        // Use same expiration logic as client registration even though RFC/SEP does not specify it.
        // This prevents a cache entry being returned as valid and then expired when we go to use it.
        if (!accessToken.isDefinitelyExpired()) {
            accessToken
        } else {
            null
        }
    }

    private fun Path.tryDeleteIfExists(): Boolean = tryFileOp(LOG) { deleteIfExists() }

    private fun Path.tryInputStreamIfExists(): InputStream? = tryFileOp(LOG) { inputStreamIfExists() }

    private fun sha1(string: String): String {
        val digest = MessageDigest.getInstance("SHA-1")
        return digest.digest(string.toByteArray(Charsets.UTF_8)).toHexString()
    }

    private fun writeKey(path: Path, consumer: (OutputStream) -> Unit) {
        LOG.debug { "writing to $path" }
        path.tryDirOp(LOG) { createParentDirectories() }

        path.tryFileOp(LOG) {
            touch(restrictToOwner = true)
            outputStream().use(consumer)
        }
    }

    // If the item is going to expire in the next 15 mins, we must treat it as already expired
    private fun Instant.isNotExpired(): Boolean = this.isAfter(Instant.now(clock).plus(EXPIRATION_THRESHOLD))

    private fun AccessToken.isDefinitelyExpired(): Boolean = refreshToken == null && !expiresAt.isNotExpired()

    private class CliCompatibleInstantDeserializer : StdDeserializer<Instant>(Instant::class.java) {
        override fun deserialize(parser: JsonParser, context: DeserializationContext): Instant {
            val dateString = parser.valueAsString

            // CLI appends UTC, which Java refuses to parse. Convert it to a Z
            val sanitized = if (dateString.endsWith("UTC")) {
                dateString.dropLast(3) + 'Z'
            } else {
                dateString
            }

            return ISO_INSTANT.parse(sanitized) { Instant.from(it) }
        }
    }

    companion object {
        val EXPIRATION_THRESHOLD = Duration.ofMinutes(15)
        private val LOG = getLogger<DiskCache>()
    }
}


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

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

import com.intellij.openapi.components.service
import com.intellij.openapi.progress.ProgressManager
import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.amazon.awssdk.services.ssooidc.model.AuthorizationPendingException
import software.amazon.awssdk.services.ssooidc.model.CreateTokenResponse
import software.amazon.awssdk.services.ssooidc.model.InvalidClientException
import software.amazon.awssdk.services.ssooidc.model.InvalidRequestException
import software.amazon.awssdk.services.ssooidc.model.SlowDownException
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
import software.aws.toolkits.jetbrains.utils.assertIsNonDispatchThread
import software.aws.toolkits.jetbrains.utils.sleepWithCancellation
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AwsTelemetry
import software.aws.toolkits.telemetry.CredentialSourceId
import software.aws.toolkits.telemetry.Result
import java.time.Clock
import java.time.Duration
import java.time.Instant

/**
 * Takes care of creating/refreshing the SSO access token required to fetch SSO-based credentials.
 */
class SsoAccessTokenProvider(
    private val ssoUrl: String,
    private val ssoRegion: String,
    private val cache: SsoCache,
    private val client: SsoOidcClient,
    private val scopes: List<String> = emptyList(),
    private val clock: Clock = Clock.systemUTC()
) {

    @TestOnly
    var authorizationCreationTime = Instant.now(clock)

    private val clientRegistrationCacheKey by lazy {
        ClientRegistrationCacheKey(
            startUrl = ssoUrl,
            scopes = scopes,
            region = ssoRegion
        )
    }
    internal val accessTokenCacheKey by lazy {
        AccessTokenCacheKey(
            connectionId = ssoRegion,
            startUrl = ssoUrl,
            scopes = scopes
        )
    }

    fun accessToken(): AccessToken {
        assertIsNonDispatchThread()

        loadAccessToken()?.let {
            return it
        }

        val token = pollForToken()

        saveAccessToken(token)

        return token
    }

    private fun registerClient(): ClientRegistration {
        loadClientRegistration()?.let {
            return it
        }

        // Based on botocore: https://github.com/boto/botocore/blob/5dc8ee27415dc97cfff75b5bcfa66d410424e665/botocore/utils.py#L1753
        val registerResponse = client.registerClient {
            it.clientType(CLIENT_REGISTRATION_TYPE)
            it.scopes(scopes)
            it.clientName("AWS Toolkit for JetBrains")
        }

        val registeredClient = ClientRegistration(
            registerResponse.clientId(),
            registerResponse.clientSecret(),
            Instant.ofEpochSecond(registerResponse.clientSecretExpiresAt())
        )

        saveClientRegistration(registeredClient)

        return registeredClient
    }

    private fun authorizeClient(clientId: ClientRegistration): Authorization {
        // Should not be cached, only good for 1 token and short lived
        val authorizationResponse = try {
            client.startDeviceAuthorization {
                it.startUrl(ssoUrl)
                it.clientId(clientId.clientId)
                it.clientSecret(clientId.clientSecret)
            }
        } catch (e: InvalidClientException) {
            invalidateClientRegistration()
            throw e
        }

        authorizationCreationTime = Instant.now(clock)

        return Authorization(
            authorizationResponse.deviceCode(),
            authorizationResponse.userCode(),
            authorizationResponse.verificationUri(),
            authorizationResponse.verificationUriComplete(),
            Instant.now(clock).plusSeconds(authorizationResponse.expiresIn().toLong()),
            authorizationResponse.interval()?.toLong()
                ?: DEFAULT_INTERVAL_SECS,
            authorizationCreationTime
        )
    }

    private fun pollForToken(): AccessToken {
        val onPendingToken = service<SsoLoginCallbackProvider>().getProvider(ssoUrl)
        val progressIndicator = ProgressManager.getInstance().progressIndicator
        val registration = registerClient()
        val authorization = authorizeClient(registration)

        progressIndicator?.text2 = message("aws.sso.signing.device.waiting", authorization.userCode)
        onPendingToken.tokenPending(authorization)

        var backOffTime = Duration.ofSeconds(authorization.pollInterval)

        while (true) {
            try {
                val tokenResponse = client.createToken {
                    it.clientId(registration.clientId)
                    it.clientSecret(registration.clientSecret)
                    it.grantType(DEVICE_GRANT_TYPE)
                    it.deviceCode(authorization.deviceCode)
                }

                onPendingToken.tokenRetrieved()

                return tokenResponse.toAccessToken(authorization.createdAt)
            } catch (e: SlowDownException) {
                backOffTime = backOffTime.plusSeconds(SLOW_DOWN_DELAY_SECS)
            } catch (e: AuthorizationPendingException) {
                // Do nothing, keep polling
            } catch (e: Exception) {
                onPendingToken.tokenRetrievalFailure(e)
                throw e
            }

            sleepWithCancellation(backOffTime, progressIndicator)
        }
    }

    fun refreshToken(currentToken: AccessToken): AccessToken {
        if (currentToken.refreshToken == null) {
            val tokenCreationTime = currentToken.createdAt

            if (tokenCreationTime != Instant.EPOCH) {
                val sessionDuration = Duration.between(Instant.now(clock), tokenCreationTime)
                val credentialSourceId = if (currentToken.startUrl == SONO_URL) CredentialSourceId.AwsId else CredentialSourceId.IamIdentityCenter
                AwsTelemetry.refreshCredentials(
                    project = null,
                    Result.Failed,
                    sessionDuration = sessionDuration.toHours().toInt(),
                    credentialSourceId = credentialSourceId,
                    reason = "Null refresh token"
                )
            }

            throw InvalidRequestException.builder().message("Requested token refresh, but refresh token was null").build()
        }

        val registration = loadClientRegistration() ?: throw InvalidClientException.builder().message("Unable to load client registration").build()

        val newToken = client.createToken {
            it.clientId(registration.clientId)
            it.clientSecret(registration.clientSecret)
            it.grantType(REFRESH_GRANT_TYPE)
            it.refreshToken(currentToken.refreshToken)
        }

        val token = newToken.toAccessToken(currentToken.createdAt)
        saveAccessToken(token)

        return token
    }

    fun invalidate() {
        if (scopes.isEmpty()) {
            cache.invalidateAccessToken(ssoUrl)
        } else {
            cache.invalidateAccessToken(accessTokenCacheKey)
        }
    }

    private fun loadClientRegistration(): ClientRegistration? = if (scopes.isEmpty()) {
        cache.loadClientRegistration(ssoRegion)?.let {
            return it
        }
    } else {
        cache.loadClientRegistration(clientRegistrationCacheKey)?.let {
            return it
        }
    }

    private fun saveClientRegistration(registration: ClientRegistration) {
        if (scopes.isEmpty()) {
            cache.saveClientRegistration(ssoRegion, registration)
        } else {
            cache.saveClientRegistration(clientRegistrationCacheKey, registration)
        }
    }

    private fun invalidateClientRegistration() {
        if (scopes.isEmpty()) {
            cache.invalidateClientRegistration(ssoRegion)
        } else {
            cache.invalidateClientRegistration(clientRegistrationCacheKey)
        }
    }

    private fun loadAccessToken(): AccessToken? = if (scopes.isEmpty()) {
        cache.loadAccessToken(ssoUrl)?.let {
            return it
        }
    } else {
        cache.loadAccessToken(accessTokenCacheKey)?.let {
            return it
        }
    }

    private fun saveAccessToken(token: AccessToken) {
        if (scopes.isEmpty()) {
            cache.saveAccessToken(ssoUrl, token)
        } else {
            cache.saveAccessToken(accessTokenCacheKey, token)
        }
    }

    private fun CreateTokenResponse.toAccessToken(creationTime: Instant): AccessToken {
        val expirationTime = Instant.now(clock).plusSeconds(expiresIn().toLong())

        return AccessToken(
            startUrl = ssoUrl,
            region = ssoRegion,
            accessToken = accessToken(),
            refreshToken = refreshToken(),
            expiresAt = expirationTime,
            createdAt = creationTime
        )
    }

    private companion object {
        const val CLIENT_REGISTRATION_TYPE = "public"
        const val DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
        const val REFRESH_GRANT_TYPE = "refresh_token"

        // Default number of seconds to poll for token, https://tools.ietf.org/html/draft-ietf-oauth-device-flow-15#section-3.5
        const val DEFAULT_INTERVAL_SECS = 5L
        const val SLOW_DOWN_DELAY_SECS = 5L
    }
}


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

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

interface SsoCache {
    fun loadClientRegistration(ssoRegion: String): ClientRegistration?
    fun saveClientRegistration(ssoRegion: String, registration: ClientRegistration)
    fun invalidateClientRegistration(ssoRegion: String)

    fun loadAccessToken(ssoUrl: String): AccessToken?
    fun saveAccessToken(ssoUrl: String, accessToken: AccessToken)
    fun invalidateAccessToken(ssoUrl: String)

    fun loadClientRegistration(cacheKey: ClientRegistrationCacheKey): ClientRegistration?
    fun saveClientRegistration(cacheKey: ClientRegistrationCacheKey, registration: ClientRegistration)
    fun invalidateClientRegistration(cacheKey: ClientRegistrationCacheKey)

    fun loadAccessToken(cacheKey: AccessTokenCacheKey): AccessToken?
    fun saveAccessToken(cacheKey: AccessTokenCacheKey, accessToken: AccessToken)
    fun invalidateAccessToken(cacheKey: AccessTokenCacheKey)
}


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

@file:Suppress("BannedImports")

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

import software.amazon.awssdk.auth.credentials.AwsCredentials
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.amazon.awssdk.services.sso.SsoClient
import software.amazon.awssdk.services.sso.model.UnauthorizedException
import software.amazon.awssdk.utils.SdkAutoCloseable
import software.amazon.awssdk.utils.cache.CachedSupplier
import software.amazon.awssdk.utils.cache.RefreshResult
import software.aws.toolkits.jetbrains.utils.assertIsNonDispatchThread
import java.time.Duration
import java.time.Instant

/**
 * [AwsCredentialsProvider] that contains all the needed hooks to perform an end to end flow of an SSO-based credential.
 *
 * This credential provider will trigger an SSO login if required, unlike the low level SDKs.
 */
class SsoCredentialProvider(
    private val ssoAccount: String,
    private val ssoRole: String,
    private val ssoClient: SsoClient,
    private val ssoAccessTokenProvider: SsoAccessTokenProvider
) : AwsCredentialsProvider, SdkAutoCloseable {
    private val sessionCache: CachedSupplier<SsoCredentialsHolder> = CachedSupplier.builder(this::refreshCredentials).build()

    override fun resolveCredentials(): AwsCredentials = sessionCache.get().credentials

    private fun refreshCredentials(): RefreshResult<SsoCredentialsHolder> {
        assertIsNonDispatchThread()

        val roleCredentials = try {
            val accessToken = ssoAccessTokenProvider.accessToken()

            ssoClient.getRoleCredentials {
                it.accessToken(accessToken.accessToken)
                it.accountId(ssoAccount)
                it.roleName(ssoRole)
            }
        } catch (e: UnauthorizedException) {
            // OIDC access token was rejected, invalidate the cache and throw
            ssoAccessTokenProvider.invalidate()
            throw e
        }

        val awsCredentials = AwsSessionCredentials.create(
            roleCredentials.roleCredentials().accessKeyId(),
            roleCredentials.roleCredentials().secretAccessKey(),
            roleCredentials.roleCredentials().sessionToken()
        )

        val expirationTime = Instant.ofEpochMilli(roleCredentials.roleCredentials().expiration())

        val ssoCredentials = SsoCredentialsHolder(awsCredentials, expirationTime)

        return RefreshResult.builder(ssoCredentials)
            .staleTime(expirationTime.minus(Duration.ofMinutes(1)))
            .prefetchTime(expirationTime.minus(Duration.ofMinutes(5)))
            .build()
    }

    override fun close() {
        sessionCache.close()
    }

    private data class SsoCredentialsHolder(val credentials: AwsSessionCredentials, val expirationTime: Instant)
}


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

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

/**
 * Callback interface to allow for UI elements to react to the different stages of the SSO login flow
 */
interface SsoLoginCallback {
    /**
     * Called when a new authorization is pending within SSO service. User should be notified so they can perform the login flow.
     */
    fun tokenPending(authorization: Authorization)

    /**
     * Called when the user successfully logs into the SSO service.
     */
    fun tokenRetrieved()

    /**
     * Called when the SSO login fails
     */
    fun tokenRetrievalFailure(e: Exception)
}


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

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

import com.intellij.ide.BrowserUtil
import com.intellij.openapi.progress.ProcessCanceledException
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.CopyUserCodeForLoginDialog
import software.aws.toolkits.jetbrains.utils.computeOnEdt
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AwsTelemetry
import software.aws.toolkits.telemetry.CredentialType
import software.aws.toolkits.telemetry.Result

interface SsoLoginCallbackProvider {
    fun getProvider(ssoUrl: String): SsoLoginCallback
}

class DefaultSsoLoginCallbackProvider : SsoLoginCallbackProvider {
    override fun getProvider(ssoUrl: String): SsoLoginCallback = when (ssoUrl) {
        SONO_URL -> BearerTokenPrompt
        else -> SsoPrompt
    }
}

object SsoPrompt : SsoLoginCallback {
    override fun tokenPending(authorization: Authorization) {
        computeOnEdt {
            val result = CopyUserCodeForLoginDialog(
                authorization.userCode,
                message("credentials.sso.login.title"),
                CredentialType.SsoProfile
            ).showAndGet()

            if (result) {
                AwsTelemetry.loginWithBrowser(project = null, Result.Succeeded, CredentialType.SsoProfile)
                BrowserUtil.browse(authorization.verificationUri)
            } else {
                AwsTelemetry.loginWithBrowser(project = null, Result.Cancelled, CredentialType.SsoProfile)
                throw ProcessCanceledException(IllegalStateException(message("credentials.sso.login.cancelled")))
            }
        }
    }

    override fun tokenRetrieved() {}

    override fun tokenRetrievalFailure(e: Exception) {
        e.notifyError(message("credentials.sso.login.failed"))
    }
}

object BearerTokenPrompt : SsoLoginCallback {
    override fun tokenPending(authorization: Authorization) {
        computeOnEdt {
            val codeCopied = CopyUserCodeForLoginDialog(
                authorization.userCode,
                message("credentials.sono.login"),
                CredentialType.BearerToken
            ).showAndGet()

            if (codeCopied) {
                AwsTelemetry.loginWithBrowser(project = null, Result.Succeeded, CredentialType.BearerToken)
                BrowserUtil.browse(authorization.verificationUri)
            } else {
                AwsTelemetry.loginWithBrowser(project = null, Result.Cancelled, CredentialType.BearerToken)
            }
        }
    }

    override fun tokenRetrieved() {}

    override fun tokenRetrievalFailure(e: Exception) {}
}


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

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

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.util.containers.orNull
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider
import software.amazon.awssdk.auth.token.credentials.SdkToken
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
import software.amazon.awssdk.core.retry.conditions.OrRetryCondition
import software.amazon.awssdk.core.retry.conditions.RetryOnExceptionsCondition
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.amazon.awssdk.services.ssooidc.SsoOidcTokenProvider
import software.amazon.awssdk.services.ssooidc.internal.OnDiskTokenManager
import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException
import software.amazon.awssdk.utils.SdkAutoCloseable
import software.amazon.awssdk.utils.cache.CachedSupplier
import software.amazon.awssdk.utils.cache.NonBlocking
import software.amazon.awssdk.utils.cache.RefreshResult
import software.aws.toolkits.core.ToolkitClientCustomizer
import software.aws.toolkits.core.clients.nullDefaultProfileFile
import software.aws.toolkits.core.credentials.ToolkitBearerTokenProvider
import software.aws.toolkits.core.credentials.ToolkitBearerTokenProviderDelegate
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.credentials.diskCache
import software.aws.toolkits.jetbrains.core.credentials.sso.AccessToken
import software.aws.toolkits.jetbrains.core.credentials.sso.DiskCache
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoAccessTokenProvider
import java.time.Duration
import java.time.Instant
import java.util.concurrent.atomic.AtomicReference

internal interface BearerTokenLogoutSupport

interface BearerTokenProvider : SdkTokenProvider, SdkAutoCloseable, ToolkitBearerTokenProviderDelegate {
    /**
     * @return The best available [SdkToken] to the provider without making network calls or prompting for user input
     */
    fun currentToken(): AccessToken?

    /**
     * @return The authentication state of [currentToken]
     */
    fun state(): BearerTokenAuthState = state(currentToken())

    /**
     * Request provider to interactively request user input to obtain a new [AccessToken]
     */
    open fun reauthenticate() {
        throw UnsupportedOperationException("Provider is not interactive and cannot reauthenticate")
    }

    open fun supportsLogout() = this is BearerTokenLogoutSupport

    open fun invalidate() {
        throw UnsupportedOperationException("Provider is not interactive and cannot be invalidated")
    }

    companion object {
        private fun tokenExpired(accessToken: AccessToken) = Instant.now().isAfter(accessToken.expiresAt)

        internal fun state(accessToken: AccessToken?) = when {
            accessToken == null -> BearerTokenAuthState.NOT_AUTHENTICATED
            tokenExpired(accessToken) -> {
                if (accessToken.refreshToken != null) {
                    BearerTokenAuthState.NEEDS_REFRESH
                } else {
                    // token is invalid if there is no refresh token
                    BearerTokenAuthState.NOT_AUTHENTICATED
                }
            }
            else -> BearerTokenAuthState.AUTHORIZED
        }
    }
}

class InteractiveBearerTokenProvider(
    startUrl: String,
    region: String,
    scopes: List<String>,
    cache: DiskCache = diskCache
) : BearerTokenProvider, BearerTokenLogoutSupport, Disposable {
    override val id = ToolkitBearerTokenProvider.ssoIdentifier(startUrl, region)
    override val displayName = ToolkitBearerTokenProvider.ssoDisplayName(startUrl)

    private val ssoOidcClient: SsoOidcClient = buildUnmanagedSsoOidcClient(region)
    private val accessTokenProvider =
        SsoAccessTokenProvider(
            startUrl,
            region,
            cache,
            ssoOidcClient,
            scopes = scopes
        )

    private val supplier = CachedSupplier.builder { refreshToken() }.prefetchStrategy(NonBlocking("AWS SSO bearer token refresher")).build()
    private val lastToken = AtomicReference<AccessToken?>()
    init {
        lastToken.set(cache.loadAccessToken(accessTokenProvider.accessTokenCacheKey))

        ApplicationManager.getApplication().messageBus.connect(this).subscribe(
            BearerTokenProviderListener.TOPIC,
            object : BearerTokenProviderListener {
                override fun invalidate(providerId: String) {
                    if (id == providerId) {
                        invalidate()
                    }
                }
            }
        )
    }

    private fun refreshToken(): RefreshResult<out SdkToken> {
        val lastToken = lastToken.get() ?: error("Token refresh started before session initialized")
        val token = if (Duration.between(Instant.now(), lastToken.expiresAt) > Duration.ofMinutes(30)) {
            lastToken
        } else {
            accessTokenProvider.refreshToken(lastToken).also {
                this.lastToken.set(it)
            }
        }

        return RefreshResult.builder(token)
            .staleTime(token.expiresAt.minus(DEFAULT_STALE_DURATION))
            .prefetchTime(token.expiresAt.minus(DEFAULT_PREFETCH_DURATION))
            .build()
    }

    override fun resolveToken() = supplier.get()

    override fun close() {
        ssoOidcClient.close()
        supplier.close()
    }

    override fun dispose() {
        close()
    }

    override fun currentToken() = lastToken.get()

    override fun invalidate() {
        accessTokenProvider.invalidate()
        lastToken.set(null)
        BearerTokenProviderListener.notifyCredUpdate(id)
    }

    override fun reauthenticate() {
        // we probably don't need to invalidate this, but we might as well since we need to login again anyways
        invalidate()
        accessTokenProvider.accessToken().also {
            lastToken.set(it)
            BearerTokenProviderListener.notifyCredUpdate(id)
        }
    }
}

public enum class BearerTokenAuthState {
    AUTHORIZED,
    NEEDS_REFRESH,
    NOT_AUTHENTICATED
}

class ProfileSdkTokenProviderWrapper(private val sessionName: String, region: String) : BearerTokenProvider, Disposable {
    override val id = ToolkitBearerTokenProvider.diskSessionIdentifier(sessionName)
    override val displayName = ToolkitBearerTokenProvider.diskSessionDisplayName(sessionName)

    private val sdkTokenManager = OnDiskTokenManager.create(sessionName)
    private val ssoOidcClient = lazy { buildUnmanagedSsoOidcClient(region) }
    private val tokenProvider = lazy {
        SsoOidcTokenProvider.builder()
            .ssoOidcClient(ssoOidcClient.value)
            .sessionName(sessionName)
            .staleTime(DEFAULT_STALE_DURATION)
            .prefetchTime(DEFAULT_PREFETCH_DURATION)
            .build()
    }

    override fun resolveToken(): SdkToken = tokenProvider.value.resolveToken()

    override fun currentToken(): AccessToken? = sdkTokenManager.loadToken().orNull()?.let {
        AccessToken(
            startUrl = it.startUrl(),
            region = it.region(),
            accessToken = it.token(),
            refreshToken = it.refreshToken(),
            expiresAt = it.expirationTime().orElseThrow()
        )
    }

    override fun close() {
        sdkTokenManager.close()
        if (ssoOidcClient.isInitialized()) {
            ssoOidcClient.value.close()
        }
        if (tokenProvider.isInitialized()) {
            tokenProvider.value.close()
        }
    }

    override fun dispose() {
        close()
    }
}

internal val DEFAULT_STALE_DURATION = Duration.ofMinutes(15)
internal val DEFAULT_PREFETCH_DURATION = Duration.ofMinutes(20)

val ssoOidcClientConfigurationBuilder: (ClientOverrideConfiguration.Builder) -> ClientOverrideConfiguration.Builder = { configuration ->
    configuration.nullDefaultProfileFile()

    // Get the existing RetryPolicy
    val existingRetryPolicy = configuration.retryPolicy()

    // Add InvalidGrantException to the RetryOnExceptionsCondition
    val updatedRetryPolicy = existingRetryPolicy.toBuilder()
        .retryCondition(
            OrRetryCondition.create(
                existingRetryPolicy.retryCondition(),
                RetryOnExceptionsCondition.create(setOf(InvalidGrantException::class.java)),
            )
        ).build()

    // Update the RetryPolicy in the configuration
    configuration.retryPolicy(updatedRetryPolicy)
}

fun buildUnmanagedSsoOidcClient(region: String): SsoOidcClient =
    AwsClientManager.getInstance()
        .createUnmanagedClient(
            AnonymousCredentialsProvider.create(),
            Region.of(region),
            clientCustomizer = ToolkitClientCustomizer { _, _, _, _, configuration ->
                ssoOidcClientConfigurationBuilder(configuration)
            }
        )


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

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

import com.intellij.openapi.application.ApplicationManager
import com.intellij.util.messages.Topic
import java.util.EventListener

interface BearerTokenProviderListener : EventListener {
    fun onChange(providerId: String) {}
    fun invalidate(providerId: String) {}

    companion object {
        @Topic.AppLevel
        val TOPIC = Topic.create("AWS SSO bearer token provider status change", BearerTokenProviderListener::class.java)

        fun notifyCredUpdate(providerId: String) {
            ApplicationManager.getApplication().messageBus.syncPublisher(BearerTokenProviderListener.TOPIC).onChange(providerId)
        }
    }
}


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

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

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.ActionToolbar
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.impl.ActionButton
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.components.JBLabel
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
import com.intellij.util.ui.components.BorderLayoutPanel
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AwsTelemetry
import software.aws.toolkits.telemetry.CredentialType
import software.aws.toolkits.telemetry.Result
import java.awt.datatransfer.StringSelection
import javax.swing.JComponent

class CopyUserCodeForLoginDialog(
    private val authCode: String,
    private val dialogTitle: String,
    private val credentialType: CredentialType
) : DialogWrapper(null) {

    private val pane = panel {
        row {
            text(message("aws.sso.signing.device.code.copy.dialog.text"), maxLineLength = -1)
        }

        row {
            cell(
                BorderLayoutPanel(5, 0).apply {
                    val action = CopyUserCodeForLogin(authCode)
                    addToCenter(JBLabel(authCode).setCopyable(true))
                    addToRight(ActionButton(action, action.templatePresentation.clone(), ActionPlaces.UNKNOWN, ActionToolbar.NAVBAR_MINIMUM_BUTTON_SIZE))
                }
            ).horizontalAlign(HorizontalAlign.CENTER)
        }
    }
    override fun createCenterPanel(): JComponent? = pane

    init {
        title = dialogTitle
        setOKButtonText(message("aws.sso.signing.device.code"))
        super.init()
    }

    override fun doOKAction() {
        CopyPasteManager.getInstance().setContents(StringSelection(authCode))
        super.doOKAction()
    }

    override fun doCancelAction() {
        super.doCancelAction()
        AwsTelemetry.loginWithBrowser(project = null, Result.Cancelled, credentialType)
    }
}

class CopyUserCodeForLogin(private val authCode: String) : AnAction(message("aws.sso.signing.device.code.copy"), "", AllIcons.Actions.Copy) {
    override fun actionPerformed(e: AnActionEvent) {
        CopyPasteManager.getInstance().setContents(StringSelection(authCode))
    }
}


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

package software.aws.toolkits.jetbrains.core.docker

import com.intellij.docker.dockerFile.parser.psi.DockerFileAddOrCopyCommand
import com.intellij.docker.dockerFile.parser.psi.DockerFileCmdCommand
import com.intellij.docker.dockerFile.parser.psi.DockerFileExposeCommand
import com.intellij.docker.dockerFile.parser.psi.DockerFileFromCommand
import com.intellij.docker.dockerFile.parser.psi.DockerFileWorkdirCommand
import com.intellij.docker.dockerFile.parser.psi.DockerPsiCommand
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.impl.source.tree.LeafPsiElement
import java.io.File

class DockerfileParser(private val project: Project) {
    fun parse(virtualFile: VirtualFile): DockerfileDetails? {
        val psiFile = PsiManager.getInstance(project).findFile(virtualFile)!!
        val contextDirectory = virtualFile.parent.path

        val lastFromCommand = psiFile.children.filterIsInstance<DockerFileFromCommand>().lastOrNull() ?: return null
        val commandsAfterLastFrom = psiFile.children.dropWhile { it != lastFromCommand }
        if (commandsAfterLastFrom.isEmpty()) {
            return null
        }

        val command = commandsAfterLastFrom.filterIsInstance<DockerFileCmdCommand>().lastOrNull()?.text?.substringAfter("CMD ")
        val portMappings = commandsAfterLastFrom.filterIsInstance<DockerFileExposeCommand>().mapNotNull {
            it.listChildren().find { child -> (child as? LeafPsiElement)?.elementType?.toString() == "INTEGER_LITERAL" }?.text?.toIntOrNull()
        }

        val copyDirectives = groupByWorkDir(commandsAfterLastFrom).flatMap { (workDir, commands) ->
            commands.filterIsInstance<DockerFileAddOrCopyCommand>()
                .filter { it.copyKeyword != null }
                .mapNotNull { cmd -> cmd.fileOrUrlList.takeIf { it.size == 2 }?.let { it.first().text to it.last().text } }
                .map { (rawLocal, rawRemote) ->
                    val local = if (rawLocal.startsWith("/") || rawLocal.startsWith(File.separatorChar)) {
                        rawLocal
                    } else {
                        "${contextDirectory.normalizeDirectory(true)}$rawLocal"
                    }
                    val remote = if (rawRemote.startsWith("/") || workDir == null) {
                        rawRemote
                    } else {
                        "${workDir.normalizeDirectory()}$rawRemote"
                    }
                    CopyDirective(local, remote)
                }
        }

        return DockerfileDetails(command, portMappings, copyDirectives)
    }

    private fun String.normalizeDirectory(matchPlatform: Boolean = false): String {
        val ch = if (matchPlatform) File.separatorChar else '/'
        return "${trimEnd(ch)}$ch"
    }

    private fun groupByWorkDir(commands: List<PsiElement>): List<Pair<String?, List<DockerPsiCommand>>> {
        val list = mutableListOf<Pair<String?, List<DockerPsiCommand>>>()
        var workDir: String? = null
        val elements = mutableListOf<DockerPsiCommand>()
        commands.forEach {
            when (it) {
                is DockerFileWorkdirCommand -> {
                    if (elements.isNotEmpty()) {
                        list.add(workDir to elements.toList())
                        elements.clear()
                    }
                    workDir = it.fileOrUrlList.first().text
                }
                is DockerPsiCommand -> elements.add(it)
            }
        }
        if (elements.isNotEmpty()) {
            list.add(workDir to elements.toList())
        }
        return list
    }

    private fun PsiElement.listChildren(): List<PsiElement> {
        var child: PsiElement? = firstChild ?: return emptyList()
        val children = mutableListOf<PsiElement>()
        while (child != null) {
            children.add(child)
            child = child.nextSibling
        }
        return children.toList()
    }
}

data class DockerfileDetails(val command: String?, val exposePorts: List<Int>, val copyDirectives: List<CopyDirective>)
data class CopyDirective(val from: String, val to: String)


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

package software.aws.toolkits.jetbrains.core.docker

import com.intellij.docker.DockerDeploymentConfiguration
import com.intellij.docker.agent.DockerAgent
import com.intellij.docker.agent.DockerAgentDeploymentConfig
import com.intellij.docker.agent.DockerAgentProgressCallback
import com.intellij.docker.agent.DockerAgentSourceType
import com.intellij.docker.agent.progress.DockerResponseItem
import com.intellij.docker.registry.DockerAgentRepositoryConfigImpl
import com.intellij.docker.registry.DockerRepositoryModel
import com.intellij.docker.remote.run.runtime.DockerAgentDeploymentConfigImpl
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import kotlinx.coroutines.future.await
import kotlinx.coroutines.runBlocking
import org.apache.commons.io.FileUtils
import org.jetbrains.annotations.TestOnly
import org.jetbrains.concurrency.await
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.services.ecr.DockerfileEcrPushRequest
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import java.io.File
import java.io.ObjectInputStream
import java.time.Instant
import java.util.UUID
import java.util.concurrent.CompletableFuture
import com.intellij.docker.getSourceFile as getDockerfileSourceFile

// it's a facade so imo we shouldn't be leaking impl details but i don't feel like rewriting tests
interface DockerRuntimeFacade {
    val agent: DockerAgent

    suspend fun pushImage(imageId: String, config: DockerRepositoryModel): CompletableFuture<Unit>
}

class ToolkitDockerAdapter(protected val project: Project, val runtimeFacade: DockerRuntimeFacade) {
    internal var agent = runtimeFacade.agent
        @TestOnly
        set

    @TestOnly
    fun buildLocalImage(dockerfile: File): String? {
        val tag = UUID.randomUUID().toString()
        val config = object : DockerAgentDeploymentConfigImpl(tag, null) {
            override fun getFile() = dockerfile

            override fun sourceType() = DockerAgentSourceType.FILE.toString()
        }

        return runBlocking { buildImage(project, agent, config, dockerfile.absolutePath, tag) }
    }

    suspend fun hackyBuildDockerfileWithUi(project: Project, pushRequest: DockerfileEcrPushRequest) =
        hackyBuildDockerfileUnderIndicator(project, pushRequest)

    suspend fun pushImage(imageId: String, config: DockerRepositoryModel) = runtimeFacade.pushImage(imageId, config)

    fun getLocalImages(): List<LocalImage> =
        agent.getImages(null).flatMap { image ->
            @Suppress("UselessCallOnNotNull")
            if (image.imageRepoTags.isNullOrEmpty()) {
                return@flatMap listOf(LocalImage(image.imageId, null))
            }

            image.imageRepoTags.map { localTag ->
                val tag = localTag.takeUnless { it == NO_TAG_TAG }
                LocalImage(image.imageId, tag)
            }
        }.toList()

    fun pullImage(
        config: DockerRepositoryModel,
        progressIndicator: ProgressIndicator? = null
    ) = agent.pullImage(
        DockerAgentRepositoryConfigImpl(config),
        object : DockerAgentProgressCallback {
            // based on RegistryRuntimeTask's impl
            override fun step(status: String, current: Long, total: Long) {
                LOG.debug { "step: status: $status, current: $current, total: $total" }
                progressIndicator?.let { indicator ->
                    val indeterminate = total == 0L
                    indicator.text2 = "$status " +
                        (if (indeterminate) "" else "${FileUtils.byteCountToDisplaySize(current)} of ${FileUtils.byteCountToDisplaySize(total)}")
                    indicator.isIndeterminate = indeterminate
                    if (!indeterminate) {
                        indicator.fraction = current.toDouble() / total.toDouble()
                    }
                }
            }

            override fun succeeded(message: String) {
                LOG.debug { "Pull from ECR succeeded: $message" }
                notifyInfo(
                    project = project,
                    title = software.aws.toolkits.resources.message("ecr.pull.title"),
                    content = message
                )
            }

            override fun failed(message: String) {
                LOG.debug { "Pull from ECR failed: $message" }
                notifyError(
                    project = project,
                    title = software.aws.toolkits.resources.message("ecr.pull.title"),
                    content = message
                )
            }
        }
    )

    // com.intellij.docker.agent.progress.DockerImageBuilder is probably the correct interface to use but need to figure out how to get an instance of it
    private suspend fun hackyBuildDockerfileUnderIndicator(project: Project, pushRequest: DockerfileEcrPushRequest): String? {
        val dockerConfig = (pushRequest.dockerBuildConfiguration.deploymentConfiguration as DockerDeploymentConfiguration)
        val tag = dockerConfig.separateImageTags.firstOrNull() ?: Instant.now().toEpochMilli().toString()
        // should never be null
        val dockerfilePath = dockerConfig.sourceFilePath ?: throw RuntimeException("Docker run configuration started with invalid source file")
        val config = object : DockerAgentDeploymentConfigImpl(tag, null) {
            override fun getFile() = project.getDockerfileSourceFile(dockerfilePath)

            override fun sourceType() = DockerAgentSourceType.FILE.toString()

            override fun getCustomContextFolder() =
                dockerConfig.contextFolderPath?.let {
                    File(it)
                } ?: super.getCustomContextFolder()
        }.withEnvs(dockerConfig.envVars.toTypedArray())
            .withBuildArgs(dockerConfig.buildArgs.toTypedArray())

        return buildImage(project, agent, config, dockerfilePath, tag)
    }

    private suspend fun buildImage(
        project: Project,
        agent: DockerAgent,
        config: DockerAgentDeploymentConfig,
        dockerfilePath: String,
        tag: String
    ): String? {
        val queue = agent.createImageBuilder().asyncBuildImage(config).await()
        val future = CompletableFuture<String?>()
        object : Task.Backgroundable(project, message("dockerfile.building", dockerfilePath), true) {
            override fun run(indicator: ProgressIndicator) {
                indicator.isIndeterminate = true
                while (true) {
                    val obj = queue.take()
                    if (obj.isEmpty()) {
                        break
                    }
                    val deserialized = ObjectInputStream(obj.inputStream()).readObject()
                    val message = (deserialized as? DockerResponseItem)?.stream
                    if (message != null && message.trim().isNotEmpty()) {
                        indicator.text2 = message
                    }

                    LOG.debug { message ?: deserialized.toString() }
                }

                future.complete(agent.getImages(null).firstOrNull { it.imageRepoTags.contains("$tag:latest") }?.imageId)
            }
        }.queue()

        return future.await()
    }

    private companion object {
        const val NO_TAG_TAG = "<none>:<none>"

        val LOG = getLogger<ToolkitDockerAdapter>()
    }
}

data class LocalImage(
    val imageId: String,
    val tag: String?
)


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

package software.aws.toolkits.jetbrains.core.executables

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.EnvironmentUtil
import com.intellij.util.text.SemVer
import com.intellij.util.text.nullize
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
import software.aws.toolkits.jetbrains.utils.FileInfoCache
import software.aws.toolkits.resources.message
import java.time.Duration

object ExecutableCommon {
    fun getCommandLine(
        path: String,
        executableName: String,
        executableType: ExecutableType<*>? = null,
        clientMetadata: ClientMetadata = ClientMetadata.DEFAULT_METADATA
    ): GeneralCommandLine {
        val sanitizedPath = path.nullize(true)
            ?: throw RuntimeException(message("executableCommon.cli_not_configured", executableName))

        // we have some env-hacks that we want to do, so we're building our own environment using the same util as GeneralCommandLine
        // GeneralCommandLine will apply some more env patches prior to process launch (see startProcess()) so this should be fine
        val effectiveEnvironment = EnvironmentUtil.getEnvironmentMap().toMutableMap()
        // apply hacks
        effectiveEnvironment.apply {
            // GitHub issue: https://github.com/aws/aws-toolkit-jetbrains/issues/645
            // strip out any AWS credentials in the parent environment
            remove("AWS_ACCESS_KEY_ID")
            remove("AWS_SECRET_ACCESS_KEY")
            remove("AWS_SESSION_TOKEN")
            // GitHub issue: https://github.com/aws/aws-toolkit-jetbrains/issues/577
            // coerce the locale to UTF-8 as specified in PEP 538
            // this is needed for Python 3.0 up to Python 3.7.0 (inclusive)
            // we can remove this once our IDE minimum version has a fix for https://youtrack.jetbrains.com/issue/PY-30780
            // currently only seeing this on OS X, so only scoping to that
            if (SystemInfo.isMac) {
                // on other platforms this could be C.UTF-8 or C.UTF8
                this["LC_CTYPE"] = "UTF-8"
                // we're not setting PYTHONIOENCODING because we might break SAM on py2.7
            }
            if (executableType is SamExecutable) {
                this["AWS_TOOLING_USER_AGENT"] = "${clientMetadata.productName}/${clientMetadata.productVersion}"
            }
        }

        return GeneralCommandLine(sanitizedPath)
            .withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.NONE)
            .withEnvironment(effectiveEnvironment)
    }

    /**
     * Compare SemVer version to predefined bounds and throw an exception if out of range.
     * Max version will always be evaluated exclusively, and min version will always be evaluated inclusively
     */
    @JvmStatic
    fun checkSemVerVersion(version: SemVer, min: SemVer, max: SemVer, executableName: String) {
        val versionOutOfRangeMessage = message(
            "executableCommon.version_wrong",
            executableName,
            min,
            max,
            version
        )
        if (version >= max) {
            throw RuntimeException("$versionOutOfRangeMessage ${message("executableCommon.version_too_high")}")
        } else if (version < min) {
            throw RuntimeException("$versionOutOfRangeMessage ${message("executableCommon.version_too_low", executableName)}")
        }
    }

    @JvmStatic
    fun checkSemVerVersionForParallelValidVersions(
        version: SemVer,
        versionRange: List<ExecutableVersionRange>,
        executableName: String
    ) {
        versionRange.forEach {
            if (it.max > version && version >= it.min) {
                return
            }
        }

        val versionRanges = versionRange.joinToString(separator = " / ", transform = { "${it.min}  â‰¤ version < ${it.max}" })
        val versionOutOfRangeMessage = message(
            "executableCommon.version_range_wrong",
            executableName,
            versionRanges,
            version
        )

        throw RuntimeException(versionOutOfRangeMessage)
    }

    /**
     * @return Version of the executable, as whatever type is tracked by the FileInfoCache object
     */
    @JvmStatic
    fun <T> getVersion(path: String, executableVersionCache: FileInfoCache<T>, executableName: String): T {
        val sanitizedPath = path.nullize(true)
            ?: throw RuntimeException(message("executableCommon.cli_not_configured", executableName))
        return executableVersionCache.evaluateBlocking(
            sanitizedPath,
            DEFAULT_TIMEOUT.toMillis().toInt()
        ).result
    }

    private val DEFAULT_TIMEOUT = Duration.ofSeconds(5)
}

data class ExecutableVersionRange(val min: SemVer, val max: SemVer)