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

package software.aws.toolkits.jetbrains.utils.rules

import com.intellij.testFramework.DisposableRule
import org.junit.rules.ExternalResource
import org.junit.runner.Description
import org.junit.runners.model.Statement
import software.aws.toolkits.jetbrains.core.MockClientManager
import software.aws.toolkits.jetbrains.core.credentials.sso.MockSsoLoginCallbackProvider
import software.aws.toolkits.jetbrains.core.credentials.sso.NoOpSsoLoginCallback
import software.aws.toolkits.jetbrains.core.credentials.sso.SsoLoginCallback
import software.aws.toolkits.jetbrains.core.credentials.sso.TestSsoPrompt
import software.aws.toolkits.jetbrains.utils.extensions.SsoLogin

class SsoLoginRule : DisposableRule() {
    override fun apply(base: Statement, description: Description): Statement {
        val annotation = description.getAnnotation(SsoLogin::class.java) ?: description.testClass.getAnnotation(SsoLogin::class.java)
        return if (annotation == null) {
            base
        } else {
            object : Statement() {
                override fun evaluate() {
                    try {
                        MockClientManager.useRealImplementations(disposable)
                        MockSsoLoginCallbackProvider.getInstance().provider = TestSsoPrompt(annotation.secretName)

                        base.evaluate()
                    } finally {
                        MockSsoLoginCallbackProvider.getInstance().provider = null
                    }
                }
            }
        }
    }
}

class SsoLoginCallbackProviderRule : ExternalResource() {
    override fun before() {
        setCallback(NoOpSsoLoginCallback)
    }

    fun setCallback(callback: SsoLoginCallback?) {
        MockSsoLoginCallbackProvider.getInstance().provider = callback
    }

    override fun after() {
        setCallback(null)
    }
}


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

package software.aws.toolkits.jetbrains.utils.ui

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import java.awt.event.MouseEvent
import javax.swing.DefaultComboBoxModel
import javax.swing.JCheckBox
import javax.swing.JTextField

class UiUtilsTest {

    @Test
    fun canBindQuickEnableEventToAComponent() {
        val component = object : JTextField() {
            override fun contains(x: Int, y: Int) = true
        }
        val checkbox = JCheckBox().apply { isSelected = false }
        val runnable = mock<Runnable>()

        component.addQuickSelect(checkbox, runnable)

        val click = MouseEvent(component, MouseEvent.BUTTON1, 0, 0, 100, 100, 1, false)
        component.mouseListeners.forEach { it.mousePressed(click); it.mouseReleased(click) }
        assertThat(checkbox.isSelected).isTrue()
        verify(runnable).run()
    }

