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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.ui.components.JBLabel
import com.intellij.ui.layout.panel
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.apprunner.AppRunnerClient
import software.amazon.awssdk.services.apprunner.model.AppRunnerException
import software.amazon.awssdk.services.apprunner.model.ServiceStatus
import software.amazon.awssdk.services.apprunner.model.ServiceSummary
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.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.services.apprunner.AppRunnerServiceNode
import software.aws.toolkits.jetbrains.services.apprunner.resources.AppRunnerResources
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.ApprunnerTelemetry
import software.aws.toolkits.telemetry.Result
import javax.swing.JComponent

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

    override fun actionPerformed(selected: AppRunnerServiceNode, e: AnActionEvent) {
        val dialog = object : DialogWrapper(selected.nodeProject) {
            init {
                init()
                setOKButtonText(message("apprunner.action.pause.confirm"))
            }

            override fun getTitle(): String = message("apprunner.action.pause")
            override fun getHelpId(): String = HelpIds.APPRUNNER_PAUSE_RESUME.id
            override fun createCenterPanel(): JComponent = panel {
                row {
                    JBLabel().apply {
                        text = message("apprunner.pause.warning", selected.service.serviceName())
                        icon = Messages.getWarningIcon()
                        iconTextGap = 8
                    }(grow)
                }
            }
        }
        if (dialog.showAndGet()) {
            val scope = projectCoroutineScope(selected.nodeProject)
            scope.launch {
                performPause(selected.nodeProject, selected.nodeProject.awsClient(), selected.service)
            }
        } else {
            ApprunnerTelemetry.pauseService(selected.nodeProject, Result.Cancelled)
        }
    }

    internal fun performPause(project: Project, client: AppRunnerClient, service: ServiceSummary) = try {
        client.pauseService { it.serviceArn(service.serviceArn()) }
        notifyInfo(
            project = project,
            title = message("apprunner.action.pause"),
            content = message("apprunner.pause.succeeded", service.serviceName())
        )
        ApprunnerTelemetry.pauseService(project, Result.Succeeded)
    } catch (e: Exception) {
        val m = if (e is AppRunnerException) {
            e.awsErrorDetails()?.errorMessage() ?: ""
        } else {
            ""
        }
        notifyError(
            title = message("apprunner.action.pause"),
            project = project,
            content = message("apprunner.pause.failed", service.serviceName(), m)
        )
        LOG.error(e) { "Exception thrown while pausing AppRunner service" }
        ApprunnerTelemetry.pauseService(project, Result.Failed)
    } finally {
        project.refreshAwsTree(AppRunnerResources.LIST_SERVICES)
    }

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


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.ui.components.JBLabel
import com.intellij.ui.layout.panel
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.apprunner.AppRunnerClient
import software.amazon.awssdk.services.apprunner.model.AppRunnerException
import software.amazon.awssdk.services.apprunner.model.ServiceStatus
import software.amazon.awssdk.services.apprunner.model.ServiceSummary
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.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.services.apprunner.AppRunnerServiceNode
import software.aws.toolkits.jetbrains.services.apprunner.resources.AppRunnerResources
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.ApprunnerTelemetry
import software.aws.toolkits.telemetry.Result
import javax.swing.JComponent

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

    override fun actionPerformed(selected: AppRunnerServiceNode, e: AnActionEvent) {
        val dialog = object : DialogWrapper(selected.nodeProject) {
            init {
                init()
                setOKButtonText(message("apprunner.action.resume.confirm"))
            }

            override fun getTitle(): String = message("apprunner.action.resume")
            override fun getHelpId(): String = HelpIds.APPRUNNER_PAUSE_RESUME.id
            override fun createCenterPanel(): JComponent = panel {
                row {
                    JBLabel().apply {
                        text = message("apprunner.resume.warning", selected.service.serviceName())
                        icon = Messages.getWarningIcon()
                        iconTextGap = 8
                    }(grow)
                }
            }
        }

        if (dialog.showAndGet()) {
            val scope = projectCoroutineScope(selected.nodeProject)
            scope.launch {
                performResume(selected.nodeProject, selected.nodeProject.awsClient(), selected.service)
            }
        } else {
            ApprunnerTelemetry.pauseService(selected.nodeProject, Result.Cancelled)
        }
    }

    internal fun performResume(project: Project, client: AppRunnerClient, service: ServiceSummary) = try {
        client.resumeService { it.serviceArn(service.serviceArn()) }
        notifyInfo(
            project = project,
            title = message("apprunner.action.resume"),
            content = message("apprunner.resume.succeeded", service.serviceName())
        )
        project.refreshAwsTree(AppRunnerResources.LIST_SERVICES)
        ApprunnerTelemetry.resumeService(project, Result.Succeeded)
    } catch (e: Exception) {
        val m = if (e is AppRunnerException) {
            e.awsErrorDetails()?.errorMessage() ?: ""
        } else {
            ""
        }
        notifyError(
            project = project,
            title = message("apprunner.action.resume"),
            content = message("apprunner.resume.failed", service.serviceName(), m)
        )
        LOG.error(e) { "Exception thrown while resuming AppRunner service" }
        ApprunnerTelemetry.resumeService(project, Result.Failed)
    } finally {
        project.refreshAwsTree(AppRunnerResources.LIST_SERVICES)
    }

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


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

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

import com.intellij.ide.browsers.BrowserLauncher
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.DumbAware
import software.amazon.awssdk.services.apprunner.model.ServiceSummary
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.apprunner.AppRunnerServiceNode
import software.aws.toolkits.telemetry.ApprunnerTelemetry
import java.awt.datatransfer.StringSelection

class CopyServiceUrlAction : SingleResourceNodeAction<AppRunnerServiceNode>(), DumbAware {
    override fun actionPerformed(selected: AppRunnerServiceNode, e: AnActionEvent) {
        CopyPasteManager.getInstance().setContents(StringSelection(selected.service.urlWithProtocol()))
        ApprunnerTelemetry.copyServiceUrl(selected.nodeProject)
    }
}

class OpenServiceUrlAction : SingleResourceNodeAction<AppRunnerServiceNode>(), DumbAware {
    override fun actionPerformed(selected: AppRunnerServiceNode, e: AnActionEvent) {
        BrowserLauncher.instance.browse(selected.service.urlWithProtocol(), project = selected.nodeProject)
        ApprunnerTelemetry.openServiceUrl(selected.nodeProject)
    }
}

private fun ServiceSummary.urlWithProtocol() = "https://${serviceUrl()}"


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

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

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import icons.AwsIcons
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.apprunner.AppRunnerServiceNode
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.checkIfLogGroupExists
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.ApprunnerTelemetry

class ViewSystemLogsAction :
    SingleResourceNodeAction<AppRunnerServiceNode>(message("apprunner.view_service_log_streams"), null, AwsIcons.Resources.CloudWatch.LOGS),
    DumbAware {
    override fun actionPerformed(selected: AppRunnerServiceNode, e: AnActionEvent) {
        val scope = projectCoroutineScope(selected.nodeProject)
        scope.launch {
            try {
                viewLogGroup(selected, "service")
            } finally {
                ApprunnerTelemetry.viewServiceLogs(selected.nodeProject)
            }
        }
    }
}

class ViewApplicationLogsAction :
    SingleResourceNodeAction<AppRunnerServiceNode>(message("apprunner.view_application_log_streams"), null, AwsIcons.Resources.CloudWatch.LOGS),
    DumbAware {
    override fun actionPerformed(selected: AppRunnerServiceNode, e: AnActionEvent) {
        val scope = projectCoroutineScope(selected.nodeProject)
        scope.launch {
            try {
                viewLogGroup(selected, "application")
            } finally {
                ApprunnerTelemetry.viewApplicationLogs(selected.nodeProject)
            }
        }
    }
}

internal fun viewLogGroup(selected: AppRunnerServiceNode, logSuffix: String) {
    val project = selected.nodeProject
    val logGroup = "/aws/apprunner/${selected.service.serviceName()}/${selected.service.serviceId()}/$logSuffix"
    try {
        val client = project.awsClient<CloudWatchLogsClient>()
        if (client.checkIfLogGroupExists(logGroup)) {
            val window = CloudWatchLogWindow.getInstance(project)
            window.showLogGroup(logGroup)
        } else {
            notifyError(project = project, content = message("apprunner.view_service_log_streams.error_not_created"))
        }
    } catch (e: Exception) {
        notifyError(project = project, content = message("apprunner.view_service_log_streams.error", logGroup))
    }
}


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

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

// Policy name and content from https://docs.aws.amazon.com/apprunner/latest/dg/security_iam_service-with-iam.html
const val APPRUNNER_ECR_DEFAULT_ROLE_NAME = "AppRunnerECRAccessRole"
const val APPRUNNER_ECR_MANAGED_POLICY = "service-role/AWSAppRunnerServicePolicyForECRAccess"
const val APPRUNNER_SERVICE_ROLE_URI = "build.apprunner.amazonaws.com"


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

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

import software.amazon.awssdk.services.apprunner.AppRunnerClient
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource

object AppRunnerResources {
    val LIST_SERVICES = ClientBackedCachedResource(AppRunnerClient::class, "apprunner.listServices") {
        listServicesPaginator { }.toList().flatMap { it.serviceSummaryList() }
    }

