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

package software.aws.toolkits.jetbrains.services.sqs

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.sqs.SqsClient
import software.amazon.awssdk.services.sqs.model.QueueAttributeName
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.services.sqs.resources.SqsResources
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.SqsQueueType
import software.aws.toolkits.telemetry.SqsTelemetry
import javax.swing.JComponent

class CreateQueueDialog(
    private val project: Project,
    private val client: SqsClient
) : DialogWrapper(project) {
    private val coroutineScope = projectCoroutineScope(project)
    val view = CreateQueuePanel()

    init {
        title = message("sqs.create.queue.title")
        setOKButtonText(message("sqs.create.queue.create"))
        setOKButtonTooltip(message("sqs.create.queue.tooltip"))

        init()
    }

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

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

    override fun doValidate(): ValidationInfo? = validateFields()

    override fun doCancelAction() {
        SqsTelemetry.createQueue(project, Result.Cancelled)
        super.doCancelAction()
    }

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

        coroutineScope.launch {
            try {
                createQueue()
                withContext(getCoroutineUiContext()) {
                    close(OK_EXIT_CODE)
                }
                project.refreshAwsTree(SqsResources.LIST_QUEUE_URLS)
                SqsTelemetry.createQueue(project, Result.Succeeded, if (view.fifoType.isSelected) SqsQueueType.Fifo else SqsQueueType.Standard)
            } catch (e: Exception) {
                // API only throws QueueNameExistsException if the request includes attributes whose values differ from those of the existing queue.
                LOG.warn(e) { message("sqs.create.queue.failed", queueName()) }
                setErrorText(e.message)
                setOKButtonText(message("sqs.create.queue.create"))
                isOKActionEnabled = true
                SqsTelemetry.createQueue(project, Result.Failed, if (view.fifoType.isSelected) SqsQueueType.Fifo else SqsQueueType.Standard)
            }
        }
    }

    private fun validateFields(): ValidationInfo? {
        if (view.queueName.text.isEmpty()) {
            return ValidationInfo(message("sqs.create.validation.empty.queue.name"), view.queueName)
        }
        if (queueName().length > MAX_LENGTH_OF_QUEUE_NAME) {
            return ValidationInfo(message("sqs.create.validation.long.queue.name", MAX_LENGTH_OF_QUEUE_NAME), view.queueName)
        }
        if (!validateCharacters(view.queueName.text)) {
            return ValidationInfo(message("sqs.create.validation.queue.name.invalid"), view.queueName)
        }

        return null
    }

    private fun validateCharacters(queueName: String): Boolean = queueName.matches("^[a-zA-Z0-9-_]*$".toRegex())

    private fun queueName(): String {
        val name = view.queueName.text.trim()
        return if (view.fifoType.isSelected) {
            name + FIFO_SUFFIX
        } else {
            name
        }
    }

    fun createQueue() {
        client.createQueue {
            it.queueName(queueName())
            if (view.fifoType.isSelected) {
                it.attributes(mutableMapOf(Pair(QueueAttributeName.FIFO_QUEUE, true.toString())))
            }
        }
    }

    private companion object {
        val LOG = getLogger<CreateQueueDialog>()
        const val FIFO_SUFFIX = ".fifo"
    }
}


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

import com.intellij.icons.AllIcons
import com.intellij.ide.HelpTooltip
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.JBColor
import com.intellij.ui.SideBorder
import software.aws.toolkits.resources.message
import java.awt.BorderLayout
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JRadioButton
import javax.swing.JTextField

class CreateQueuePanel {
    lateinit var component: JPanel
        private set
    lateinit var queueName: JTextField
        private set
    lateinit var standardType: JRadioButton
        private set
    lateinit var fifoType: JRadioButton
        private set
    lateinit var queueNameContextHelp: JLabel
        private set
    lateinit var fifoSuffix: JPanel
        private set
    lateinit var textPanel: JPanel
        private set

    init {
        setRadioButton()
        setTooltip()
        setFields()
    }

    private fun setRadioButton() {
        fifoSuffixLabel.isVisible = false
        fifoType.addActionListener {
            fifoSuffixLabel.isVisible = true
        }
        standardType.addActionListener {
            fifoSuffixLabel.isVisible = false
        }
    }

    private fun setTooltip() {
        queueNameContextHelp.icon = AllIcons.General.ContextHelp
        HelpTooltip().apply {
            setDescription(message("sqs.queue.name.tooltip"))
            installOn(queueNameContextHelp)
        }
    }

    private fun setFields() {
        queueName.apply {
            border = IdeBorderFactory.createBorder(SideBorder.TOP or SideBorder.BOTTOM or SideBorder.LEFT)
        }
        fifoSuffix.apply {
            background = queueName.background
            layout = BorderLayout()
            border = IdeBorderFactory.createBorder(SideBorder.TOP or SideBorder.BOTTOM or SideBorder.RIGHT)
            add(fifoSuffixLabel, BorderLayout.WEST)
        }
    }