    @Test
    fun canFindAnElementFromAListModel() {
        val listModel = DefaultComboBoxModel<String>(arrayOf("hello", "world"))
        assertThat(listModel.find { it == "hello" }).isEqualTo("hello")
        assertThat(listModel.find { it == "bye" }).isNull()
    }
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.application.impl.NonBlockingReadActionImpl
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotificationProvider
import com.intellij.ui.EditorNotificationsImpl
import javax.swing.JComponent

@Suppress("UNUSED_PARAMETER")
fun <T : EditorNotificationProvider, U : EditorNotificationPanel> getEditorNotifications(
    project: Project,
    editor: FileEditor,
    provider: Class<T>,
    key: Key<U>
): JComponent? {
    PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
    NonBlockingReadActionImpl.waitForAsyncTaskCompletion()

    return EditorNotificationsImpl.getNotificationPanels(editor)[provider]
}


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

package software.aws.toolkits.jetbrains.utils.rules

import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor
import org.jetbrains.annotations.NotNull

internal class FakeCPython : CPythonSdkFlavor() {
    @NotNull
    override fun getName(): String = "FakeCPython"
}


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

typealias ThreadLeakTracker = com.intellij.testFramework.ThreadTracker


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

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

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class ParameterDescriptionsTest {
    @Test
    fun `parameter descriptions json can be loaded`() {
        assertThat(loadParameterDescriptions().environmentParameters.instanceTypes).isNotEmpty
    }
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.application.impl.NonBlockingReadActionImpl
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotificationProvider
import com.intellij.ui.EditorNotifications
import com.intellij.ui.EditorNotificationsImpl
import javax.swing.JComponent

@Suppress("UNUSED_PARAMETER")
fun <T : EditorNotificationProvider, U : EditorNotificationPanel> getEditorNotifications(
    project: Project,
    editor: FileEditor,
    provider: Class<T>,
    key: Key<U>
): JComponent? {
    PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
    NonBlockingReadActionImpl.waitForAsyncTaskCompletion()

    val editorNotifications = EditorNotifications.getInstance(project) as EditorNotificationsImpl
    editorNotifications.completeAsyncTasks()

    return editorNotifications.getNotificationPanels(editor)[provider]
}


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

package software.aws.toolkits.jetbrains.utils.rules

import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor
import com.jetbrains.python.sdk.flavors.PyFlavorData
import org.jetbrains.annotations.NotNull

internal class FakeCPython : CPythonSdkFlavor<PyFlavorData.Empty>() {
    @NotNull
    override fun getName(): String = "FakeCPython"
}


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

package software.aws.toolkits.jetbrains.gateway

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.util.ExecUtil
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceOrNull
import com.intellij.openapi.util.Disposer
import com.intellij.remoteDev.downloader.JetBrainsClientDownloaderConfigurationProvider
import com.intellij.remoteDev.downloader.TestJetBrainsClientDownloaderConfigurationProvider
import com.intellij.remoteDev.hostStatus.UnattendedHostStatus
import com.intellij.testFramework.ApplicationExtension
import com.intellij.testFramework.registerOrReplaceServiceInstance
import com.intellij.util.io.HttpRequests
import com.intellij.util.net.NetUtils
import com.jetbrains.gateway.api.ConnectionRequestor
import com.jetbrains.gateway.api.GatewayConnectionHandle
import com.jetbrains.rd.util.lifetime.isNotAlive
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.TestFactory
import org.junit.jupiter.api.Timeout
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable
import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.RegisterExtension
import org.junit.jupiter.api.io.TempDir
import software.amazon.awssdk.services.codecatalyst.CodeCatalystClient
import software.amazon.awssdk.services.codecatalyst.model.ConflictException
import software.amazon.awssdk.services.codecatalyst.model.DevEnvironmentStatus
import software.amazon.awssdk.services.codecatalyst.model.InstanceType
import software.aws.toolkits.core.utils.Waiters.waitUntil
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.MockClientManager
import software.aws.toolkits.jetbrains.core.credentials.ManagedBearerSsoConnection
import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeCatalystConnection
import software.aws.toolkits.jetbrains.core.credentials.pinning.ConnectionPinningManager
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_REGION
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
import software.aws.toolkits.jetbrains.core.tools.MockToolManagerRule
import software.aws.toolkits.jetbrains.core.tools.ToolManager
import software.aws.toolkits.jetbrains.gateway.connection.IDE_BACKEND_DIR
import software.aws.toolkits.jetbrains.gateway.connection.StdOutResult
import software.aws.toolkits.jetbrains.gateway.connection.ThinClientTrackerService
import software.aws.toolkits.jetbrains.gateway.connection.caws.CawsCommandExecutor
import software.aws.toolkits.jetbrains.gateway.connection.resultFromStdOut
import software.aws.toolkits.jetbrains.services.caws.isSubscriptionFreeTier
import software.aws.toolkits.jetbrains.services.ssm.SsmPlugin
import software.aws.toolkits.jetbrains.utils.FrameworkTestUtils
import software.aws.toolkits.jetbrains.utils.extensions.DevEnvironmentExtension
import software.aws.toolkits.jetbrains.utils.extensions.DisposerAfterAllExtension
import software.aws.toolkits.jetbrains.utils.extensions.SsoLogin
import java.nio.file.Path
import java.time.Duration
import java.util.concurrent.TimeUnit
import kotlin.reflect.KFunction
import kotlin.time.ExperimentalTime

@OptIn(ExperimentalTime::class)
@ExtendWith(ApplicationExtension::class)
@SsoLogin("codecatalyst-test-account")
@DisabledIfEnvironmentVariable(named = "IS_PROD", matches = "false")
class DevEnvConnectTest : AfterAllCallback {
    companion object {
        @JvmField
        @RegisterExtension
        val disposableExtension = DisposerAfterAllExtension()

        private lateinit var connection: ManagedBearerSsoConnection

        @JvmField
        @RegisterExtension
        val environmentExtension = DevEnvironmentExtension({ connection }) { client, builder ->
            val space = client.listSpacesPaginator {}.items().map { it.name() }.let { spaces ->
                spaces.firstOrNull { it == "aws-toolkit-jetbrains-test-space" }
                    ?: spaces.firstOrNull { space ->
                        !isSubscriptionFreeTier(client, space)
                    }
            } ?: error("CodeCatalyst user doesn't have access to a paid space")

            val projectName = "aws-jetbrains-toolkit-integ-test-project"
            val project = try {
                client.createProject {
                    it.spaceName(space)
                    it.displayName(projectName)
                    it.description("Project used by AWS Toolkit Jetbrains integration tests")
                }.name()
            } catch (e: ConflictException) {
                client.getProject {
                    it.spaceName(space)
                    it.name(projectName)
                }.name()
            }

            builder.spaceName(space)
            builder.projectName(project)
            builder.ides({ ide ->
                ide.name("IntelliJ")
                ide.runtime("public.ecr.aws/jetbrains/iu:release")
            })
            builder.persistentStorage { storage ->
                storage.sizeInGiB(16)
            }
            builder.instanceType(InstanceType.DEV_STANDARD1_MEDIUM)
            builder.inactivityTimeoutMinutes(15)
            builder.repositories(emptyList())
        }
    }

    private val client: CodeCatalystClient by lazy {
        AwsClientManager.getInstance().getClient(connection.getConnectionSettings())
    }

    private val environment by lazy {
        environmentExtension.environment
    }

    private val ssmFactory by lazy {
        CawsCommandExecutor(
            client,
            environment.id,
            environment.spaceName,
            environment.projectName
        )
    }

    private val hostToken = System.getenv("CWM_HOST_STATUS_OVER_HTTP_TOKEN")

    private val localPort by lazy {
        NetUtils.findAvailableSocketPort()
    }

    private val lazyPortForward = lazy {
        ssmFactory.portForward(localPort, 63342)
    }

    private val endpoint by lazy {
        "http://localhost:$localPort/codeWithMe/unattendedHostStatus?token=$hostToken"
    }

    @BeforeEach
    fun setUp(@TempDir tempDir: Path) {
        FrameworkTestUtils.ensureBuiltInServerStarted()

        val disposable = disposableExtension.disposable
        serviceOrNull<ThinClientTrackerService>()
            ?: ApplicationManager
                .getApplication()
                .registerOrReplaceServiceInstance(ThinClientTrackerService::class.java, ThinClientTrackerService(), disposableExtension.disposable)

        MockClientManager.useRealImplementations(disposableExtension.disposable)
        MockToolManagerRule.useRealTools(disposable)

        // TODO: some sort of race happening where this somehow returns before the executable is usable?
        println(
            ExecUtil.execAndGetOutput(
                GeneralCommandLine(ToolManager.getInstance().getOrInstallTool(SsmPlugin).path.toAbsolutePath().toString())
            )
        )

        // can probably abstract this out as an extension
        // force auth to complete now
        connection = ManagedBearerSsoConnection(SONO_URL, SONO_REGION, listOf("codecatalyst:read_write"))
        Disposer.register(disposable, connection)
        // pin connection to avoid dialog prompt
        ConnectionPinningManager.getInstance().setPinnedConnection(CodeCatalystConnection.getInstance(), connection)
        (connection.getConnectionSettings().tokenProvider.delegate as BearerTokenProvider).reauthenticate()

        (service<JetBrainsClientDownloaderConfigurationProvider>() as TestJetBrainsClientDownloaderConfigurationProvider).apply {
            guestConfigFolder = tempDir.resolve("config")
            guestSystemFolder = tempDir.resolve("system")
            guestLogFolder = tempDir.resolve("log")
        }
    }

    private lateinit var connectionHandle: GatewayConnectionHandle

    @TestFactory
    @Timeout(value = 5, unit = TimeUnit.MINUTES)
    fun `test connect to devenv`(): Iterator<DynamicTest> = sequence<DynamicTest> {
        connectionHandle = runBlocking {
            CawsConnectionProvider().connect(
                mapOf(
                    CawsConnectionParameters.CAWS_SPACE to environment.spaceName,
                    CawsConnectionParameters.CAWS_PROJECT to environment.projectName,
                    CawsConnectionParameters.CAWS_ENV_ID to environment.id,
                ),
                ConnectionRequestor.Local
            )
        } ?: error("null connection handle")

        yield(test(::`wait for environment ready`))

        // inject token to backend launcher script to enable the host status endpoint
        println(
            ssmFactory.executeSshCommand {
                it.addToRemoteCommand(
                    """
                    grep -q "CWM_HOST_STATUS_OVER_HTTP_TOKEN" $IDE_BACKEND_DIR/bin/remote-dev-server.sh || sed -i.bak '2iexport CWM_HOST_STATUS_OVER_HTTP_TOKEN=$hostToken' $IDE_BACKEND_DIR/bin/remote-dev-server.sh
                    """.trimIndent()
                )
            }
        )

        yieldAll(
            listOf(
                test(::`poll for bootstrap script availability`),
                test(::`wait for backend start`),
                test(::`wait for backend connect`)
            )
        )
    }.iterator()

    fun `wait for environment ready`() = runBlocking {
        waitUntil(
            succeedOn = {
                it.status() == DevEnvironmentStatus.RUNNING
            },
            failOn = {
                it.status() == DevEnvironmentStatus.FAILED || it.status() == DevEnvironmentStatus.DELETED
            },
            maxDuration = Duration.ofMinutes(2),
            call = {
                client.getDevEnvironment {
                    it.spaceName(environment.spaceName)
                    it.projectName(environment.projectName)
                    it.id(environment.id)
                }
            }
        )
    }

    fun `poll for bootstrap script availability`() = runBlocking {
        waitUntil(
            succeedOn = {
                it == StdOutResult.SUCCESS
            },
            failOn = {
                connectionHandle.lifetime.isNotAlive
            },
            maxDuration = Duration.ofMinutes(5),
            call = {
                // condition looks inverted because we want failure if script not found
                ssmFactory
                    .executeCommandNonInteractive("sh", "-c", "test -z \"\$(find /tmp -name \"start-ide.sh\" 2>/dev/null)\" && echo false || echo true")
                    .resultFromStdOut()
            }
        )
    }

    fun `wait for backend start`() = runBlocking {
        // make sure port forward is alive
        lazyPortForward.value

        waitUntil(
            succeedOn = {
                it != null
            },
            failOn = { connectionHandle.lifetime.isNotAlive },
            maxDuration = Duration.ofMinutes(5),
            call = {
                tryOrNull {
                    HttpRequests.request(endpoint).readString()
                }
            }
        )
    }

    fun `wait for backend connect`() = runBlocking {
        waitUntil(
            succeedOn = { status ->
                status?.projects?.any { it.users.size > 1 } == true
            },
            failOn = { connectionHandle.lifetime.isNotAlive },
            maxDuration = Duration.ofMinutes(5),
            call = {
                // can potentially have a socket reset which will lead to a very confusing error that's hard to debug
                // due to the Gateway connection executor continuing to run
                tryOrNull {
                    UnattendedHostStatus.fromJson(HttpRequests.request(endpoint).readString())
                }
            }
        )

        ThinClientTrackerService.getInstance().closeThinClient(environment.id)
    }

    override fun afterAll(context: ExtensionContext) {
        if (lazyPortForward.isInitialized()) {
            lazyPortForward.value.destroyProcess()
        }
    }

    private fun test(testRef: KFunction<*>) = DynamicTest.dynamicTest(testRef.name) { testRef.call() }
}


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

package software.aws.toolkits.jetbrains.gateway

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.util.io.HttpRequests
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.TestFactory

class GatewayEcrVersionsKtTest {
    @TestFactory
    fun `test manifest images are valid`(): Iterator<DynamicTest> {
        val uriCache = mutableMapOf<String, List<String>>()
        return sequence<DynamicTest> {
            val images = gatewayManifest().images
            images.forEach { product ->
                val validUris = uriCache.computeIfAbsent(product.productCode) { productCode ->
                    fetchTagsForIde(productCode).map {
                        "public.ecr.aws/jetbrains/${productCode.lowercase()}:$it"
                    }
                }

                yield(DynamicTest.dynamicTest("${product.ecrImage} exists") { assertThat(validUris).contains(product.ecrImage) })
            }
        }.iterator()
    }

    private fun fetchTagsForIde(productCode: String) =
        HttpRequests.request("https://public.ecr.aws/v2/jetbrains/${productCode.lowercase()}/tags/list")
            .tuner {
                it.setRequestProperty("Authorization", "Bearer $ecrPublicToken")
            }.connect {
                mapper.readValue<EcrPublicListTagsResponse>(it.readString()).tags
            }

    private companion object {
        val mapper = jacksonObjectMapper()
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)

        val ecrPublicToken by lazy {
            mapper.readValue<EcrPublicToken>(HttpRequests.request("https://public.ecr.aws/token/").readString()).token
        }
    }
}

data class EcrPublicToken(
    val token: String
)

data class EcrPublicListTagsResponse(
    val name: String,
    val tags: List<String>
)


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

package icons

import com.intellij.openapi.util.IconLoader
import javax.swing.Icon

/**
 * Lives in `icons` package due to that is how [com.intellij.openapi.util.IconLoader.getReflectiveIcon] works
 */
object AwsGatewayIcons {
    @JvmField val GATEWAY_RUNNING = load("/icons/gateway/running.svg") // 16x16

