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

package software.aws.toolkits.jetbrains.services.codewhisperer.util

import software.amazon.awssdk.services.codewhisperer.model.ArtifactType
import software.amazon.awssdk.services.codewhisperer.model.CodeScanFindingsSchema
import software.amazon.awssdk.services.codewhisperer.model.CodeScanStatus
import software.amazon.awssdk.services.codewhisperer.model.CodeWhispererResponseMetadata
import software.amazon.awssdk.services.codewhisperer.model.CreateCodeScanRequest
import software.amazon.awssdk.services.codewhisperer.model.CreateCodeScanResponse
import software.amazon.awssdk.services.codewhisperer.model.CreateCodeScanUploadUrlRequest
import software.amazon.awssdk.services.codewhisperer.model.GetCodeScanRequest
import software.amazon.awssdk.services.codewhisperer.model.GetCodeScanResponse
import software.amazon.awssdk.services.codewhisperer.model.ListCodeScanFindingsRequest
import software.amazon.awssdk.services.codewhisperer.model.ListCodeScanFindingsResponse
import software.amazon.awssdk.services.codewhisperer.model.ProgrammingLanguage
import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlRequest
import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeAnalysisRequest
import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeAnalysisResponse
import software.amazon.awssdk.services.codewhispererruntime.model.ListCodeAnalysisFindingsRequest
import software.amazon.awssdk.services.codewhispererruntime.model.ListCodeAnalysisFindingsResponse
import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeAnalysisRequest
import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeAnalysisResponse

// TODO: rename to be a more imformative name
private fun ProgrammingLanguage.transform(): software.amazon.awssdk.services.codewhispererruntime.model.ProgrammingLanguage =
    software.amazon.awssdk.services.codewhispererruntime.model.ProgrammingLanguage.builder()
        .languageName(this.languageName())
        .build()

private fun ArtifactType.transform(): software.amazon.awssdk.services.codewhispererruntime.model.ArtifactType =
    software.amazon.awssdk.services.codewhispererruntime.model.ArtifactType.fromValue(this.toString())

private fun CodeScanFindingsSchema.transform(): software.amazon.awssdk.services.codewhispererruntime.model.CodeAnalysisFindingsSchema =
    software.amazon.awssdk.services.codewhispererruntime.model.CodeAnalysisFindingsSchema.fromValue(
        this.toString().replace("scan", "analysis")
    )

private fun software.amazon.awssdk.services.codewhispererruntime.model.CodeAnalysisStatus.transform(): CodeScanStatus =
    CodeScanStatus.fromValue(this.toString())

fun CreateCodeScanUploadUrlRequest.transform(): CreateUploadUrlRequest =
    CreateUploadUrlRequest.builder()
        .contentMd5(this.contentMd5())
        .artifactType(this.artifactType().transform())
        .build()

fun CreateCodeScanRequest.transform(): StartCodeAnalysisRequest =
    StartCodeAnalysisRequest.builder()
        .artifacts(this.artifacts().entries.map { it.key.transform() to it.value }.toMap())
        .programmingLanguage(this.programmingLanguage().transform())
        .clientToken(this.clientToken())
        .build()

fun StartCodeAnalysisResponse.transform(): CreateCodeScanResponse =
    CreateCodeScanResponse.builder()
        .jobId(this.jobId())
        .status(this.status().transform())
        .errorMessage(this.errorMessage())
        .responseMetadata(CodeWhispererResponseMetadata.create(this.responseMetadata()))
        .sdkHttpResponse(this.sdkHttpResponse())
        .build() as CreateCodeScanResponse

fun GetCodeScanRequest.transform(): GetCodeAnalysisRequest =
    GetCodeAnalysisRequest.builder()
        .jobId(this.jobId())
        .build()

fun GetCodeAnalysisResponse.transform(): GetCodeScanResponse =
    GetCodeScanResponse.builder()
        .status(this.status().transform())
        .errorMessage(this.errorMessage())
        .responseMetadata(CodeWhispererResponseMetadata.create(this.responseMetadata()))
        .sdkHttpResponse(this.sdkHttpResponse())
        .build() as GetCodeScanResponse

fun ListCodeScanFindingsRequest.transform(): ListCodeAnalysisFindingsRequest =
    ListCodeAnalysisFindingsRequest.builder()
        .jobId(this.jobId())
        .nextToken(this.nextToken())
        .codeAnalysisFindingsSchema(this.codeScanFindingsSchema().transform())
        .build()

fun ListCodeAnalysisFindingsResponse.transform(): ListCodeScanFindingsResponse = ListCodeScanFindingsResponse
    .builder()
    .codeScanFindings(this.codeAnalysisFindings())
    .nextToken(this.nextToken())
    .responseMetadata(CodeWhispererResponseMetadata.create(this.responseMetadata()))
    .sdkHttpResponse(this.sdkHttpResponse())
    .build() as ListCodeScanFindingsResponse


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

package software.aws.toolkits.jetbrains.services.codewhisperer.util

import com.intellij.notification.NotificationAction
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import kotlinx.coroutines.yield
import software.amazon.awssdk.services.codewhispererruntime.model.Completion
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
import software.aws.toolkits.jetbrains.core.credentials.ManagedBearerSsoConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.loginSso
import software.aws.toolkits.jetbrains.core.credentials.maybeReauthProviderIfNeeded
import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererConnection
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererLoginLearnMoreAction
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererSsoLearnMoreAction
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.ConnectWithAwsToContinueActionError
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.ConnectWithAwsToContinueActionWarn
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.DoNotShowAgainActionError
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.DoNotShowAgainActionWarn
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererExpired
import software.aws.toolkits.jetbrains.services.codewhisperer.model.Chunk
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererService
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.jetbrains.utils.notifyWarn
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodewhispererCompletionType

fun VirtualFile.content(): String = VfsUtil.loadText(this)

// we call it a chunk every 10 lines of code
// [[L1, L2, ...L10], [L11, L12, ...L20]...]
// use VirtualFile.toCodeChunk instead
suspend fun String.toCodeChunk(path: String): List<Chunk> {
    val chunks = this.trimEnd()

    var chunksOfStringsPreprocessed = chunks
        .split("\n")
        .chunked(10)
        .map { chunkContent ->
            yield()
            chunkContent.joinToString(separator = "\n").trimEnd()
        }

    // special process for edge case: first since first chunk is never referenced by other chunk, we define first 3 lines of its content referencing the first
    chunksOfStringsPreprocessed = listOf(
        chunksOfStringsPreprocessed
            .first()
            .split("\n")
            .take(3)
            .joinToString(separator = "\n").trimEnd()
    ) + chunksOfStringsPreprocessed

    return chunksOfStringsPreprocessed.mapIndexed { index, chunkContent ->
        yield()
        val nextChunkContent = if (index == chunksOfStringsPreprocessed.size - 1) {
            chunkContent
        } else {
            chunksOfStringsPreprocessed[index + 1]
        }
        Chunk(
            content = chunkContent,
            path = path,
            nextChunk = nextChunkContent
        )
    }
}

// we refer 10 lines of code as "Code Chunk"
// [[L1, L2, ...L10], [L11, L12, ...L20]...]
// use VirtualFile.toCodeChunk
// TODO: path as param is weird
fun VirtualFile.toCodeChunk(path: String): Sequence<Chunk> = sequence {
    var prevChunk: String? = null
    inputStream.bufferedReader(Charsets.UTF_8).useLines {
        val iter = it.chunked(10).iterator()
        while (iter.hasNext()) {
            val currentChunk = iter.next().joinToString("\n").trimEnd()

            // chunk[0]
            if (prevChunk == null) {
                val first3Lines = currentChunk.split("\n").take(3).joinToString("\n").trimEnd()
                yield(Chunk(content = first3Lines, path = path, nextChunk = currentChunk))
            } else {
                // chunk[1]...chunk[n-1]
                prevChunk?.let { chunk ->
                    yield(Chunk(content = chunk, path = path, nextChunk = currentChunk))
                }
            }

            prevChunk = currentChunk
        }

        prevChunk?.let { lastChunk ->
            // chunk[n]
            yield(Chunk(content = lastChunk, path = path, nextChunk = lastChunk))
        }
    }
}

object CodeWhispererUtil {
    fun checkCompletionType(
        results: List<Completion>,
        noRecommendation: Boolean
    ): CodewhispererCompletionType {
        if (noRecommendation) {
            return CodewhispererCompletionType.Unknown
        }
        return if (results[0].content().contains("\n")) {
            CodewhispererCompletionType.Block
        } else {
            CodewhispererCompletionType.Line
        }
    }

    // return true if every recommendation is empty
    fun checkEmptyRecommendations(recommendations: List<Completion>): Boolean =
        recommendations.all { it.content().isEmpty() }

    fun notifyWarnCodeWhispererUsageLimit(project: Project? = null) {
        notifyWarn(
            message("codewhisperer.notification.usage_limit.warn.title"),
            message("codewhisperer.notification.usage_limit.codesuggestion.warn.content"),
            project,
        )
    }