    companion object {
        val fifoSuffixLabel = JLabel(".fifo").apply {
            foreground = JBColor.GRAY
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.sqs

import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.sqs.SqsClient
import software.amazon.awssdk.services.sqs.model.QueueAttributeName
import software.amazon.awssdk.services.sqs.model.SqsException
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.SqsTelemetry
import javax.swing.JComponent

class EditAttributesDialog(
    private val project: Project,
    private val client: SqsClient,
    private val queue: Queue,
    private val attributes: Map<QueueAttributeName, String>
) : DialogWrapper(project) {
    private val coroutineScope = projectCoroutineScope(project)
    val view = EditAttributesPanel()

    init {
        title = message("sqs.edit.attributes")
        setOKButtonText(message("sqs.edit.attributes.save"))
        populateFields()
        init()
    }

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

    override fun doValidate(): ValidationInfo? {
        val sliderIssue = view.visibilityTimeout.validate() ?: view.deliveryDelay.validate() ?: view.waitTime.validate()
        if (sliderIssue != null) {
            return sliderIssue
        }
        return try {
            view.retentionPeriod.validateContent()
            view.messageSize.validateContent()
            null
        } catch (e: ConfigurationException) {
            ValidationInfo(e.title)
        }
    }

    override fun doCancelAction() {
        SqsTelemetry.editQueueParameters(project, Result.Cancelled, queue.telemetryType())
        super.doCancelAction()
    }

    override fun doOKAction() {
        if (!isOKActionEnabled) {
            return
        }
        isOKActionEnabled = false
        coroutineScope.launch {
            try {
                updateAttributes()
                notifyInfo(
                    project = project,
                    title = message("sqs.service_name"),
                    content = message("sqs.edit.attributes.updated", queue.queueName)
                )
                SqsTelemetry.editQueueParameters(project, Result.Succeeded, queue.telemetryType())
                withContext(getCoroutineUiContext()) {
                    close(OK_EXIT_CODE)
                }
            } catch (e: SqsException) {
                LOG.error(e) { "Updating queue parameters failed" }
                setErrorText(e.message)
                isOKActionEnabled = true
                SqsTelemetry.editQueueParameters(project, Result.Failed, queue.telemetryType())
            }
        }
    }

    private fun populateFields() {
        view.visibilityTimeout.value = attributes[QueueAttributeName.VISIBILITY_TIMEOUT]?.toIntOrNull() ?: MIN_VISIBILITY_TIMEOUT
        view.messageSize.text = attributes[QueueAttributeName.MAXIMUM_MESSAGE_SIZE]
        view.retentionPeriod.text = attributes[QueueAttributeName.MESSAGE_RETENTION_PERIOD]
        view.deliveryDelay.value = attributes[QueueAttributeName.DELAY_SECONDS]?.toIntOrNull() ?: MIN_DELIVERY_DELAY
        view.waitTime.value = attributes[QueueAttributeName.RECEIVE_MESSAGE_WAIT_TIME_SECONDS]?.toIntOrNull() ?: MIN_WAIT_TIME
    }

    internal fun updateAttributes() {
        client.setQueueAttributes {
            it.queueUrl(queue.queueUrl)
            it.attributes(
                mutableMapOf(
                    QueueAttributeName.VISIBILITY_TIMEOUT to view.visibilityTimeout.value.toString(),
                    QueueAttributeName.MAXIMUM_MESSAGE_SIZE to view.messageSize.text,
                    QueueAttributeName.MESSAGE_RETENTION_PERIOD to view.retentionPeriod.text,
                    QueueAttributeName.DELAY_SECONDS to view.deliveryDelay.value.toString(),
                    QueueAttributeName.RECEIVE_MESSAGE_WAIT_TIME_SECONDS to view.waitTime.value.toString()
                )
            )
        }
    }

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


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

import com.intellij.ide.HelpTooltip
import com.intellij.ui.components.fields.IntegerField
import software.aws.toolkits.jetbrains.ui.SliderPanel
import software.aws.toolkits.resources.message
import javax.swing.JPanel

class EditAttributesPanel {
    lateinit var component: JPanel
        private set
    lateinit var visibilityTimeout: SliderPanel
        private set
    lateinit var messageSize: IntegerField
        private set
    lateinit var retentionPeriod: IntegerField
        private set
    lateinit var deliveryDelay: SliderPanel
        private set
    lateinit var waitTime: SliderPanel
        private set

    private fun createUIComponents() {
        visibilityTimeout = SliderPanel(
            MIN_VISIBILITY_TIMEOUT,
            MAX_VISIBILITY_TIMEOUT,
            MIN_VISIBILITY_TIMEOUT,
            MIN_VISIBILITY_TIMEOUT,
            MAX_VISIBILITY_TIMEOUT,
            VISIBILITY_TIMEOUT_TICK,
            VISIBILITY_TIMEOUT_TICK * 5,
            false
        )
        IntegerField("", MIN_VISIBILITY_TIMEOUT, MAX_VISIBILITY_TIMEOUT)
        HelpTooltip().apply {
            setDescription(message("sqs.edit.attributes.visibility_timeout.tooltip"))
            installOn(visibilityTimeout.slider)
            installOn(visibilityTimeout.textField)
        }
        messageSize = IntegerField("", MIN_MESSAGE_SIZE_LIMIT, MAX_MESSAGE_SIZE_LIMIT)
        HelpTooltip().apply {
            setDescription(message("sqs.edit.attributes.message_size.tooltip", MIN_MESSAGE_SIZE_LIMIT, MAX_MESSAGE_SIZE_LIMIT))
            installOn(messageSize)
        }
        retentionPeriod = IntegerField("", MIN_RETENTION_PERIOD, MAX_RETENTION_PERIOD)
        HelpTooltip().apply {
            setDescription(message("sqs.edit.attributes.retention_period.tooltip", MIN_RETENTION_PERIOD, MAX_RETENTION_PERIOD))
            installOn(retentionPeriod)
        }
        deliveryDelay = SliderPanel(
            MIN_DELIVERY_DELAY,
            MAX_DELIVERY_DELAY,
            MIN_DELIVERY_DELAY,
            MIN_DELIVERY_DELAY,
            MAX_DELIVERY_DELAY,
            DELIVERY_DELAY_TICK,
            DELIVERY_DELAY_TICK * 5,
            false
        )
        IntegerField("", MIN_DELIVERY_DELAY, MAX_DELIVERY_DELAY)
        HelpTooltip().apply {
            setDescription(message("sqs.edit.attributes.delivery_delay.tooltip"))
            installOn(deliveryDelay.textField)
            installOn(deliveryDelay.slider)
        }
        waitTime = SliderPanel(MIN_WAIT_TIME, MAX_WAIT_TIME, MIN_WAIT_TIME, MIN_WAIT_TIME, MAX_WAIT_TIME, WAIT_TIME_TICK, WAIT_TIME_TICK * 5, true)
        HelpTooltip().apply {
            setDescription(message("sqs.edit.attributes.wait_time.tooltip"))
            installOn(waitTime.textField)
            installOn(waitTime.slider)
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.sqs

import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.resources.message

// TODO This does not support FIPS

/**
 * @param queueUrl The format for queueUrl is https://sqs.<region>.amazonaws.com/<accountId>/<queueName>
 * queueName cannot contain '/', so it is safe enough to do string manipulation on it
 */
class Queue(val queueUrl: String, val region: AwsRegion) {
    val accountId: String by lazy {
        val id = queueUrl.substringBeforeLast("/").substringAfterLast("/")
        if ((id == queueUrl) || (id.length != 12) || id.isBlank()) {
            throw IllegalArgumentException(message("sqs.url.parse_error"))
        } else {
            id
        }
    }

    val queueName: String by lazy {
        val name = queueUrl.substringAfterLast("/")
        if (name == queueUrl || name.isBlank()) {
            throw IllegalArgumentException(message("sqs.url.parse_error"))
        } else {
            name
        }
    }

    val arn = "arn:${region.partitionId}:sqs:${region.id}:$accountId:$queueName"
    val isFifo: Boolean by lazy { queueName.endsWith(".fifo") }
}


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

package software.aws.toolkits.jetbrains.services.sqs

import com.intellij.openapi.project.Project
import icons.AwsIcons
import software.amazon.awssdk.services.sqs.SqsClient
import software.aws.toolkits.jetbrains.core.credentials.activeRegion
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerResourceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerServiceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.CacheBackedAwsExplorerServiceRootNode
import software.aws.toolkits.jetbrains.services.sqs.resources.SqsResources
import software.aws.toolkits.jetbrains.services.sqs.toolwindow.SqsWindow
import software.aws.toolkits.resources.message

class SqsServiceNode(project: Project, service: AwsExplorerServiceNode) :
    CacheBackedAwsExplorerServiceRootNode<String>(project, service, SqsResources.LIST_QUEUE_URLS) {
    override fun displayName(): String = message("explorer.node.sqs")
    override fun toNode(child: String): AwsExplorerNode<*> = SqsQueueNode(nodeProject, child)
}

class SqsQueueNode(
    project: Project,
    val queueUrl: String
) : AwsExplorerResourceNode<String>(
    project,
    SqsClient.SERVICE_NAME,
    queueUrl,
    AwsIcons.Resources.Sqs.SQS_QUEUE
) {
    val queue = Queue(queueUrl, nodeProject.activeRegion())

    override fun resourceType() = "queue"

    override fun resourceArn(): String = queue.arn

    override fun displayName(): String = queue.queueName

    override fun onDoubleClick() {
        SqsWindow.getInstance(nodeProject).pollMessage(queue)
    }
}


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

package software.aws.toolkits.jetbrains.services.sqs

import org.intellij.lang.annotations.Language
import software.amazon.awssdk.services.sqs.SqsClient
import software.amazon.awssdk.services.sqs.model.QueueAttributeName
import software.amazon.awssdk.services.sqs.model.SqsException
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.telemetry.SqsQueueType

const val MAX_NUMBER_OF_POLLED_MESSAGES = 10
const val MAX_LENGTH_OF_POLLED_MESSAGES = 1024
const val MAX_LENGTH_OF_FIFO_ID = 128
const val MAX_LENGTH_OF_QUEUE_NAME = 80

// Maximum length of queue name is 80, but the maximum will be 75 for FIFO queues due to '.fifo' suffix
const val MAX_LENGTH_OF_FIFO_QUEUE_NAME = 75

// Queue attribute limits
const val MIN_DELIVERY_DELAY = 0
const val MAX_DELIVERY_DELAY = 900
const val DELIVERY_DELAY_TICK = (MAX_DELIVERY_DELAY - MIN_DELIVERY_DELAY) / 30
const val MIN_MESSAGE_SIZE_LIMIT = 1024
const val MAX_MESSAGE_SIZE_LIMIT = 262144
const val MIN_RETENTION_PERIOD = 60
const val MAX_RETENTION_PERIOD = 1209600
const val MIN_VISIBILITY_TIMEOUT = 0
const val MAX_VISIBILITY_TIMEOUT = 43200
const val VISIBILITY_TIMEOUT_TICK = (MAX_VISIBILITY_TIMEOUT - MIN_VISIBILITY_TIMEOUT) / 30
const val MIN_WAIT_TIME = 0
const val MAX_WAIT_TIME = 20
const val WAIT_TIME_TICK = 1

const val sqsPolicyStatementArray = "Statement"

// Extension function to get telemetry type from Queue
fun Queue.telemetryType() = if (isFifo) SqsQueueType.Fifo else SqsQueueType.Standard

/*
 * Get the approximate number of messages from a queue. Returns null when there is a service exception
 * thrown, or the value returned is not an int.
 * @param queueUrl The queue url to retrieve the approximate number of messages from
 */
fun SqsClient.approximateNumberOfMessages(queueUrl: String): Int? = try {
    getQueueAttributes {
        it.queueUrl(queueUrl)
        it.attributeNames(QueueAttributeName.APPROXIMATE_NUMBER_OF_MESSAGES)
    }.attributes().getValue(QueueAttributeName.APPROXIMATE_NUMBER_OF_MESSAGES).toIntOrNull()
} catch (e: SqsException) {
    getLogger<SqsClient>().error(e) { "SqsClient threw an exception getting approximate number of messages" }
    null
}

/**
 * Create a policy statement that allows sending SNS messages to an SQS queue. The Sid
 * matches how the console does sid (so it won't duplicate it), and the overall policy
 * matches how the console does it.
 */
@Language("JSON")
fun createSqsSnsSubscribePolicyStatement(sqsArn: String, snsArn: String): String =
    """
    {
        "Sid": "topic-subscription-$snsArn",
        "Effect": "Allow",
        "Principal": {
            "AWS": "*"
        },
        "Action": "SQS:SendMessage",
        "Resource": "$sqsArn",
        "Condition": {
            "ArnLike": {
                "aws:SourceArn": "$snsArn"
            }
        }
    }
    """

/**
 * When a queue is created with the default parameters, the policy is null when returned with `getQueueAttributes`
 * (even though in the console it shows up properly) so we have to create our own whole policy document if that happens
 */
@Language("JSON")
fun createSqsPolicy(arn: String): String =
    """
    {
        "Version": "2012-10-17",
        "Id": "$arn/SQSDefaultPolicy"
    }
    """


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

package software.aws.toolkits.jetbrains.services.sqs

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.iam.IamClient
import software.amazon.awssdk.services.iam.model.ContextEntry
import software.amazon.awssdk.services.iam.model.ContextKeyTypeEnum
import software.amazon.awssdk.services.iam.model.PolicyEvaluationDecisionType
import software.amazon.awssdk.services.sns.SnsClient
import software.amazon.awssdk.services.sqs.SqsClient
import software.amazon.awssdk.services.sqs.model.QueueAttributeName
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.SqsTelemetry
import javax.swing.JComponent

class SubscribeSnsDialog(
    private val project: Project,
    private val queue: Queue
) : DialogWrapper(project) {
    private val coroutineScope = projectCoroutineScope(project)
    private val snsClient: SnsClient = project.awsClient()
    private val sqsClient: SqsClient = project.awsClient()
    private val iamClient: IamClient = project.awsClient()

    val view = SubscribeSnsPanel(project)

    init {
        title = message("sqs.subscribe.sns")
        setOKButtonText(message("sqs.subscribe.sns.subscribe"))

        init()
    }

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

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

    override fun doValidate(): ValidationInfo? {
        if (topicSelected().isEmpty()) {
            return ValidationInfo(message("sqs.subscribe.sns.validation.empty_topic"), view.topicSelector)
        }
        return null
    }

    override fun doCancelAction() {
        SqsTelemetry.subscribeSns(project, Result.Cancelled, queue.telemetryType())
        super.doCancelAction()
    }

    override fun doOKAction() {
        if (!isOKActionEnabled) {
            return
        }
        val topicArn = topicSelected()
        setOKButtonText(message("sqs.subscribe.sns.in_progress"))
        isOKActionEnabled = false

        coroutineScope.launch {
            try {
                val policy = sqsClient.getQueueAttributes {
                    it.queueUrl(queue.queueUrl)
                    it.attributeNames(QueueAttributeName.POLICY)
                }.attributes()[QueueAttributeName.POLICY]

                if (needToEditPolicy(policy)) {
                    val continueAdding = withContext(getCoroutineUiContext()) {
                        ConfirmQueuePolicyDialog(project, sqsClient, queue, topicArn, policy, view.component).showAndGet()
                    }
                    if (!continueAdding) {
                        setOKButtonText(message("sqs.subscribe.sns.subscribe"))
                        isOKActionEnabled = true
                        return@launch
                    }
                }
                subscribe(topicArn)
                withContext(getCoroutineUiContext()) {
                    close(OK_EXIT_CODE)
                }
                notifyInfo(message("sqs.service_name"), message("sqs.subscribe.sns.success", topicSelected()), project)
                SqsTelemetry.subscribeSns(project, Result.Succeeded, queue.telemetryType())
            } catch (e: Exception) {
                LOG.warn(e) { message("sqs.subscribe.sns.failed", queue.queueName, topicArn) }
                setErrorText(e.message)
                setOKButtonText(message("sqs.subscribe.sns.subscribe"))
                isOKActionEnabled = true
                SqsTelemetry.subscribeSns(project, Result.Failed, queue.telemetryType())
            }
        }
    }

    internal fun subscribe(arn: String) {
        snsClient.subscribe {
            it.topicArn(arn)
            it.protocol(PROTOCOL)
            it.endpoint(queue.arn)
        }
    }

    private fun topicSelected(): String = view.topicSelector.selected()?.topicArn() ?: ""

    private fun needToEditPolicy(existingPolicy: String?): Boolean {
        existingPolicy ?: return true

        val allowed = iamClient.simulateCustomPolicy {
            it.contextEntries(
                ContextEntry.builder()
                    .contextKeyType(ContextKeyTypeEnum.STRING)
                    .contextKeyName("aws:SourceArn")
                    .contextKeyValues(topicSelected())
                    .build()
            )
            it.actionNames("sqs:SendMessage")
            it.resourceArns(queue.arn)
            it.policyInputList(existingPolicy)
        }.evaluationResults().first()

        return allowed.evalDecision() != PolicyEvaluationDecisionType.ALLOWED
    }

    private companion object {
        val LOG = getLogger<SubscribeSnsDialog>()
        const val PROTOCOL = "sqs"
    }
}


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

import com.intellij.icons.AllIcons
import com.intellij.ide.HelpTooltip
import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleListCellRenderer
import software.amazon.awssdk.services.sns.model.Topic
import software.aws.toolkits.jetbrains.services.sns.resources.SnsResources
import software.aws.toolkits.jetbrains.services.sns.resources.getName
import software.aws.toolkits.jetbrains.ui.ResourceSelector
import software.aws.toolkits.resources.message
import javax.swing.JLabel
import javax.swing.JPanel

class SubscribeSnsPanel(private val project: Project) {
    lateinit var component: JPanel
        private set
    lateinit var topicSelector: ResourceSelector<Topic>
        private set
    lateinit var selectContextHelp: JLabel
        private set

    init {
        selectContextHelp.icon = AllIcons.General.ContextHelp
        HelpTooltip().apply {
            setDescription(message("sqs.subscribe.sns.select.tooltip"))
            installOn(selectContextHelp)
        }
    }

    private fun createUIComponents() {
        topicSelector = ResourceSelector.builder()
            .resource(SnsResources.LIST_TOPICS)
            .customRenderer(SimpleListCellRenderer.create("") { it.getName() })
            .awsConnection(project)
            .build()
    }
}


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.sqs.ConfigureLambdaDialog
import software.aws.toolkits.jetbrains.services.sqs.SqsQueueNode
import software.aws.toolkits.resources.message

class ConfigureLambdaAction : SingleResourceNodeAction<SqsQueueNode>(message("sqs.configure.lambda")), DumbAware {
    override fun actionPerformed(selected: SqsQueueNode, e: AnActionEvent) {
        ConfigureLambdaDialog(selected.nodeProject, selected.queue).show()
    }
}


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

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

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.ui.table.TableView
import software.amazon.awssdk.services.sqs.model.Message
import software.aws.toolkits.resources.message
import java.awt.datatransfer.StringSelection

class CopyMessageAction(private val table: TableView<Message>) : DumbAwareAction(message("sqs.copy.message", 1), null, AllIcons.Actions.Copy) {
    override fun update(e: AnActionEvent) {
        e.presentation.isEnabled = table.selectedObjects.size > 0
        e.presentation.text = message("sqs.copy.message", table.selectedObjects.size)
    }

    override fun actionPerformed(e: AnActionEvent) {
        // get an immutable view of the selected items
        val messages = table.selectedObjects.toList()
        if (messages.isEmpty()) {
            return
        }
        CopyPasteManager.getInstance().setContents(StringSelection(messages.joinToString("\n") { it.body() }))
    }
}


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.project.DumbAwareAction
import icons.AwsIcons
import software.amazon.awssdk.services.sqs.SqsClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.services.sqs.CreateQueueDialog
import software.aws.toolkits.resources.message

class CreateQueueAction : DumbAwareAction(
    message("sqs.create.queue"),
    null,
    AwsIcons.Resources.Sqs.SQS_QUEUE
) {
    override fun actionPerformed(e: AnActionEvent) {
        val project = e.getRequiredData(PlatformDataKeys.PROJECT)
        val client: SqsClient = project.awsClient()
        CreateQueueDialog(project, client).show()
    }
}


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

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

import com.intellij.openapi.application.ApplicationManager
import software.amazon.awssdk.services.sqs.SqsClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.explorer.actions.DeleteResourceAction
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.services.sqs.SqsQueueNode
import software.aws.toolkits.jetbrains.services.sqs.resources.SqsResources
import software.aws.toolkits.jetbrains.services.sqs.toolwindow.SqsWindow
import software.aws.toolkits.resources.message

class DeleteQueueAction : DeleteResourceAction<SqsQueueNode>(message("sqs.delete.queue.action")) {
    override fun performDelete(selected: SqsQueueNode) {
        val project = selected.nodeProject
        val client = project.awsClient<SqsClient>()
        ApplicationManager.getApplication().invokeAndWait {
            SqsWindow.getInstance(project).closeQueue(selected.queueUrl)
        }
        client.deleteQueue { it.queueUrl(selected.queueUrl) }
        project.refreshAwsTree(SqsResources.LIST_QUEUE_URLS)
    }

    override val comment: String = message("resource.delete.warning_text", "queue")
}


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.progress.PerformInBackgroundOption
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.DumbAware
import software.amazon.awssdk.services.sqs.SqsClient
import software.amazon.awssdk.services.sqs.model.QueueAttributeName
import software.amazon.awssdk.services.sqs.model.SqsException
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.sqs.EditAttributesDialog
import software.aws.toolkits.jetbrains.services.sqs.SqsQueueNode
import software.aws.toolkits.jetbrains.services.sqs.telemetryType
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.SqsTelemetry

class EditAttributesAction : SingleResourceNodeAction<SqsQueueNode>(message("sqs.edit.attributes.action")), DumbAware {
    override fun actionPerformed(selected: SqsQueueNode, e: AnActionEvent) {
        ProgressManager.getInstance().run(
            object : Task.Backgroundable(
                selected.project,
                message("sqs.edit.attributes.retrieving_from_service"),
                false,
                PerformInBackgroundOption.ALWAYS_BACKGROUND
            ) {
                override fun run(indicator: ProgressIndicator) {
                    val client = selected.nodeProject.awsClient<SqsClient>()
                    val queue = selected.queue
                    val attributes = try {
                        client.getQueueAttributes {
                            it.queueUrl(queue.queueUrl)
                            it.attributeNames(QueueAttributeName.ALL)
                        }.attributes()
                    } catch (e: SqsException) {
                        LOG.error(e) { "Getting queue parameters failed" }
                        notifyError(
                            project = project,
                            title = message("sqs.service_name"),
                            content = message("sqs.edit.attributes.failed", queue.queueName)
                        )
                        SqsTelemetry.editQueueParameters(project, Result.Failed, queue.telemetryType())
                        return
                    }
                    runInEdt {
                        EditAttributesDialog(selected.nodeProject, client, queue, attributes).show()
                    }
                }
            }
        )
    }

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


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.sqs.SqsQueueNode
import software.aws.toolkits.jetbrains.services.sqs.toolwindow.SqsWindow
import software.aws.toolkits.resources.message

class PollMessageAction : SingleResourceNodeAction<SqsQueueNode>(message("sqs.poll.message")), DumbAware {
    override fun actionPerformed(selected: SqsQueueNode, e: AnActionEvent) {
        SqsWindow.getInstance(selected.nodeProject).pollMessage(selected.queue)
    }
}


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import kotlinx.coroutines.runBlocking
import software.amazon.awssdk.services.sqs.SqsClient
import software.amazon.awssdk.services.sqs.model.PurgeQueueInProgressException
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.services.sqs.Queue
import software.aws.toolkits.jetbrains.services.sqs.approximateNumberOfMessages
import software.aws.toolkits.jetbrains.services.sqs.telemetryType
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.SqsTelemetry

class PurgeQueueAction(
    private val project: Project,
    private val client: SqsClient,
    private val queue: Queue
) : DumbAwareAction(message("sqs.purge_queue.action")) {
    private val edtContext = getCoroutineUiContext()

    override fun actionPerformed(e: AnActionEvent) {
        ProgressManager.getInstance().run(
            object : Task.Backgroundable(
                project,
                message("sqs.purge_queue"),
                false,
                ALWAYS_BACKGROUND
            ) {
                override fun run(indicator: ProgressIndicator) {
                    val numMessages = client.approximateNumberOfMessages(queue.queueUrl) ?: 0
                    val response = runBlocking(edtContext) {
                        Messages.showOkCancelDialog(
                            project,
                            message("sqs.purge_queue.confirm", queue.queueName, numMessages),
                            message("sqs.purge_queue.confirm.title"),
                            Messages.getYesButton(),
                            Messages.getNoButton(),
                            Messages.getWarningIcon()
                        )
                    }
                    if (response != Messages.YES) {
                        SqsTelemetry.purgeQueue(project, Result.Cancelled, queue.telemetryType())
                        return
                    }
                    try {
                        client.purgeQueue { it.queueUrl(queue.queueUrl) }
                        LOG.info { "Started purging ${queue.queueUrl}" }
                        notifyInfo(
                            project = project,
                            title = message("aws.notification.title"),
                            content = message("sqs.purge_queue.succeeded", queue.queueUrl)
                        )
                        SqsTelemetry.purgeQueue(project, Result.Succeeded, queue.telemetryType())
                    } catch (e: PurgeQueueInProgressException) {
                        LOG.warn { "${queue.queueUrl} already has a query purge in progress" }
                        notifyError(
                            project = project,
                            content = message("sqs.purge_queue.failed.60_seconds", queue.queueUrl)
                        )
                        SqsTelemetry.purgeQueue(project, Result.Succeeded, queue.telemetryType())
                    } catch (e: Exception) {
                        LOG.error(e) { "Exception thrown while trying to purge query ${queue.queueUrl}" }
                        notifyError(
                            project = project,
                            content = message("sqs.purge_queue.failed", queue.queueUrl)
                        )
                        SqsTelemetry.purgeQueue(project, Result.Failed, queue.telemetryType())
                    }
                }
            }
        )
    }

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


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import software.amazon.awssdk.services.sqs.SqsClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.sqs.SqsQueueNode
import software.aws.toolkits.resources.message

class PurgeQueueNodeAction : SingleResourceNodeAction<SqsQueueNode>(message("sqs.purge_queue.action")), DumbAware {
    override fun actionPerformed(selected: SqsQueueNode, e: AnActionEvent) {
        val project = selected.nodeProject
        val client: SqsClient = project.awsClient()
        PurgeQueueAction(project, client, selected.queue).actionPerformed(e)
    }
}


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.sqs.SqsQueueNode
import software.aws.toolkits.jetbrains.services.sqs.toolwindow.SqsWindow
import software.aws.toolkits.resources.message

class SendMessageAction : SingleResourceNodeAction<SqsQueueNode>(message("sqs.send.message")), DumbAware {
    override fun actionPerformed(selected: SqsQueueNode, e: AnActionEvent) {
        SqsWindow.getInstance(selected.nodeProject).sendMessage(selected.queue)
    }
}


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.sqs.SqsQueueNode
import software.aws.toolkits.jetbrains.services.sqs.SubscribeSnsDialog
import software.aws.toolkits.resources.message

class SubscribeSnsAction : SingleResourceNodeAction<SqsQueueNode>(message("sqs.subscribe.sns")), DumbAware {
    override fun update(selected: SqsQueueNode, e: AnActionEvent) {
        // TODO: Amazon SNS isn't currently compatible with FIFO queues.
        e.presentation.isVisible = !selected.queue.isFifo
    }

    override fun actionPerformed(selected: SqsQueueNode, e: AnActionEvent) {
        SubscribeSnsDialog(selected.nodeProject, selected.queue).show()
    }
}


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

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

import software.amazon.awssdk.services.sqs.SqsClient
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.Resource

object SqsResources {
    val LIST_QUEUE_URLS: Resource.Cached<List<String>> = ClientBackedCachedResource(SqsClient::class, "sqs.list_queues") {
        listQueuesPaginator().queueUrls().toList()
    }
}


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

import com.intellij.icons.AllIcons
import com.intellij.ide.HelpTooltip
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.components.JBTextField
import software.aws.toolkits.jetbrains.services.sqs.MAX_LENGTH_OF_FIFO_ID
import software.aws.toolkits.resources.message
import java.util.UUID
import javax.swing.JLabel
import javax.swing.JPanel

class FifoPanel {
    lateinit var component: JPanel
        private set
    lateinit var deduplicationId: JBTextField
        private set
    lateinit var groupId: JBTextField
        private set
    lateinit var deduplicationContextHelp: JLabel
        private set
    lateinit var groupContextHelp: JLabel
        private set

    init {
        deduplicationContextHelp.icon = AllIcons.General.ContextHelp
        HelpTooltip().apply {
            setDescription(message("sqs.message.deduplication_id.tooltip"))
            installOn(deduplicationContextHelp)
        }
        groupContextHelp.icon = AllIcons.General.ContextHelp
        HelpTooltip().apply {
            setDescription(message("sqs.message.group_id.tooltip"))
            installOn(groupContextHelp)
        }
        deduplicationId.emptyText.text = message("sqs.required.empty.text")
        groupId.emptyText.text = message("sqs.required.empty.text")
        clear()
    }

    fun validateFields(): List<ValidationInfo> = listOfNotNull(
        validateDedupeId(),
        validateGroupId()
    )

    fun clear(isSend: Boolean = false) {
        deduplicationId.text = UUID.randomUUID().toString()
        if (!isSend) {
            groupId.text = ""
        }
    }

    private fun validateDedupeId(): ValidationInfo? = when {
        deduplicationId.text.length > MAX_LENGTH_OF_FIFO_ID -> {
            ValidationInfo(message("sqs.message.validation.long.id"), deduplicationId)
        }
        deduplicationId.text.isBlank() -> {
            ValidationInfo(message("sqs.message.validation.empty.deduplication_id"), deduplicationId)
        }
        else -> {
            null
        }
    }

    private fun validateGroupId(): ValidationInfo? = when {
        groupId.text.length > MAX_LENGTH_OF_FIFO_ID -> {
            ValidationInfo(message("sqs.message.validation.long.id"), groupId)
        }
        groupId.text.isBlank() -> {
            ValidationInfo(message("sqs.message.validation.empty.group_id"), groupId)
        }
        else -> {
            null
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.sqs.toolwindow

import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.TableSpeedSearch
import com.intellij.ui.table.TableView
import com.intellij.util.ui.ListTableModel
import software.amazon.awssdk.services.sqs.model.Message
import software.aws.toolkits.resources.message
import javax.swing.JComponent
import javax.swing.JTable

class MessagesTable {
    val component: JComponent
    val table: TableView<Message>
    val tableModel = ListTableModel<Message>(
        arrayOf(
            MessageIdColumn(),
            MessageBodyColumn(),
            MessageSenderIdColumn(),
            MessageDateColumn()
        ),
        mutableListOf<Message>()
    )

    init {
        table = TableView(tableModel).apply {
            autoscrolls = true
            // Disable the header so the user cannot sort or resize columns
            tableHeader.isEnabled = false
            autoResizeMode = JTable.AUTO_RESIZE_LAST_COLUMN
            emptyText.text = message("sqs.message_table_initial_text", message("sqs.poll.message"))
        }

        TableSpeedSearch(table)
        component = ScrollPaneFactory.createScrollPane(table)
    }

    fun reset() {
        while (tableModel.rowCount != 0) {
            tableModel.removeRow(0)
        }
    }

    fun setBusy(busy: Boolean) {
        table.setPaintBusy(busy)
        if (busy) {
            table.emptyText.text = message("loading_resource.loading")
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.sqs.toolwindow

import com.intellij.icons.AllIcons
import com.intellij.ide.HelpTooltip
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.CommonShortcuts
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.Separator
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.ui.PopupHandler
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.sqs.SqsClient
import software.amazon.awssdk.services.sqs.model.Message
import software.amazon.awssdk.services.sqs.model.QueueAttributeName
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.sqs.MAX_NUMBER_OF_POLLED_MESSAGES
import software.aws.toolkits.jetbrains.services.sqs.Queue
import software.aws.toolkits.jetbrains.services.sqs.actions.CopyMessageAction
import software.aws.toolkits.jetbrains.services.sqs.actions.PurgeQueueAction
import software.aws.toolkits.jetbrains.services.sqs.approximateNumberOfMessages
import software.aws.toolkits.resources.message
import javax.swing.JButton
import javax.swing.JLabel
import javax.swing.JPanel

class PollMessagePane(
    private val project: Project,
    private val client: SqsClient,
    private val queue: Queue
) {
    private val coroutineScope = projectCoroutineScope(project)
    private val bgContext = getCoroutineBgContext()

    lateinit var component: JPanel
        private set
    lateinit var messagesAvailableLabel: JLabel
        private set
    lateinit var tablePanel: SimpleToolWindowPanel
        private set
    lateinit var pollButton: JButton
        private set
    lateinit var pollHelpLabel: JLabel
        private set

    val messagesTable = MessagesTable()

    private fun createUIComponents() {
        tablePanel = SimpleToolWindowPanel(false, true)
    }

    init {
        tablePanel.setContent(messagesTable.component)
        pollButton.addActionListener {
            poll()
        }
        pollHelpLabel.icon = AllIcons.General.ContextHelp
        HelpTooltip().apply {
            setDescription(message("sqs.poll.warning.text"))
            installOn(pollHelpLabel)
        }
        coroutineScope.launch {
            getAvailableMessages()
        }
        addActionsToTable()
    }

    suspend fun requestMessages() {
        try {
            withContext(bgContext) {
                val polledMessages: List<Message> = client.receiveMessage {
                    it.queueUrl(queue.queueUrl)
                    it.attributeNames(QueueAttributeName.ALL)
                    it.maxNumberOfMessages(MAX_NUMBER_OF_POLLED_MESSAGES)
                    // Make poll a real peek by setting the visibility timout to 0, so messages can be
                    // requested by other consumers of the queue immediately
                    it.visibilityTimeout(0)
                }.messages().distinctBy { it.messageId() }

                messagesTable.tableModel.addRows(polledMessages)

                messagesTable.table.emptyText.text = message("sqs.message.no_messages")
            }
        } catch (e: Exception) {
            messagesTable.table.emptyText.text = message("sqs.failed_to_poll_messages")
        } finally {
            messagesTable.setBusy(busy = false)
        }
    }

    suspend fun getAvailableMessages() {
        try {
            withContext(bgContext) {
                val numMessages = client.approximateNumberOfMessages(queue.queueUrl)
                messagesAvailableLabel.text = message("sqs.messages.available.text", numMessages ?: 0)
            }
        } catch (e: Exception) {
            messagesAvailableLabel.text = message("sqs.failed_to_load_total")
        }
    }

    private fun addActionsToTable() {
        val actionGroup = DefaultActionGroup().apply {
            add(CopyMessageAction(messagesTable.table).apply { registerCustomShortcutSet(CommonShortcuts.getCopy(), component) })
            add(Separator.create())
            add(PurgeQueueAction(project, client, queue))
        }
        PopupHandler.installPopupHandler(
            messagesTable.table,
            actionGroup,
            ActionPlaces.EDITOR_POPUP,
            ActionManager.getInstance()
        )
    }

    private fun poll() = coroutineScope.launch {
        // TODO: Add debounce
        messagesTable.setBusy(busy = true)
        messagesTable.reset()
        requestMessages()
        getAvailableMessages()
    }
}


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

package software.aws.toolkits.jetbrains.services.sqs.toolwindow

import com.intellij.util.ui.ColumnInfo
import software.amazon.awssdk.services.sqs.model.Message
import software.amazon.awssdk.services.sqs.model.MessageSystemAttributeName
import software.aws.toolkits.jetbrains.services.sqs.MAX_LENGTH_OF_POLLED_MESSAGES
import software.aws.toolkits.jetbrains.utils.ui.ResizingDateColumnRenderer
import software.aws.toolkits.jetbrains.utils.ui.ResizingTextColumnRenderer
import software.aws.toolkits.jetbrains.utils.ui.WrappingCellRenderer
import software.aws.toolkits.resources.message
import javax.swing.table.TableCellRenderer

class MessageIdColumn : ColumnInfo<Message, String>(message("sqs.message.message_id")) {
    private val renderer = ResizingTextColumnRenderer()
    override fun valueOf(item: Message?): String? = item?.messageId()
    override fun isCellEditable(item: Message?): Boolean = false
    override fun getRenderer(item: Message?): TableCellRenderer? = renderer
}

class MessageBodyColumn : ColumnInfo<Message, String>(message("sqs.message.message_body")) {
    // Truncated the message body to show up to 1024 characters, as it can be up to 256KB in size. Cannot limit the retrieved message size through API.
    private val renderer = WrappingCellRenderer(wrapOnSelection = true, wrapOnToggle = false, truncateAfterChars = MAX_LENGTH_OF_POLLED_MESSAGES)

    override fun valueOf(item: Message?): String? = item?.body()
    override fun isCellEditable(item: Message?): Boolean = false
    override fun getRenderer(item: Message?): TableCellRenderer? = renderer
}

class MessageSenderIdColumn : ColumnInfo<Message, String>(message("sqs.message.sender_id")) {
    private val renderer = ResizingTextColumnRenderer()
    override fun valueOf(item: Message?): String? = item?.attributes()?.getValue(MessageSystemAttributeName.SENDER_ID)
    override fun isCellEditable(item: Message?): Boolean = false
    override fun getRenderer(item: Message?): TableCellRenderer? = renderer
}

class MessageDateColumn : ColumnInfo<Message, String>(message("sqs.message.timestamp")) {
    private val renderer = ResizingDateColumnRenderer(showSeconds = true)
    override fun valueOf(item: Message?): String? = item?.attributes()?.getValue(MessageSystemAttributeName.SENT_TIMESTAMP)
    override fun isCellEditable(item: Message?): Boolean = false
    override fun getRenderer(item: Message?): TableCellRenderer? = renderer
}


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

package software.aws.toolkits.jetbrains.services.sqs.toolwindow

import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComponentValidator
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.components.JBTextArea
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.sqs.SqsClient
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.services.sqs.Queue
import software.aws.toolkits.jetbrains.services.sqs.telemetryType
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.SqsTelemetry
import javax.swing.JButton
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JScrollPane

class SendMessagePane(
    private val project: Project,
    private val client: SqsClient,
    private val queue: Queue,
    disposable: Disposable
) {
    private val coroutineScope = disposableCoroutineScope(disposable)

    lateinit var component: JPanel
        private set
    lateinit var inputText: JBTextArea
        private set
    lateinit var sendButton: JButton
        private set
    lateinit var clearButton: JButton
        private set
    lateinit var messageSentLabel: JLabel
        private set
    lateinit var fifoFields: FifoPanel
        private set
    lateinit var scrollPane: JScrollPane
        private set
    private val edt = getCoroutineUiContext()

    init {
        loadComponents()
        setButtons()
        setFields()
    }

    private fun loadComponents() {
        if (!queue.isFifo) {
            fifoFields.component.isVisible = false
        }
        messageSentLabel.isVisible = false
    }

    private fun setButtons() {
        sendButton.addActionListener {
            coroutineScope.launch { sendMessage() }
        }
        clearButton.addActionListener {
            runBlocking { clear() }
            messageSentLabel.isVisible = false
        }
    }

    private fun setFields() {
        scrollPane.apply {
            border = IdeBorderFactory.createBorder()
        }
        inputText.apply {
            emptyText.text = message("sqs.send.message.body.empty.text")
        }
    }

    suspend fun sendMessage() {
        if (!validateFields()) {
            return
        }
        try {
            withContext(getCoroutineBgContext()) {
                val messageId = client.sendMessage {
                    it.queueUrl(queue.queueUrl)
                    it.messageBody(inputText.text)
                    if (queue.isFifo) {
                        it.messageDeduplicationId(fifoFields.deduplicationId.text)
                        it.messageGroupId(fifoFields.groupId.text)
                    }
                }.messageId()
                messageSentLabel.text = message("sqs.send.message.success", messageId)
            }
            clear(isSend = true)
            SqsTelemetry.sendMessage(project, Result.Succeeded, queue.telemetryType())
        } catch (e: Exception) {
            messageSentLabel.text = message("sqs.failed_to_send_message")
            SqsTelemetry.sendMessage(project, Result.Failed, queue.telemetryType())
            clear(isSend = true)
        } finally {
            messageSentLabel.isVisible = true
        }
    }

    suspend fun validateFields(): Boolean {
        val validationIssues = mutableListOf<ValidationInfo>().apply {
            if (inputText.text.isEmpty()) {
                add(ValidationInfo(message("sqs.message.validation.empty.message.body"), inputText))
            }
            if (queue.isFifo) {
                addAll(fifoFields.validateFields())
            }
        }
        return if (validationIssues.isEmpty()) {
            true
        } else {
            withContext(getCoroutineUiContext()) {
                validationIssues.forEach { validationIssue ->
                    val errorComponent = validationIssue.component ?: inputText
                    ComponentValidator
                        .createPopupBuilder(validationIssue, null)
                        .setCancelOnClickOutside(true)
                        .createPopup()
                        .showUnderneathOf(errorComponent)
                }
            }
            false
        }
    }

    suspend fun clear(isSend: Boolean = false) = withContext(edt) {
        inputText.text = ""
        if (queue.isFifo) {
            fifoFields.clear(isSend)
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.sqs.toolwindow

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.ui.content.Content
import com.intellij.ui.content.ContentManager
import software.amazon.awssdk.services.sqs.SqsClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.services.sqs.Queue
import software.aws.toolkits.jetbrains.services.sqs.telemetryType
import software.aws.toolkits.telemetry.SqsTelemetry

class SqsWindow(private val project: Project) {
    private val client: SqsClient = project.awsClient()

    fun pollMessage(queue: Queue) {
        showQueue(queue).pollMessage()
    }

    fun sendMessage(queue: Queue) {
        showQueue(queue).sendMessage()
    }

    private fun showQueue(queue: Queue): SqsWindowUi {
        ApplicationManager.getApplication().assertIsDispatchThread()

        SqsTelemetry.openQueue(project, queue.telemetryType())

        val toolWindow = SqsWindowFactory.getToolWindow(project)
        val contentManager = toolWindow.contentManager

        val sqsViewContent = findQueue(queue.queueUrl) ?: createSqsView(contentManager, queue)

        toolWindow.show()
        contentManager.setSelectedContent(sqsViewContent, true)

        return sqsViewContent.component as SqsWindowUi
    }

    fun findQueue(queueUrl: String): Content? {
        ApplicationManager.getApplication().assertIsDispatchThread()

        val toolWindow = SqsWindowFactory.getToolWindow(project)
        val contentManager = toolWindow.contentManager

        return contentManager.contents.find {
            val component = it.component
            component is SqsWindowUi && component.queue.queueUrl == queueUrl
        }
    }

    private fun createSqsView(contentManager: ContentManager, queue: Queue): Content {
        val sqsView = SqsWindowUi(project, client, queue)
        val sqsViewContent = contentManager.factory.createContent(sqsView.component, queue.queueName, false)
        sqsViewContent.isCloseable = true
        sqsViewContent.isPinnable = true
        contentManager.addContent(sqsViewContent)

        return sqsViewContent
    }

    fun closeQueue(queueUrl: String) {
        ApplicationManager.getApplication().assertIsDispatchThread()

        findQueue(queueUrl)?.let {
            SqsWindowFactory.getToolWindow(project).contentManager.removeContent(it, true)
        }
    }

    companion object {
        fun getInstance(project: Project): SqsWindow = project.service()
    }
}


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

package software.aws.toolkits.jetbrains.services.sqs.toolwindow

import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.openapi.wm.ToolWindowManager
import software.aws.toolkits.resources.message

class SqsWindowFactory : ToolWindowFactory, DumbAware {
    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        // No content, it will be shown when queues are viewed
        runInEdt {
            toolWindow.installWatcher(toolWindow.contentManager)
        }
    }

    override fun init(toolWindow: ToolWindow) {
        toolWindow.stripeTitle = message("sqs.toolwindow")
    }

    override fun shouldBeAvailable(project: Project): Boolean = false

    companion object {
        private const val TOOL_WINDOW_ID = "aws.sqs"

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


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

package software.aws.toolkits.jetbrains.services.sqs.toolwindow

import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.ui.components.JBTabbedPane
import com.intellij.util.ui.JBUI
import software.amazon.awssdk.services.sqs.SqsClient
import software.aws.toolkits.jetbrains.services.sqs.Queue
import software.aws.toolkits.resources.message

class SqsWindowUi(private val project: Project, private val client: SqsClient, val queue: Queue) : SimpleToolWindowPanel(false, true), Disposable {
    private val mainPanel = JBTabbedPane().apply {
        tabComponentInsets = JBUI.emptyInsets()
        border = JBUI.Borders.empty()
        add(message("sqs.queue.polled.messages"), PollMessagePane(project, client, queue).component)
        add(message("sqs.send.message"), SendMessagePane(project, client, queue, this@SqsWindowUi).component)
    }

    init {
        setContent(mainPanel)
    }

    fun pollMessage() {
        mainPanel.selectedIndex = POLL_MESSAGE_PANE
    }

    fun sendMessage() {
        mainPanel.selectedIndex = SEND_MESSAGE_PANE
    }

    override fun dispose() {
    }

    companion object {
        const val POLL_MESSAGE_PANE = 0
        const val SEND_MESSAGE_PANE = 1
    }
}


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

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

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.util.ExecUtil
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.io.Decompressor
import com.intellij.util.system.CpuArch
import org.jetbrains.annotations.VisibleForTesting
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.getTextFromUrl
import software.aws.toolkits.jetbrains.core.saveFileFromUrl
import software.aws.toolkits.jetbrains.core.tools.BaseToolType
import software.aws.toolkits.jetbrains.core.tools.DocumentedToolType
import software.aws.toolkits.jetbrains.core.tools.FourPartVersion
import software.aws.toolkits.jetbrains.core.tools.ManagedToolType
import software.aws.toolkits.jetbrains.core.tools.Tool
import software.aws.toolkits.jetbrains.core.tools.ToolType
import software.aws.toolkits.jetbrains.core.tools.VersionRange
import software.aws.toolkits.jetbrains.core.tools.until
import software.aws.toolkits.jetbrains.utils.checkSuccess
import software.aws.toolkits.telemetry.ToolId
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import kotlin.streams.asSequence

object SsmPlugin : ManagedToolType<FourPartVersion>, DocumentedToolType<FourPartVersion>, BaseToolType<FourPartVersion>() {
    private val hasDpkg by lazy { hasCommand("dpkg-deb") }
    private val hasRpm2Cpio by lazy { hasCommand("rpm2cpio") }

    override val telemetryId: ToolId = ToolId.SessionManagerPlugin
    override val displayName: String = "AWS Session Manager Plugin"

    override fun supportedVersions(): VersionRange<FourPartVersion> = FourPartVersion(1, 2, 0, 0) until FourPartVersion(2, 0, 0, 0)

    override fun downloadVersion(version: FourPartVersion, destinationDir: Path, indicator: ProgressIndicator?): Path {
        val downloadUrl = when {
            SystemInfo.isWindows -> windowsUrl(version)
            SystemInfo.isMac -> macUrl(version)
            SystemInfo.isLinux && hasDpkg && CpuArch.isArm64() -> ubuntuArm64Url(version)
            SystemInfo.isLinux && hasDpkg && CpuArch.isIntel64() -> ubuntuI64Url(version)
            SystemInfo.isLinux && hasRpm2Cpio && CpuArch.isArm64() -> linuxArm64Url(version)
            SystemInfo.isLinux && hasRpm2Cpio && CpuArch.isIntel64() -> linuxI64Url(version)
            else -> throw IllegalStateException("Failed to find compatible SSM plugin: SystemInfo=${SystemInfo.OS_NAME}, Arch=${SystemInfo.OS_ARCH}")
        }

        val fileName = downloadUrl.substringAfterLast("/")
        val destination = destinationDir.resolve(fileName)

        saveFileFromUrl(downloadUrl, destination, indicator)

        return destination
    }

    override fun installVersion(downloadArtifact: Path, destinationDir: Path, indicator: ProgressIndicator?) {
        when (val extension = downloadArtifact.fileName.toString().substringAfterLast(".")) {
            "zip" -> extractZip(downloadArtifact, destinationDir)
            "rpm" -> runInstall(
                GeneralCommandLine("sh", "-c", """rpm2cpio "$downloadArtifact" | (mkdir -p "$destinationDir" && cd "$destinationDir" && cpio -idmv)""")
            )
            "deb" -> runInstall(GeneralCommandLine("sh", "-c", """mkdir -p "$destinationDir" && dpkg-deb -x "$downloadArtifact" "$destinationDir""""))
            else -> throw IllegalStateException("Unknown extension $extension")
        }
    }

    override fun determineLatestVersion(): FourPartVersion = parseVersion(getTextFromUrl(VERSION_FILE))

    override fun parseVersion(output: String): FourPartVersion = FourPartVersion.parse(output)

    override fun toTool(installDir: Path): Tool<ToolType<FourPartVersion>> {
        val executableName = if (SystemInfo.isWindows) {
            "session-manager-plugin.exe"
        } else {
            "session-manager-plugin"
        }

        return Files.walk(installDir).use { files ->
            files.asSequence().filter { it.fileName.toString() == executableName && Files.isExecutable(it) }
                .map { Tool(this, it) }
                .firstOrNull()
        } ?: throw IllegalStateException("Failed to locate $executableName under $installDir")
    }

    override fun documentationUrl() =
        "https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html"

    @VisibleForTesting
    fun windowsUrl(version: FourPartVersion) = "$BASE_URL/${version.displayValue()}/windows/SessionManagerPlugin.zip"

    @VisibleForTesting
    fun macUrl(version: FourPartVersion) = "$BASE_URL/${version.displayValue()}/mac/sessionmanager-bundle.zip"

    @VisibleForTesting
    fun ubuntuArm64Url(version: FourPartVersion) = "$BASE_URL/${version.displayValue()}/ubuntu_arm64/session-manager-plugin.deb"

    @VisibleForTesting
    fun ubuntuI64Url(version: FourPartVersion) = "$BASE_URL/${version.displayValue()}/ubuntu_64bit/session-manager-plugin.deb"

    @VisibleForTesting
    fun linuxArm64Url(version: FourPartVersion) = "$BASE_URL/${version.displayValue()}/linux_arm64/session-manager-plugin.rpm"

    @VisibleForTesting
    fun linuxI64Url(version: FourPartVersion) = "$BASE_URL/${version.displayValue()}/linux_64bit/session-manager-plugin.rpm"

    private fun runInstall(cmd: GeneralCommandLine) {
        val processOutput = ExecUtil.execAndGetOutput(cmd, INSTALL_TIMEOUT.toMillis().toInt())

        if (!processOutput.checkSuccess(LOGGER)) {
            throw IllegalStateException("Failed to extract $displayName\nSTDOUT:${processOutput.stdout}\nSTDERR:${processOutput.stderr}")
        }
    }

    private fun extractZip(downloadArtifact: Path, destinationDir: Path) {
        val decompressor = Decompressor.Zip(downloadArtifact).withZipExtensions()
        if (!SystemInfo.isWindows) {
            decompressor.extract(destinationDir)
            return
        }

        // on windows there is a zip inside a zip :(
        val tempDir = Files.createTempDirectory(id)
        decompressor.extract(tempDir)

        val intermediateZip = tempDir.resolve("package.zip")
        Decompressor.Zip(intermediateZip).withZipExtensions().extract(destinationDir)
    }

    private fun hasCommand(cmd: String): Boolean {
        val output = ExecUtil.execAndGetOutput(GeneralCommandLine("sh", "-c", "command -v $cmd"), EXECUTION_TIMEOUT.toMillis().toInt())
        return output.exitCode == 0
    }
    private val LOGGER = getLogger<SsmPlugin>()
    private const val BASE_URL = "https://s3.us-east-1.amazonaws.com/session-manager-downloads/plugin"
    private const val VERSION_FILE = "$BASE_URL/latest/VERSION"
    private val EXECUTION_TIMEOUT = Duration.ofSeconds(5)
    private val INSTALL_TIMEOUT = Duration.ofSeconds(30)
}


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

package software.aws.toolkits.jetbrains.services.sts

import software.amazon.awssdk.services.sts.StsClient
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import java.time.Duration

object StsResources {
    val ACCOUNT = ClientBackedCachedResource(StsClient::class, "sts.account", expiry = Duration.ofDays(1)) {
        callerIdentity.account()
    }
    val USER = ClientBackedCachedResource(StsClient::class, "sts.user", expiry = Duration.ofDays(1)) {
        callerIdentity.userId()
    }
}


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

package software.aws.toolkits.jetbrains.services.telemetry

import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.util.SystemInfo
import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.jetbrains.settings.AwsSettings

data class ClientMetadata(
    val productName: AWSProduct = AWSProduct.AWS_TOOLKIT_FOR_JET_BRAINS,
    val productVersion: String = AwsToolkit.PLUGIN_VERSION,
    val clientId: String = AwsSettings.getInstance().clientId.toString(),
    val parentProduct: String = ApplicationNamesInfo.getInstance().fullProductNameWithEdition,
    val parentProductVersion: String = ApplicationInfo.getInstance().build.baselineVersion.toString(),
    val os: String = SystemInfo.OS_NAME,
    val osVersion: String = SystemInfo.OS_VERSION
) {
    companion object {
        val DEFAULT_METADATA = ClientMetadata()
    }
}


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

package software.aws.toolkits.jetbrains.services.telemetry

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.cognitoidentity.CognitoIdentityClient
import software.amazon.awssdk.services.cognitoidentity.model.Credentials
import software.amazon.awssdk.services.cognitoidentity.model.GetCredentialsForIdentityRequest
import software.amazon.awssdk.services.cognitoidentity.model.GetIdRequest
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.telemetry.CachedIdentityStorage
import java.time.temporal.ChronoUnit

/**
 * [AwsCredentialsProvider] implementation that uses the Amazon Cognito Identity
 * service to create temporary, short-lived sessions to use for authentication
 *
 * @constructor Creates a new AwsCredentialsProvider that uses credentials from a Cognito Identity pool.
 * @property identityPool The name of the pool to create users from
 * @param cacheStorage A storage solution to cache an identity ID, disabled if null
 */
class AwsCognitoCredentialsProvider(
    private val identityPool: String,
    private val cognitoClient: CognitoIdentityClient,
    cacheStorage: CachedIdentityStorage? = null
) : AwsCredentialsProvider, SdkAutoCloseable {
    private val identityIdProvider = AwsCognitoIdentityProvider(cognitoClient, identityPool, cacheStorage)
    private val cacheSupplier = CachedSupplier.builder(this::updateCognitoCredentials)
        .prefetchStrategy(NonBlocking("Cognito Identity Credential Refresh"))
        .build()

    override fun resolveCredentials(): AwsCredentials = cacheSupplier.get()

    private fun updateCognitoCredentials(): RefreshResult<AwsSessionCredentials> {
        val credentialsForIdentity = credentialsForIdentity()
        val sessionCredentials = AwsSessionCredentials.create(
            credentialsForIdentity.accessKeyId(),
            credentialsForIdentity.secretKey(),
            credentialsForIdentity.sessionToken()
        )
        val actualExpiration = credentialsForIdentity.expiration()

        return RefreshResult.builder(sessionCredentials)
            .staleTime(actualExpiration.minus(1, ChronoUnit.MINUTES))
            .prefetchTime(actualExpiration.minus(5, ChronoUnit.MINUTES))
            .build()
    }

    private fun credentialsForIdentity(): Credentials {
        val identityId = identityIdProvider.identityId
        val request = GetCredentialsForIdentityRequest.builder().identityId(identityId).build()

        return cognitoClient.getCredentialsForIdentity(request).credentials()
    }

    override fun close() {
        cognitoClient.close()
        cacheSupplier.close()
    }
}

private class AwsCognitoIdentityProvider(
    private val cognitoClient: CognitoIdentityClient,
    private val identityPoolId: String,
    private val cacheStorage: CachedIdentityStorage? = null
) {
    val identityId: String by lazy {
        loadFromCache() ?: createNewIdentity()
    }

    private fun loadFromCache(): String? = cacheStorage?.loadIdentity(identityPoolId)

    private fun createNewIdentity(): String {
        val request = GetIdRequest.builder().identityPoolId(identityPoolId).build()
        val newIdentityId = cognitoClient.getId(request).identityId()

        cacheStorage?.storeIdentity(identityPoolId, newIdentityId)

        return newIdentityId
    }
}


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

package software.aws.toolkits.jetbrains.services.telemetry

import com.intellij.openapi.util.registry.Registry
import kotlinx.coroutines.withContext
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.cognitoidentity.CognitoIdentityClient
import software.amazon.awssdk.services.toolkittelemetry.ToolkitTelemetryClient
import software.amazon.awssdk.services.toolkittelemetry.model.MetadataEntry
import software.amazon.awssdk.services.toolkittelemetry.model.MetricDatum
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
import software.aws.toolkits.core.clients.nullDefaultProfileFile
import software.aws.toolkits.core.telemetry.MetricEvent
import software.aws.toolkits.core.telemetry.TelemetryPublisher
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.AwsSdkClient
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import kotlin.streams.toList

class DefaultTelemetryPublisher(
    private val clientMetadata: ClientMetadata = ClientMetadata.DEFAULT_METADATA,
    private val clientProvider: () -> ToolkitTelemetryClient
) : TelemetryPublisher {
    constructor() : this(clientProvider = { createDefaultTelemetryClient() })

    private val lazyClient = lazy { clientProvider() }
    private val client by lazyClient

    override suspend fun publish(metricEvents: Collection<MetricEvent>) {
        withContext(getCoroutineBgContext()) {
            client.postMetrics {
                it.awsProduct(clientMetadata.productName)
                it.awsProductVersion(clientMetadata.productVersion)
                it.clientID(clientMetadata.clientId)
                it.os(clientMetadata.os)
                it.osVersion(clientMetadata.osVersion)
                it.parentProduct(clientMetadata.parentProduct)
                it.parentProductVersion(clientMetadata.parentProductVersion)
                it.metricData(metricEvents.toMetricData())
            }
        }
    }

    override suspend fun sendFeedback(sentiment: Sentiment, comment: String, metadata: Map<String, String>) {
        withContext(getCoroutineBgContext()) {
            client.postFeedback {
                it.awsProduct(clientMetadata.productName)
                it.awsProductVersion(clientMetadata.productVersion)
                it.os(clientMetadata.os)
                it.osVersion(clientMetadata.osVersion)
                it.parentProduct(clientMetadata.parentProduct)
                it.parentProductVersion(clientMetadata.parentProductVersion)
                it.sentiment(sentiment)
                it.comment(comment)
                if (metadata.isNotEmpty()) {
                    it.metadata(metadata.map { (k, v) -> MetadataEntry.builder().key(k).value(v).build() })
                }
            }
        }
    }

    private fun Collection<MetricEvent>.toMetricData(): Collection<MetricDatum> = this
        .flatMap { metricEvent ->
            metricEvent.data.map { datum ->
                val metricName = datum.name
                MetricDatum.builder()
                    .epochTimestamp(metricEvent.createTime.toEpochMilli())
                    .metricName(metricName)
                    .unit(datum.unit)
                    .value(datum.value)
                    .passive(datum.passive)
                    .metadata(
                        datum.metadata.entries.stream().map {
                            MetadataEntry.builder()
                                .key(it.key)
                                .value(it.value)
                                .build()
                        }.toList() + listOf(
                            MetadataEntry.builder()
                                .key(METADATA_AWS_ACCOUNT)
                                .value(metricEvent.awsAccount)
                                .build(),
                            MetadataEntry.builder()
                                .key(METADATA_AWS_REGION)
                                .value(metricEvent.awsRegion)
                                .build()
                        )
                    )
                    .build()
            }
        }

    override fun close() {
        if (lazyClient.isInitialized()) {
            client.close()
        }
    }

    private companion object {
        private const val METADATA_AWS_ACCOUNT = "awsAccount"
        private const val METADATA_AWS_REGION = "awsRegion"

        private fun createDefaultTelemetryClient(): ToolkitTelemetryClient {
            val region = Region.of(Registry.get("aws.telemetry.region").asString())
            val sdkClient = AwsSdkClient.getInstance()

            return AwsClientManager.getInstance().createUnmanagedClient(
                credProvider = AwsCognitoCredentialsProvider(
                    Registry.get("aws.telemetry.identityPool").asString(),
                    CognitoIdentityClient.builder()
                        .credentialsProvider(AnonymousCredentialsProvider.create())
                        .region(region)
                        .httpClient(sdkClient.sharedSdkClient())
                        .nullDefaultProfileFile()
                        .build()
                ),
                region = region,
                endpointOverride = Registry.get("aws.telemetry.endpoint").asString()
            ) { _, _, _, _, clientOverrideConfiguration -> clientOverrideConfiguration.nullDefaultProfileFile() }
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.telemetry

import com.intellij.execution.filters.TextConsoleBuilderFactory
import com.intellij.execution.ui.ConsoleView
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.DefaultProjectFactory
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.FrameWrapper
import com.intellij.openapi.util.Disposer
import com.intellij.ui.JBColor
import com.intellij.util.ui.components.BorderLayoutPanel
import software.aws.toolkits.core.telemetry.MetricEvent
import software.aws.toolkits.jetbrains.AwsToolkit
import javax.swing.BorderFactory
import javax.swing.JComponent

class OpenTelemetryAction : DumbAwareAction() {
    override fun actionPerformed(event: AnActionEvent) {
        TelemetryDialog().show()
    }

    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = AwsToolkit.isDeveloperMode()
    }

    private class TelemetryDialog : FrameWrapper(null), TelemetryListener {
        private val consoleView: ConsoleView by lazy {
            TextConsoleBuilderFactory.getInstance().createBuilder(DefaultProjectFactory.getInstance().defaultProject).apply {
                setViewer(true)
            }.console
        }

        init {
            title = "Telemetry Viewer"
            component = createContent()
        }

        private fun createContent(): JComponent {
            val panel = BorderLayoutPanel()
            val consoleComponent = consoleView.component

            val actionGroup = DefaultActionGroup(*consoleView.createConsoleActions())
            val toolbar = ActionManager.getInstance().createActionToolbar("AWS.TelemetryViewer", actionGroup, false)

            toolbar.setTargetComponent(consoleComponent)

            panel.addToLeft(toolbar.component)
            panel.addToCenter(consoleComponent)

            // Add a border to make things look nicer.
            consoleComponent.border = BorderFactory.createLineBorder(JBColor.GRAY)

            val telemetryService = TelemetryService.getInstance()
            telemetryService.addListener(this)
            Disposer.register(this) { telemetryService.removeListener(this) }
            Disposer.register(this, consoleView)

            return panel
        }

        override fun onTelemetryEvent(event: MetricEvent) {
            consoleView.print(event.toString() + "\n", ConsoleViewContentType.NORMAL_OUTPUT)
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.telemetry

import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.telemetry.DefaultMetricEvent
import software.aws.toolkits.core.telemetry.DefaultMetricEvent.Companion.METADATA_INVALID
import software.aws.toolkits.core.telemetry.DefaultMetricEvent.Companion.METADATA_NA
import software.aws.toolkits.core.telemetry.DefaultMetricEvent.Companion.METADATA_NOT_SET
import software.aws.toolkits.core.telemetry.DefaultTelemetryBatcher
import software.aws.toolkits.core.telemetry.MetricEvent
import software.aws.toolkits.core.telemetry.TelemetryBatcher
import software.aws.toolkits.core.telemetry.TelemetryPublisher
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.core.credentials.activeRegion
import software.aws.toolkits.jetbrains.core.credentials.getConnectionSettings
import software.aws.toolkits.jetbrains.core.experiments.ToolkitExperimentManager
import software.aws.toolkits.jetbrains.core.getResourceIfPresent
import software.aws.toolkits.jetbrains.services.sts.StsResources
import software.aws.toolkits.jetbrains.settings.AwsSettings
import software.aws.toolkits.jetbrains.ui.feedback.ENABLED_EXPERIMENTS
import java.util.concurrent.atomic.AtomicBoolean

data class MetricEventMetadata(
    val awsAccount: String = METADATA_NA,
    val awsRegion: String = METADATA_NA
)

interface TelemetryListener {
    fun onTelemetryEvent(event: MetricEvent)
}

abstract class TelemetryService(private val publisher: TelemetryPublisher, private val batcher: TelemetryBatcher) : Disposable {
    private val isDisposing = AtomicBoolean(false)
    private val listeners = mutableSetOf<TelemetryListener>()

    init {
        setTelemetryEnabled(AwsSettings.getInstance().isTelemetryEnabled)
    }

    fun record(connectionSettings: ConnectionSettings?, buildEvent: MetricEvent.Builder.() -> Unit) {
        val metricEventMetadata = when (connectionSettings) {
            is ConnectionSettings -> MetricEventMetadata(
                awsAccount = connectionSettings.activeAwsAccountIfKnown() ?: METADATA_NOT_SET,
                awsRegion = connectionSettings.region.id
            )
            else -> MetricEventMetadata()
        }
        record(metricEventMetadata, buildEvent)
    }

    fun record(project: Project?, buildEvent: MetricEvent.Builder.() -> Unit) {
        // It is possible that a race can happen if we record telemetry but project has been closed, i.e. async actions
        val metricEventMetadata = if (project != null) {
            if (project.isDisposed) {
                MetricEventMetadata(
                    awsAccount = METADATA_INVALID,
                    awsRegion = METADATA_INVALID
                )
            } else {
                MetricEventMetadata(
                    awsAccount = project.getConnectionSettings()?.activeAwsAccountIfKnown() ?: METADATA_NOT_SET,
                    awsRegion = project.activeRegion().id
                )
            }
        } else {
            MetricEventMetadata()
        }
        record(metricEventMetadata, buildEvent)
    }

    private fun ConnectionSettings.activeAwsAccountIfKnown(): String? = tryOrNull { this.getResourceIfPresent(StsResources.ACCOUNT) }

    @Synchronized
    fun setTelemetryEnabled(isEnabled: Boolean) {
        batcher.onTelemetryEnabledChanged(isEnabled and TELEMETRY_ENABLED)
    }

    fun addListener(listener: TelemetryListener) {
        listeners.add(listener)
    }

    fun removeListener(listener: TelemetryListener) {
        listeners.remove(listener)
    }

    override fun dispose() {
        if (!isDisposing.compareAndSet(false, true)) {
            return
        }

        listeners.clear()

        batcher.shutdown()
        publisher.close()
    }

    fun record(metricEventMetadata: MetricEventMetadata, buildEvent: MetricEvent.Builder.() -> Unit) {
        val builder = DefaultMetricEvent.builder()
        builder.awsAccount(metricEventMetadata.awsAccount)
        builder.awsRegion(metricEventMetadata.awsRegion)

        buildEvent(builder)

        val event = builder.build()

        runCatching {
            listeners.forEach { it.onTelemetryEvent(event) }
        }

        batcher.enqueue(event)
    }

    suspend fun sendFeedback(sentiment: Sentiment, comment: String, metadata: Map<String, String> = emptyMap()) {
        val experiments = ToolkitExperimentManager.enabledExperiments().joinToString(",") { it.id }
        publisher.sendFeedback(sentiment, comment, metadata + (ENABLED_EXPERIMENTS to experiments))
    }

    companion object {
        private const val TELEMETRY_KEY = "aws.toolkits.enableTelemetry"
        private val TELEMETRY_ENABLED = System.getProperty(TELEMETRY_KEY)?.toBoolean() ?: true

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

class DefaultTelemetryService : TelemetryService {
    constructor() : super(publisher, batcher)

    private companion object {
        private val publisher: TelemetryPublisher by lazy { DefaultTelemetryPublisher() }
        private val batcher: TelemetryBatcher by lazy { DefaultTelemetryBatcher(publisher) }
    }
}


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

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
import software.aws.toolkits.resources.message
import java.util.UUID
import java.util.prefs.Preferences

interface AwsSettings {
    var isTelemetryEnabled: Boolean
    var promptedForTelemetry: Boolean
    var useDefaultCredentialRegion: UseAwsCredentialRegion
    var profilesNotification: ProfilesNotification
    val clientId: UUID

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

enum class ProfilesNotification(private val description: String) {
    Always(message("settings.profiles.always")),
    OnFailure(message("settings.profiles.on_failure")),
    Never(message("settings.profiles.never"));

    override fun toString(): String = description
}

enum class UseAwsCredentialRegion(private val description: String) {
    Always(message("settings.credentials.prompt_for_default_region_switch.always.description")),
    Prompt(message("settings.credentials.prompt_for_default_region_switch.ask.description")),
    Never(message("settings.credentials.prompt_for_default_region_switch.never.description"));

    override fun toString(): String = description
}

@State(name = "aws", storages = [Storage("aws.xml")])
class DefaultAwsSettings : PersistentStateComponent<AwsConfiguration>, AwsSettings {
    private val preferences = Preferences.userRoot().node(this.javaClass.canonicalName)
    private var state = AwsConfiguration()

    override fun getState(): AwsConfiguration = state

    override fun loadState(state: AwsConfiguration) {
        this.state = state
    }

    override var isTelemetryEnabled: Boolean
        get() = state.isTelemetryEnabled ?: true
        set(value) {
            state.isTelemetryEnabled = value
            TelemetryService.getInstance().setTelemetryEnabled(value)
        }

    override var promptedForTelemetry: Boolean
        get() = state.promptedForTelemetry ?: false
        set(value) {
            state.promptedForTelemetry = value
        }

    override var useDefaultCredentialRegion: UseAwsCredentialRegion
        get() = state.useDefaultCredentialRegion?.let { UseAwsCredentialRegion.valueOf(it) } ?: UseAwsCredentialRegion.Prompt
        set(value) {
            state.useDefaultCredentialRegion = value.name
        }

    override var profilesNotification: ProfilesNotification
        get() = state.profilesNotification?.let { ProfilesNotification.valueOf(it) } ?: ProfilesNotification.Always
        set(value) {
            state.profilesNotification = value.name
        }

    override val clientId: UUID
        @Synchronized get() {
            val id = when {
                ApplicationManager.getApplication().isUnitTestMode || System.getProperty("robot-server.port") != null -> "ffffffff-ffff-ffff-ffff-ffffffffffff"
                isTelemetryEnabled == false -> "11111111-1111-1111-1111-111111111111"
                else -> {
                    preferences.get(CLIENT_ID_KEY, UUID.randomUUID().toString()).also {
                        preferences.put(CLIENT_ID_KEY, it.toString())
                    }
                }
            }

            return UUID.fromString(id)
        }

    companion object {
        const val CLIENT_ID_KEY = "CLIENT_ID"
    }
}

data class AwsConfiguration(
    var isTelemetryEnabled: Boolean? = null,
    var promptedForTelemetry: Boolean? = null,
    var useDefaultCredentialRegion: String? = null,
    var profilesNotification: String? = null
)

class ShowSettingsAction : AnAction(message("aws.settings.show.label")), DumbAware {
    override fun actionPerformed(e: AnActionEvent) {
        ShowSettingsUtil.getInstance().showSettingsDialog(e.getRequiredData(LangDataKeys.PROJECT), AwsSettingsConfigurable::class.java)
    }
}