    @JvmField val GATEWAY_STOPPED = load("/icons/gateway/stopped.svg") // 16x16

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


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

package software.aws.toolkits.jetbrains.gateway

import com.intellij.icons.AllIcons
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.rd.createNestedDisposable
import com.intellij.openapi.rd.util.launchChildIOBackground
import com.intellij.openapi.rd.util.launchIOBackground
import com.intellij.openapi.rd.util.launchOnUiAnyModality
import com.intellij.openapi.rd.util.startUnderBackgroundProgressAsync
import com.intellij.openapi.rd.util.startUnderModalProgressAsync
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.BuildNumber
import com.intellij.openapi.util.Disposer
import com.intellij.remoteDev.downloader.CodeWithMeClientDownloader
import com.intellij.ui.components.JBTabbedPane
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
import com.intellij.ui.dsl.gridLayout.VerticalAlign
import com.intellij.ui.layout.panel
import com.intellij.util.ui.JBFont
import com.jetbrains.gateway.api.ConnectionRequestor
import com.jetbrains.gateway.api.GatewayConnectionHandle
import com.jetbrains.gateway.api.GatewayConnectionProvider
import com.jetbrains.gateway.api.GatewayUI
import com.jetbrains.rd.framework.util.launch
import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.coroutineScope
import org.jetbrains.concurrency.AsyncPromise
import org.jetbrains.concurrency.await
import software.amazon.awssdk.services.codecatalyst.CodeCatalystClient
import software.amazon.awssdk.services.codecatalyst.model.DevEnvironmentStatus
import software.aws.toolkits.core.utils.AttributeBagKey
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.sono.SonoCredentialManager
import software.aws.toolkits.jetbrains.core.credentials.sono.lazilyGetUserId
import software.aws.toolkits.jetbrains.core.utils.buildList
import software.aws.toolkits.jetbrains.gateway.connection.GET_IDE_BACKEND_VERSION_COMMAND
import software.aws.toolkits.jetbrains.gateway.connection.GitSettings
import software.aws.toolkits.jetbrains.gateway.connection.IDE_BACKEND_DIR
import software.aws.toolkits.jetbrains.gateway.connection.caws.CawsCommandExecutor
import software.aws.toolkits.jetbrains.gateway.connection.workflow.CloneCode
import software.aws.toolkits.jetbrains.gateway.connection.workflow.CopyScripts
import software.aws.toolkits.jetbrains.gateway.connection.workflow.InstallPluginBackend.InstallLocalPluginBackend
import software.aws.toolkits.jetbrains.gateway.connection.workflow.InstallPluginBackend.InstallMarketplacePluginBackend
import software.aws.toolkits.jetbrains.gateway.connection.workflow.PrimeSshAgent
import software.aws.toolkits.jetbrains.gateway.connection.workflow.TabbedWorkflowEmitter
import software.aws.toolkits.jetbrains.gateway.connection.workflow.installBundledPluginBackend
import software.aws.toolkits.jetbrains.gateway.connection.workflow.v2.StartBackendV2
import software.aws.toolkits.jetbrains.gateway.welcomescreen.WorkspaceListStateChangeContext
import software.aws.toolkits.jetbrains.gateway.welcomescreen.WorkspaceNotifications
import software.aws.toolkits.jetbrains.services.caws.CawsProject
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.jetbrains.utils.execution.steps.StepExecutor
import software.aws.toolkits.jetbrains.utils.execution.steps.StepWorkflow
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodecatalystTelemetry
import java.time.Duration
import java.util.UUID
import javax.swing.JLabel
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
import com.intellij.ui.dsl.builder.panel as panelv2
import software.aws.toolkits.telemetry.Result as TelemetryResult

@ExperimentalTime
class CawsConnectionProvider : GatewayConnectionProvider {
    companion object {
        val CAWS_CONNECTION_PARAMETERS = AttributeBagKey.create<Map<String, String>>("CAWS_CONNECTION_PARAMETERS")
        private val LOG = getLogger<CawsConnectionProvider>()
    }

    override fun isApplicable(parameters: Map<String, String>): Boolean = parameters.containsKey(CawsConnectionParameters.CAWS_ENV_ID)

    override suspend fun connect(parameters: Map<String, String>, requestor: ConnectionRequestor): GatewayConnectionHandle? {
        val connectionParams = try {
            CawsConnectionParameters.fromParameters(parameters)
        } catch (e: Exception) {
            LOG.error(e) { "Caught exception while building connection settings" }
            Messages.showErrorDialog(e.message ?: message("general.unknown_error"), message("caws.workspace.connection.failed"))
            return null
        }

        val connectionSettings = try {
            SonoCredentialManager.getInstance().getSettingsAndPromptAuth()
        } catch (e: ProcessCanceledException) {
            return null
        }

        val userId = lazilyGetUserId()

        val spaceName = connectionParams.space
        val projectName = connectionParams.project
        val envId = connectionParams.envId
        val id = WorkspaceIdentifier(CawsProject(spaceName, projectName), envId)

        val lifetime = Lifetime.Eternal.createNested()
        val workflowDisposable = Lifetime.Eternal.createNestedDisposable()

        return CawsGatewayConnectionHandle(lifetime, envId) {
            // reference lost with all the blocks
            it.let { gatewayHandle ->
                val view = JBTabbedPane()
                val workflowEmitter = TabbedWorkflowEmitter(view, workflowDisposable)

                fun handleException(e: Throwable) {
                    if (e is ProcessCanceledException || e is CancellationException) {
                        CodecatalystTelemetry.connect(project = null, userId = userId, result = TelemetryResult.Cancelled)
                        LOG.warn { "Connect to dev environment cancelled" }
                    } else {
                        CodecatalystTelemetry.connect(project = null, userId = userId, result = TelemetryResult.Failed, reason = e.javaClass.simpleName)
                        LOG.error(e) { "Caught exception while connecting to dev environment" }
                    }
                    lifetime.terminate()
                }

                // TODO: Describe env to validate JB ide is set on it
                lifetime.launch {
                    try {
                        val cawsClient = connectionSettings.awsClient<CodeCatalystClient>()
                        val environmentActions = WorkspaceActions(spaceName, projectName, envId, cawsClient)
                        val executor = CawsCommandExecutor(cawsClient, envId, spaceName, projectName)

                        // should probably consider logging output to logger as well
                        // on failure we should display meaningful error and put retry button somewhere
                        lifetime.startUnderModalProgressAsync(
                            title = message("caws.connecting.waiting_for_environment"),
                            canBeCancelled = true,
                            isIndeterminate = true,
                        ) {
                            val timeBeforeEnvIsRunningCheck = System.currentTimeMillis()
                            var validateEnvIsRunningResult = TelemetryResult.Succeeded
                            var errorMessageDuringStateValidation: String? = null
                            try {
                                validateEnvironmentIsRunning(indicator, environmentActions)
                            } catch (e: Exception) {
                                validateEnvIsRunningResult = TelemetryResult.Failed
                                errorMessageDuringStateValidation = e.message
                                throw e
                            } finally {
                                CodecatalystTelemetry.devEnvironmentWorkflowStatistic(
                                    project = null,
                                    userId = userId,
                                    result = validateEnvIsRunningResult,
                                    duration = (System.currentTimeMillis() - timeBeforeEnvIsRunningCheck).toDouble(),
                                    codecatalystDevEnvironmentWorkflowStep = "validateEnvRunning",
                                    codecatalystDevEnvironmentWorkflowError = errorMessageDuringStateValidation
                                )
                            }

                            lifetime.launchIOBackground {
                                ApplicationManager.getApplication().messageBus.syncPublisher(WorkspaceNotifications.TOPIC)
                                    .environmentStarted(
                                        WorkspaceListStateChangeContext(
                                            WorkspaceIdentifier(CawsProject(spaceName, projectName), envId)
                                        )
                                    )
                            }

                            val pluginPath = "$IDE_BACKEND_DIR/plugins/${AwsToolkit.pluginPath().fileName}"
                            var retries = 3
                            val startTimeToCheckInstallation = System.currentTimeMillis()

                            val toolkitInstallSettings: ToolkitInstallSettings? = coroutineScope {
                                while (retries > 0) {
                                    indicator.checkCanceled()
                                    val pluginIsInstalled = executor.remoteDirectoryExists(
                                        pluginPath,
                                        timeout = Duration.ofSeconds(15)
                                    )

                                    when (pluginIsInstalled) {
                                        null -> {
                                            if (retries == 1) {
                                                return@coroutineScope null
                                            } else {
                                                retries--
                                                continue
                                            }
                                        }

                                        true -> return@coroutineScope ToolkitInstallSettings.None
                                        false -> return@coroutineScope connectionParams.toolkitInstallSettings
                                    }
                                }
                            } as ToolkitInstallSettings?

                            toolkitInstallSettings ?: let {
                                // environment is non-responsive to SSM; restart
                                LOG.warn { "Restarting $envId since it appears unresponsive to SSM Run-Command" }
                                val timeTakenToCheckInstallation = System.currentTimeMillis() - startTimeToCheckInstallation
                                CodecatalystTelemetry.devEnvironmentWorkflowStatistic(
                                    project = null,
                                    userId = userId,
                                    result = TelemetryResult.Failed,
                                    codecatalystDevEnvironmentWorkflowStep = "ToolkitInstallationSSMCheck",
                                    codecatalystDevEnvironmentWorkflowError = "Timeout/Unknown error while connecting to Dev Env via SSM",
                                    duration = timeTakenToCheckInstallation.toDouble()
                                )
                                coroutineScope {
                                    launchChildIOBackground {
                                        environmentActions.stopEnvironment()
                                        GatewayUI.getInstance().connect(parameters)
                                    }
                                }
                                gatewayHandle.terminate()
                                return@startUnderModalProgressAsync JLabel()
                            }

                            lifetime.startUnderBackgroundProgressAsync(message("caws.download.thin_client"), isIndeterminate = true) {
                                val (backendVersion, getBackendVersionTime) = measureTimedValue {
                                    tryOrNull {
                                        executor.executeCommandNonInteractive(
                                            "sh",
                                            "-c",
                                            GET_IDE_BACKEND_VERSION_COMMAND,
                                            timeout = Duration.ofSeconds(15)
                                        ).stdout
                                    }
                                }
                                CodecatalystTelemetry.devEnvironmentWorkflowStatistic(
                                    project = null,
                                    userId = userId,
                                    result = if (backendVersion != null) TelemetryResult.Succeeded else TelemetryResult.Failed,
                                    duration = getBackendVersionTime.toDouble(DurationUnit.MILLISECONDS),
                                    codecatalystDevEnvironmentWorkflowStep = "getBackendVersion"
                                )

                                if (backendVersion.isNullOrBlank()) {
                                    LOG.warn { "Could not determine backend version to prefetch thin client" }
                                } else {
                                    val (clientPaths, downloadClientTime) = measureTimedValue {
                                        BuildNumber.fromStringOrNull(backendVersion)?.asStringWithoutProductCode()?.let { build ->
                                            LOG.info { "Fetching client for version: $build" }
                                            CodeWithMeClientDownloader.downloadClientAndJdk(build, indicator)
                                        }
                                    }

                                    CodecatalystTelemetry.devEnvironmentWorkflowStatistic(
                                        project = null,
                                        userId = userId,
                                        result = if (clientPaths != null) TelemetryResult.Succeeded else TelemetryResult.Failed,
                                        duration = downloadClientTime.toDouble(DurationUnit.MILLISECONDS),
                                        codecatalystDevEnvironmentWorkflowStep = "downloadThinClient"
                                    )
                                }
                            }

                            runBackendWorkflow(
                                view,
                                workflowEmitter,
                                userId,
                                indicator,
                                lifetime.createNested(),
                                parameters,
                                executor,
                                id,
                                connectionParams.gitSettings,
                                toolkitInstallSettings
                            ).await()
                        }.invokeOnCompletion { e ->
                            if (e == null) {
                                CodecatalystTelemetry.connect(project = null, userId = userId, result = TelemetryResult.Succeeded)
                                lifetime.onTermination {
                                    Disposer.dispose(workflowDisposable)
                                }
                            } else {
                                handleException(e)
                                if (e is ProcessCanceledException || e is CancellationException) {
                                    return@invokeOnCompletion
                                }
                                runInEdt {
                                    DialogBuilder().apply {
                                        setCenterPanel(
                                            panelv2 {
                                                row {
                                                    icon(AllIcons.General.ErrorDialog).verticalAlign(VerticalAlign.TOP)

                                                    panel {
                                                        row {
                                                            label(message("caws.workspace.connection.failed")).applyToComponent {
                                                                font = JBFont.regular().asBold()
                                                            }
                                                        }

                                                        row {
                                                            label(e.message ?: message("general.unknown_error"))
                                                        }
                                                    }
                                                }

                                                if (view.tabCount != 0) {
                                                    collapsibleGroup(message("general.logs"), false) {
                                                        row {
                                                            cell(view)
                                                                .horizontalAlign(HorizontalAlign.FILL)
                                                        }
                                                    }.expanded = false
                                                    // TODO: can't seem to reliably force a terminal redraw on initial expand
                                                }
                                            }
                                        )

                                        addOkAction()
                                        addCancelAction()
                                        okAction.setText(message("settings.retry"))
                                        setOkOperation {
                                            dialogWrapper.close(DialogWrapper.OK_EXIT_CODE)
                                            GatewayUI.getInstance().connect(parameters)
                                        }
                                    }.show()
                                    Disposer.dispose(workflowDisposable)
                                }
                            }
                        }
                    } catch (e: Exception) {
                        handleException(e)
                        if (e is ProcessCanceledException || e is CancellationException) {
                            return@launch
                        }

                        runInEdt {
                            Messages.showErrorDialog(e.message ?: message("general.unknown_error"), message("caws.workspace.connection.failed"))
                        }
                        throw e
                    }
                }

                return@let panel {
                    row {
                        view(grow)
                    }
                }
            }
        }
    }