    fun notifyErrorCodeWhispererUsageLimit(project: Project? = null, isCodeScan: Boolean = false) {
        notifyError(
            "",
            if (!isCodeScan) {
                message("codewhisperer.notification.usage_limit.codesuggestion.warn.content")
            } else {
                message("codewhisperer.notification.usage_limit.codescan.warn.content")
            },
            project,
        )
    }

    // show when user login with Accountless
    fun notifyWarnAccountless() = notifyWarn(
        "",
        message("codewhisperer.notification.accountless.warn.message"),
        null,
        listOf(CodeWhispererSsoLearnMoreAction(), ConnectWithAwsToContinueActionWarn(), DoNotShowAgainActionWarn())
    )

    // show after user selects Don't Show Again in Accountless login message
    fun notifyInfoAccountless() = notifyInfo(
        "",
        message("codewhisperer.notification.accountless.info.dont.show.again.message"),
        null,
        listOf(CodeWhispererLoginLearnMoreAction())
    )

    // show when user login with Accountless and Accountless is not supported by CW
    fun notifyErrorAccountless() = notifyError(
        "",
        message("codewhisperer.notification.accountless.error.message"),
        null,
        listOf(CodeWhispererSsoLearnMoreAction(), ConnectWithAwsToContinueActionError(), DoNotShowAgainActionError())
    )

    fun isAccessTokenExpired(project: Project): Boolean {
        val tokenProvider = tokenProvider(project) ?: return false
        val state = tokenProvider.state()
        return state == BearerTokenAuthState.NEEDS_REFRESH
    }

    fun isRefreshTokenExpired(project: Project): Boolean {
        val tokenProvider = tokenProvider(project) ?: return false
        val state = tokenProvider.state()
        return state == BearerTokenAuthState.NOT_AUTHENTICATED
    }

    // This will be called only when there's a CW connection, but it has expired(either accessToken or refreshToken)
    // 1. If connection is expired, try to refresh
    // 2. If not able to refresh, requesting re-login by showing a notification
    // 3. The notification will be shown
    //   3.1 At most once per IDE restarts.
    //   3.2 At most once after IDE restarts,
    //   for example, when user performs security scan or fetch code completion for the first time
    // Return true if need to re-auth, false otherwise
    fun promptReAuth(project: Project, isPluginStarting: Boolean = false): Boolean {
        if (CodeWhispererService.hasReAuthPromptBeenShown()) return false
        if (!isCodeWhispererExpired(project)) return false
        val tokenProvider = tokenProvider(project) ?: return false
        return maybeReauthProviderIfNeeded(project, tokenProvider) {
            runInEdt {
                notifyConnectionExpiredRequestReauth(project)
                if (!isPluginStarting) {
                    CodeWhispererService.markReAuthPromptShown()
                }
            }
        }
    }

    private fun notifyConnectionExpiredRequestReauth(project: Project) {
        if (CodeWhispererExplorerActionManager.getInstance().getConnectionExpiredDoNotShowAgain()) {
            return
        }
        notifyError(
            message("toolkit.sso_expire.dialog.title"),
            message("toolkit.sso_expire.dialog_message"),
            project,
            listOf(
                NotificationAction.create(message("toolkit.sso_expire.dialog.yes_button")) { _, notification ->
                    reconnectCodeWhisperer(project)
                    notification.expire()
                },
                NotificationAction.create(message("toolkit.sso_expire.dialog.no_button")) { _, notification ->
                    CodeWhispererExplorerActionManager.getInstance().setConnectionExpiredDoNotShowAgain(true)
                    notification.expire()
                }
            )
        )
    }

    fun getConnectionStartUrl(connection: ToolkitConnection?): String? {
        connection ?: return null
        if (connection !is ManagedBearerSsoConnection) return null
        return connection.startUrl
    }

    private fun tokenProvider(project: Project) = (
        ToolkitConnectionManager
            .getInstance(project)
            .activeConnectionForFeature(CodeWhispererConnection.getInstance()) as? AwsBearerTokenConnection
        )
        ?.getConnectionSettings()
        ?.tokenProvider
        ?.delegate as? BearerTokenProvider

    fun reconnectCodeWhisperer(project: Project) {
        val connection = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(CodeWhispererConnection.getInstance())
        if (connection !is ManagedBearerSsoConnection) return
        ApplicationManager.getApplication().executeOnPooledThread {
            loginSso(project, connection.startUrl, connection.region, connection.scopes)
        }
    }
}

enum class CaretMovement {
    NO_CHANGE, MOVE_FORWARD, MOVE_BACKWARD
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.util

import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.roots.TestSourcesFilter
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiClassOwner
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiJavaFile
import com.intellij.psi.PsiPackage
import com.intellij.psi.search.GlobalSearchScope
import kotlinx.coroutines.yield
import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes

// version1: Utilize PSI import elements to resolve imported files
object JavaCodeWhispererFileCrawler : CodeWhispererFileCrawler() {
    override val fileExtension: String = ".java"
    override val testFilenamePattern: Regex = """(?:Test([^/\\]+)\.java|([^/\\]+)Test\.java)$""".toRegex()

    override fun guessSourceFileName(tstFileName: String): String = tstFileName.substring(0, tstFileName.length - "Test.java".length) + ".java"

    override fun listRelevantFilesInEditors(psiFile: PsiFile): List<VirtualFile> = FileEditorManager.getInstance(psiFile.project).openFiles.toList().filter {
        it.name != psiFile.virtualFile.name &&
            it.extension == psiFile.virtualFile.extension &&
            !TestSourcesFilter.isTestSources(it, psiFile.project)
    }

    override suspend fun listFilesImported(psiFile: PsiFile): List<VirtualFile> {
        if (psiFile !is PsiJavaFile) return emptyList()
        val result = mutableListOf<VirtualFile>()
        val imports = runReadAction { psiFile.importList?.allImportStatements }
        val activeFiles = FileEditorManager.getInstance(psiFile.project).openFiles.toSet()

        // only consider imported files which belong users' own package, thus [isInLocalFileSystem && isWritable]
        val fileHandleLambda = { virtualFile: VirtualFile ->
            if (virtualFile.isInLocalFileSystem && virtualFile.isWritable) {
                // prioritize active files on users' editor
                if (activeFiles.contains(virtualFile)) {
                    result.add(0, virtualFile)
                } else {
                    result.add(virtualFile)
                }
            }
        }

        imports?.forEach {
            yield()
            runReadAction { it.resolve() }?.let { psiElement ->
                // case like import javax.swing.*;
                if (psiElement is PsiPackage) {
                    val filesInPackage = psiElement.getFiles(GlobalSearchScope.allScope(psiFile.project)).mapNotNull { it.virtualFile }
                    filesInPackage.forEach { file ->
                        fileHandleLambda(file)
                    }
                } else {
                    // single file import
                    runReadAction {
                        psiElement.containingFile.virtualFile?.let { virtualFile ->
                            // file within users' project
                            fileHandleLambda(virtualFile)
                        }
                    }
                }
            }
        }

        return result
    }

    override fun listFilesWithinSamePackage(targetFile: PsiFile): List<VirtualFile> = runReadAction {
        targetFile.containingDirectory?.files?.mapNotNull {
            // exclude target file
            if (it != targetFile) {
                it.virtualFile
            } else {
                null
            }
        }.orEmpty()
    }

    override fun findFocalFileForTest(psiFile: PsiFile): VirtualFile? = findSourceFileByName(psiFile) ?: findRelevantFileFromEditors(psiFile)

    // psiFile = "MainTest.java", targetFileName = "Main.java"
    private fun findSourceFileByName(psiFile: PsiFile): VirtualFile? {
        val module = ModuleUtilCore.findModuleForFile(psiFile)

        return module?.rootManager?.getSourceRoots(JavaModuleSourceRootTypes.PRODUCTION)?.let { srcRoot ->
            srcRoot
                .map { root -> VfsUtil.collectChildrenRecursively(root) }
                .flatten()
                .find { !it.isDirectory && it.isWritable && it.name.contains(guessSourceFileName(psiFile.name)) }
        }
    }