    val LIST_CONNECTIONS = ClientBackedCachedResource(AppRunnerClient::class, "apprunner.listConnections") {
        listConnectionsPaginator { }.toList().flatMap { it.connectionSummaryList() }
    }
}


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

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

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.apprunner.AppRunnerClient
import software.amazon.awssdk.services.apprunner.model.AppRunnerException
import software.amazon.awssdk.services.apprunner.model.ConfigurationSource
import software.amazon.awssdk.services.apprunner.model.CreateServiceRequest
import software.amazon.awssdk.services.apprunner.model.ImageRepositoryType
import software.amazon.awssdk.services.apprunner.model.SourceCodeVersionType
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.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.services.apprunner.resources.AppRunnerResources
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AppRunnerServiceSource
import software.aws.toolkits.telemetry.ApprunnerTelemetry
import software.aws.toolkits.telemetry.Result
import javax.swing.JComponent

class CreationDialog(private val project: Project, ecrUri: String? = null) :
    DialogWrapper(project) {
    private val coroutineScope = projectCoroutineScope(project)
    val panel = CreationPanel(project, ecrUri)

    init {
        super.init()
        title = message("apprunner.creation.title")
        setOKButtonText(message("general.create_button"))
    }

    override fun createCenterPanel(): JComponent = panel.component

    override fun doCancelAction() {
        ApprunnerTelemetry.createService(project, Result.Cancelled, deploymentTypeFromPanel(panel))
        super.doCancelAction()
    }

    override fun doOKAction() {
        if (!isOKActionEnabled) {
            return
        }
        isOKActionEnabled = false
        setOKButtonText(message("general.create_in_progress"))
        panel.component.apply()
        coroutineScope.launch {
            try {
                val client = project.awsClient<AppRunnerClient>()
                val request = buildRequest(panel)
                // TODO use the operation id to allow opening logs? Unfortunately, it takes up to 30
                // seconds to create so maybe not?
                client.createService(request)
                notifyInfo(
                    project = project,
                    title = message("apprunner.creation.started.title"),
                    content = message("apprunner.creation.started")
                )
                withContext(getCoroutineUiContext()) {
                    close(OK_EXIT_CODE)
                    project.refreshAwsTree(AppRunnerResources.LIST_SERVICES)
                }
                ApprunnerTelemetry.createService(project, Result.Succeeded, deploymentTypeFromPanel(panel))
            } catch (e: Exception) {
                if (e is AppRunnerException) {
                    setErrorText(e.awsErrorDetails()?.errorMessage() ?: message("apprunner.creation.failed"))
                } else {
                    setErrorText(message("apprunner.creation.failed"))
                }
                LOG.error(e) { "Exception thrown while creating AppRunner Service" }
                ApprunnerTelemetry.createService(project, Result.Failed, deploymentTypeFromPanel(panel))
            } finally {
                isOKActionEnabled = true
                setOKButtonText(message("general.create_button"))
            }
        }
    }

    internal fun buildRequest(panel: CreationPanel): CreateServiceRequest {
        val request = CreateServiceRequest.builder()
            .serviceName(panel.name)
            .instanceConfiguration {
                it.cpu(panel.cpu)
                it.memory(panel.memory)
            }
        when {
            panel.ecr.isSelected -> {
                request.sourceConfiguration { source ->
                    source.autoDeploymentsEnabled(panel.automaticDeployment.isSelected)
                    source.imageRepository { image ->
                        image.imageRepositoryType(ImageRepositoryType.ECR)
                        image.imageIdentifier(panel.containerUri)
                        image.imageConfiguration {
                            it.port(panel.port.toString())
                            it.runtimeEnvironmentVariables(panel.environmentVariables.envVars)
                            panel.startCommand?.let { s -> it.startCommand(s) }
                        }
                    }
                    source.authenticationConfiguration {
                        it.accessRoleArn(panel.ecrPolicy.selected()?.arn)
                    }
                }
            }
            panel.ecrPublic.isSelected -> {
                request.sourceConfiguration { source ->
                    source.imageRepository { image ->
                        image.imageRepositoryType(ImageRepositoryType.ECR_PUBLIC)
                        image.imageIdentifier(panel.containerUri)
                        image.imageConfiguration {
                            it.port(panel.port.toString())
                            it.runtimeEnvironmentVariables(panel.environmentVariables.envVars)
                            panel.startCommand?.let { s -> it.startCommand(s) }
                        }
                    }
                }
            }
            panel.repo.isSelected -> {
                request.sourceConfiguration { source ->
                    source.autoDeploymentsEnabled(panel.automaticDeployment.isSelected)
                    source.codeRepository { repo ->
                        repo.codeConfiguration {
                            if (panel.manualDeployment.isSelected) {
                                it.configurationSource(ConfigurationSource.REPOSITORY)
                            } else {
                                it.configurationSource(ConfigurationSource.API)
                                it.codeConfigurationValues { codeConfig ->
                                    codeConfig.runtime(panel.runtime)
                                    codeConfig.port(panel.port.toString())
                                    codeConfig.buildCommand(panel.buildCommand)
                                    codeConfig.startCommand(panel.startCommand)
                                    codeConfig.runtimeEnvironmentVariables(panel.environmentVariables.envVars)
                                }
                            }
                        }
                        repo.repositoryUrl(panel.repository)
                        repo.sourceCodeVersion {
                            it.type(SourceCodeVersionType.BRANCH)
                            it.value(panel.branch)
                        }
                    }
                    source.authenticationConfiguration { it.connectionArn(panel.connection.selected()?.connectionArn()) }
                }
            }
            else -> throw IllegalStateException("AppRunner creation dialog had no type selected!")
        }
        return request.build()
    }

    private fun deploymentTypeFromPanel(panel: CreationPanel) = when {
        panel.repo.isSelected -> AppRunnerServiceSource.Repository
        panel.ecrPublic.isSelected -> AppRunnerServiceSource.EcrPublic
        panel.ecr.isSelected -> AppRunnerServiceSource.Ecr
        else -> AppRunnerServiceSource.Unknown
    }

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


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

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

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBRadioButton
import com.intellij.ui.layout.GrowPolicy
import com.intellij.ui.layout.or
import com.intellij.ui.layout.panel
import com.intellij.ui.layout.selected
import software.amazon.awssdk.services.apprunner.model.ConnectionSummary
import software.amazon.awssdk.services.apprunner.model.Runtime
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.services.apprunner.resources.APPRUNNER_ECR_DEFAULT_ROLE_NAME
import software.aws.toolkits.jetbrains.services.apprunner.resources.APPRUNNER_ECR_MANAGED_POLICY
import software.aws.toolkits.jetbrains.services.apprunner.resources.APPRUNNER_SERVICE_ROLE_URI
import software.aws.toolkits.jetbrains.services.apprunner.resources.AppRunnerResources
import software.aws.toolkits.jetbrains.services.iam.CreateIamServiceRoleDialog
import software.aws.toolkits.jetbrains.services.iam.IamResources
import software.aws.toolkits.jetbrains.services.iam.IamRole
import software.aws.toolkits.jetbrains.ui.KeyValueTextField
import software.aws.toolkits.jetbrains.ui.ResourceSelector
import software.aws.toolkits.jetbrains.ui.intTextField
import software.aws.toolkits.jetbrains.utils.toHumanReadable
import software.aws.toolkits.jetbrains.utils.ui.contextualHelp
import software.aws.toolkits.jetbrains.utils.ui.installOnParent
import software.aws.toolkits.jetbrains.utils.ui.selected
import software.aws.toolkits.jetbrains.utils.ui.visibleIf
import software.aws.toolkits.resources.message
import javax.swing.DefaultComboBoxModel

class CreationPanel(private val project: Project, ecrUri: String? = null) {
    internal companion object {
        // These are not in the model so we unfortunately have to keep our own list
        val memoryValues = listOf("2 GB", "3 GB", "4 GB")
        val cpuValues = listOf("1 vCPU", "2 vCPU")
    }

    val environmentVariables = KeyValueTextField()
    lateinit var connection: ResourceSelector<ConnectionSummary>
        private set
    lateinit var ecrPolicy: ResourceSelector<IamRole>
        private set
    var name: String = ""
        private set
    var cpu: String = cpuValues.first()
        private set
    var memory: String = memoryValues.first()
        private set
    var port: Int = 80
        private set
    var containerUri: String = ecrUri ?: ""
        private set
    var startCommand: String? = null
        internal set(value) {
            field = value?.ifBlank { null }
        }
    var repository: String = ""
        internal set(value) {
            // Remove the trailing slash or else it will show up in the service with two //
            field = value.trim().removeSuffix("/")
        }
    var branch: String = ""
        internal set
    var runtime: Runtime? = null
        internal set
    var buildCommand: String = ""
        private set

    internal val ecr = JBRadioButton(message("apprunner.creation.panel.source.ecr"), true)
    internal val ecrPublic = JBRadioButton(message("apprunner.creation.panel.source.ecr_public"), false)
    internal val repo = JBRadioButton(message("apprunner.creation.panel.source.repository"), false)

    internal val automaticDeployment = JBRadioButton(message("apprunner.creation.panel.deployment.automatic"), true).apply {
        toolTipText = message("apprunner.creation.panel.deployment.automatic.tooltip")
    }
    internal val manualDeployment = JBRadioButton(message("apprunner.creation.panel.deployment.manual"), false).apply {
        toolTipText = message("apprunner.creation.panel.deployment.manual.tooltip")
    }

    internal val repoConfigFromSettings = JBRadioButton(message("apprunner.creation.panel.repository.api"), true).apply {
        toolTipText = message("apprunner.creation.panel.repository.api.tooltip")
    }
    internal val repoConfigFromFile = JBRadioButton(message("apprunner.creation.panel.repository.file"), false).apply {
        toolTipText = message("apprunner.creation.panel.repository.file.tooltip")
    }

    val imagePanel = panel {
        row(message("apprunner.creation.panel.image.uri")) {
            textField(::containerUri)
                .apply { component.emptyText.text = "111111111111.dkr.ecr.us-east-1.amazonaws.com/name:tag" }
                .withErrorOnApplyIf(message("apprunner.creation.panel.image.uri.missing")) { it.text.isBlank() }
                .constraints(pushX)
        }

        row(message("apprunner.creation.panel.start_command")) {
            textField({ startCommand ?: "" }, { startCommand = it })
                .apply { component.toolTipText = message("apprunner.creation.panel.start_command.image.tooltip") }
                .constraints(pushX)
        }

        row(message("apprunner.creation.panel.port")) {
            intTextField(::port, range = IntRange(1, 65535))
        }

        row {
            label(message("apprunner.creation.panel.image.access_role"))
                .apply {
                    component.toolTipText = message("apprunner.creation.panel.image.access_role.tooltip")
                }
                .visibleIf(ecr.selected)
            ecrPolicy = ResourceSelector.builder()
                .resource { IamResources.LIST_ALL }
                .awsConnection(project)
                .build()
                .apply {
                    selectedItem { it.name == APPRUNNER_ECR_DEFAULT_ROLE_NAME }
                    toolTipText = message("apprunner.creation.panel.image.access_role.tooltip")
                }
            ecrPolicy(grow)
                .growPolicy(GrowPolicy.MEDIUM_TEXT)
                .withErrorOnApplyIf(message("apprunner.creation.panel.image.access_role.missing")) { it.selected() == null }
                .visibleIf(ecr.selected)
            button(message("general.create_button")) {
                val iamRoleDialog = CreateIamServiceRoleDialog(
                    project,
                    project.awsClient(),
                    APPRUNNER_SERVICE_ROLE_URI,
                    APPRUNNER_ECR_MANAGED_POLICY,
                    APPRUNNER_ECR_DEFAULT_ROLE_NAME
                )
                if (iamRoleDialog.showAndGet()) {
                    iamRoleDialog.name.let { newRole ->
                        ecrPolicy.reload(forceFetch = true)
                        ecrPolicy.selectedItem { role -> role.name == newRole }
                    }
                }
            }.visibleIf(ecr.selected)
        }
        row {
            label(message("apprunner.creation.panel.cpu"))
            comboBox(DefaultComboBoxModel(CreationPanel.cpuValues.toTypedArray()), { cpu }, { it?.let { cpu = it } })
                .withErrorOnApplyIf(message("apprunner.creation.panel.cpu.missing")) { it.selected() == null }
                .constraints(pushX, growX)
            label(message("apprunner.creation.panel.memory"))
            comboBox(DefaultComboBoxModel(CreationPanel.memoryValues.toTypedArray()), { memory }, { it?.let { memory = it } })
                .withErrorOnApplyIf(message("apprunner.creation.panel.memory.missing")) { it.selected() == null }
                .constraints(pushX, growX)
        }
    }

    val repoSettings = panel {
        row(message("apprunner.creation.panel.repository.runtime")) {
            comboBox(DefaultComboBoxModel(Runtime.knownValues().toTypedArray()), { runtime }, { runtime = it })
                .apply {
                    component.toolTipText = message("apprunner.creation.panel.repository.runtime.tooltip")
                    component.renderer = SimpleListCellRenderer.create("") { it?.toString()?.toHumanReadable() }
                }
                .withErrorOnApplyIf(message("apprunner.creation.panel.repository.runtime.missing")) { it.selected() == null }
                .constraints(pushX, growX)
            label(message("apprunner.creation.panel.port"))
            intTextField(::port, range = IntRange(1, 65535))
        }
        row(message("apprunner.creation.panel.repository.build_command")) {
            textField(::buildCommand)
                .apply { component.toolTipText = message("apprunner.creation.panel.repository.build_command.tooltip") }
                .withErrorOnApplyIf(message("apprunner.creation.panel.repository.build_command.missing")) { it.text.isBlank() }
                .constraints(pushX, growX)
        }
        row(message("apprunner.creation.panel.start_command")) {
            textField({ startCommand ?: "" }, { startCommand = it })
                .apply { component.toolTipText = message("apprunner.creation.panel.start_command.repo.tooltip") }
                .withErrorOnApplyIf(message("apprunner.creation.panel.start_command.missing")) { it.text.isBlank() }
                .constraints(pushX, growX)
        }.largeGapAfter()
    }

    val repoPanel = panel {
        row(message("apprunner.creation.panel.repository.connection")) {
            cell(isFullWidth = true) {
                connection = ResourceSelector.builder()
                    .resource { AppRunnerResources.LIST_CONNECTIONS }
                    .customRenderer(SimpleListCellRenderer.create("") { "${it.connectionName()} (${it.providerTypeAsString().toHumanReadable()})" })
                    .awsConnection(project)
                    .build()
                connection(growX, pushX)
                    .withErrorOnApplyIf(message("apprunner.creation.panel.repository.connection.missing")) { it.isLoading || it.selected() == null }
                contextualHelp(message("apprunner.creation.panel.repository.connection.help"))
            }
        }
        row {
            label(message("apprunner.creation.panel.repository.url")).apply {
                component.toolTipText = message("apprunner.creation.panel.repository.url.tooltip")
            }
            textField(::repository, columns = 20).constraints(growX)
            label(message("apprunner.creation.panel.repository.branch"))
            textField(::branch, columns = 15).constraints(growX)
        }
        row(message("apprunner.creation.panel.repository.configuration")) {
            buttonGroup {
                cell(isFullWidth = true) {
                    repoConfigFromSettings()
                    repoConfigFromFile()
                }
            }
        }
        row {
            repoSettings(growX)
                .installOnParent { repoConfigFromSettings.isSelected }
                .visibleIf(repoConfigFromSettings.selected)
        }
        row {
            label(message("apprunner.creation.panel.cpu"))
            comboBox(DefaultComboBoxModel(CreationPanel.cpuValues.toTypedArray()), { cpu }, { it?.let { cpu = it } })
                .withErrorOnApplyIf(message("apprunner.creation.panel.cpu.missing")) { it.selected() == null }
                .constraints(pushX, growX)
            label(message("apprunner.creation.panel.memory"))
            comboBox(DefaultComboBoxModel(CreationPanel.memoryValues.toTypedArray()), { memory }, { it?.let { memory = it } })
                .withErrorOnApplyIf(message("apprunner.creation.panel.memory.missing")) { it.selected() == null }
                .constraints(pushX, growX)
        }
    }

    val component: DialogPanel = panel {
        row(message("apprunner.creation.panel.name")) {
            textField(::name)
                .withErrorOnApplyIf(message("apprunner.creation.panel.name.missing")) { it.text.isNullOrBlank() }
                .constraints(pushX)
        }
        row(message("apprunner.creation.panel.source")) {
            buttonGroup {
                cell {
                    ecr()
                    ecrPublic()
                    repo()
                }
            }
        }
        row(message("apprunner.creation.panel.deployment")) {
            // ECR public disables selecting deployment options
            buttonGroup {
                cell {
                    manualDeployment()
                    automaticDeployment()
                }
            }
        }.visibleIf(ecr.selected.or(repo.selected))
        row {
            subRowIndent = 0
            cell(isFullWidth = true) {
                // TODO HACK making this flow right means we don't have issues with weird spacing above or
                // to the left
                repoPanel(grow)
                    .installOnParent { repo.isSelected }
                    .visibleIf(repo.selected)

                imagePanel(grow)
                    .installOnParent { ecr.isSelected || ecrPublic.isSelected }
                    .visibleIf(ecr.selected.or(ecrPublic.selected))
            }
        }
        row(message("apprunner.creation.panel.environment")) {
            environmentVariables(growX)
        }
    }
}


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

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

import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider
import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
import software.amazon.awssdk.core.interceptor.Context
import software.amazon.awssdk.core.interceptor.ExecutionAttributes
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.codecatalyst.CodeCatalystClientBuilder
import software.amazon.awssdk.services.codecatalyst.model.CodeCatalystException
import software.aws.toolkits.core.ToolkitClientCustomizer
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import java.net.URI

class CawsClientCustomizer : ToolkitClientCustomizer {
    override fun customize(
        credentialProvider: AwsCredentialsProvider?,
        tokenProvider: SdkTokenProvider?,
        regionId: String,
        builder: AwsClientBuilder<*, *>,
        clientOverrideConfiguration: ClientOverrideConfiguration.Builder
    ) {
        if (builder is CodeCatalystClientBuilder) {
            builder.region(Region.US_WEST_2)
            builder.endpointOverride(
                URI.create(CawsEndpoints.CAWS_PROD_API)
            )
            clientOverrideConfiguration.addExecutionInterceptor(object : ExecutionInterceptor {
                override fun onExecutionFailure(context: Context.FailedExecution, executionAttributes: ExecutionAttributes) {
                    val exception = context.exception()
                    if (exception is CodeCatalystException) {
                        context.httpResponse().ifPresent { response ->
                            response.firstMatchingHeader("x-amzn-served-from").ifPresent {
                                LOG.warn { "Hit service exception. ${exception.requestId()} was served from $it" }
                            }

                            LOG.debug {
                                val headers = response.headers()
                                    .filter { header -> relevantHeaders.any { it.equals(header.key, ignoreCase = true) } }

                                "Additional headers for ${exception.requestId()}: $headers"
                            }
                        }
                    }
                }
            })
        }
    }

    companion object {
        private val LOG = getLogger<CawsClientCustomizer>()
        private val relevantHeaders = listOf(
            "x-amz-apigw-id",
            "x-amz-cf-id",
            "x-amz-cf-pop",
            "x-amzn-remapped-content-length",
            "x-amzn-remapped-x-amzn-requestid",
            "x-amzn-requestid",
            "x-amzn-served-from",
            "x-amzn-trace-id",
            "x-cache",
            "x-request-id"
        )
    }
}


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

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

import software.amazon.awssdk.services.codecatalyst.CodeCatalystClient
import software.amazon.awssdk.services.codecatalyst.model.ListProjectsRequest

fun CodeCatalystClient.listAccessibleProjectsPaginator(listProjectsRequest: (ListProjectsRequest.Builder) -> Unit) = listProjectsPaginator {
    it.filters({ filter ->
        filter.key("hasAccessTo")
        filter.values("true")
    })

    listProjectsRequest(it)
}


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

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

import com.intellij.dvcs.repo.ClonePathProvider
import com.intellij.dvcs.ui.CloneDvcsValidationUtils
import com.intellij.dvcs.ui.SelectChildTextFieldWithBrowseButton
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.vcs.CheckoutProvider
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtensionComponent
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.SearchTextField
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.JBList
import com.intellij.ui.layout.listCellRenderer
import com.intellij.ui.layout.panel
import com.intellij.util.ui.StatusText
import com.intellij.util.ui.UIUtil
import git4idea.checkout.GitCheckoutProvider
import git4idea.commands.Git
import git4idea.remote.GitRememberedInputs
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.codecatalyst.CodeCatalystClient
import software.aws.toolkits.core.ClientConnectionSettings
import software.aws.toolkits.core.utils.createParentDirectories
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.CollaborationToolsUIUtil
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.credentials.sono.lazilyGetUserId
import software.aws.toolkits.jetbrains.services.caws.pat.generateAndStorePat
import software.aws.toolkits.jetbrains.services.caws.pat.patExists
import software.aws.toolkits.jetbrains.ui.connection.SonoLoginOverlay
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodecatalystTelemetry
import java.net.URI
import java.nio.file.Paths
import javax.swing.JComponent
import javax.swing.JSeparator
import software.aws.toolkits.telemetry.Result as TelemetryResult

class CawsCloneDialogComponent(
    private val project: Project,
    private val modalityState: ModalityState
) : VcsCloneDialogExtensionComponent() {
    private lateinit var client: CodeCatalystClient
    private lateinit var cawsConnectionSettings: ClientConnectionSettings<*>

    private val repoListModel = CollectionComboBoxModel<CawsCodeRepository>()
    private val repoList = JBList(repoListModel).apply {
        setPaintBusy(true)
        setEmptyText(message("loading_resource.loading"))
        cellRenderer = listCellRenderer { value, _, _ -> text = value.presentableString }

        addListSelectionListener { _ ->
            selectedValue?.let {
                browseButton.trySetChildPath(it.name)
                dialogStateListener.onOkActionEnabled(true)
            }
        }
    }

    private val browseButton = SelectChildTextFieldWithBrowseButton(
        ClonePathProvider.defaultParentDirectoryPath(project, GitRememberedInputs.getInstance())
    ).apply {
        val chooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
        addBrowseFolderListener(message("caws.clone_dialog_title"), message("caws.clone_dialog_description"), project, chooserDescriptor)
    }

    override fun doClone(checkoutListener: CheckoutProvider.Listener) {
        val repository = repoList.selectedValue ?: throw RuntimeException("Repository was not selected")
        ApplicationManager.getApplication().executeOnPooledThread {
            val userId = lazilyGetUserId()
            try {
                // TODO: show progress bar here so it doesn't look like we're stuck
                val url = AwsResourceCache.getInstance().getResource(CawsResources.cloneUrls(repository), cawsConnectionSettings).toCompletableFuture().get()

                val user = URI(url).userInfo.trim('@')
                if (!patExists(user)) {
                    // TODO: prompt if this is OK before generating and storing
                    // TODO: we should check that the current client's "identity" matches the desired user, but the REST client doesn't return
                    //       that information like the graphql endpoint does
                    generateAndStorePat(client, user)
                }

                val destination = Paths.get(browseButton.text).toAbsolutePath()
                destination.createParentDirectories()
                val parentDirectory = destination.parent
                val parentDirectoryVfs = VfsUtil.findFile(parentDirectory, true)
                    ?: throw RuntimeException("VFS could not find specified directory: $parentDirectory")
                val directoryName = destination.fileName.toString()
                runInEdt {
                    GitCheckoutProvider.clone(project, Git.getInstance(), checkoutListener, parentDirectoryVfs, url, directoryName, parentDirectory.toString())
                }
                // GitCheckoutProvider.clone is async, but assume any issues is with JB instead of us
                CodecatalystTelemetry.localClone(project = null, userId = userId, result = TelemetryResult.Succeeded)
            } catch (e: Exception) {
                CodecatalystTelemetry.localClone(project = null, userId = userId, result = TelemetryResult.Failed)
                throw e
            }
        }
    }

    override fun doValidateAll(): List<ValidationInfo> {
        if (repoList.selectedValue == null) {
            return listOf(ValidationInfo(message("caws.workspace.details.repository_validation"), repoList))
        }

        val directoryValidation = CloneDvcsValidationUtils.checkDirectory(browseButton.text, browseButton.textField)
        if (directoryValidation != null) {
            return listOf(directoryValidation)
        }

        return emptyList()
    }

    private fun drawPanel(connectionSettings: ClientConnectionSettings<*>): JComponent {
        cawsConnectionSettings = connectionSettings
        client = AwsClientManager.getInstance().getClient<CodeCatalystClient>(cawsConnectionSettings)

        val panel = panel {
            row {
                val searchField = SearchTextField(false)
                CollaborationToolsUIUtil.attachSearch(repoList, searchField) {
                    it.presentableString
                }
                val label = CawsLetterBadge(connectionSettings)

                cell(isFullWidth = true) {
                    searchField.textEditor(growX)
                    JSeparator(JSeparator.VERTICAL)(growY)
                    component(label)
                }
            }

            row {
                scrollPane(repoList)
                    .constraints(growX)
            }

            row(message("caws.clone_dialog_directory")) {
                browseButton()
            }
        }

        disposableCoroutineScope(this).launch {
            try {
                val cache = AwsResourceCache.getInstance()
                val projects = cache.getResource(CawsResources.ALL_PROJECTS, cawsConnectionSettings).await()
                projects.forEach { cawsProject ->
                    val items = cache.getResource(CawsResources.codeRepositories(cawsProject), cawsConnectionSettings).await()
                    items.forEach { item ->
                        val url = cache.getResource(
                            CawsResources.cloneUrls(CawsCodeRepository(cawsProject.space, cawsProject.project, item.name)),
                            cawsConnectionSettings
                        ).toCompletableFuture().get()
                        if (url.contains(CawsEndpoints.CAWS_GIT_PATTERN)) {
                            repoListModel.add(item)
                        }
                    }
                }

                with(getCoroutineUiContext()) {
                    repoList.setEmptyText(StatusText.getDefaultEmptyText())
                }
            } catch (e: Exception) {
                LOG.warn(e) { "Failed to load repositories" }
                with(getCoroutineUiContext()) {
                    val emptyText = repoList.emptyText
                    emptyText.clear()
                    emptyText.appendLine(
                        AllIcons.General.Error,
                        message("caws.clone_dialog_repository_loading_error"),
                        SimpleTextAttributes.REGULAR_ATTRIBUTES,
                        null
                    )
                }
            } finally {
                with(getCoroutineUiContext()) {
                    repoList.setPaintBusy(false)
                }
            }
        }

        return panel
    }

    override fun getView(): JComponent =
        SonoLoginOverlay(project, this) { drawPanel(it) }
            .apply {
                border = IdeBorderFactory.createEmptyBorder(UIUtil.PANEL_REGULAR_INSETS)
            }

    override fun onComponentSelected() {
    }

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


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

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

import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtension
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtensionComponent
import icons.AwsIcons
import software.aws.toolkits.resources.message
import javax.swing.Icon

class CawsCloneDialogExtension : VcsCloneDialogExtension {
    override fun createMainComponent(project: Project): VcsCloneDialogExtensionComponent {
        throw RuntimeException("Should never be called")
    }

    override fun createMainComponent(project: Project, modalityState: ModalityState): VcsCloneDialogExtensionComponent =
        CawsCloneDialogComponent(project, modalityState)

    override fun getIcon(): Icon = AwsIcons.Logos.CODE_CATALYST_MEDIUM

    override fun getName(): String = message("caws.title")
}


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

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

data class CawsCodeRepository(
    val space: String,
    val project: String,
    val name: String
) {
    val presentableString by lazy { "$space/$project/$name" }
}


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

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

object CawsConstants {
    const val CAWS_ENV_PROJECT_DIR = "/projects"
    const val CAWS_ENV_IDE_BACKEND_DIR = "/aws/mde/ide-runtimes/jetbrains/runtime/"
    const val CAWS_ENV_AUTH_TOKEN_VAR = "__MDE_ENV_API_AUTHORIZATION_TOKEN"
    const val CAWS_ENV_ORG_NAME_VAR = "__DEV_ENVIRONMENT_ORGANIZATION_NAME"
    const val CAWS_ENV_PROJECT_NAME_VAR = "__DEV_ENVIRONMENT_PROJECT_NAME"
    const val CAWS_ENV_ID_VAR = "__DEV_ENVIRONMENT_ID"
    const val DEVFILE_YAML_NAME = "devfile.yaml"
}


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

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

object CawsEndpoints {
    const val CAWS_DOCS = "https://docs.aws.amazon.com/codecatalyst/latest/userguide/welcome.html"
    const val CAWS_DEV_ENV_MARKETING = "https://codecatalyst.aws/explore/dev-environments"
    const val TOOLKIT_CAWS_DOCS = "https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/codecatalyst-service.html"

    const val CAWS_PROD_API = "https://public.codecatalyst.global.api.aws"
    private const val CAWS_PROD_CONSOLE_BASE = "https://codecatalyst.aws/"

    // TODO: fix this heuristic
    const val CAWS_GIT_PATTERN = "codecatalyst.aws"

    object ConsoleFactory {
        fun baseUrl() = CAWS_PROD_CONSOLE_BASE
        private fun space(space: String) = baseUrl() + "spaces/$space/"
        private fun project(project: CawsProject) = space(project.space) + "projects/${project.project}"

        fun marketing() = baseUrl() + "explore"
        fun pricing() = baseUrl() + "explore/pricing"
        fun userHome() = baseUrl() + "user/view"

        fun devWorkspaceHome(project: CawsProject) = project(project) + "/dev-environments"
        fun projectHome(project: CawsProject) = project(project) + "/view"
        fun repositoryHome(project: CawsProject) = project(project) + "/source-repositories"
    }
}


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

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

import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.util.AuthData
import git4idea.remote.GitHttpAuthDataProvider
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.sono.SonoCredentialManager
import software.aws.toolkits.jetbrains.services.caws.pat.generateAndStorePat
import software.aws.toolkits.jetbrains.services.caws.pat.getPat
import software.aws.toolkits.jetbrains.utils.computeOnEdt
import software.aws.toolkits.resources.message
import java.time.Instant
import java.util.concurrent.atomic.AtomicLong

class CawsHttpAuthProvider : GitHttpAuthDataProvider {
    // globally only offer to make a new PAT at most once every 15s
    private val lastRefreshPrompt = AtomicLong(0)

    // framework will actually call this twice on first reject
    override fun forgetPassword(project: Project, url: String, authData: AuthData) {
        synchronized(this) {
            val now = Instant.now().epochSecond
            if (now - lastRefreshPrompt.getAndSet(now) < 15) {
                return
            }
        }

        val yesNo = computeOnEdt(ModalityState.defaultModalityState()) {
            MessageDialogBuilder.yesNo(
                message("caws.clone.invalid_pat"),
                message("caws.clone.invalid_pat.help")
            )
                .ask(project)
        }

        if (yesNo) {
            generateAndStorePat(SonoCredentialManager.getInstance(project).getSettingsAndPromptAuth().awsClient(), authData.login)
        }
    }

    override fun getAuthData(project: Project, url: String, login: String): AuthData? {
        if (url.contains(CawsEndpoints.CAWS_GIT_PATTERN)) {
            return getPat(login)?.let { AuthData(login, it.getPasswordAsString()) }
        }

        return null
    }
}


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

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

import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.ui.JBColor
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.MacUIUtil
import org.jdesktop.swingx.graphics.ColorUtilities
import software.amazon.awssdk.services.codecatalyst.model.CodeCatalystException
import software.aws.toolkits.core.ClientConnectionSettings
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import java.awt.Color
import java.awt.Dimension
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.RenderingHints
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.geom.Ellipse2D
import javax.swing.JLabel
import javax.swing.SwingConstants

class CawsLetterBadge(connectionSettings: ClientConnectionSettings<*>) : JLabel() {
    private val displayName: String
    init {
        val (displayName, email) = try {
            AwsResourceCache.getInstance().getResourceNow(CawsResources.PERSON, connectionSettings).let {
                it.displayName() to it.primaryEmail().email()
            }
        } catch (e: CodeCatalystException) {
            LOG.warn(e) { "Exception occurred while fetching user email" }
            "" to ""
        }
        this.displayName = displayName

        text = getInitials(displayName)
        font = JBFont.h3().asBold()
        foreground = JBColor(Color.WHITE, Color.BLACK)
        horizontalAlignment = SwingConstants.CENTER
        isOpaque = false
        preferredSize = Dimension(32, 32)

        addMouseListener(object : MouseAdapter() {
            override fun mouseClicked(e: MouseEvent?) {
                JBPopupFactory.getInstance()
                    .createActionGroupPopup(
                        email,
                        ActionManager.getInstance().getAction("aws.toolkit.sono.id.actions") as ActionGroup,
                        DataManager.getInstance().getDataContext(this@CawsLetterBadge),
                        null,
                        false
                    )
                    .showUnderneathOf(this@CawsLetterBadge)
            }
        })
    }

    private val color by lazy {
        color(displayName)
    }

    override fun paintComponent(g: Graphics) {
        g as Graphics2D
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
        g.setRenderingHint(
            RenderingHints.KEY_STROKE_CONTROL,
            if (MacUIUtil.USE_QUARTZ) RenderingHints.VALUE_STROKE_PURE else RenderingHints.VALUE_STROKE_NORMALIZE
        )
        g.color = color

        // we can probably do math to figure out the center of the letter but that is too much work right now
        val circle = Ellipse2D.Double(0.0, 0.0, width.toDouble(), height.toDouble())
        g.fill(circle)

        ui.paint(g, this)
    }

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

        // shamelessly stolen from Avatar defined in CAWSUIComponents
        private val nameReplaceRegex = Regex("[&/\\#,+()$~%.'\":*?<>{}0-9]")
        private fun getInitials(name: String): String {
            val names = name.replace(nameReplaceRegex, "")
                .split(" ")
                .filter { it.length > 0 }

            if (names.size == 0) {
                return "${name.firstOrNull() ?: ""}"
            }
            val firstInitial = names.first().first()
            if (names.size == 1) {
                return "$firstInitial"
            }
            val lastInitial = names.last().first()

            return "$firstInitial$lastInitial"
        }

        private fun hue(string: String): Int {
            val hash = string.fold(7) { hash, char ->
                (hash shl 5) - hash + char.toString().codePointAt(0)
            }

            return (Math.abs(hash) % 36) * 10 + 10
        }

        private const val saturation = 0.65f
        private const val luminosity = 0.35f
        private const val luminosity_dark = 0.75f
        private fun color(string: String): Color {
            val hue = hue(string) / 360f

            return JBColor(
                ColorUtilities.HSLtoRGB(hue, saturation, luminosity),
                ColorUtilities.HSLtoRGB(hue, saturation, luminosity_dark)
            )
        }
    }
}


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

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

data class CawsProject(
    val space: String,
    val project: String
)


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

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

import com.intellij.ui.CellRendererPanel
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.SeparatorWithText
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.components.panels.OpaquePanel
import java.awt.BorderLayout
import java.awt.Component
import javax.accessibility.AccessibleContext
import javax.swing.JList
import javax.swing.ListCellRenderer
import javax.swing.border.Border

class CawsProjectListRenderer(private val loadingRenderer: ListCellRenderer<in CawsProject>) : ColoredListCellRenderer<CawsProject>() {
    override fun getListCellRendererComponent(list: JList<out CawsProject>?, value: CawsProject?, index: Int, selected: Boolean, hasFocus: Boolean): Component {
        val c = super.getListCellRendererComponent(list, value, index, selected, hasFocus)
        list ?: return c
        if (list.model.size == 0) {
            // probably still loading
            return loadingRenderer.getListCellRendererComponent(list, value, index, selected, hasFocus)
        }

        val component = c as? SimpleColoredComponent ?: return c
        value ?: return c

        if (index == -1) {
            // if not a popup
            return c
        }

        val panel = object : CellRendererPanel() {
            init {
                layout = BorderLayout()
            }

            private val myContext: AccessibleContext = component.getAccessibleContext()
            override fun getAccessibleContext(): AccessibleContext {
                return myContext
            }

            override fun setBorder(border: Border?) {
                // we do not want to outer UI to add a border to that JPanel
                // see com.intellij.ide.ui.laf.darcula.ui.DarculaComboBoxUI.CustomComboPopup#customizeListRendererComponent
                component.border = border
            }
        }

        component.isOpaque = true
        panel.isOpaque = true
        panel.background = list.background
        panel.add(component, BorderLayout.CENTER)

        if (index == 0 || list.model.getElementAt(index - 0).space != value.space) {
            val separator = SeparatorWithText()
            separator.caption = value.space
            val wrapper = OpaquePanel(BorderLayout())
            wrapper.add(separator, BorderLayout.CENTER)
            wrapper.background = list.background

            panel.add(wrapper, BorderLayout.NORTH)
        }

        return panel
    }

    override fun customizeCellRenderer(list: JList<out CawsProject>, value: CawsProject?, index: Int, selected: Boolean, hasFocus: Boolean) {
        value ?: return
        if (index == -1) {
            // if not a popup
            append("${value.space} - ${value.project}")
        } else {
            append(value.project)
        }
    }
}


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

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

import software.amazon.awssdk.services.codecatalyst.CodeCatalystClient
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import java.time.Duration

object CawsResources {
    val ID = ClientBackedCachedResource(CodeCatalystClient::class, "caws.person.id", Duration.ofDays(1)) {
        val session = verifySession {}

        session.identity()
    }

    val PERSON = ClientBackedCachedResource(CodeCatalystClient::class, "caws.person", Duration.ofDays(1)) {
        val session = verifySession {}

        getUserDetails { it.id(session.identity()) }
    }

    val ALL_SPACES = ClientBackedCachedResource(CodeCatalystClient::class, "caws.orgs", Duration.ofDays(1)) {
        listSpacesPaginator {}
            .items()
            .map { it.name() }
    }

    val ALL_PROJECTS = ClientBackedCachedResource(CodeCatalystClient::class, "caws.projects", Duration.ofDays(1)) {
        val spaces = listSpacesPaginator {}
            .items()
            .map { it.name() }

        spaces.flatMap { space ->
            listAccessibleProjectsPaginator { it.spaceName(space) }
                .items()
                .map { CawsProject(space = space, project = it.name()) }
        }
    }

    fun codeRepositories(cawsProject: CawsProject) =
        ClientBackedCachedResource(CodeCatalystClient::class, "caws.codeRepos.${cawsProject.space}.${cawsProject.project}", Duration.ofDays(1)) {
            listSourceRepositoriesPaginator {
                it.spaceName(cawsProject.space)
                it.projectName(cawsProject.project)
            }.items().map {
                CawsCodeRepository(cawsProject.space, cawsProject.project, it.name())
            }
        }

    fun cloneUrls(cawsCodeRepository: CawsCodeRepository) =
        ClientBackedCachedResource(
            CodeCatalystClient::class,
            "caws.codeRepos.${cawsCodeRepository.space}.${cawsCodeRepository.project}.${cawsCodeRepository.name}.cloneUrls",
            Duration.ofDays(1)
        ) {
            getSourceRepositoryCloneUrls {
                it.spaceName(cawsCodeRepository.space)
                it.projectName(cawsCodeRepository.project)
                it.sourceRepositoryName(cawsCodeRepository.name)
            }.https()
        }
}


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

import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.wm.StatusBar
import com.intellij.openapi.wm.StatusBarWidget
import com.intellij.openapi.wm.StatusBarWidgetFactory
import com.intellij.util.Consumer
import java.awt.Component
import java.awt.event.MouseEvent

private const val WIDGET_ID = "CawsSpaceProjectInfo"

class CawsStatusBarInstaller : StatusBarWidgetFactory {
    private val spaceName: String? = System.getenv(CawsConstants.CAWS_ENV_ORG_NAME_VAR)
    private val projectName: String? = System.getenv(CawsConstants.CAWS_ENV_PROJECT_NAME_VAR)

    override fun getId(): String = WIDGET_ID

    override fun getDisplayName(): String = "$spaceName/$projectName"

    override fun isAvailable(project: Project): Boolean = spaceName != null && projectName != null

    override fun createWidget(project: Project): StatusBarWidget = CawsSpaceProjectInfo(spaceName, projectName)

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

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

private class CawsSpaceProjectInfo(val spaceName: String?, val projectName: String?) :
    StatusBarWidget,
    StatusBarWidget.TextPresentation {

    override fun ID(): String = WIDGET_ID

    override fun getPresentation(): StatusBarWidget.WidgetPresentation = this

    override fun getTooltipText(): String = "$spaceName/$projectName"
    override fun getClickConsumer(): Consumer<MouseEvent>? = null

    override fun getText(): String = "$spaceName/$projectName"

    override fun getAlignment(): Float = Component.CENTER_ALIGNMENT

    override fun dispose() {}

    override fun install(statusBar: StatusBar) {}
}


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

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

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider
import com.jetbrains.jsonSchema.extension.JsonSchemaProviderFactory
import com.jetbrains.jsonSchema.extension.SchemaType
import com.jetbrains.jsonSchema.impl.JsonSchemaVersion
import com.jetbrains.jsonSchema.remote.JsonFileResolver
import software.aws.toolkits.resources.message

class DevFileSchemaProviderFactory : JsonSchemaProviderFactory {
    override fun getProviders(project: Project): List<JsonSchemaFileProvider> = listOf(
        object : JsonSchemaFileProvider {
            override fun getName(): String = message("caws.devfile.schema")

            override fun isAvailable(file: VirtualFile): Boolean = file.name.matches(Regex("devfile\\.y[a]?ml", RegexOption.IGNORE_CASE))

            override fun getSchemaFile(): VirtualFile? = JsonFileResolver.urlToFile(schemaUrl)

            override fun getSchemaType(): SchemaType = SchemaType.remoteSchema

            override fun getSchemaVersion(): JsonSchemaVersion = JsonSchemaVersion.SCHEMA_7
        }
    )

    private companion object {
        const val schemaUrl: String = "https://github.com/devfile/api/releases/latest/download/devfile.json"
    }
}


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

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

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

class OpenCawsProfileAction : DumbAwareAction() {
    override fun actionPerformed(e: AnActionEvent) {
        BrowserUtil.browse(CawsEndpoints.ConsoleFactory.userHome())
    }
}


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

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

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.HttpUriRequest
import org.apache.http.entity.ContentType
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClientBuilder
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.services.caws.CawsConstants
import software.aws.toolkits.jetbrains.services.caws.envclient.models.CreateDevfileRequest
import software.aws.toolkits.jetbrains.services.caws.envclient.models.CreateDevfileResponse
import software.aws.toolkits.jetbrains.services.caws.envclient.models.GetStatusResponse
import software.aws.toolkits.jetbrains.services.caws.envclient.models.StartDevfileRequest
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message

class CawsEnvironmentClient(
    private val endpoint: String = "http://127.0.0.1:1339",
    private val httpClient: CloseableHttpClient = HttpClientBuilder.create().build()
) : Disposable {
    private val objectMapper = jacksonObjectMapper().also {
        it.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
        it.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    }

    private val authToken: String? by lazy {
        System.getenv(CawsConstants.CAWS_ENV_AUTH_TOKEN_VAR)
    }

    /**
     * Create a devfile for the project.
     */
    fun createDevfile(request: CreateDevfileRequest): CreateDevfileResponse {
        val body = objectMapper.writeValueAsString(request)
        val httpRequest = HttpPost("$endpoint/devfile/create").also {
            it.entity = StringEntity(body, ContentType.APPLICATION_JSON)
        }
        val response = execute(httpRequest)
        return objectMapper.readValue(response.entity.content)
    }

    /**
     * Start an action with the payload.
     */
    fun startDevfile(request: StartDevfileRequest) {
        val body = objectMapper.writeValueAsString(request)
        val httpRequest = HttpPost("$endpoint/start").also {
            it.entity = StringEntity(body, ContentType.APPLICATION_JSON)
        }

        // currently response will never be read if the call succeeds since the env restarts. the api impl, however does try to return 200
        try {
            execute(httpRequest).use {
                if (it.statusLine.statusCode != 200) {
                    val message = try {
                        message("caws.rebuild.devfile.failed_server", objectMapper.readTree(it.entity.content).get("message").asText())
                    } catch (e: Exception) {
                        LOG.error(e) { "Couldn't parse response from /start API" }
                        message("caws.rebuild.devfile.failed", request.location ?: "null")
                    }

                    notifyError(message("caws.rebuild.failed.title"), message)
                }
            }
        } catch (e: Exception) {
            throw IllegalStateException(message("caws.rebuild.failed.title"), e)
        }
    }

    /**
     * Get status and action type
     */
    fun getStatus(): GetStatusResponse {
        val request = HttpGet("$endpoint/status")
        val response = execute(request)
        return objectMapper.readValue(response.entity.content)
    }

    private fun execute(request: HttpUriRequest): CloseableHttpResponse {
        request.addHeader("Authorization", authToken)
        return httpClient.execute(request)
    }

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

    companion object {
        fun getInstance() = service<CawsEnvironmentClient>()

        private val LOG = getLogger<CawsEnvironmentClient>()
    }
}


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

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

/**
 * Project to create a devfile from.
 * @param path
 */
data class CreateDevfileRequest(
    val path: String
)


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

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

/**
 * Devfile object construct.
 * @param location
 */
data class CreateDevfileResponse(
    val location: String
)


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

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

/**
 * @param message A description of the error condition
 */
data class Error(
    /* A description of the error condition */
    val message: String? = null
)


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

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

import com.fasterxml.jackson.annotation.JsonEnumDefaultValue
import com.fasterxml.jackson.annotation.JsonValue

/**
 * Status object construct.
 * @param actionId
 * @param status
 * @param message
 */
data class GetStatusResponse(
    val actionId: String? = null,
    val status: Status? = null,
    val message: String? = null,
    val location: String? = null
) {
    /**
     * Values: PENDING,STABLE,CHANGED,IMAGES-UPDATE-AVAILABLE
     */
    enum class Status(@JsonValue val value: kotlin.String) {
        PENDING("PENDING"),
        STABLE("STABLE"),
        CHANGED("CHANGED"),
        IMAGES_UPDATE_AVAILABLE("IMAGES-UPDATE-AVAILABLE"),

        @JsonEnumDefaultValue
        UNKNOWN("UNKNOWN")
    }
}


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

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

data class StartDevfileRequest(
    val location: String? = null,
    val recreateHomeVolumes: Boolean? = null
)


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

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

import com.intellij.credentialStore.CredentialAttributes
import com.intellij.credentialStore.Credentials
import com.intellij.credentialStore.generateServiceName
import com.intellij.ide.passwordSafe.PasswordSafe
import software.amazon.awssdk.services.codecatalyst.CodeCatalystClient
import software.amazon.awssdk.services.codecatalyst.model.ServiceQuotaExceededException
import software.aws.toolkits.resources.message

private val SUBSYSTEM = "AWS Toolkit - ${message("code.aws")} PAT"

private fun credentialAttributes(user: String) = CredentialAttributes(generateServiceName(SUBSYSTEM, user))

fun getPat(user: String) =
    PasswordSafe.instance.get(credentialAttributes(user))

fun patExists(user: String) = getPat(user) != null

fun generateAndStorePat(cawsClient: CodeCatalystClient, user: String) {
    // ideally we invalidate any existing PAT but we don't have that information

    val pat = try {
        cawsClient.createAccessToken {
            it.name("$user-AwsJetBrainsToolkit-${System.currentTimeMillis()}")
        }.secret()
    } catch (e: ServiceQuotaExceededException) {
        // warn user has too many PATs
        throw e
    }

    val credentialAttributes = credentialAttributes(user)
    PasswordSafe.instance.set(credentialAttributes, Credentials(user, pat))
}


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

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

import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.services.caws.CawsCodeRepository
import software.aws.toolkits.jetbrains.services.caws.CawsProject

class CawsProjectSettings(private val project: Project) {
    // TODO: state needs a message bus so we can clean up random reads everywhere
    val state = CawsProjectSettingsState()

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

data class CawsProjectSettingsState(
    var cawsProject: CawsProject? = null,
    var codeRepo: CawsCodeRepository? = null
)


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

package software.aws.toolkits.jetbrains.services.cloudformation

import com.intellij.openapi.application.ApplicationManager
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.amazon.awssdk.services.cloudformation.model.ChangeSetStatus
import software.amazon.awssdk.services.cloudformation.model.CloudFormationException
import software.amazon.awssdk.services.cloudformation.model.DescribeStacksRequest
import software.amazon.awssdk.services.cloudformation.model.Stack
import software.amazon.awssdk.services.cloudformation.model.StackStatus
import software.aws.toolkits.core.utils.wait
import software.aws.toolkits.resources.message
import java.time.Duration

fun CloudFormationClient.executeChangeSetAndWait(stackName: String, changeSet: String) {
    val isCreate = try {
        val describeStack = this.describeStacks { it.stackName(stackName) }
        describeStack.stacks().firstOrNull()?.stackStatus() == StackStatus.REVIEW_IN_PROGRESS
    } catch (e: CloudFormationException) {
        if (e.message?.contains("Stack with id $stackName does not exist") == true) {
            true
        } else {
            throw e
        }
    }

    this.executeChangeSet { it.stackName(stackName).changeSetName(changeSet) }

    if (isCreate) {
        this.waitForStackCreateComplete(stackName)
    } else {
        this.waitForStackUpdateComplete(stackName)
    }
}

fun CloudFormationClient.describeStack(stackName: String, callback: (Stack?) -> Unit) {
    ApplicationManager.getApplication().executeOnPooledThread {
        val stack = this.describeStacks { it.stackName(stackName) }.stacks().firstOrNull()
        callback(stack)
    }
}

fun CloudFormationClient.describeStackForSync(stackName: String, enableParamsAndTags: (Boolean) -> Unit, callback: (Stack?) -> Unit) {
    ApplicationManager.getApplication().executeOnPooledThread {
        try {
            enableParamsAndTags(false)
            val stack = this.describeStacks { it.stackName(stackName) }.stacks().firstOrNull()
            callback(stack)
        } catch (e: Exception) {
            /* no-op */
        } finally {
            enableParamsAndTags(true)
        }
    }
}

private val CFN_CREATE_FAILURE_TERMINAL_STATES = setOf(
    StackStatus.CREATE_FAILED,
    StackStatus.DELETE_COMPLETE,
    StackStatus.DELETE_FAILED,
    StackStatus.ROLLBACK_FAILED,
    StackStatus.ROLLBACK_COMPLETE
)

fun CloudFormationClient.waitForStackCreateComplete(
    stackName: String,
    maxAttempts: Int = 720,
    delay: Duration = Duration.ofSeconds(5)
) {
    wait(
        call = { this.describeStacks(DescribeStacksRequest.builder().stackName(stackName).build()).stacks()[0] },
        success = { stack -> stack.stackStatus() == StackStatus.CREATE_COMPLETE },
        fail = { stack ->
            if (stack.stackStatus() in CFN_CREATE_FAILURE_TERMINAL_STATES) {
                message("cloudformation.create_stack.failed", stack.stackName(), stack.stackStatus())
            } else {
                null
            }
        },
        failByException = { e ->
            if (e is CloudFormationException && e.awsErrorDetails().errorCode() == "ValidationError") {
                message("cloudformation.create_stack.failed_validation", stackName)
            } else {
                null
            }
        },
        timeoutErrorMessage = message("cloudformation.create_stack.timeout", stackName, maxAttempts * delay.seconds),
        attempts = maxAttempts,
        delay = delay
    )
}

private val CFN_UPDATE_FAILURE_TERMINAL_STATES = setOf(
    StackStatus.UPDATE_ROLLBACK_COMPLETE,
    StackStatus.UPDATE_ROLLBACK_FAILED
)

fun CloudFormationClient.waitForStackUpdateComplete(
    stackName: String,
    maxAttempts: Int = 720,
    delay: Duration = Duration.ofSeconds(5)
) {
    wait(
        call = { this.describeStacks(DescribeStacksRequest.builder().stackName(stackName).build()).stacks()[0] },
        success = { stack -> stack.stackStatus() == StackStatus.UPDATE_COMPLETE },
        fail = { stack ->
            if (stack.stackStatus() in CFN_UPDATE_FAILURE_TERMINAL_STATES) {
                message("cloudformation.update_stack.failed", stack.stackName(), stack.stackStatus())
            } else {
                null
            }
        },
        failByException = { e ->
            if (e is CloudFormationException && e.awsErrorDetails().errorCode() == "ValidationError") {
                message("cloudformation.update_stack.failed_validation", stackName)
            } else {
                null
            }
        },
        timeoutErrorMessage = message("cloudformation.update_stack.timeout", stackName, maxAttempts * delay.seconds),
        attempts = maxAttempts,
        delay = delay
    )
}

private val CFN_DELETE_FAILURE_TERMINAL_STATES = setOf(
    StackStatus.CREATE_FAILED,
    StackStatus.DELETE_FAILED,
    StackStatus.ROLLBACK_FAILED,
    StackStatus.UPDATE_ROLLBACK_IN_PROGRESS,
    StackStatus.UPDATE_ROLLBACK_FAILED
)

fun CloudFormationClient.waitForStackDeletionComplete(
    stackName: String,
    maxAttempts: Int = 120,
    delay: Duration = Duration.ofSeconds(20)
) {
    wait(
        call = { this.describeStacks(DescribeStacksRequest.builder().stackName(stackName).build()).stacks()[0] },
        success = { stack -> stack.stackStatus() == StackStatus.DELETE_COMPLETE },
        fail = { stack ->
            if (stack.stackStatus() in CFN_DELETE_FAILURE_TERMINAL_STATES) {
                message("cloudformation.delete_stack.failed", stack.stackName(), stack.stackStatus())
            } else {
                null
            }
        },
        successByException = { e -> e is CloudFormationException && e.awsErrorDetails().errorCode() == "ValidationError" },
        timeoutErrorMessage = message("cloudformation.delete_stack.timeout", stackName, maxAttempts * delay.seconds),
        attempts = maxAttempts,
        delay = delay
    )
}

fun CloudFormationClient.waitForChangeSetCreateComplete(
    stackName: String,
    changeSetName: String,
    maxAttempts: Int = 120,
    delay: Duration = Duration.ofSeconds(2)
) {
    wait(
        call = {
            describeChangeSet {
                it.stackName(stackName)
                it.changeSetName(changeSetName)
            }
        },
        success = { it.status() == ChangeSetStatus.CREATE_COMPLETE },
        fail = {
            if (it.status() == ChangeSetStatus.FAILED) {
                it.statusReason()
            } else {
                null
            }
        },
        attempts = maxAttempts,
        delay = delay
    )
}


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

package software.aws.toolkits.jetbrains.services.cloudformation

import com.intellij.openapi.project.Project
import icons.AwsIcons
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.amazon.awssdk.services.cloudformation.model.StackStatus
import software.amazon.awssdk.services.cloudformation.model.StackSummary
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.cloudformation.resources.CloudFormationResources
import software.aws.toolkits.jetbrains.services.cloudformation.stack.StackWindowManager
import software.aws.toolkits.jetbrains.utils.toHumanReadable
import software.aws.toolkits.resources.message

class CloudFormationServiceNode(project: Project, service: AwsExplorerServiceNode) : CacheBackedAwsExplorerServiceRootNode<StackSummary>(
    project,
    service,
    CloudFormationResources.ACTIVE_STACKS
) {
    override fun displayName(): String = message("explorer.node.cloudformation")
    override fun toNode(child: StackSummary): AwsExplorerNode<*> = CloudFormationStackNode(nodeProject, child.stackName(), child.stackStatus(), child.stackId())
}

class CloudFormationStackNode(
    project: Project,
    val stackName: String,
    private val stackStatus: StackStatus,
    val stackId: String
) : AwsExplorerResourceNode<String>(
    project,
    CloudFormationClient.SERVICE_NAME,
    stackName,
    AwsIcons.Resources.CLOUDFORMATION_STACK
) {
    override fun resourceType() = "stack"

    override fun resourceArn() = stackId

    override fun displayName() = stackName

    override fun statusText(): String = stackStatus.toString().toHumanReadable()

    override fun onDoubleClick() {
        StackWindowManager.getInstance(nodeProject).openStack(stackName, stackId)
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudformation

import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import org.jetbrains.yaml.YAMLFileType
import org.jetbrains.yaml.YAMLLanguage
import org.jetbrains.yaml.psi.YAMLSequence
import software.aws.toolkits.jetbrains.services.cloudformation.yaml.YamlCloudFormationTemplate
import software.aws.toolkits.resources.message
import java.io.File

interface CloudFormationTemplate {
    fun resources(): Sequence<Resource>
    fun parameters(): Sequence<Parameter>
    fun globals(): Map<String, NamedMap>

    fun getResourceByName(logicalName: String): Resource? = resources().firstOrNull { it.logicalName == logicalName }

    fun saveTo(file: File) {
        FileUtil.createIfNotExists(file)
        file.writeText(text())
    }

    fun text(): String

    companion object {
        fun parse(project: Project, templateFile: VirtualFile): CloudFormationTemplate = when {
            isYaml(templateFile) -> YamlCloudFormationTemplate(project, templateFile)
            else -> throw UnsupportedOperationException("Only YAML CloudFormation templates are supported")
        }

        fun convertPsiToResource(psiElement: PsiElement): Resource? = when (psiElement.language) {
            YAMLLanguage.INSTANCE -> YamlCloudFormationTemplate.convertPsiToResource(psiElement)
            else -> throw UnsupportedOperationException("Only YAML CloudFormation templates are supported")
        }

        private fun isYaml(templateFile: VirtualFile): Boolean = templateFile.fileType == YAMLFileType.YML ||
            templateFile.extension?.toLowerCase() in YAML_EXTENSIONS

        private val YAML_EXTENSIONS = setOf("yaml", "yml")
    }
}

interface NamedMap {
    val logicalName: String

    fun getScalarProperty(key: String): String
    fun getOptionalScalarProperty(key: String): String?
    fun setScalarProperty(key: String, value: String)
    fun getSequenceProperty(key: String): YAMLSequence
    fun getOptionalSequenceProperty(key: String): YAMLSequence?
}

interface Resource : NamedMap {
    val cloudFormationTemplate: CloudFormationTemplate
    fun isType(requestedType: String): Boolean
    fun type(): String?
    fun getScalarMetadata(key: String): String
    fun getOptionalScalarMetadata(key: String): String?
}

interface Parameter : NamedMap {
    fun defaultValue(): String?
    fun description(): String?
    fun constraintDescription(): String?
}

class CloudFormationParameter(private val delegate: NamedMap) : NamedMap by delegate, Parameter {

    override fun defaultValue(): String? = getOptionalScalarProperty("Default")

    override fun description(): String? = getOptionalScalarProperty("Description")

    override fun constraintDescription(): String? = getOptionalScalarProperty("ConstraintDescription")
}

class MutableParameter(private val copyFrom: Parameter) : Parameter {
    private var defaultValue: String? = copyFrom.defaultValue()
    private val description: String? = copyFrom.description()
    private val constraintDescription: String? = copyFrom.constraintDescription()

    override val logicalName: String
        get() = copyFrom.logicalName

    override fun getScalarProperty(key: String): String {
        throw NotImplementedError()
    }

    override fun getOptionalScalarProperty(key: String): String? {
        throw NotImplementedError()
    }

    override fun setScalarProperty(key: String, value: String) {
        throw NotImplementedError()
    }

    override fun getSequenceProperty(key: String): YAMLSequence {
        throw NotImplementedError()
    }

    override fun getOptionalSequenceProperty(key: String): YAMLSequence? {
        throw NotImplementedError()
    }

    override fun defaultValue(): String? = defaultValue

    override fun description(): String? = description

    override fun constraintDescription(): String? = constraintDescription

    fun setDefaultValue(value: String?) {
        defaultValue = value
    }
}

/**
 * Merge remote parameters from a CloudFormation stack to construct a preferred [Parameter] list.
 *
 * @return The merged preferred [Parameter] list
 */
fun List<Parameter>.mergeRemoteParameters(remoteParameters: List<software.amazon.awssdk.services.cloudformation.model.Parameter>): List<Parameter> =
    this.map { templateParameter ->
        val mutableParameter = MutableParameter(templateParameter)
        remoteParameters.find { it.parameterKey() == templateParameter.logicalName }?.let {
            mutableParameter.setDefaultValue(it.parameterValue())
        }
        mutableParameter
    }.toList()

/**
 * Validate if the cloudformation template has any valid resources at all
 *
 * @param virtualFile SAM template file
 * @return null if there are any valid resources, or an error message otherwise.
 */
fun Project.validateSamTemplateHasResources(virtualFile: VirtualFile): String? {
    val path = virtualFile.path
    CloudFormationTemplateIndex
        .listResources(this, { true }, virtualFile)
        .ifEmpty { return message("serverless.application.deploy.error.no_resources", path) }
    return null
}


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

package software.aws.toolkits.jetbrains.services.cloudformation

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.DataIndexer
import com.intellij.util.indexing.DefaultFileTypeSpecificInputFilter
import com.intellij.util.indexing.FileBasedIndex
import com.intellij.util.indexing.FileBasedIndexExtension
import com.intellij.util.indexing.FileContent
import com.intellij.util.indexing.ID
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.KeyDescriptor
import org.jetbrains.yaml.YAMLFileType
import org.jetbrains.yaml.psi.YAMLKeyValue
import software.aws.toolkits.jetbrains.services.cloudformation.yaml.YamlCloudFormationTemplate
import java.io.DataInput
import java.io.DataOutput

class CloudFormationTemplateIndex : FileBasedIndexExtension<String, MutableList<IndexedResource>>() {
    private val fileFilter by lazy {
        val supportedFiles = arrayOf(YAMLFileType.YML)

        object : DefaultFileTypeSpecificInputFilter(*supportedFiles) {
            override fun acceptInput(file: VirtualFile): Boolean = file.isInLocalFileSystem
        }
    }

    override fun getValueExternalizer(): DataExternalizer<MutableList<IndexedResource>> = object : DataExternalizer<MutableList<IndexedResource>> {
        override fun save(dataOutput: DataOutput, value: MutableList<IndexedResource>) {
            dataOutput.writeInt(value.size)
            value.forEach { resource -> resource.save(dataOutput) }
        }

        override fun read(dataInput: DataInput): MutableList<IndexedResource> {
            val resourceCount = dataInput.readInt()
            val resources = mutableListOf<IndexedResource>()
            repeat(resourceCount) {
                resources.add(IndexedResource.read(dataInput))
            }
            return resources
        }
    }

    override fun getName(): ID<String, MutableList<IndexedResource>> = NAME

    override fun getIndexer(): DataIndexer<String, MutableList<IndexedResource>, FileContent> = DataIndexer { fileContent ->
        val indexedResources = mutableMapOf<String, MutableList<IndexedResource>>()

        fileContent.psiFile.acceptNode(
            object : PsiElementVisitor() {
                override fun visitElement(element: PsiElement) {
                    super.visitElement(element)
                    val parent = element.parent as? YAMLKeyValue ?: return
                    if (parent.value != element) return

                    val resource = YamlCloudFormationTemplate.convertPsiToResource(parent) ?: return
                    val resourceType = resource.type() ?: return
                    IndexedResource.from(resource)?.let {
                        indexedResources.computeIfAbsent(resourceType) { mutableListOf() }.add(it)
                    }
                }
            }
        )

        indexedResources
    }

    override fun getKeyDescriptor(): KeyDescriptor<String> = EnumeratorStringDescriptor.INSTANCE

    override fun getVersion(): Int = 3

    override fun getInputFilter(): FileBasedIndex.InputFilter = fileFilter

    override fun dependsOnFileContent(): Boolean = true

    companion object {
        private val NAME: ID<String, MutableList<IndexedResource>> = ID.create("CloudFormationTemplateIndex")

        private fun PsiElement.acceptNode(visitor: PsiElementVisitor) {
            accept(visitor)

            if (children.isNotEmpty()) {
                children.forEach { it.acceptNode(visitor) }
            }
        }

        fun listResources(
            project: Project,
            resourceTypeFilter: (String) -> Boolean = { true },
            virtualFile: VirtualFile? = null
        ): Collection<IndexedResource> {
            val index = FileBasedIndex.getInstance()
            val scope = virtualFile?.let { GlobalSearchScope.fileScope(project, it) } ?: GlobalSearchScope.projectScope(project)
            return ApplicationManager.getApplication().runReadAction<Collection<IndexedResource>> {
                index.getAllKeys(NAME, project)
                    .asSequence()
                    .filter(resourceTypeFilter)
                    .mapNotNull { index.getValues(NAME, it, scope) }
                    .filter { it.isNotEmpty() }
                    .flatten()
                    .flatten()
                    .toList()
            }
        }

        fun listResourcesByType(project: Project, type: String): Collection<IndexedResource> = listResources(project, resourceTypeFilter = { it == type })

        fun listFunctions(project: Project, virtualFile: VirtualFile? = null): Collection<IndexedFunction> =
            listResources(project, virtualFile = virtualFile).filterIsInstance(IndexedFunction::class.java)
    }
}