    private fun validateEnvironmentIsRunning(
        indicator: ProgressIndicator,
        environmentActions: WorkspaceActions
    ) {
        when (val status = environmentActions.getEnvironmentDetails().status()) {
            DevEnvironmentStatus.PENDING, DevEnvironmentStatus.STARTING -> environmentActions.waitForTaskReady(indicator)
            DevEnvironmentStatus.RUNNING -> {
            }
            DevEnvironmentStatus.STOPPING -> {
                environmentActions.waitForTaskStopped(indicator)
                environmentActions.startEnvironment()
                environmentActions.waitForTaskReady(indicator)
            }
            DevEnvironmentStatus.STOPPED -> {
                environmentActions.startEnvironment()
                environmentActions.waitForTaskReady(indicator)
            }
            DevEnvironmentStatus.DELETING, DevEnvironmentStatus.DELETED -> throw IllegalStateException("Environment is deleted, unable to start")
            else -> throw IllegalStateException("Unknown state $status")
        }
    }

    private fun runBackendWorkflow(
        view: JBTabbedPane,
        workflowEmitter: TabbedWorkflowEmitter,
        userId: String,
        indicator: ProgressIndicator,
        lifetime: LifetimeDefinition,
        parameters: Map<String, String>,
        executor: CawsCommandExecutor,
        envId: WorkspaceIdentifier,
        gitSettings: GitSettings,
        toolkitInstallSettings: ToolkitInstallSettings,
    ): AsyncPromise<Unit> {
        val remoteScriptPath = "/tmp/${UUID.randomUUID()}"
        val remoteProjectName = (gitSettings as? GitSettings.GitRepoSettings)?.repoName

        val steps = buildList {
            add(CopyScripts(remoteScriptPath, executor))

            when (gitSettings) {
                is GitSettings.CloneGitSettings -> {
                    if (gitSettings.repo.scheme == "ssh") {
                        // TODO: we should probably use JB's SshConnectionService/ConnectionBuilder since they have better ssh agent support than we could write
                        add(PrimeSshAgent(gitSettings))
                    }
                    add(CloneCode(remoteScriptPath, gitSettings, executor))
                }

                is GitSettings.CawsOwnedRepoSettings,
                is GitSettings.NoRepo -> {
                }
            }

            when (toolkitInstallSettings) {
                is ToolkitInstallSettings.None -> {}
                is ToolkitInstallSettings.UseSelf -> {
                    add(installBundledPluginBackend(executor, remoteScriptPath, IDE_BACKEND_DIR))
                }
                is ToolkitInstallSettings.UseArbitraryLocalPath -> {
                    add(InstallLocalPluginBackend(toolkitInstallSettings, executor, remoteScriptPath, IDE_BACKEND_DIR))
                }
                is ToolkitInstallSettings.UseMarketPlace -> {
                    add(InstallMarketplacePluginBackend(null, executor, remoteScriptPath, IDE_BACKEND_DIR))
                }
            }

            add(StartBackendV2(lifetime, indicator, envId, remoteProjectName))
        }

        val promise = AsyncPromise<Unit>()
        fun start() {
            lifetime.launchOnUiAnyModality {
                view.removeAll()
            }

            indicator.fraction = 0.0
            val workflow = object : StepWorkflow(steps) {
                override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
                    runInEdt(ModalityState.any()) {
                        indicator.isIndeterminate = false
                    }

                    topLevelSteps.forEachIndexed { i, step ->
                        indicator.checkCanceled()
                        runInEdt(ModalityState.any()) {
                            indicator.fraction = i.toDouble() / steps.size
                            indicator.text = step.stepName
                        }

                        val start = System.currentTimeMillis()
                        var error: Throwable? = null
                        try {
                            step.run(context, stepEmitter)
                        } catch (e: Throwable) {
                            error = e
                            throw e
                        } finally {
                            val time = System.currentTimeMillis() - start
                            LOG.info { "${step.stepName} took ${time}ms" }

                            val result = when (error) {
                                null -> TelemetryResult.Succeeded
                                is ProcessCanceledException, is CancellationException -> TelemetryResult.Cancelled
                                else -> TelemetryResult.Failed
                            }

                            CodecatalystTelemetry.devEnvironmentWorkflowStatistic(
                                project = null,
                                userId = userId,
                                result = result,
                                duration = time.toDouble(),
                                codecatalystDevEnvironmentWorkflowStep = step.stepName,
                                codecatalystDevEnvironmentWorkflowError = error?.javaClass?.simpleName
                            )
                        }
                    }
                }
            }

            StepExecutor(project = null, workflow, workflowEmitter)
                .also {
                    it.addContext(CAWS_CONNECTION_PARAMETERS, parameters)
                    lifetime.onTermination {
                        it.getProcessHandler().destroyProcess()
                    }

                    it.onSuccess = {
                        promise.setResult(Unit)
                    }

                    it.onError = { throwable ->
                        promise.setError(throwable)
                    }

                    it.startExecution()
                }
        }

        start()