    /**
     * check files in editors and pick one which has most substring matches to the target
     */
    private fun findRelevantFileFromEditors(psiFile: PsiFile): VirtualFile? = searchRelevantFileInEditors(psiFile) { myPsiFile ->
        myPsiFile as PsiClassOwner
        // (1)
        val classAndMethod = myPsiFile.classes.mapNotNull { clazz ->
            // class name itself + its method names
            listOfNotNull(clazz.name) +
                clazz.methods.mapNotNull { method -> method.name }
        }.flatten()

        classAndMethod
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.util

import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.jetbrains.python.psi.PyFile
import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes

object PythonCodeWhispererFileCrawler : CodeWhispererFileCrawler() {
    override val fileExtension: String = ".py"
    override val testFilenamePattern: Regex = Regex("""(?:test_([^/\\]+)\.py|([^/\\]+)_test\.py)${'$'}""")
    override suspend fun listFilesImported(psiFile: PsiFile): List<VirtualFile> = emptyList()

    // TODO: Add implementation once we enable python cross file
    override fun listRelevantFilesInEditors(psiFile: PsiFile): List<VirtualFile> = emptyList()

    override fun guessSourceFileName(tstFileName: String): String {
        assert(testFilenamePattern.matches(tstFileName))
        return tstFileName.substring(5)
    }

    override fun listFilesWithinSamePackage(psiFile: PsiFile): List<VirtualFile> {
        val targetPackagePath = psiFile.virtualFile.let {
            it.path.removeSuffix(it.name)
        }
        return listFilesUnderProjectRoot(psiFile.project).filter {
            val packagePath = it.path.removeSuffix(it.name)
            targetPackagePath == packagePath && it != psiFile.virtualFile
        }
    }

    override fun findFocalFileForTest(psiFile: PsiFile): VirtualFile? = findSourceFileByName(psiFile) ?: findRelevantFileFromEditors(psiFile)

    private fun findSourceFileByName(psiFile: PsiFile): VirtualFile? {
        val module = ModuleUtilCore.findModuleForFile(psiFile)

        return module?.rootManager?.getSourceRoots(JavaModuleSourceRootTypes.PRODUCTION)?.let { srcRoot ->
            srcRoot
                .map { root -> VfsUtil.collectChildrenRecursively(root) }
                .flatten()
                .find { !it.isDirectory && it.isWritable && it.name.contains(guessSourceFileName(psiFile.name)) }
        }
    }

    /**
     * check files in editors and pick one which has most substring matches to the target
     */
    private fun findRelevantFileFromEditors(psiFile: PsiFile): VirtualFile? = searchRelevantFileInEditors(psiFile) { myPsiFile ->
        myPsiFile as PyFile
        // (1)
        val classAndMethod = myPsiFile.topLevelClasses.mapNotNull {
            // class name itself + its method names
            listOfNotNull(it.name) +
                it.methods.mapNotNull { method -> method.name }
        }.flatten()

        // (2)
        val topLevelFunc = myPsiFile.topLevelFunctions.mapNotNull { it.name }

        classAndMethod + topLevelFunc
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic

import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.LightVirtualFile
import software.amazon.awssdk.arns.Arn
import software.amazon.awssdk.services.cloudcontrol.CloudControlClient
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.amazon.awssdk.services.cloudformation.model.RegistryType
import software.amazon.awssdk.services.cloudformation.model.Visibility
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.Resource
import software.aws.toolkits.jetbrains.core.map
import software.aws.toolkits.jetbrains.services.s3.resources.S3Resources

object CloudControlApiResources {
    fun listResources(typeName: String): Resource<List<DynamicResource>> =
        when (typeName) {
            S3_BUCKET -> S3Resources.LIST_BUCKETS.map { it.name() }
            else -> ClientBackedCachedResource(CloudControlClient::class, "cloudcontrolapi.dynamic.resources.$typeName") {
                this.listResourcesPaginator { req -> req.typeName(typeName) }
                    .flatMap { page -> page.resourceDescriptions().map { it.identifier() } }
            }
        }.map { DynamicResource(resourceTypeFromResourceTypeName(typeName), it) }

    fun resourceTypeFromResourceTypeName(typeName: String): ResourceType {
        val (_, svc, type) = typeName.split("::")
        return ResourceType(typeName, svc, type)
    }

    fun listResources(resourceType: ResourceType): Resource<List<DynamicResource>> = listResources(resourceType.fullName)

    fun getResourceDisplayName(identifier: String): String =
        if (identifier.startsWith("arn:")) {
            Arn.fromString(identifier).resourceAsString()
        } else {
            identifier
        }

    fun getResourceSchema(resourceType: String): Resource.Cached<VirtualFile> =
        ClientBackedCachedResource(CloudFormationClient::class, "cloudformation.dynamic.resources.schema.$resourceType") {
            val schema = this.describeType {
                it.type(RegistryType.RESOURCE)
                it.typeName(resourceType)
            }.schema()
            LightVirtualFile("${resourceType}Schema.json", schema)
        }

    fun listTypes(): Resource.Cached<List<String>> = ClientBackedCachedResource(CloudFormationClient::class, "cloudformation.listTypes") {
        this.listTypesPaginator {
            it.visibility(Visibility.PUBLIC)
            it.type(RegistryType.RESOURCE)
        }.flatMap { it.typeSummaries().map { it.typeName() } }
    }
    private const val S3_BUCKET = "AWS::S3::Bucket"
}

data class ResourceDetails(val operations: List<PermittedOperation>, val arnRegex: String?, val documentation: String?)

enum class PermittedOperation {
    CREATE, READ, UPDATE, DELETE, LIST
}


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

package software.aws.toolkits.jetbrains.services.dynamic

import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.services.cloudcontrol.model.Operation
import software.amazon.awssdk.services.cloudcontrol.model.OperationStatus
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.services.dynamic.explorer.OpenResourceModelSourceAction

class CreateResourceFileStatusHandler(private val project: Project) : DynamicResourceStateMutationHandler {
    private val resourceCreationProgressTracker: MutableMap<String, VirtualFile> = mutableMapOf()

    init {
        project.messageBus.connect(project).subscribe(DynamicResourceUpdateManager.DYNAMIC_RESOURCE_STATE_CHANGED, this)
    }

    fun recordResourceBeingCreated(token: String, file: VirtualFile) {
        resourceCreationProgressTracker[token] = file
    }

    override fun mutationStatusChanged(state: ResourceMutationState) {
        if (state.operation == Operation.CREATE && state.status == OperationStatus.SUCCESS && state.resourceIdentifier != null) {
            runInEdt {
                resourceCreationProgressTracker[state.token]?.let { FileEditorManager.getInstance(project).closeFile(it) }
                resourceCreationProgressTracker.remove(state.token)
            }

            val dynamicResourceIdentifier = DynamicResourceIdentifier(state.connectionSettings, state.resourceType, state.resourceIdentifier)
            val model = OpenViewEditableDynamicResourceVirtualFile.getResourceModel(
                project,
                state.connectionSettings.awsClient(),
                state.resourceType,
                state.resourceIdentifier
            ) ?: return
            val file = ViewEditableDynamicResourceVirtualFile(
                dynamicResourceIdentifier,
                model
            )
            OpenViewEditableDynamicResourceVirtualFile.openFile(project, file, OpenResourceModelSourceAction.READ, state.resourceType)
        }
    }

    @TestOnly
    fun getNumberOfResourcesBeingCreated(): Int = resourceCreationProgressTracker.size

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


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

package software.aws.toolkits.jetbrains.services.dynamic

import com.intellij.ide.browsers.BrowserLauncher
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotifications
import com.jetbrains.jsonSchema.ide.JsonSchemaService
import software.aws.toolkits.jetbrains.core.experiments.isEnabled
import software.aws.toolkits.resources.message

class DynamicResourceFileActionProvider :
    EditorNotifications.Provider<DynamicResourceFileActionProvider.DynamicResourceVirtualFilePanel>() {
    override fun getKey(): Key<DynamicResourceVirtualFilePanel> = KEY

    override fun createNotificationPanel(file: VirtualFile, fileEditor: FileEditor, project: Project):
        DynamicResourceVirtualFilePanel? {
        if (!JsonResourceModificationExperiment.isEnabled()) return null
        return when (file) {
            is CreateDynamicResourceVirtualFile ->
                DynamicResourceVirtualFilePanel(
                    project,
                    file,
                    message("dynamic_resources.create_resource_instruction"),
                    "dynamic.resource.editor.submitResourceCreationRequest"
                ).also { DynamicResourceSchemaMapping.getInstance().addResourceSchemaMapping(project, file) }
            is ViewEditableDynamicResourceVirtualFile ->
                when (file.isWritable) {
                    true -> DynamicResourceVirtualFilePanel(
                        project,
                        file,
                        message("dynamic_resources.update_resource_instruction"),
                        "dynamic.resource.editor.submitResourceUpdateRequest"
                    ).also { DynamicResourceSchemaMapping.getInstance().addResourceSchemaMapping(project, file) }
                    false -> DynamicResourceVirtualFilePanel(
                        project,
                        file,
                        message("dynamic_resources.edit_resource_instruction"),
                        "dynamic.resource.editor.enableEditingResource"
                    ).also { JsonSchemaService.Impl.get(project).reset() }
                }
            else -> null
        }
    }

    class DynamicResourceVirtualFilePanel(project: Project, file: DynamicResourceVirtualFile, text: String, actionId: String) : EditorNotificationPanel() {
        init {
            text(text)
            DynamicResourceSupportedTypes.getInstance().getDocs(file.dynamicResourceType)?.let { docUrl ->
                createActionLabel(message("dynamic_resources.type.explorer.view_documentation")) {
                    BrowserLauncher.instance.browse(docUrl, project = project)
                }
            }

            val action = ActionManager.getInstance().getAction(actionId)
            action.templateText?.let {
                createActionLabel(it) {
                    executeAction(actionId)
                    EditorNotifications.getInstance(project).updateNotifications(file)
                }
            }
        }
    }

    companion object {
        val KEY = Key.create<DynamicResourceVirtualFilePanel>("software.aws.toolkits.jetbrains.core.dynamic.resource.file.actions")
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic

import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration
import com.jetbrains.jsonSchema.ide.JsonSchemaService
import org.jetbrains.annotations.TestOnly

class DynamicResourceSchemaMapping {
    private val currentlyActiveResourceTypes: MutableSet<String> = mutableSetOf()

    fun addResourceSchemaMapping(
        project: Project,
        file: DynamicResourceVirtualFile
    ) {
        val configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project).findMappingForFile(file)
        if (configuration == null) {
            currentlyActiveResourceTypes.add(file.dynamicResourceType)
            JsonSchemaService.Impl.get(project).reset()
        }
    }

    fun getCurrentlyActiveResourceTypes(): Set<String> = currentlyActiveResourceTypes

    @TestOnly
    fun removeCurrentlyActiveResourceTypes(project: Project) {
        currentlyActiveResourceTypes.clear()
        JsonSchemaService.Impl.get(project).reset()
    }

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


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

package software.aws.toolkits.jetbrains.services.dynamic

import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.amazon.awssdk.services.cloudformation.model.ProvisioningType
import software.amazon.awssdk.services.cloudformation.model.Visibility
import kotlin.coroutines.coroutineContext

class DynamicResourcesProvider(private val cfnClient: CloudFormationClient) {
    suspend fun listSupportedTypes(): List<ResourceType> = withContext(coroutineContext) {
        val mutable = async {
            cfnClient.listTypesPaginator {
                it.visibility(Visibility.PUBLIC)
                it.provisioningType(ProvisioningType.FULLY_MUTABLE)
            }
        }

        val immutable = async {
            cfnClient.listTypesPaginator {
                it.visibility(Visibility.PUBLIC)
                it.provisioningType(ProvisioningType.IMMUTABLE)
            }
        }
        val types = mutable.await() + immutable.await()

        types.flatMap { resp ->
            resp.typeSummaries().map { summary ->
                CloudControlApiResources.resourceTypeFromResourceTypeName(summary.typeName())
            }
        }
    }
}

data class ResourceType(val fullName: String, val service: String, val name: String)
data class DynamicResource(val type: ResourceType, val identifier: String)


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

package software.aws.toolkits.jetbrains.services.dynamic

import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.cloudcontrol.model.OperationStatus
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.explorer.ExplorerToolWindow
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceTelemetryResources.addOperationToTelemetry
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.DynamicresourceTelemetry
import software.aws.toolkits.telemetry.Result
import java.time.temporal.ChronoUnit
import java.util.concurrent.atomic.AtomicBoolean

class DynamicResourceStateChangedNotificationHandler(private val project: Project) : DynamicResourceStateMutationHandler {
    private val refreshRequired = AtomicBoolean(false)
    override fun mutationStatusChanged(state: ResourceMutationState) {
        if (state.status == OperationStatus.SUCCESS) {
            notifyInfo(
                message(
                    "dynamic_resources.operation_status_notification_title",
                    state.resourceIdentifier ?: state.resourceType,
                    state.operation.name.toLowerCase()
                ),
                message(
                    "dynamic_resources.operation_status_success",
                    state.resourceIdentifier ?: state.resourceType,
                    state.operation.name.toLowerCase()
                ),
                project
            )
            DynamicresourceTelemetry.mutateResource(
                project,
                Result.Succeeded,
                state.resourceType,
                addOperationToTelemetry(state.operation),
                ChronoUnit.MILLIS.between(state.startTime, DynamicResourceTelemetryResources.getCurrentTime()).toDouble()
            )
        } else if (state.status == OperationStatus.FAILED) {
            if (state.message.isNullOrBlank()) {
                displayErrorMessage(
                    state,
                    message(
                        "dynamic_resources.operation_status_failed_no_message",
                        state.resourceIdentifier ?: state.resourceType,
                        state.operation.name.toLowerCase()
                    )
                )
            } else {
                displayErrorMessage(
                    state,
                    message(
                        "dynamic_resources.operation_status_failed",
                        state.resourceIdentifier ?: state.resourceType,
                        state.operation.name.toLowerCase(),
                        state.message
                    )
                )
            }
            DynamicresourceTelemetry.mutateResource(
                project,
                Result.Failed,
                state.resourceType,
                addOperationToTelemetry(state.operation),
                ChronoUnit.MILLIS.between(state.startTime, DynamicResourceTelemetryResources.getCurrentTime()).toDouble()
            )
        }
        AwsResourceCache.getInstance().clear(CloudControlApiResources.listResources(state.resourceType), state.connectionSettings)
        refreshRequired.set(true)
    }

    private fun displayErrorMessage(state: ResourceMutationState, errorMessage: String) {
        notifyError(
            message(
                "dynamic_resources.operation_status_notification_title",
                state.resourceIdentifier ?: state.resourceType,
                state.operation.name.toLowerCase()
            ),
            errorMessage,
            project
        )
    }

    override fun statusCheckComplete() {
        runInEdt {
            if (refreshRequired.getAndSet(false)) {
                ExplorerToolWindow.getInstance(project).invalidateTree()
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic

import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Alarm
import com.intellij.util.AlarmFactory
import com.intellij.util.messages.Topic
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.cloudcontrol.CloudControlClient
import software.amazon.awssdk.services.cloudcontrol.model.Operation
import software.amazon.awssdk.services.cloudcontrol.model.OperationStatus
import software.amazon.awssdk.services.cloudcontrol.model.ProgressEvent
import software.amazon.awssdk.services.cloudcontrol.model.RequestTokenNotFoundException
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceTelemetryResources.addOperationToTelemetry
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DynamicResourceOperation
import software.aws.toolkits.telemetry.DynamicresourceTelemetry
import software.aws.toolkits.telemetry.Result
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.ConcurrentLinkedQueue

internal class DynamicResourceUpdateManager(private val project: Project) {
    // TODO: Make DynamicResourceUpdateManager an application-level service

    private val pendingMutations = ConcurrentLinkedQueue<ResourceMutationState>()
    private val coroutineScope = projectCoroutineScope(project)
    private val alarm: Alarm =
        AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, project)

    fun deleteResource(dynamicResourceIdentifier: DynamicResourceIdentifier) {
        coroutineScope.launch {
            try {
                val client = dynamicResourceIdentifier.connectionSettings.awsClient<CloudControlClient>()
                val progress = client.deleteResource {
                    it.typeName(dynamicResourceIdentifier.resourceType)
                    it.identifier(dynamicResourceIdentifier.resourceIdentifier)
                }.progressEvent()
                startCheckingProgress(dynamicResourceIdentifier.connectionSettings, progress, DynamicResourceTelemetryResources.getCurrentTime())
            } catch (e: Exception) {
                e.notifyError(
                    message(
                        "dynamic_resources.operation_status_notification_title",
                        dynamicResourceIdentifier.resourceIdentifier,
                        message("general.delete").toLowerCase()
                    ),
                    project
                )
                DynamicresourceTelemetry.mutateResource(
                    project,
                    Result.Failed,
                    dynamicResourceIdentifier.resourceType,
                    addOperationToTelemetry(Operation.DELETE),
                    0.0
                )
            }
        }
    }

    fun updateResource(dynamicResourceIdentifier: DynamicResourceIdentifier, patchOperation: String) {
        coroutineScope.launch {
            try {
                val client = dynamicResourceIdentifier.connectionSettings.awsClient<CloudControlClient>()
                val progress = client.updateResource {
                    it.typeName(dynamicResourceIdentifier.resourceType)
                    it.identifier(dynamicResourceIdentifier.resourceIdentifier)
                    it.patchDocument(patchOperation)
                }.progressEvent()
                startCheckingProgress(dynamicResourceIdentifier.connectionSettings, progress, DynamicResourceTelemetryResources.getCurrentTime())
            } catch (e: Exception) {
                e.notifyError(
                    message(
                        "dynamic_resources.operation_status_notification_title",
                        dynamicResourceIdentifier.resourceIdentifier,
                        message("dynamic_resources.editor.submitResourceUpdateRequest_text").toLowerCase()
                    ),
                    project
                )
                DynamicresourceTelemetry.mutateResource(
                    project,
                    Result.Failed,
                    dynamicResourceIdentifier.resourceType,
                    addOperationToTelemetry(Operation.UPDATE),
                    0.0
                )
            }
        }
    }

    fun createResource(connectionSettings: ConnectionSettings, dynamicResourceType: String, desiredState: String, file: VirtualFile) {
        coroutineScope.launch {
            try {
                val client = connectionSettings.awsClient<CloudControlClient>()
                val progress = client.createResource {
                    it.typeName(dynamicResourceType)
                    it.desiredState(desiredState)
                }.progressEvent()

                CreateResourceFileStatusHandler.getInstance(project).recordResourceBeingCreated(progress.requestToken(), file)
                startCheckingProgress(connectionSettings, progress, DynamicResourceTelemetryResources.getCurrentTime())
            } catch (e: Exception) {
                e.notifyError(
                    message("dynamic_resources.operation_status_notification_title", dynamicResourceType, message("general.create".decapitalize())),
                    project
                )
                DynamicresourceTelemetry.mutateResource(project, Result.Failed, dynamicResourceType, addOperationToTelemetry(Operation.CREATE), 0.0)
            }
        }
    }

    private fun startCheckingProgress(connectionSettings: ConnectionSettings, progress: ProgressEvent, startTime: Instant) {
        pendingMutations.add(ResourceMutationState.fromEvent(connectionSettings, progress, startTime))
        if (pendingMutations.size == 1) {
            alarm.addRequest({ getProgress() }, 0)
        }
    }

    fun getUpdateStatus(dynamicResourceIdentifier: DynamicResourceIdentifier): ResourceMutationState? =
        pendingMutations.find {
            it.connectionSettings == dynamicResourceIdentifier.connectionSettings &&
                it.resourceType == dynamicResourceIdentifier.resourceType &&
                it.resourceIdentifier == dynamicResourceIdentifier.resourceIdentifier
        }

    private fun getProgress() {
        var size = pendingMutations.size
        while (size > 0) {
            val mutation = pendingMutations.remove()

            val client = mutation.connectionSettings.awsClient<CloudControlClient>()
            val (progressEvent, shouldDropFromPendingQueue) = try {
                val progress = client.getResourceRequestStatus { it.requestToken(mutation.token) }
                progress.progressEvent() to progress.progressEvent().operationStatus().isTerminal()
            } catch (e: Exception) {
                when (e) {
                    is RequestTokenNotFoundException -> {
                        e.notifyError(
                            message(
                                "dynamic_resources.operation_status_notification_title",
                                mutation.resourceIdentifier ?: mutation.resourceType,
                                mutation.operation.name.toLowerCase()
                            ),
                            project
                        )
                        DynamicresourceTelemetry.mutateResource(
                            project,
                            Result.Failed,
                            mutation.resourceType,
                            addOperationToTelemetry(mutation.operation),
                            ChronoUnit.MILLIS.between(mutation.startTime, DynamicResourceTelemetryResources.getCurrentTime()).toDouble()
                        )
                        null to true
                    }
                    else -> null to false
                }
            }
            val updatedMutation = when (progressEvent) {
                is ProgressEvent -> mutation.copy(
                    status = progressEvent.operationStatus(),
                    resourceIdentifier = progressEvent.identifier(),
                    message = progressEvent.statusMessage()
                )
                else -> mutation
            }
            if (updatedMutation != mutation) {
                project.messageBus.syncPublisher(DYNAMIC_RESOURCE_STATE_CHANGED).mutationStatusChanged(updatedMutation)
            }

            if (!shouldDropFromPendingQueue) {
                pendingMutations.add(updatedMutation)
            }

            size--
        }
        project.messageBus.syncPublisher(DYNAMIC_RESOURCE_STATE_CHANGED).statusCheckComplete()

        if (pendingMutations.size != 0) {
            alarm.addRequest({ getProgress() }, DEFAULT_DELAY)
        }
    }

    companion object {
        private const val DEFAULT_DELAY = 500
        val DYNAMIC_RESOURCE_STATE_CHANGED: Topic<DynamicResourceStateMutationHandler> = Topic.create(
            "Resource State Changed",
            DynamicResourceStateMutationHandler::class.java
        )

        fun OperationStatus.isTerminal() = this in setOf(OperationStatus.SUCCESS, OperationStatus.CANCEL_COMPLETE, OperationStatus.FAILED)

        fun getInstance(project: Project): DynamicResourceUpdateManager = project.service()
    }
}

interface DynamicResourceStateMutationHandler {
    fun mutationStatusChanged(state: ResourceMutationState)
    fun statusCheckComplete() {}
}

data class DynamicResourceIdentifier(val connectionSettings: ConnectionSettings, val resourceType: String, val resourceIdentifier: String)

data class ResourceMutationState(
    val connectionSettings: ConnectionSettings,
    val token: String,
    val operation: Operation,
    val resourceType: String,
    val status: OperationStatus,
    val resourceIdentifier: String?,
    val message: String?,
    val startTime: Instant
) {
    companion object {
        fun fromEvent(connectionSettings: ConnectionSettings, progress: ProgressEvent, startTime: Instant) =
            ResourceMutationState(
                connectionSettings = connectionSettings,
                token = progress.requestToken(),
                operation = progress.operation(),
                resourceType = progress.typeName(),
                status = progress.operationStatus(),
                resourceIdentifier = progress.identifier(),
                message = progress.statusMessage(),
                startTime = startTime
            )
    }
}

object DynamicResourceTelemetryResources {
    fun addOperationToTelemetry(operation: Operation): DynamicResourceOperation = when (operation) {
        Operation.CREATE -> DynamicResourceOperation.Create
        Operation.UPDATE -> DynamicResourceOperation.Update
        Operation.DELETE -> DynamicResourceOperation.Delete
        else -> DynamicResourceOperation.Unknown
    }

    fun getCurrentTime(): Instant = Instant.now()
}


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

package software.aws.toolkits.jetbrains.services.dynamic

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.openapi.components.service
import software.aws.toolkits.jetbrains.utils.runUnderProgressIfNeeded
import software.aws.toolkits.resources.message

class DynamicResourceSupportedTypes {

    private val supportedTypes by lazy {
        runUnderProgressIfNeeded(null, message("dynamic_resources.loading_manifest"), cancelable = false) {
            this.javaClass.getResourceAsStream("/cloudapi/dynamic_resources.json")?.use { resourceStream ->
                MAPPER.readValue<Map<String, ResourceDetails>>(resourceStream)
            } ?: throw RuntimeException("dynamic resource manifest not found")
        }
    }

    fun getSupportedTypes(): List<String> = supportedTypes.filterValues { it.operations.contains(PermittedOperation.LIST) }.keys.toList()

    fun getDocs(resourceType: String) = supportedTypes[resourceType]?.documentation

    companion object {
        fun getInstance(): DynamicResourceSupportedTypes = service()
        private val MAPPER = jacksonObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic

import com.intellij.json.JsonFileType
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.psi.codeStyle.CodeStyleManager
import com.intellij.psi.util.PsiUtilCore
import com.intellij.testFramework.LightVirtualFile
import software.amazon.awssdk.services.cloudcontrol.CloudControlClient
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.services.dynamic.explorer.OpenResourceModelSourceAction
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DynamicresourceTelemetry

sealed class DynamicResourceVirtualFile(fileName: String, val dynamicResourceType: String, fileContent: String) :
    LightVirtualFile(
        fileName,
        JsonFileType.INSTANCE,
        fileContent
    )

class CreateDynamicResourceVirtualFile(val connectionSettings: ConnectionSettings, dynamicResourceType: String) :
    DynamicResourceVirtualFile(
        message("dynamic_resources.create_resource_file_name", dynamicResourceType),
        dynamicResourceType,
        InitialCreateDynamicResourceContent.initialContent
    )

class ViewEditableDynamicResourceVirtualFile(val dynamicResourceIdentifier: DynamicResourceIdentifier, fileContent: String) :
    DynamicResourceVirtualFile(
        CloudControlApiResources.getResourceDisplayName(dynamicResourceIdentifier.resourceIdentifier),
        dynamicResourceIdentifier.resourceType,
        fileContent
    )

object InitialCreateDynamicResourceContent {
    const val initialContent = "{}"
}

object OpenViewEditableDynamicResourceVirtualFile {
    fun openFile(project: Project, file: ViewEditableDynamicResourceVirtualFile, sourceAction: OpenResourceModelSourceAction, resourceType: String) {
        WriteCommandAction.runWriteCommandAction(project) {
            CodeStyleManager.getInstance(project).reformat(PsiUtilCore.getPsiFile(project, file))
            if (sourceAction == OpenResourceModelSourceAction.READ) {
                file.isWritable = false
                DynamicresourceTelemetry.getResource(project, success = true, resourceType = resourceType)
            } else if (sourceAction == OpenResourceModelSourceAction.EDIT) {
                file.isWritable = true
            }
            FileEditorManager.getInstance(project).openFile(file, true)
        }
    }

    fun getResourceModel(project: Project, client: CloudControlClient, resourceType: String, resourceIdentifier: String): String? = try {
        client.getResource {
            it.typeName(resourceType)
            it.identifier(resourceIdentifier)
        }
            .resourceDescription()
            .properties()
    } catch (e: Exception) {
        notifyError(
            project = project,
            title = message("dynamic_resources.fetch.fail.title"),
            content = message("dynamic_resources.fetch.fail.content", resourceIdentifier)
        )
        DynamicresourceTelemetry.getResource(project, success = false, resourceType = resourceType)
        null
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic

import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.EditorNotifications
import software.aws.toolkits.jetbrains.core.experiments.ToolkitExperiment
import software.aws.toolkits.jetbrains.core.experiments.ToolkitExperimentStateChangedListener
import software.aws.toolkits.jetbrains.core.experiments.suggest
import software.aws.toolkits.resources.message

object JsonResourceModificationExperiment : ToolkitExperiment(
    "jsonResourceModification",
    { message("dynamic_resources.experiment.title") },
    { message("dynamic_resources.experiment.description") }
)

class SuggestEditExperimentListener : FileEditorManagerListener {
    override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
        if (file is DynamicResourceVirtualFile) {
            JsonResourceModificationExperiment.suggest()
        }
    }
}

class UpdateOnExperimentState(private val project: Project) : ToolkitExperimentStateChangedListener {
    override fun enableSettingsStateChanged(toolkitExperiment: ToolkitExperiment) {
        if (toolkitExperiment is JsonResourceModificationExperiment) {
            with(EditorNotifications.getInstance(project)) {
                FileEditorManager.getInstance(project).openFiles.filterIsInstance<DynamicResourceVirtualFile>().forEach { updateNotifications(it) }
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic

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 software.aws.toolkits.jetbrains.core.getResourceNow

class ResourceSchemaProviderFactory : JsonSchemaProviderFactory {
    override fun getProviders(project: Project): List<JsonSchemaFileProvider> {
        val schemaProviders = mutableListOf<JsonSchemaFileProvider>()
        DynamicResourceSchemaMapping.getInstance().getCurrentlyActiveResourceTypes().forEach {
            val schemaFile = object : JsonSchemaFileProvider {
                override fun isAvailable(file: VirtualFile): Boolean =
                    file is DynamicResourceVirtualFile && file.dynamicResourceType == it && file.isWritable

                override fun getName(): String = "$it schema"

                override fun getSchemaFile(): VirtualFile? = project.getResourceNow(CloudControlApiResources.getResourceSchema(it))

                override fun getSchemaVersion(): JsonSchemaVersion = JsonSchemaVersion.SCHEMA_7

                override fun getSchemaType(): SchemaType = SchemaType.embeddedSchema
            }
            schemaProviders.add(schemaFile)
        }
        return schemaProviders
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.editor.actions

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import software.aws.toolkits.resources.message

class EnableEditingResourceAction : AnAction(message("dynamic_resources.editor.enableEditingResource_text")) {
    override fun actionPerformed(e: AnActionEvent) {
        val psiFile = e.getData(CommonDataKeys.PSI_FILE) ?: throw Exception("file not found")
        val file = psiFile.virtualFile
        file?.isWritable = true
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.editor.actions

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.ui.Messages
import software.aws.toolkits.jetbrains.services.dynamic.CreateDynamicResourceVirtualFile
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceUpdateManager
import software.aws.toolkits.jetbrains.services.dynamic.InitialCreateDynamicResourceContent
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message

class SubmitResourceCreationRequestAction : AnAction(message("general.create")) {

    override fun actionPerformed(e: AnActionEvent) {
        val psiFile = e.getData(CommonDataKeys.PSI_FILE)
        val file = psiFile?.virtualFile as? CreateDynamicResourceVirtualFile ?: return
        val resourceType = file.dynamicResourceType

        val contentString = psiFile.text
        val continueWithContent = if (contentString == InitialCreateDynamicResourceContent.initialContent) {
            Messages.showYesNoDialog(
                psiFile.project,
                message("dynamic_resources.create_resource_file_empty"),
                message("dynamic_resources.create_resource_file_empty_title"),
                Messages.getWarningIcon()
            ) == Messages.YES
        } else {
            true
        }
        if (continueWithContent) {
            notifyInfo(
                message("dynamic_resources.resource_creation", resourceType),
                message("dynamic_resources.begin_resource_creation", resourceType),
                psiFile.project
            )
            DynamicResourceUpdateManager.getInstance(psiFile.project).createResource(file.connectionSettings, file.dynamicResourceType, contentString, file)
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.editor.actions

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.flipkart.zjsonpatch.JsonDiff
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.Messages.showYesNoDialog
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceUpdateManager
import software.aws.toolkits.jetbrains.services.dynamic.ViewEditableDynamicResourceVirtualFile
import software.aws.toolkits.resources.message

class SubmitResourceUpdateRequestAction : AnAction(message("dynamic_resources.editor.submitResourceUpdateRequest_text")) {
    override fun actionPerformed(e: AnActionEvent) {
        val psiFile = e.getData(CommonDataKeys.PSI_FILE)
        val file = psiFile?.virtualFile as? ViewEditableDynamicResourceVirtualFile ?: return

        val content = psiFile.text
        val patchOperations = JsonDiff.asJson(mapper.readTree(file.inputStream), mapper.readTree(content))
        if (patchOperations.isEmpty) {
            if (showYesNoDialog(
                    psiFile.project,
                    message("dynamic_resources.update_resource_no_changes_made"),
                    message("dynamic_resources.update_resource_no_changes_made_title"),
                    Messages.getWarningIcon()
                ) == Messages.NO
            ) {
                file.isWritable = false
            }
        } else {
            file.isWritable = false
            DynamicResourceUpdateManager.getInstance(psiFile.project).updateResource(file.dynamicResourceIdentifier, patchOperations.toPrettyString())
        }
    }

    companion object {
        val mapper = jacksonObjectMapper()
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer

import com.intellij.icons.AllIcons
import com.intellij.ide.projectView.PresentationData
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceSupportedTypes
import software.aws.toolkits.jetbrains.settings.DynamicResourcesConfigurable
import software.aws.toolkits.jetbrains.settings.DynamicResourcesSettings
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DynamicresourceTelemetry

class DynamicResourceSelectorNode(nodeProject: Project) : AwsExplorerNode<Unit>(nodeProject, Unit, AllIcons.Actions.Edit) {
    override fun displayName() = message("explorer.node.other.add_remove")

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

    override fun onDoubleClick() {
        ShowSettingsUtil.getInstance().showSettingsDialog(null, DynamicResourcesConfigurable::class.java)
        DynamicresourceTelemetry.selectResources(nodeProject)
    }

    override fun update(presentation: PresentationData) {
        val remaining = DynamicResourceSupportedTypes.getInstance().getSupportedTypes().size - DynamicResourcesSettings.getInstance().selected.size
        presentation.tooltip = message("dynamic_resources.add_remove_resources_tooltip", remaining)
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer

import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.ui.EditorNotifications
import software.amazon.awssdk.services.cloudcontrol.model.UnsupportedActionException
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.getConnectionSettingsOrThrow
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerEmptyNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.ResourceActionNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.ResourceParentNode
import software.aws.toolkits.jetbrains.core.getResourceNow
import software.aws.toolkits.jetbrains.services.dynamic.CloudControlApiResources
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResource
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceIdentifier
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceUpdateManager
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceUpdateManager.Companion.isTerminal
import software.aws.toolkits.jetbrains.services.dynamic.OpenViewEditableDynamicResourceVirtualFile
import software.aws.toolkits.jetbrains.services.dynamic.ViewEditableDynamicResourceVirtualFile
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DynamicresourceTelemetry
import software.aws.toolkits.telemetry.Result

class DynamicResourceResourceTypeNode(project: Project, val resourceType: String) :
    AwsExplorerNode<String>(project, resourceType, null),
    ResourceParentNode,
    ResourceActionNode {

    override fun displayName(): String = resourceType
    override fun isAlwaysShowPlus(): Boolean = true

    // calls to CloudAPI time-expensive and likely to throttle
    override fun isAlwaysExpand(): Boolean = false

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

    override fun getChildrenInternal(): List<AwsExplorerNode<*>> = try {
        nodeProject.getResourceNow(CloudControlApiResources.listResources(resourceType))
            .map { DynamicResourceNode(nodeProject, it) }
            .also { DynamicresourceTelemetry.listResource(project = nodeProject, success = true, resourceType = resourceType) }
    } catch (e: Exception) {
        when (e) {
            is UnsupportedActionException -> {
                DynamicresourceTelemetry.listResource(project = nodeProject, result = Result.Cancelled, resourceType = resourceType)
                listOf(AwsExplorerEmptyNode(nodeProject, message("dynamic_resources.unavailable_in_region", region.id)))
            }
            else -> {
                DynamicresourceTelemetry.listResource(project = nodeProject, success = false, resourceType = resourceType)
                throw e
            }
        }
    }

    override fun actionGroupName(): String = "aws.toolkit.explorer.dynamic.resource.type"
}

class UnavailableDynamicResourceTypeNode(project: Project, resourceType: String) : AwsExplorerNode<String>(project, resourceType, null) {
    override fun statusText(): String = message("dynamic_resources.unavailable_in_region", region.id)
    override fun getChildren(): List<AwsExplorerNode<*>> = emptyList()
    override fun isAlwaysLeaf(): Boolean = true
}

class DynamicResourceNode(project: Project, val resource: DynamicResource) :
    AwsExplorerNode<DynamicResource>(project, resource, null),
    ResourceActionNode {

    override fun actionGroupName() = "aws.toolkit.explorer.dynamic.resource"
    override fun displayName(): String = CloudControlApiResources.getResourceDisplayName(resource.identifier)

    override fun statusText(): String? {
        val state = DynamicResourceUpdateManager.getInstance(nodeProject)
            .getUpdateStatus(DynamicResourceIdentifier(nodeProject.getConnectionSettingsOrThrow(), resource.type.fullName, resource.identifier))?.takeIf {
                !it.status.isTerminal()
            }
            ?: return null
        return "${state.operation} ${state.status}"
    }

    override fun isAlwaysShowPlus(): Boolean = false
    override fun isAlwaysLeaf(): Boolean = true
    override fun getChildren(): List<AwsExplorerNode<*>> = emptyList()
    override fun onDoubleClick() = openResourceModelInEditor(OpenResourceModelSourceAction.READ)

    fun openResourceModelInEditor(sourceAction: OpenResourceModelSourceAction) {
        val dynamicResourceIdentifier = DynamicResourceIdentifier(nodeProject.getConnectionSettingsOrThrow(), resource.type.fullName, resource.identifier)
        val openFiles = FileEditorManager.getInstance(nodeProject).openFiles.filter {
            it is ViewEditableDynamicResourceVirtualFile && it.dynamicResourceIdentifier == dynamicResourceIdentifier
        }
        if (openFiles.isEmpty()) {
            object : Task.Backgroundable(nodeProject, message("dynamic_resources.fetch.indicator_title", resource.identifier), true) {
                override fun run(indicator: ProgressIndicator) {
                    indicator.text = message("dynamic_resources.fetch.fetch")
                    val model = OpenViewEditableDynamicResourceVirtualFile.getResourceModel(
                        nodeProject,
                        nodeProject.awsClient(),
                        resource.type.fullName,
                        resource.identifier
                    ) ?: return
                    val file = ViewEditableDynamicResourceVirtualFile(
                        dynamicResourceIdentifier,
                        model
                    )

                    indicator.text = message("dynamic_resources.fetch.open")
                    OpenViewEditableDynamicResourceVirtualFile.openFile(nodeProject, file, sourceAction, resource.type.fullName)
                }
            }.queue()
        } else {
            val openFile = openFiles.first()
            if (sourceAction == OpenResourceModelSourceAction.EDIT) {
                openFile.isWritable = true
            }
            FileEditorManager.getInstance(nodeProject).openFile(openFile, true)
            EditorNotifications.getInstance(nodeProject).updateNotifications(openFile)
        }
    }

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

enum class OpenResourceModelSourceAction {
    READ, EDIT
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer

import com.intellij.ide.util.treeView.AbstractTreeNode
import software.aws.toolkits.jetbrains.core.explorer.AwsExplorerTreeStructureProvider
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerRootNode

class DynamicResourceTreeStructureProvider : AwsExplorerTreeStructureProvider() {
    override fun modify(parent: AbstractTreeNode<*>, children: MutableCollection<AbstractTreeNode<*>>): MutableCollection<AbstractTreeNode<*>> {
        if (parent is OtherResourcesNode) {
            val list = children.toMutableList()
            // Forces the filter node to the start of the list
            val index = list.indexOfFirst { it is DynamicResourceSelectorNode }
            if (index > 0) {
                list.add(0, list.removeAt(index))
            }

            return list
        }

        if (parent !is AwsExplorerRootNode) {
            return children
        }

        val list = children.toMutableList()
        // Forces the other resources node to the end of the list
        val index = list.indexOfFirst { it is OtherResourcesNode }
        if (index >= 0) {
            list.add(list.removeAt(index))
        }

        return list
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer

import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerServiceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.ResourceActionNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.ResourceParentNode
import software.aws.toolkits.jetbrains.core.getResourceNow
import software.aws.toolkits.jetbrains.services.dynamic.CloudControlApiResources
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceSupportedTypes
import software.aws.toolkits.jetbrains.settings.DynamicResourcesSettings
import software.aws.toolkits.resources.message

class OtherResourcesNode(project: Project, service: AwsExplorerServiceNode) :
    AwsExplorerNode<AwsExplorerServiceNode>(project, service, null),
    ResourceParentNode,
    ResourceActionNode {

    override fun displayName(): String = message("explorer.node.other")
    override fun isAlwaysShowPlus(): Boolean = true

    override fun getChildren(): List<AwsExplorerNode<*>> = super.getChildren()
    override fun getChildrenInternal(): List<AwsExplorerNode<*>> {
        val shouldShow = DynamicResourcesSettings.getInstance().selected
        val resourcesAvailableInRegion = nodeProject.getResourceNow(CloudControlApiResources.listTypes()).toSet()

        return listOf(DynamicResourceSelectorNode(nodeProject)) + DynamicResourceSupportedTypes.getInstance().getSupportedTypes()
            .filter { it in shouldShow }
            .map { resourceType ->
                if (resourceType in resourcesAvailableInRegion) {
                    DynamicResourceResourceTypeNode(nodeProject, resourceType)
                } else {
                    UnavailableDynamicResourceTypeNode(nodeProject, resourceType)
                }
            }
    }

    override fun actionGroupName(): String = "aws.toolkit.explorer.dynamic.more.resources"
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.DumbAwareAction
import software.aws.toolkits.jetbrains.settings.DynamicResourcesConfigurable
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DynamicresourceTelemetry

class AddResourcesToExplorerAction : DumbAwareAction(
    { message("explorer.node.other.add_remove") },
    AllIcons.Actions.Edit
) {
    override fun actionPerformed(e: AnActionEvent) {
        ShowSettingsUtil.getInstance().showSettingsDialog(null, DynamicResourcesConfigurable::class.java)
        DynamicresourceTelemetry.selectResources(e.project)
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleExplorerNodeAction
import software.aws.toolkits.jetbrains.services.dynamic.explorer.DynamicResourceNode
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.DynamicresourceTelemetry
import java.awt.datatransfer.StringSelection

class CopyIdentifierAction :
    SingleExplorerNodeAction<DynamicResourceNode>(message("explorer.copy_identifier"), icon = AllIcons.Actions.Copy),
    DumbAware {
    override fun actionPerformed(selected: DynamicResourceNode, e: AnActionEvent) {
        CopyPasteManager.getInstance().setContents(StringSelection(selected.resource.identifier))
        DynamicresourceTelemetry.copyIdentifier(selected.nodeProject, resourceType = selected.resource.type.fullName)
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer.actions

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.DumbAware
import com.intellij.psi.codeStyle.CodeStyleManager
import com.intellij.psi.util.PsiUtilCore
import software.aws.toolkits.jetbrains.core.credentials.getConnectionSettingsOrThrow
import software.aws.toolkits.jetbrains.core.experiments.isEnabled
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleExplorerNodeAction
import software.aws.toolkits.jetbrains.services.dynamic.CreateDynamicResourceVirtualFile
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceSchemaMapping
import software.aws.toolkits.jetbrains.services.dynamic.JsonResourceModificationExperiment
import software.aws.toolkits.jetbrains.services.dynamic.explorer.DynamicResourceResourceTypeNode
import software.aws.toolkits.resources.message

class CreateResourceAction :
    SingleExplorerNodeAction<DynamicResourceResourceTypeNode>(message("dynamic_resources.type.explorer.create_resource")), DumbAware {

    override fun actionPerformed(selected: DynamicResourceResourceTypeNode, e: AnActionEvent) {
        val file = CreateDynamicResourceVirtualFile(
            selected.nodeProject.getConnectionSettingsOrThrow(),
            selected.value
        )
        // TODO: Populate the file with required properties in the schema

        DynamicResourceSchemaMapping.getInstance().addResourceSchemaMapping(selected.nodeProject, file)
        WriteCommandAction.runWriteCommandAction(selected.nodeProject) {
            CodeStyleManager.getInstance(selected.nodeProject).reformat(PsiUtilCore.getPsiFile(selected.nodeProject, file))
            FileEditorManager.getInstance(selected.nodeProject).openFile(file, true)
            file.isWritable = true
        }
    }

    override fun update(selected: DynamicResourceResourceTypeNode, e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = JsonResourceModificationExperiment.isEnabled()
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.credentials.getConnectionSettingsOrThrow
import software.aws.toolkits.jetbrains.core.experiments.isEnabled
import software.aws.toolkits.jetbrains.core.explorer.DeleteResourceDialog
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleExplorerNodeAction
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceIdentifier
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceUpdateManager
import software.aws.toolkits.jetbrains.services.dynamic.JsonResourceModificationExperiment
import software.aws.toolkits.jetbrains.services.dynamic.ViewEditableDynamicResourceVirtualFile
import software.aws.toolkits.jetbrains.services.dynamic.explorer.DynamicResourceNode
import software.aws.toolkits.resources.message

class DynamicResourceDeleteResourceAction :
    SingleExplorerNodeAction<DynamicResourceNode>(message("dynamic_resources.delete_resource"), icon = AllIcons.Actions.Cancel),
    DumbAware {

    override fun actionPerformed(selected: DynamicResourceNode, e: AnActionEvent) {
        val resourceType = selected.resource.type.fullName
        val response = DeleteResourceDialog(selected.nodeProject, resourceType, selected.displayName()).showAndGet()
        if (response) {
            val dynamicResourceIdentifier = DynamicResourceIdentifier(
                selected.nodeProject.getConnectionSettingsOrThrow(),
                selected.resource.type.fullName,
                selected.resource.identifier
            )
            val fileEditorManager = FileEditorManager.getInstance(selected.nodeProject)
            fileEditorManager.openFiles.forEach {
                if (it is ViewEditableDynamicResourceVirtualFile && it.dynamicResourceIdentifier == dynamicResourceIdentifier) {
                    ApplicationManager.getApplication().invokeAndWait { fileEditorManager.closeFile(it) }
                }
            }
            DynamicResourceUpdateManager.getInstance(selected.nodeProject).deleteResource(dynamicResourceIdentifier)
        }
    }

    override fun update(selected: DynamicResourceNode, e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = JsonResourceModificationExperiment.isEnabled()
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer.actions

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.experiments.isEnabled
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleExplorerNodeAction
import software.aws.toolkits.jetbrains.services.dynamic.JsonResourceModificationExperiment
import software.aws.toolkits.jetbrains.services.dynamic.explorer.DynamicResourceNode
import software.aws.toolkits.jetbrains.services.dynamic.explorer.OpenResourceModelSourceAction
import software.aws.toolkits.resources.message

class OpenFileForUpdateAction :
    SingleExplorerNodeAction<DynamicResourceNode>(message("dynamic_resources.openFileForUpdate_text")),
    DumbAware {
    override fun actionPerformed(selected: DynamicResourceNode, e: AnActionEvent) {
        selected.openResourceModelInEditor(OpenResourceModelSourceAction.EDIT)
    }

    override fun update(selected: DynamicResourceNode, e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = JsonResourceModificationExperiment.isEnabled()
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer.actions

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleExplorerNodeAction
import software.aws.toolkits.jetbrains.services.dynamic.explorer.DynamicResourceNode
import software.aws.toolkits.jetbrains.services.dynamic.explorer.OpenResourceModelSourceAction
import software.aws.toolkits.resources.message

class OpenReadOnlyFileAction :
    SingleExplorerNodeAction<DynamicResourceNode>(message("dynamic_resources.openReadOnlyFile_text")),
    DumbAware {
    override fun actionPerformed(selected: DynamicResourceNode, e: AnActionEvent) {
        selected.openResourceModelInEditor(OpenResourceModelSourceAction.READ)
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamic.explorer.actions

import com.intellij.ide.browsers.BrowserLauncher
import com.intellij.openapi.actionSystem.AnActionEvent
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleExplorerNodeAction
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceSupportedTypes
import software.aws.toolkits.jetbrains.services.dynamic.explorer.DynamicResourceResourceTypeNode
import software.aws.toolkits.resources.message

class ViewDocumentationAction : SingleExplorerNodeAction<DynamicResourceResourceTypeNode>(message("dynamic_resources.type.explorer.view_documentation")) {
    private val supportedType = DynamicResourceSupportedTypes.getInstance()
    override fun actionPerformed(selected: DynamicResourceResourceTypeNode, e: AnActionEvent) {
        supportedType.getDocs(selected.resourceType)?.let { docUrl ->
            BrowserLauncher.instance.browse(docUrl, project = e.project)
        }
    }

    override fun update(selected: DynamicResourceResourceTypeNode, e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = supportedType.getDocs(selected.resourceType) != null
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamodb

import software.amazon.awssdk.services.dynamodb.model.AttributeValue
import java.util.Base64

typealias SearchResults = List<Map<String, DynamoAttribute<*>>>

private const val QUOTE = '"'

/**
 * See:
 * * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html,
 * * https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html
 */
sealed class DynamoAttribute<T>(val value: T) {
    abstract val dataType: String

    open fun stringRepresentation() = value.toString()
}

class StringAttribute(value: String) : DynamoAttribute<String>(value) {
    override val dataType: String = "S"

    override fun stringRepresentation(): String = QUOTE + value + QUOTE
}

class BooleanAttribute(value: Boolean) : DynamoAttribute<Boolean>(value) {
    override val dataType: String = "BOOL"
}

class NumberAttribute(value: String) : DynamoAttribute<String>(value) {
    override val dataType: String = "N"
}

class BinaryAttribute(value: ByteArray) : DynamoAttribute<ByteArray>(value) {
    override val dataType: String = "B"

    override fun stringRepresentation(): String = Base64.getEncoder().encodeToString(value)
}

object NullAttribute : DynamoAttribute<Boolean>(/*Dynamo always expects the NUL field to contain true */true) {
    override val dataType: String = "NUL"

    override fun stringRepresentation(): String = "<null>"
}

class StringSetAttribute(value: List<String>) : DynamoAttribute<List<String>>(value) {
    override val dataType: String = "SS"

    override fun stringRepresentation(): String = value.joinToString(prefix = "{", postfix = "}") {
        QUOTE + it + QUOTE
    }
}

class NumberSetAttribute(value: List<String>) : DynamoAttribute<List<String>>(value) {
    override val dataType: String = "NS"

    override fun stringRepresentation(): String = value.joinToString(prefix = "{", postfix = "}")
}

class BinarySetAttribute(value: List<ByteArray>) : DynamoAttribute<List<ByteArray>>(value) {
    override val dataType: String = "BS"

    override fun stringRepresentation(): String = value.joinToString(prefix = "{", postfix = "}") {
        Base64.getEncoder().encodeToString(it)
    }
}

class MapAttribute(value: Map<String, DynamoAttribute<*>>) : DynamoAttribute<Map<String, DynamoAttribute<*>>>(value) {
    override val dataType: String = "M"

    override fun stringRepresentation(): String = value.entries.joinToString(prefix = "{", postfix = "}") {
        "$QUOTE${it.key}$QUOTE: {$QUOTE${it.value.dataType}$QUOTE: ${it.value.stringRepresentation()}}"
    }
}

class ListAttribute(value: List<DynamoAttribute<*>>) : DynamoAttribute<List<DynamoAttribute<*>>>(value) {
    override val dataType: String = "L"

    override fun stringRepresentation(): String = value.joinToString(prefix = "[", postfix = "]") {
        "{$QUOTE${it.dataType}$QUOTE: ${it.stringRepresentation()}}"
    }
}

fun AttributeValue.toAttribute(): DynamoAttribute<*> = when {
    this.s() != null -> StringAttribute(this.s())
    this.b() != null -> BinaryAttribute(this.b().asByteArray())
    this.bool() != null -> BooleanAttribute(this.bool())
    this.n() != null -> NumberAttribute(this.n())
    this.nul() != null -> NullAttribute
    this.hasSs() -> StringSetAttribute(this.ss())
    this.hasNs() -> NumberSetAttribute(this.ns())
    this.hasBs() -> BinarySetAttribute(this.bs().map { it.asByteArray() })
    this.hasM() -> MapAttribute(this.m().mapValues { it.value.toAttribute() })
    this.hasL() -> ListAttribute(this.l().map { it.toAttribute() })
    else -> throw UnsupportedOperationException(this.toString())
}


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

package software.aws.toolkits.jetbrains.services.dynamodb

import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource

object DynamoDbResources {
    val LIST_TABLES = ClientBackedCachedResource(DynamoDbClient::class, "dynamodb.list_tables") {
        listTablesPaginator().tableNames().toList()
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamodb

import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import software.amazon.awssdk.services.dynamodb.model.ExecuteStatementRequest
import software.amazon.awssdk.services.dynamodb.model.ExecuteStatementResponse

object DynamoDbUtils {
    fun DynamoDbClient.executeStatementPaginator(request: ExecuteStatementRequest): Sequence<ExecuteStatementResponse> =
        // Partiql does not have paginators, do it manually
        generateSequence(
            seed = this.executeStatement(request.toBuilder().build()),
            nextFunction = {
                it.nextToken()?.let { token ->
                    this.executeStatement(request.toBuilder().nextToken(token).build())
                }
            },
        )
}


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

package software.aws.toolkits.jetbrains.services.dynamodb

data class Index(
    val displayName: String,
    val indexName: String?,
    val partitionKey: String,
    val sortKey: String?
)


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

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

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileEditorManager
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.explorer.actions.DeleteResourceAction
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.services.dynamodb.DynamoDbResources
import software.aws.toolkits.jetbrains.services.dynamodb.editor.DynamoDbVirtualFile
import software.aws.toolkits.jetbrains.services.dynamodb.explorer.DynamoDbTableNode

class DeleteTableAction : DeleteResourceAction<DynamoDbTableNode>() {
    override fun performDelete(selected: DynamoDbTableNode) {
        val project = selected.nodeProject
        val client = project.awsClient<DynamoDbClient>()

        val fileEditorManager = FileEditorManager.getInstance(selected.nodeProject)
        fileEditorManager.openFiles.forEach {
            if (it is DynamoDbVirtualFile && it.tableArn == selected.resourceArn()) {
                // Wait so that we know it closes successfully, otherwise this operation is not a success
                ApplicationManager.getApplication().invokeAndWait {
                    fileEditorManager.closeFile(it)
                }
            }
        }

        client.deleteTable { it.tableName(selected.displayName()) }
        project.refreshAwsTree(DynamoDbResources.LIST_TABLES)
    }
}


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

package software.aws.toolkits.jetbrains.services.dynamodb.editor

import com.intellij.ide.FileIconProvider
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import icons.AwsIcons
import javax.swing.Icon

class DynamoDbFileIconProvider : FileIconProvider {
    override fun getIcon(file: VirtualFile, flags: Int, project: Project?): Icon? = if (file is DynamoDbVirtualFile) {
        AwsIcons.Resources.DynamoDb.TABLE
    } else {
        null
    }
}