        return promise
    }
}

data class CawsConnectionParameters(
    val space: String,
    val project: String,
    val envId: String,
    val gitSettings: GitSettings,
    val toolkitInstallSettings: ToolkitInstallSettings,
) {
    companion object {
        const val CAWS_SPACE = "aws.codecatalyst.space"
        const val CAWS_PROJECT = "aws.codecatalyst.project"
        const val CAWS_ENV_ID = "aws.codecatalyst.env.id"
        const val CAWS_GIT_REPO_NAME = "aws.codecatalyst.git.repo.name"
        const val CAWS_UNLINKED_GIT_REPO_URL = "aws.caws.unlinked.git.repo.url"
        const val CAWS_UNLINKED_GIT_REPO_BRANCH = "aws.caws.unlinked.git.repo.branch"
        const val DEV_SETTING_USE_BUNDLED_TOOLKIT = "aws.caws.dev.use.bundled.toolkit"
        const val DEV_SETTING_TOOLKIT_PATH = "aws.caws.dev.toolkit.path"
        const val DEV_SETTING_S3_STAGING = "aws.caws.dev.s3.staging"

        fun fromParameters(parameters: Map<String, String>): CawsConnectionParameters {
            val spaceName = parameters[CAWS_SPACE] ?: error("Missing required parameter: CAWS space name")
            val projectName = parameters[CAWS_PROJECT] ?: throw IllegalStateException("Missing required parameter: CAWS project name")
            val envId = parameters[CAWS_ENV_ID] ?: throw IllegalStateException("Missing required parameter: CAWS environment id")
            val repoName = parameters[CAWS_GIT_REPO_NAME]
            val gitRepoUrl = parameters[CAWS_UNLINKED_GIT_REPO_URL]
            val gitRepoBranch = parameters[CAWS_UNLINKED_GIT_REPO_BRANCH]
            val useBundledToolkit = parameters[DEV_SETTING_USE_BUNDLED_TOOLKIT]?.toBoolean()
            val toolkitPath = parameters[DEV_SETTING_TOOLKIT_PATH]
            val s3StagingBucket = parameters[DEV_SETTING_S3_STAGING]

            val gitSettings =
                if (repoName != null) {
                    GitSettings.CawsOwnedRepoSettings(repoName)
                } else if (!gitRepoUrl.isNullOrEmpty() && !gitRepoBranch.isNullOrEmpty()) {
                    GitSettings.CloneGitSettings(gitRepoUrl, gitRepoBranch)
                } else {
                    GitSettings.NoRepo
                }

            val providedInstallSettings =
                if (useBundledToolkit == true) {
                    ToolkitInstallSettings.UseSelf
                } else if (toolkitPath?.isNotBlank() == true && s3StagingBucket?.isNotBlank() == true) {
                    ToolkitInstallSettings.UseArbitraryLocalPath(toolkitPath, s3StagingBucket)
                } else {
                    ToolkitInstallSettings.UseMarketPlace
                }

            return CawsConnectionParameters(
                spaceName,
                projectName,
                envId,
                gitSettings,
                providedInstallSettings
            )
        }
    }
}


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

package software.aws.toolkits.jetbrains.gateway

import com.intellij.ide.BrowserUtil
import com.jetbrains.gateway.api.GatewayConnector
import com.jetbrains.gateway.api.GatewayConnectorDocumentation
import com.jetbrains.gateway.api.GatewayConnectorView
import com.jetbrains.gateway.api.GatewayRecentConnections
import com.jetbrains.rd.util.lifetime.Lifetime
import icons.AwsIcons
import software.aws.toolkits.jetbrains.core.credentials.sono.SonoCredentialManager
import software.aws.toolkits.jetbrains.gateway.welcomescreen.ExistingWorkspaces
import software.aws.toolkits.jetbrains.services.caws.CawsEndpoints
import software.aws.toolkits.jetbrains.utils.runUnderProgressIfNeeded
import software.aws.toolkits.resources.message
import java.awt.Component
import javax.swing.Icon
import javax.swing.JComponent

class CawsConnector : GatewayConnector {
    override val icon: Icon = AwsIcons.Logos.AWS_SMILE_LARGE

    override fun getTitle(): String = message("code.aws")

    override fun getDescription(): String = message("caws.create_workspace_description")

    override fun getActionText(): String = if (isSignedIn()) {
        message("caws.workspace.new")
    } else {
        message("caws.login")
    }

    override fun getConnectorId() = CONNECTOR_ID

    override fun getTitleAdornment(): JComponent? = null

    override fun createView(lifetime: Lifetime): GatewayConnectorView = object : GatewayConnectorView {
        override val component: JComponent
            get() {
                if (!isSignedIn()) {
                    runUnderProgressIfNeeded(null, message("credentials.sono.login.pending"), true) {
                        SonoCredentialManager.loginSono(null)
                    }
                }

                return cawsWizard(lifetime)
            }
    }

    override fun getDocumentationAction() = GatewayConnectorDocumentation(true) {
        BrowserUtil.browse(CawsEndpoints.CAWS_DEV_ENV_MARKETING)
    }

    override fun getRecentConnections(setContentCallback: (Component) -> Unit): GatewayRecentConnections = object : GatewayRecentConnections {
        override val recentsIcon: Icon = AwsIcons.Logos.AWS_SMILE_SMALL

        override fun createRecentsView(lifetime: Lifetime): JComponent = ExistingWorkspaces(setContentCallback, lifetime).getComponent()

        override fun getRecentsTitle(): String = message("code.aws.workspaces")

        override fun updateRecentView() {}
    }

    private fun isSignedIn() = SonoCredentialManager
        .getInstance()
        .hasPreviouslyConnected()

    companion object {
        const val CONNECTOR_ID = "aws.codecatalyst.connector"
    }
}


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

package software.aws.toolkits.jetbrains.gateway

import com.intellij.ide.browsers.BrowserLauncher
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.rd.createNestedDisposable
import com.intellij.openapi.rd.util.launchOnUi
import com.intellij.openapi.rd.util.startChildIOBackgroundAsync
import com.intellij.openapi.rd.util.startUnderModalProgressAsync
import com.intellij.openapi.rd.util.withUiContext
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.setEmptyState
import com.intellij.openapi.util.Disposer
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBLoadingPanel
import com.intellij.ui.components.JBRadioButton
import com.intellij.ui.components.panels.Wrapper
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.COLUMNS_MEDIUM
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.Row
import com.intellij.ui.dsl.builder.TopGap
import com.intellij.ui.dsl.builder.actionListener
import com.intellij.ui.dsl.builder.bind
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.columns
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.selected
import com.intellij.ui.dsl.builder.toMutableProperty
import com.intellij.ui.layout.not
import com.intellij.ui.layout.selected
import com.jetbrains.gateway.api.GatewayUI
import com.jetbrains.gateway.welcomeScreen.MultistagePanel
import com.jetbrains.gateway.welcomeScreen.MultistagePanelContainer
import com.jetbrains.gateway.welcomeScreen.MultistagePanelDelegate
import com.jetbrains.rd.util.lifetime.Lifetime
import software.amazon.awssdk.services.codecatalyst.CodeCatalystClient
import software.amazon.awssdk.services.codecatalyst.model.InstanceType
import software.aws.toolkits.core.ClientConnectionSettings
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.sono.lazilyGetUserId
import software.aws.toolkits.jetbrains.core.utils.buildMap
import software.aws.toolkits.jetbrains.gateway.connection.IdeBackendActions
import software.aws.toolkits.jetbrains.gateway.welcomescreen.recursivelySetBackground
import software.aws.toolkits.jetbrains.gateway.welcomescreen.setDefaultBackgroundAndBorder
import software.aws.toolkits.jetbrains.services.caws.CawsCodeRepository
import software.aws.toolkits.jetbrains.services.caws.CawsEndpoints
import software.aws.toolkits.jetbrains.services.caws.CawsProject
import software.aws.toolkits.jetbrains.services.caws.CawsResources
import software.aws.toolkits.jetbrains.services.caws.InactivityTimeout
import software.aws.toolkits.jetbrains.services.caws.isSubscriptionFreeTier
import software.aws.toolkits.jetbrains.services.caws.isSupportedInFreeTier
import software.aws.toolkits.jetbrains.services.caws.listAccessibleProjectsPaginator
import software.aws.toolkits.jetbrains.services.caws.loadParameterDescriptions
import software.aws.toolkits.jetbrains.settings.CawsSpaceTracker
import software.aws.toolkits.jetbrains.ui.AsyncComboBox
import software.aws.toolkits.jetbrains.utils.ui.find
import software.aws.toolkits.jetbrains.utils.ui.selected
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodecatalystCreateDevEnvironmentRepoType
import software.aws.toolkits.telemetry.CodecatalystTelemetry
import java.awt.BorderLayout
import java.awt.event.ItemEvent
import javax.swing.JComponent
import software.aws.toolkits.telemetry.Result as TelemetryResult

class CawsSettings(
    // core bindings
    var project: CawsProject? = null,
    var productType: GatewayProduct? = null,
    var linkedRepoName: String? = null,
    var linkedRepoBranch: BranchSummary? = null,
    var createBranchName: String = "",
    var unlinkedRepoUrl: String = "",
    var unlinkedRepoBranch: String? = null,
    var alias: String = "",
    var cloneType: CawsWizardCloneType = CawsWizardCloneType.NONE,
    var instanceType: InstanceType = InstanceType.DEV_STANDARD1_SMALL,
    var persistentStorage: Int? = 0,
    var inactivityTimeout: InactivityTimeout = InactivityTimeout.DEFAULT_TIMEOUT,

    // dev settings
    var useBundledToolkit: Boolean = false,
    var s3StagingBucket: String = "",
    var toolkitLocation: String = "",

    // intermediate values
    var connectionSettings: ClientConnectionSettings<*>? = null,
    var branchCloneType: BranchCloneType = BranchCloneType.EXISTING,
    var is3P: Boolean = false
)

fun cawsWizard(lifetime: Lifetime, settings: CawsSettings = CawsSettings()) = MultistagePanelContainer(
    listOf(
        CawsInstanceSetupPanel(lifetime)
    ),
    settings,
    object : MultistagePanelDelegate<CawsSettings> {
        override fun onMultistagePanelBack(context: CawsSettings) {
            GatewayUI.getInstance().reset()
            CodecatalystTelemetry.createDevEnvironment(project = null, userId = lazilyGetUserId(), result = TelemetryResult.Cancelled)
        }

        override fun onMultistagePanelDone(context: CawsSettings) {
            val productType = context.productType ?: throw RuntimeException("CAWS wizard finished but productType was not set")
            val connectionSettings = context.connectionSettings ?: throw RuntimeException("CAWS wizard finished but connectionSettings was not set")

            lifetime.startUnderModalProgressAsync(
                title = message("caws.creating_workspace"),
                canBeCancelled = false,
                isIndeterminate = true
            ) {
                val userId = lazilyGetUserId()
                val start = System.currentTimeMillis()
                val env = try {
                    val cawsClient = connectionSettings.awsClient<CodeCatalystClient>()
                    if (context.cloneType == CawsWizardCloneType.UNLINKED_3P) {
                        error("Not implemented")
                    }

                    if (context.is3P) {
                        context.branchCloneType = BranchCloneType.EXISTING
                    }

                    if (context.branchCloneType == BranchCloneType.NEW_FROM_EXISTING) {
                        withTextAboveProgressBar(message("caws.creating_branch")) {
                            cawsClient.createSourceRepositoryBranch {
                                val project = context.project ?: throw RuntimeException("project was null")
                                val commitId = context.linkedRepoBranch?.headCommitId ?: throw RuntimeException("source commit id was not defined")
                                it.spaceName(project.space)
                                it.projectName(project.project)
                                it.sourceRepositoryName(context.linkedRepoName)
                                it.name(context.createBranchName)
                                it.headCommitId(commitId)
                            }
                        }
                    }

                    IdeBackendActions.createWorkspace(cawsClient, context).also {
                        val repoType = when (context.cloneType) {
                            CawsWizardCloneType.CAWS -> CodecatalystCreateDevEnvironmentRepoType.Linked
                            CawsWizardCloneType.UNLINKED_3P -> CodecatalystCreateDevEnvironmentRepoType.Unlinked
                            CawsWizardCloneType.NONE -> CodecatalystCreateDevEnvironmentRepoType.None
                        }
                        CodecatalystTelemetry.devEnvironmentWorkflowStatistic(
                            project = null,
                            userId = userId,
                            result = TelemetryResult.Succeeded,
                            duration = (System.currentTimeMillis() - start).toDouble(),
                            codecatalystDevEnvironmentWorkflowStep = "createDevEnvironment"
                        )
                        CodecatalystTelemetry.createDevEnvironment(
                            project = null,
                            userId = userId,
                            codecatalystCreateDevEnvironmentRepoType = repoType,
                            result = TelemetryResult.Succeeded
                        )
                    }
                } catch (e: Exception) {
                    val message = message("caws.workspace.creation.failed")
                    getLogger<CawsInstanceSetupPanel>().error(e) { message }
                    withUiContext {
                        Messages.showErrorDialog(e.message ?: message("general.unknown_error"), message)
                    }
                    CodecatalystTelemetry.devEnvironmentWorkflowStatistic(
                        project = null,
                        userId = userId,
                        result = TelemetryResult.Failed,
                        duration = (System.currentTimeMillis() - start).toDouble(),
                        codecatalystDevEnvironmentWorkflowStep = "createDevEnvironment"
                    )
                    CodecatalystTelemetry.createDevEnvironment(project = null, userId = userId, result = TelemetryResult.Failed)
                    return@startUnderModalProgressAsync
                }

                val parameters = mapOf(
                    CawsConnectionParameters.CAWS_SPACE to env.identifier.project.space,
                    CawsConnectionParameters.CAWS_PROJECT to env.identifier.project.project,
                    CawsConnectionParameters.CAWS_ENV_ID to env.identifier.id,
                    CawsConnectionParameters.DEV_SETTING_USE_BUNDLED_TOOLKIT to context.useBundledToolkit.toString(),
                    CawsConnectionParameters.DEV_SETTING_S3_STAGING to context.s3StagingBucket,
                    CawsConnectionParameters.DEV_SETTING_TOOLKIT_PATH to context.toolkitLocation,
                ) + buildMap {
                    when (context.cloneType) {
                        CawsWizardCloneType.CAWS -> {
                            val repoName = context.linkedRepoName ?: throw RuntimeException("CAWS wizard finished but linkedRepoName was not set")
                            put(CawsConnectionParameters.CAWS_GIT_REPO_NAME, repoName)
                        }

                        CawsWizardCloneType.UNLINKED_3P -> {
                            val branch = context.unlinkedRepoBranch ?: throw RuntimeException("CAWS wizard finished but unlinkedRepoBranch was not set")
                            put(CawsConnectionParameters.CAWS_UNLINKED_GIT_REPO_URL, context.unlinkedRepoUrl)
                            put(CawsConnectionParameters.CAWS_UNLINKED_GIT_REPO_BRANCH, branch)
                        }

                        CawsWizardCloneType.NONE -> {}
                    }
                }

                withUiContext {
                    GatewayUI.getInstance().connect(parameters)
                }
            }
        }
    }
)

class CawsInstanceSetupPanel(private val lifetime: Lifetime) : MultistagePanel<CawsSettings> {
    private lateinit var panel: EnvironmentDetailsPanel

    override fun getComponent(context: CawsSettings): JComponent {
        panel = EnvironmentDetailsPanel(context, lifetime)
        return panel.getComponent()
    }

    override fun init(context: CawsSettings, canGoBackAndForthConsumer: (Boolean, Boolean) -> Unit) {
    }

    override fun onEnter(context: CawsSettings, isForward: Boolean) {}

    override suspend fun onGoingToLeave(context: CawsSettings, isForward: Boolean): Boolean {
        if (isForward) {
            return panel.runValidation()
        }

        return true
    }

    override fun onLeave(context: CawsSettings, isForward: Boolean) {}

    override fun shouldSkip(context: CawsSettings, isForward: Boolean) = false

    override fun forwardButtonText(): String = message("caws.create_workspace")
}

class EnvironmentDetailsPanel(private val context: CawsSettings, lifetime: Lifetime) : CawsLoadingPanel(lifetime) {
    private val disposable = lifetime.createNestedDisposable()
    private val environmentParameters = loadParameterDescriptions().environmentParameters
    private lateinit var createPanel: DialogPanel

    override val title = context.project?.let { message("caws.workspace.details.project_specific_title", it.project) }
        ?: message("caws.workspace.details.title")

    override fun getContent(connectionSettings: ClientConnectionSettings<*>): JComponent {
        context.connectionSettings = connectionSettings
        val client = AwsClientManager.getInstance().getClient<CodeCatalystClient>(connectionSettings)
        val spaces = getSpaces(client)
        return if (spaces.isEmpty()) {
            InfoPanel()
                .addLine(message("caws.workspace.details.introduction_message"))
                .addAction(message("general.get_started")) {
                    BrowserLauncher.instance.browse(CawsEndpoints.ConsoleFactory.baseUrl())
                }
                .addAction(message("general.refresh")) { lifetime.launchOnUi { startLoading() } }
        } else panel {
            row(message("caws.workspace.ide_label")) {
                bottomGap(BottomGap.MEDIUM)
                ideVersionComboBox(disposable, context::productType)
            }

            lateinit var branchOptions: Row
            lateinit var newBranchOption: Cell<JBRadioButton>
            lateinit var newBranch: Row
            lateinit var cloneRepoButton: Cell<JBRadioButton>
            lateinit var existingBranchOption: Cell<JBRadioButton>
            val existingProject = context.project
            val existingRepo = context.linkedRepoName

            if (existingRepo != null) {
                context.cloneType = CawsWizardCloneType.CAWS
            }

            panel {
                group(message("caws.workspace.settings.repository_header"), indent = false) {
                    row {
                        comment(message("caws.workspace.clone.info_repo"))
                    }

                    buttonsGroup {
                        row {
                            cloneRepoButton = radioButton(message("caws.workspace.details.clone_repo"), CawsWizardCloneType.CAWS).applyToComponent {
                                isSelected = context.cloneType == CawsWizardCloneType.CAWS
                            }.actionListener { event, component ->
                                if (!context.is3P) {
                                    branchOptions.visible(cloneRepoButton.component.isSelected)
                                    if (newBranchOption.component.isSelected) {
                                        newBranch.visible(true)
                                    }
                                }
                            }

                            radioButton(message("caws.workspace.details.create_empty_dev_env"), CawsWizardCloneType.NONE).applyToComponent {
                                isSelected = context.cloneType == CawsWizardCloneType.NONE
                            }
                                .actionListener { event, component ->
                                    branchOptions.visible(cloneRepoButton.component.isSelected)
                                    newBranch.visible(cloneRepoButton.component.isSelected)
                                    existingBranchOption.component.isSelected = true
                                }
                        }
                    }.bind({ context.cloneType }, { context.cloneType = it })

                    row {
                        label(message("caws.workspace.clone.info"))
                    }.visibleIf(cloneRepoButton.selected)

                    val projectCombo = AsyncComboBox<CawsProject> { label, value, _ ->
                        value ?: return@AsyncComboBox
                        label.text = "${value.project} (${value.space})"
                    }
                    Disposer.register(disposable, projectCombo)

                    row(message("caws.project")) {
                        cell(projectCombo)
                            .bindItem(context::project.toMutableProperty())
                            .errorOnApply(message("caws.workspace.details.project_validation")) { it.selectedItem == null }
                            .columns(COLUMNS_MEDIUM)
                    }

                    val linkedRepoCombo = AsyncComboBox<SourceRepository> { label, value, _ -> label.text = value?.name }
                    val linkedBranchCombo = AsyncComboBox<BranchSummary> { label, value, _ -> label.text = value?.name }
                    Disposer.register(disposable, linkedRepoCombo)
                    Disposer.register(disposable, linkedBranchCombo)

                    row(message("caws.repository")) {
                        cell(linkedRepoCombo)
                            .bind(
                                { it.selected()?.name },
                                { i, v -> i.selectedItem = i.model.find { it.name == v } },
                                context::linkedRepoName.toMutableProperty()
                            )
                            .errorOnApply(message("caws.workspace.details.repository_validation")) { it.isVisible && it.selectedItem == null }
                            .columns(COLUMNS_MEDIUM)
                        projectCombo.addActionListener {
                            linkedRepoCombo.proposeModelUpdate { model ->
                                projectCombo.selected()?.let { project ->
                                    val repositories = getRepoNames(project, client)
                                    repositories.forEach { model.addElement(it) }
                                }
                            }
                        }
                    }.visibleIf(cloneRepoButton.selected)

                    if (!existingRepo.isNullOrEmpty()) {
                        linkedBranchCombo.proposeModelUpdate { model ->
                            val project = existingProject ?: throw RuntimeException("existingProject was null after null check")
                            getBranchNames(project, existingRepo, client).forEach { model.addElement(it) }
                        }
                    }

                    row {
                        label(message("caws.workspace.details.branch_title"))
                    }.visibleIf(cloneRepoButton.selected)

                    row { comment(message("caws.workspace.details.create_branch_comment")) }.visibleIf(cloneRepoButton.selected)

                    buttonsGroup {
                        branchOptions = row {
                            newBranchOption = radioButton(message("caws.workspace.details.branch_new"), BranchCloneType.NEW_FROM_EXISTING)
                                .applyToComponent {
                                    isSelected = context.branchCloneType == BranchCloneType.NEW_FROM_EXISTING
                                }.bindSelected(
                                    { context.branchCloneType == BranchCloneType.NEW_FROM_EXISTING },
                                    { if (it) context.branchCloneType = BranchCloneType.NEW_FROM_EXISTING }
                                ).actionListener { event, component ->
                                    newBranch.visibleIf(component.selected)
                                }

                            existingBranchOption = radioButton(message("caws.workspace.details.branch_existing"), BranchCloneType.EXISTING)
                                .applyToComponent {
                                    isSelected = context.branchCloneType == BranchCloneType.EXISTING
                                }.bindSelected(
                                    { context.branchCloneType == BranchCloneType.EXISTING },
                                    { if (it) context.branchCloneType = BranchCloneType.EXISTING }
                                )
                        }.apply { visible(cloneRepoButton.component.isSelected) }
                    }.bind({ context.branchCloneType }, { context.branchCloneType = it })

                    newBranch = row(message("caws.workspace.details.branch_new")) {
                        textField().bindText(context::createBranchName)
                            .errorOnApply(message("caws.workspace.details.branch_new_validation")) {
                                it.isVisible && it.text.isNullOrBlank()
                            }
                    }.apply {
                        visible(context.branchCloneType == BranchCloneType.NEW_FROM_EXISTING && cloneRepoButton.component.isSelected)
                    }

                    row(message("caws.workspace.details.branch_existing")) {
                        cell(linkedBranchCombo)
                            .bindItem(context::linkedRepoBranch.toMutableProperty())
                            .errorOnApply(message("caws.workspace.details.branch_validation")) { it.isVisible && it.selectedItem == null }
                            .columns(COLUMNS_MEDIUM)

                        linkedRepoCombo.addActionListener {
                            linkedBranchCombo.proposeModelUpdate { model ->
                                projectCombo.selected()?.let { project ->
                                    linkedRepoCombo.selected()?.let { repo ->
                                        context.is3P = isRepo3P(project, repo.name)
                                        if (context.is3P) {
                                            branchOptions.visible(false)
                                            newBranch.visible(false)
                                        } else {
                                            if (cloneRepoButton.component.isSelected) {
                                                branchOptions.visible(true)
                                                if (newBranchOption.component.isSelected) {
                                                    newBranch.visible(true)
                                                }
                                            }
                                        }
                                        val branches = getBranchNames(project, repo.name, client)
                                        branches.forEach { model.addElement(it) }
                                    }
                                }
                            }
                        }
                        contextHelp(message("caws.one.branch.per.dev.env.comment"))
                    }.visibleIf(cloneRepoButton.selected)

                    // need here to force comboboxes to load
                    getProjects(client, spaces).apply {
                        forEach { projectCombo.addItem(it) }
                        projectCombo.selectedItem = existingProject
                            ?: firstOrNull { it.space == CawsSpaceTracker.getInstance().lastSpaceName() }
                            ?: firstOrNull()
                    }

                    val propertyGraph = PropertyGraph()
                    val projectProperty = propertyGraph.property(projectCombo.selected())
                    projectCombo.addItemListener {
                        if (it.stateChange == ItemEvent.SELECTED) {
                            projectProperty.set(it.item as CawsProject?)
                        }
                    }

                    row(message("caws.workspace.details.alias.label")) {
                        topGap(TopGap.MEDIUM)
                        // TODO: would be nice to have mutable combobox with existing projects
                        textField()
                            .bindText(context::alias)
                            .columns(COLUMNS_MEDIUM)
                            .applyToComponent {
                                setEmptyState(message("general.optional"))
                            }
                    }.contextHelp(message("caws.alias.instruction.text"))

                    row {
                        placeholder()
                    }.bottomGap(BottomGap.MEDIUM)

                    group(message("caws.workspace.settings"), indent = false) {
                        row {
                            val wrapper = Wrapper().apply { isOpaque = false }
                            val loadingPanel = JBLoadingPanel(BorderLayout(), disposable).apply {
                                add(wrapper, BorderLayout.CENTER)
                            }
                            val content = { space: String? ->
                                envConfigPanel(space?.let { isSubscriptionFreeTier(client, it) } ?: false)
                            }

                            wrapper.setContent(content(projectProperty.get()?.space))

                            val getDialogPanel = { wrapper.targetComponent as DialogPanel }
                            cell(loadingPanel)
                                .onApply { getDialogPanel().apply() }
                                .onReset { getDialogPanel().reset() }
                                .onIsModified { getDialogPanel().isModified() }

                            projectProperty.afterChange {
                                lifetime.launchOnUi {
                                    loadingPanel.startLoading()
                                    val panel = startChildIOBackgroundAsync { content(it?.space) }.await()
                                    wrapper.setContent(panel)
                                    loadingPanel.stopLoading()
                                }
                            }
                        }
                    }

                    if (AwsToolkit.isDeveloperMode()) {
                        group(message("caws.workspace.details.developer_tool_settings")) {
                            lateinit var useBundledToolkit: Cell<JBCheckBox>
                            row {
                                useBundledToolkit = checkBox(message("caws.workspace.details.use_bundled_toolkit")).bindSelected(context::useBundledToolkit)
                            }

                            panel {
                                row(message("caws.workspace.details.backend_toolkit_location")) {
                                    textFieldWithBrowseButton(
                                        message("caws.workspace.details.toolkit_location"),
                                        fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileDescriptor()
                                    ).bindText(context::toolkitLocation)
                                }

                                row(message("caws.workspace.details.s3_bucket")) {
                                    textField()
                                        .bindText(context::s3StagingBucket)
                                        .columns(COLUMNS_MEDIUM)
                                }
                            }.visibleIf(useBundledToolkit.selected.not())
                        }
                    }
                }
            }
        }.also {
            setDefaultBackgroundAndBorder(it)
            it.registerValidators(disposable)
            createPanel = it
        }.let {
            ScrollPaneFactory.createScrollPane(it, true)
        }
    }

    private fun getSpaces(client: CodeCatalystClient) = client.listSpacesPaginator { }
        .items()
        .map { it.name() }

    private fun getProjects(client: CodeCatalystClient, spaces: List<String>) = spaces
        .flatMap { space ->
            client.listAccessibleProjectsPaginator { it.spaceName(space) }.items()
                .map { project -> CawsProject(space, project.name()) }
        }
        .sortedByDescending { it.project }

    private fun getRepoNames(project: CawsProject, client: CodeCatalystClient) = client.listSourceRepositoriesPaginator {
        it.spaceName(project.space)
        it.projectName(project.project)
    }
        .items()
        .map { it.toSourceRepository() }
        .sortedBy { it.name }

    private fun isRepo3P(project: CawsProject, repo: String): Boolean {
        val connectionSettings = context.connectionSettings ?: throw RuntimeException("ConnectionSettings was not set")
        val url = AwsResourceCache.getInstance().getResource(
            CawsResources.cloneUrls(CawsCodeRepository(project.space, project.project, repo)),
            connectionSettings
        ).toCompletableFuture().get()
        return !url.contains(CawsEndpoints.CAWS_GIT_PATTERN)
    }

    private fun getBranchNames(project: CawsProject, repo: String, client: CodeCatalystClient) =
        client.listSourceRepositoryBranchesPaginator {
            it.spaceName(project.space)
            it.projectName(project.project)
            it.sourceRepositoryName(repo)
        }
            .items()
            .map { summary ->
                val branchName = summary.name()

                BranchSummary(
                    if (branchName.startsWith(BRANCH_PREFIX)) {
                        branchName.substringAfter(BRANCH_PREFIX)
                    } else {
                        branchName
                    },
                    summary.headCommitId()
                )
            }
            .sortedBy { it.name }

    private fun envConfigPanel(isFreeTier: Boolean) =
        panel {
            if (isFreeTier) {
                row {
                    comment(message("caws.compute.size.in.free.tier.comment"))
                }
            }

            cawsEnvironmentSize(
                environmentParameters,
                context::instanceType,
                isFreeTier
            )

            row {
                label(message("caws.workspace.details.persistent_storage_title"))
                comboBox(
                    PersistentStorageOptions(environmentParameters.persistentStorageSize.filter { it > 0 }, isFreeTier),
                    SimpleListCellRenderer.create { label, value, _ ->
                        label.isEnabled = if (isFreeTier) {
                            value.isSupportedInFreeTier()
                        } else {
                            true
                        }
                        label.text = message("caws.storage.value", value)
                    }
                ).bindItem(context::persistentStorage.toMutableProperty())
            }.bottomGap(BottomGap.MEDIUM).contextHelp(message("caws.workspace.details.persistent_storage_comment"))

            row {
                cawsEnvironmentTimeout(context::inactivityTimeout)
            }.contextHelp(message("caws.workspace.details.inactivity_timeout_comment"))
        }.apply {
            recursivelySetBackground(this)
        }

    fun runValidation(): Boolean {
        try {
            if (createPanel.validateAll().isEmpty()) {
                createPanel.apply()
                return true
            }
        } catch (e: UninitializedPropertyAccessException) { // error is displayed on the panel
        }
        return false
    }

    companion object {
        const val BRANCH_PREFIX = "refs/heads/"
    }
}

enum class CawsWizardCloneType {
    CAWS,
    UNLINKED_3P,
    NONE
}

enum class BranchCloneType {
    EXISTING,
    NEW_FROM_EXISTING
}

class PersistentStorageOptions(items: List<Int>, private val subscriptionIsFreeTier: Boolean) : CollectionComboBoxModel<Int>(items) {
    override fun setSelectedItem(item: Any?) {
        if (subscriptionIsFreeTier) {
            if (item != 16) {
                super.setSelectedItem(16)
            }
        } else {
            super.setSelectedItem(item)
        }
    }
}


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

package software.aws.toolkits.jetbrains.gateway

import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.rd.createNestedDisposable
import com.intellij.openapi.rd.util.launchIOBackground
import com.intellij.openapi.rd.util.launchOnUi
import com.intellij.openapi.rd.util.withUiAnyModalityContext
import com.intellij.openapi.ui.VerticalFlowLayout
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBLoadingPanel
import com.intellij.ui.components.labels.LinkLabel
import com.intellij.ui.components.labels.LinkListener
import com.intellij.ui.components.panels.NonOpaquePanel
import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.components.BorderLayoutPanel
import com.jetbrains.rd.util.lifetime.Lifetime
import icons.AwsIcons
import software.aws.toolkits.core.ClientConnectionSettings
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.gateway.welcomescreen.BottomLineBorder
import software.aws.toolkits.jetbrains.gateway.welcomescreen.DEFAULT_WELCOME_BORDER
import software.aws.toolkits.jetbrains.gateway.welcomescreen.PANEL_SIDE_INSET
import software.aws.toolkits.jetbrains.gateway.welcomescreen.PANEL_TOP_INSET
import software.aws.toolkits.jetbrains.gateway.welcomescreen.recursivelySetBackground
import software.aws.toolkits.jetbrains.services.caws.CawsLetterBadge
import software.aws.toolkits.jetbrains.services.caws.CawsResources
import software.aws.toolkits.jetbrains.ui.connection.SonoLoginOverlay
import software.aws.toolkits.resources.message
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.event.ActionEvent
import java.util.concurrent.atomic.AtomicReference
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.SwingConstants
import javax.swing.border.EmptyBorder

abstract class CawsLoadingPanel(protected val lifetime: Lifetime, private val setContentCallback: ((Component) -> Unit)? = null) : JPanel() {
    private val disposable = lifetime.createNestedDisposable()
    private val titleBar = Wrapper()
    private val content = Wrapper()
    private val loadingPanel by lazy {
        JBLoadingPanel(BorderLayout(), disposable).also {
            it.isOpaque = false
            it.add(content, BorderLayout.CENTER)
        }
    }
    private val loadingJob = AtomicReference<ProgressIndicator>()

    private val sonoOverlay by lazy {
        object : SonoLoginOverlay(null, lifetime.createNestedDisposable(), {
            border = null
            BorderLayoutPanel().apply {
                addToTop(titleBar)
                addToCenter(loadingPanel)

                recursivelySetBackground(this)

                doLoadContent(it)
            }
        }) {
            override fun initBorders() {
                background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
                border = DEFAULT_WELCOME_BORDER
                isOpaque = true
            }
        }
    }

    abstract val title: String

    open val showSpaceSelector: Boolean = false

    abstract fun getContent(connectionSettings: ClientConnectionSettings<*>): JComponent

    open fun getComponent(): JComponent = sonoOverlay

    protected fun startLoading() = sonoOverlay.redraw()

    private fun doLoadContent(connectionSettings: ClientConnectionSettings<*>) {
        content.removeAll()

        loadingPanel.startLoading()

        val newJob = EmptyProgressIndicator()
        val oldJob = loadingJob.getAndSet(newJob)
        oldJob?.cancel()
        lifetime.launchIOBackground {
            val titleContent = createTitleBar(title, connectionSettings)
            titleBar.setContent(titleContent)

            val contentPanel = getContent(connectionSettings)
            withUiAnyModalityContext {
                if (!newJob.isCanceled) {
                    val oldContent = content.targetComponent
                    if (oldContent is Disposable) {
                        Disposer.dispose(oldContent)
                    }

                    content.setContent(contentPanel)
                    contentPanel.preferredSize = Dimension(contentPanel.width, contentPanel.height + titleContent.height)
                }
            }
        }.invokeOnCompletion { throwable ->
            runInEdt(ModalityState.any()) {
                loadingPanel.stopLoading()
                if (throwable != null) {
                    content.setContent(buildLoadError(throwable))
                }
            }
        }
    }

    protected fun buildLoadError(e: Throwable) =
        InfoPanel()
            .addLine(message("caws.list_workspaces_failed"), isError = true)
            .addLine(e.message ?: message("general.unknown_error"), isError = true)
            .addAction(message("settings.retry")) { lifetime.launchOnUi { startLoading() } }
            .also {
                LOG.error(e) { "Error while loading content" }
            }

    protected fun noRepoWizard(callback: (Component) -> Unit) {
        callback(
            cawsWizard(
                lifetime,
                CawsSettings()
            )
        )
    }

    protected class InfoPanel : NonOpaquePanel(VerticalFlowLayout(VerticalFlowLayout.MIDDLE)) {
        init {
            add(
                JBLabel().apply {
                    icon = AwsIcons.Logos.AWS_SMILE_LARGE
                    horizontalAlignment = JBLabel.CENTER
                }
            )

            border = EmptyBorder(BORDER_INSET)
        }

        fun addLine(message: String, isError: Boolean = false) = apply {
            val line = JBLabel()
            line.isOpaque = false
            line.text = "$message"
            line.horizontalAlignment = SwingConstants.CENTER

            if (isError) {
                line.foreground = UIUtil.getErrorForeground()
            }

            add(line)
        }

        fun addAction(message: String, handler: (ActionEvent) -> Unit) = apply {
            val action = ActionLink(message, handler)
            action.horizontalAlignment = SwingConstants.CENTER

            add(action)
        }

        fun addDefaultActionButton(message: String, handler: (ActionEvent) -> Unit) = apply {
            add(
                NonOpaquePanel(FlowLayout()).apply {
                    add(
                        JButton(message).apply {
                            isOpaque = false
                            putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
                            addActionListener(handler)
                        }
                    )
                }
            )
        }
    }

    private fun createTitleBar(title: String, connectionSettings: ClientConnectionSettings<*>): JComponent {
        val rightPanel = BorderLayoutPanel().apply {
            isOpaque = false
            addToCenter(createConnectionSettingsPanel(connectionSettings))
        }

        return BorderLayoutPanel().apply {
            isOpaque = false
            border = BottomLineBorder(
                WelcomeScreenUIManager.getSeparatorColor(),
                BORDER_INSET
            )

            addToLeft(
                BorderLayoutPanel(10, 0).apply {
                    isOpaque = false

                    addToCenter(
                        JBLabel(title).apply {
                            font = JBFont.h3().asBold()
                        }
                    )

                    if (showSpaceSelector) {
                        addToRight(
                            CawsSpacePopupComboLabel(connectionSettings) { startLoading() }
                        )
                    }
                }
            )
            addToRight(rightPanel)
        }
    }

    private fun createConnectionSettingsPanel(connectionSettings: ClientConnectionSettings<*>): JComponent = BorderLayoutPanel(20, 0).apply {
        isOpaque = false

        setContentCallback?.let {
            addToLeft(
                LinkLabel<Void>(
                    message("caws.workspace.new"),
                    null,
                    LinkListener { _, _ ->
                        noRepoWizard(it)
                    }
                ).apply {
                    isVisible = false
                    // don't show until spaces have been loaded
                    AwsResourceCache.getInstance().getResource(CawsResources.ALL_SPACES, connectionSettings).thenAccept {
                        runInEdt(ModalityState.any()) {
                            isVisible = true
                        }
                    }
                }
            )
        }

        addToCenter(
            CawsLetterBadge(connectionSettings)
        )
    }

    companion object {
        private val LOG = getLogger<CawsLoadingPanel>()
        private val BORDER_INSET = JBUI.insets(PANEL_TOP_INSET, PANEL_SIDE_INSET, UIUtil.LARGE_VGAP, PANEL_SIDE_INSET)
    }
}