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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs

import software.amazon.awssdk.services.cloudwatchlogs.model.FilteredLogEvent
import software.amazon.awssdk.services.cloudwatchlogs.model.OutputLogEvent

data class LogStreamEntry(val message: String, val timestamp: Long)

fun OutputLogEvent.toLogStreamEntry() = LogStreamEntry(message()?.trim() ?: "", timestamp() ?: 0)
fun FilteredLogEvent.toLogStreamEntry() = LogStreamEntry(message()?.trim() ?: "", timestamp() ?: 0)


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions

import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.explorer.actions.DeleteResourceAction
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogsNode
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.resources.CloudWatchResources
import software.aws.toolkits.resources.message

class DeleteGroupAction : DeleteResourceAction<CloudWatchLogsNode>(message("cloudwatch.logs.delete_log_group")) {
    override fun performDelete(selected: CloudWatchLogsNode) {
        val client: CloudWatchLogsClient = selected.nodeProject.awsClient()

        CloudWatchLogWindow.getInstance(selected.nodeProject).closeLogGroup(selected.logGroupName)

        client.deleteLogGroup { it.logGroupName(selected.logGroupName) }
        selected.nodeProject.refreshAwsTree(CloudWatchResources.LIST_LOG_GROUPS)
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamDownloadToFileTask
import software.aws.toolkits.resources.message

class DownloadLogStreamToFileAction(
    private val project: Project,
    private val client: CloudWatchLogsClient,
    private val logGroup: String,
    private val logStream: String?
) : AnAction(message("cloudwatch.logs.save_action"), null, AllIcons.Actions.MenuSaveall), DumbAware {
    override fun actionPerformed(e: AnActionEvent) {
        logStream ?: return
        ProgressManager.getInstance().run(LogStreamDownloadToFileTask(project, client, logGroup, logStream, ""))
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.resources.message

class ExportActionGroup(
    private val project: Project,
    private val client: CloudWatchLogsClient,
    private val logGroup: String,
    private val logStream: () -> String?
) : ActionGroup(message("cloudwatch.logs.export"), null, AllIcons.Actions.Download), DumbAware {
    init {
        isPopup = true
    }

    override fun getChildren(e: AnActionEvent?): Array<AnAction> = arrayOf(
        OpenLogStreamInEditorAction(project, client, logGroup, logStream()),
        DownloadLogStreamToFileAction(project, client, logGroup, logStream())
    )
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.core.coroutines.asContextElement
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamEntry
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudwatchlogsTelemetry

class OpenCurrentInEditorAction(
    private val project: Project,
    private val logStream: String,
    private val tableEntries: () -> List<LogStreamEntry>
) : AnAction(message("cloudwatch.logs.open_in_editor"), null, AllIcons.Actions.MenuOpen),
    DumbAware {
    private val coroutineScope = projectCoroutineScope(project)

    override fun actionPerformed(e: AnActionEvent) {
        val modality = e.dataContext.getData(PlatformDataKeys.CONTEXT_COMPONENT)?.let { ModalityState.stateForComponent(it) }
            ?: ModalityState.defaultModalityState()

        coroutineScope.launch(modality.asContextElement()) {
            val success = OpenStreamInEditor.open(project, logStream, tableEntries().buildStringFromLogs())
            CloudwatchlogsTelemetry.viewCurrentMessagesInEditor(project, success = success, value = tableEntries().size.toDouble())
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogsNode
import software.aws.toolkits.resources.message

class OpenLogGroupAction : SingleResourceNodeAction<CloudWatchLogsNode>(message("cloudwatch.logs.open")), DumbAware {
    override fun actionPerformed(selected: CloudWatchLogsNode, e: AnActionEvent) {
        CloudWatchLogWindow.getInstance(selected.nodeProject).showLogGroup(selected.logGroupName)
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.jetbrains.core.coroutines.asContextElement
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamDownloadTask
import software.aws.toolkits.resources.message

class OpenLogStreamInEditorAction(
    private val project: Project,
    private val client: CloudWatchLogsClient,
    private val logGroup: String,
    private val logStream: String?
) : AnAction(message("cloudwatch.logs.open_in_editor"), null, AllIcons.Actions.MenuOpen), DumbAware {
    override fun actionPerformed(e: AnActionEvent) {
        logStream ?: return
        val modality = e.dataContext.getData(PlatformDataKeys.CONTEXT_COMPONENT)?.let { ModalityState.stateForComponent(it) }
            ?: ModalityState.defaultModalityState()

        ProgressManager.getInstance().run(LogStreamDownloadTask(project, modality.asContextElement(), client, logGroup, logStream))
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions

import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileTypes.PlainTextLanguage
import com.intellij.openapi.project.Project
import com.intellij.testFramework.ReadOnlyLightVirtualFile
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.cloudwatchlogs.model.OutputLogEvent
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamEntry
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.toLogStreamEntry
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message

object OpenStreamInEditor {
    suspend fun open(project: Project, logStream: String, fileContent: String): Boolean {
        val file = ReadOnlyLightVirtualFile(logStream, PlainTextLanguage.INSTANCE, fileContent)
        return withContext(getCoroutineUiContext()) {
            // set virtual file to read only
            FileEditorManager.getInstance(project).openFile(file, true, true).ifEmpty {
                if (!fileContent.isBlank()) {
                    notifyError(message("cloudwatch.logs.open_in_editor_failed"))
                    return@withContext false
                }
            }
            true
        }
    }
}

// This is named differently because buildStringFromLogs with two different lists has the same type on JVM, yay type erasure
fun List<OutputLogEvent>.buildStringFromLogsOutput() = map { it.toLogStreamEntry() }.buildStringFromLogs()

fun List<LogStreamEntry>.buildStringFromLogs() = buildString {
    this@buildStringFromLogs.forEach { log ->
        val msg = log.message
        append(msg)
        if (!msg.endsWith('\n')) {
            append('\n')
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions

import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.ui.table.TableView
import kotlinx.coroutines.runBlocking
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamEntry
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudwatchlogsTelemetry
import java.time.Duration

class ShowLogsAroundActionGroup(
    private val project: Project,
    private val logGroup: String,
    private val logStream: String,
    private val treeTable: TableView<LogStreamEntry>
) : ActionGroup(message("cloudwatch.logs.show_logs_around"), null, null), DumbAware {
    init {
        isPopup = true
    }

    override fun getChildren(e: AnActionEvent?): Array<AnAction> = arrayOf(
        ShowLogsAround(project, logGroup, logStream, treeTable, message("general.time.one_minute"), Duration.ofMinutes(1)),
        ShowLogsAround(project, logGroup, logStream, treeTable, message("general.time.five_minutes"), Duration.ofMinutes(5)),
        ShowLogsAround(project, logGroup, logStream, treeTable, message("general.time.ten_minutes"), Duration.ofMinutes(10))
    )
}

private class ShowLogsAround(
    private val project: Project,
    private val logGroup: String,
    private val logStream: String,
    private val treeTable: TableView<LogStreamEntry>,
    timeMessage: String,
    private val duration: Duration
) : AnAction(timeMessage, null, null), DumbAware {
    override fun actionPerformed(e: AnActionEvent) = runBlocking {
        CloudwatchlogsTelemetry.showEventsAround(project, success = true, value = duration.toMillis().toDouble())
        val project = e.getRequiredData(PlatformDataKeys.PROJECT)
        val window = CloudWatchLogWindow.getInstance(project)
        val selectedRow = treeTable.selectedRow
        if (selectedRow >= treeTable.listTableModel.rowCount || selectedRow < 0) {
            return@runBlocking
        }
        val selectedObject = treeTable.listTableModel.getItem(selectedRow) ?: return@runBlocking
        window.showLogStream(logGroup, logStream, selectedObject, duration)
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedSendChannelException
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.annotations.TestOnly
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogsActor
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudwatchlogsTelemetry

class TailLogsAction(private val project: Project, private val channel: () -> Channel<CloudWatchLogsActor.Message>) :
    ToggleAction(message("cloudwatch.logs.tail"), null, AllIcons.RunConfigurations.Scroll_down),
    DumbAware {
    private val coroutineScope = projectCoroutineScope(project)
    private var isSelected = false
    var logStreamingJob: Job? = null
        private set

        @TestOnly get

    override fun isSelected(e: AnActionEvent): Boolean = isSelected

    override fun setSelected(e: AnActionEvent, state: Boolean) {
        setSelected(state)
    }

    fun setSelected(state: Boolean) {
        CloudwatchlogsTelemetry.tailStream(project, enabled = state)
        isSelected = state
        if (state) {
            startTailing()
        } else {
            stopTailing()
        }
    }

    private fun startTailing() {
        logStreamingJob = coroutineScope.launch {
            while (true) {
                try {
                    channel().send(CloudWatchLogsActor.Message.LoadForward)
                    delay(1000)
                } catch (e: ClosedSendChannelException) {
                    // Channel is closed, so break out of the while loop and kill the coroutine
                    break
                }
            }
        }
    }

    private fun stopTailing() {
        logStreamingJob?.cancel()
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.ui.table.TableView
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamEntry
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.editor.LogStreamMessageColumn
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudwatchlogsTelemetry

class WrapLogsAction(private val project: Project, private val getCurrentTableView: () -> TableView<LogStreamEntry>) :
    ToggleAction(message("cloudwatch.logs.wrap"), null, AllIcons.Actions.ToggleSoftWrap),
    DumbAware {
    private val messageColumn = 1
    private var isSelected = false
    override fun isSelected(e: AnActionEvent): Boolean = isSelected

    override fun setSelected(e: AnActionEvent, state: Boolean) {
        CloudwatchlogsTelemetry.wrapEvents(project, enabled = state)
        isSelected = state
        val tableView = getCurrentTableView()
        if (isSelected) {
            wrap(tableView)
        } else {
            unwrap(tableView)
        }
    }

    private fun wrap(table: TableView<LogStreamEntry>) {
        // The ExpandableItemsHandler also unwraps the box even if word wrap is turned
        // on, so enable/disable it based on if we are wrapping or not
        table.setExpandableItemsEnabled(false)
        (table.listTableModel.columnInfos[messageColumn] as? LogStreamMessageColumn)?.wrap()
        table.redrawTable()
    }

    private fun unwrap(table: TableView<LogStreamEntry>) {
        table.setExpandableItemsEnabled(true)
        (table.listTableModel.columnInfos[messageColumn] as? LogStreamMessageColumn)?.unwrap()
        table.redrawTable()
    }

    private fun TableView<LogStreamEntry>.redrawTable() {
        val selection = selectedRows
        val row = rowAtPoint(visibleRect.location).takeIf { it >= 0 } ?: 0
        runInEdt {
            listTableModel.fireTableDataChanged()
            // maintain the top cell at the top
            scrollRectToVisible(getCellRect(row, 0, true))
            // reselect because fireTableDataChanged unselects. Selection can be non-contiguous
            selection.forEach {
                addRowSelectionInterval(it, it)
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.editor

import com.intellij.icons.AllIcons
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.util.Disposer
import com.intellij.ui.SearchTextField
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.ConnectionState
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogsActor
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights.QueryEditorDialog
import software.aws.toolkits.jetbrains.utils.ui.onEmpty
import software.aws.toolkits.jetbrains.utils.ui.onEnter
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudWatchResourceType
import software.aws.toolkits.telemetry.CloudwatchinsightsTelemetry
import software.aws.toolkits.telemetry.CloudwatchlogsTelemetry
import software.aws.toolkits.telemetry.InsightsDialogOpenSource
import javax.swing.JPanel

class CloudWatchLogGroup(
    private val project: Project,
    private val logGroup: String
) : Disposable {
    private val coroutineScope = disposableCoroutineScope(this)
    lateinit var content: JPanel
        private set

    private val edtContext = getCoroutineUiContext()

    private lateinit var tablePanel: SimpleToolWindowPanel
    private lateinit var locationInformation: LocationBreadcrumbs
    private lateinit var searchField: SearchTextField
    private lateinit var breadcrumbHolder: JPanel

    private val client: CloudWatchLogsClient
    private val connection: ConnectionSettings
    private val groupTable: LogGroupTable
    private var searchGroupTable: LogGroupTable? = null

    private fun createUIComponents() {
        tablePanel = SimpleToolWindowPanel(false, true)
        searchField = SearchTextField(false).also {
            it.textEditor.emptyText.text = message("cloudwatch.logs.filter_loggroup")
        }
    }

    init {

        connection = when (val state = AwsConnectionManager.getInstance(project).connectionState) {
            is ConnectionState.ValidConnection -> ConnectionSettings(state.credentials, state.region)
            else -> throw IllegalStateException(state.shortMessage)
        }
        client = AwsClientManager.getInstance().getClient(connection.credentials, connection.region)
        groupTable = LogGroupTable(project, client, logGroup, LogGroupTable.TableType.LIST)

        val locationCrumbs = LocationCrumbs(project, logGroup)
        locationInformation.crumbs = locationCrumbs.crumbs
        breadcrumbHolder.border = locationCrumbs.border

        Disposer.register(this, groupTable)
        tablePanel.setContent(groupTable.component)
        addToolbar()
        addSearch()

        refreshTable()
    }

    private fun addSearch() {
        searchField.onEmpty {
            val oldTable = searchGroupTable
            searchGroupTable = null
            coroutineScope.launch(edtContext) {
                tablePanel.setContent(groupTable.component)
                // Dispose the old one if it was not null
                oldTable?.let { launch { Disposer.dispose(it) } }
            }
        }
        searchField.onEnter {
            val oldTable = searchGroupTable
            // If it is not empty do a search
            if (searchField.text.isNotEmpty()) {
                // This is thread safe because the actionPerformed is run on the UI thread
                CloudwatchlogsTelemetry.searchGroup(project, true)
                val table = LogGroupTable(project, client, logGroup, LogGroupTable.TableType.FILTER)
                Disposer.register(this@CloudWatchLogGroup, table)
                searchGroupTable = table
                coroutineScope.launch(edtContext) {
                    tablePanel.setContent(table.component)
                    oldTable?.let { launch { Disposer.dispose(it) } }
                }
                coroutineScope.launch {
                    table.channel.send(CloudWatchLogsActor.Message.LoadInitialFilter(searchField.text))
                }
            }
        }
    }

    private fun addToolbar() {
        val actionGroup = DefaultActionGroup()
        actionGroup.addAction(
            object : DumbAwareAction(message("general.refresh"), null, AllIcons.Actions.Refresh) {
                override fun actionPerformed(e: AnActionEvent) {
                    CloudwatchlogsTelemetry.refresh(project, CloudWatchResourceType.LogGroup)
                    refreshTable()
                }
            }
        )
        actionGroup.addAction(
            object : DumbAwareAction(message("cloudwatch.logs.query"), null, AllIcons.Actions.Find) {
                override fun actionPerformed(e: AnActionEvent) {
                    QueryEditorDialog(project, connection, logGroup).show()
                    CloudwatchinsightsTelemetry.openEditor(project, InsightsDialogOpenSource.LogGroup)
                }
            }
        )
        val toolbar = ActionManager.getInstance().createActionToolbar("CloudWatchLogStream", actionGroup, false)
        toolbar.setTargetComponent(tablePanel.component)
        tablePanel.toolbar = toolbar.component
    }

    internal fun refreshTable() {
        coroutineScope.launch { groupTable.channel.send(CloudWatchLogsActor.Message.LoadInitial) }
    }

    override fun dispose() {}
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.editor

import com.intellij.icons.AllIcons
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.Constraints
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.util.Disposer
import com.intellij.ui.SearchTextField
import com.intellij.ui.components.breadcrumbs.Breadcrumbs
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogsActor
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamEntry
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions.OpenCurrentInEditorAction
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions.TailLogsAction
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions.WrapLogsAction
import software.aws.toolkits.jetbrains.utils.ui.onEmpty
import software.aws.toolkits.jetbrains.utils.ui.onEnter
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudWatchResourceType
import software.aws.toolkits.telemetry.CloudwatchlogsTelemetry
import java.time.Duration
import javax.swing.JPanel

class CloudWatchLogStream(
    private val project: Project,
    private val logGroup: String,
    private val logStream: String,
    private val previousEvent: LogStreamEntry? = null,
    private val duration: Duration? = null,
    streamLogs: Boolean = false
) : Disposable {
    private val coroutineScope = disposableCoroutineScope(this)
    lateinit var content: JPanel
        private set
    private lateinit var breadcrumbHolder: JPanel
    private lateinit var locationInformation: Breadcrumbs
    private lateinit var tablePanel: SimpleToolWindowPanel
    private lateinit var searchField: SearchTextField

    private val edtContext = getCoroutineUiContext()

    private val client: CloudWatchLogsClient = project.awsClient()
    private val logStreamTable: LogStreamTable = LogStreamTable(project, client, logGroup, logStream, LogStreamTable.TableType.LIST).also {
        Disposer.register(this@CloudWatchLogStream, it)
    }
    private var searchStreamTable: LogStreamTable? = null
    private val tailLogsAction = TailLogsAction(project) { searchStreamTable?.channel ?: logStreamTable.channel }

    private fun createUIComponents() {
        tablePanel = SimpleToolWindowPanel(false, true)
        searchField = SearchTextField(false).also {
            it.textEditor.emptyText.text = message("cloudwatch.logs.filter_logs")
        }
    }

    init {
        tablePanel.setContent(logStreamTable.component)

        val locationCrumbs = LocationCrumbs(project, logGroup, logStream)
        locationInformation.crumbs = locationCrumbs.crumbs
        breadcrumbHolder.border = locationCrumbs.border

        addActionToolbar()
        addSearchListener()

        refreshTable()

        if (streamLogs) {
            tailLogsAction.setSelected(true)
        }
    }

    private fun addSearchListener() {
        // If the text field is emptied, like what the x button does, clear the table and dispose the old one if it exists
        // This leads to a weird UX where we search if enter is pressed but if the text in the box is deleted we clear the
        // search state.
        // TODO can we do better?
        searchField.onEmpty {
            val oldTable = searchStreamTable
            searchStreamTable = null
            coroutineScope.launch(edtContext) {
                tablePanel.setContent(logStreamTable.component)
                // Dispose the old one if it was not null
                oldTable?.let { launch { Disposer.dispose(it) } }
            }
        }
        // Add action listener on enter to search. This is needed so we don't make a super costly network call
        // for every letter that is typed in
        searchField.onEnter {
            val oldTable = searchStreamTable
            // If it is not empty do a search
            if (searchField.text.isNotEmpty()) {
                // This is thread safe because the actionPerformed is run on the UI thread
                CloudwatchlogsTelemetry.searchStream(project, true)
                val table = LogStreamTable(project, client, logGroup, logStream, LogStreamTable.TableType.FILTER)
                Disposer.register(this@CloudWatchLogStream, table)
                searchStreamTable = table
                coroutineScope.launch(edtContext) {
                    tablePanel.setContent(table.component)
                    oldTable?.let { launch { Disposer.dispose(it) } }
                }
                coroutineScope.launch {
                    table.channel.send(CloudWatchLogsActor.Message.LoadInitialFilter(searchField.text))
                }
            }
        }
    }

    private fun addActionToolbar() {
        val actionGroup = DefaultActionGroup()
        actionGroup.addAction(
            object : AnAction(message("general.refresh"), null, AllIcons.Actions.Refresh), DumbAware {
                override fun actionPerformed(e: AnActionEvent) {
                    refreshTable()
                    CloudwatchlogsTelemetry.refresh(project, CloudWatchResourceType.LogStream)
                }
            },
            Constraints.FIRST
        )
        actionGroup.add(
            OpenCurrentInEditorAction(project, logStream) {
                searchStreamTable?.logsTable?.listTableModel?.items ?: logStreamTable.logsTable.listTableModel.items
            }
        )
        actionGroup.add(tailLogsAction)
        actionGroup.add(WrapLogsAction(project) { searchStreamTable?.logsTable ?: logStreamTable.logsTable })
        val toolbar = ActionManager.getInstance().createActionToolbar("CloudWatchLogStream", actionGroup, false)
        toolbar.setTargetComponent(tablePanel.component)
        tablePanel.toolbar = toolbar.component
    }

    internal fun refreshTable() = coroutineScope.launch {
        if (searchField.text.isNotEmpty() && searchStreamTable != null) {
            searchStreamTable?.channel?.send(CloudWatchLogsActor.Message.LoadInitialFilter(searchField.text.trim()))
        } else if (previousEvent != null && duration != null) {
            logStreamTable.channel.send(CloudWatchLogsActor.Message.LoadInitialRange(previousEvent, duration))
        } else {
            logStreamTable.channel.send(CloudWatchLogsActor.Message.LoadInitial)
        }
    }

    override fun dispose() {}
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.editor

import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.SideBorder
import com.intellij.ui.components.breadcrumbs.Breadcrumbs
import com.intellij.ui.components.breadcrumbs.Crumb
import kotlinx.coroutines.runBlocking
import software.aws.toolkits.jetbrains.core.credentials.activeCredentialProvider
import software.aws.toolkits.jetbrains.core.credentials.activeRegion
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow
import software.aws.toolkits.resources.message
import java.awt.event.ActionEvent
import javax.swing.AbstractAction
import javax.swing.border.Border

class LocationBreadcrumbs : Breadcrumbs() {
    init {
        onSelect { crumb, event ->
            crumb.contextActions.firstOrNull()?.actionPerformed(ActionEvent(event, ActionEvent.ACTION_PERFORMED, ""))
        }
    }

    override fun getAttributes(crumb: Crumb): TextAttributes? = getKey(crumb)?.let { EditorColorsManager.getInstance().globalScheme.getAttributes(it) }

    private fun getKey(crumb: Crumb): TextAttributesKey? = if (isHovered(crumb) && crumb.contextActions.isNotEmpty()) {
        EditorColors.BREADCRUMBS_HOVERED
    } else {
        EditorColors.BREADCRUMBS_DEFAULT
    }
}

// This is different from LocationBreadcrumbs because createUiComponents in Kotlin does not have access to the constructor arguments
class LocationCrumbs(project: Project, logGroup: String, logStream: String? = null) {
    // This is made available instead of set because it needs to be on different components depending on the window
    val border: Border = IdeBorderFactory.createBorder(SideBorder.BOTTOM)

    val crumbs = listOfNotNull(
        Crumb.Impl(null, project.activeCredentialProvider().displayName, null, listOf()),
        Crumb.Impl(null, project.activeRegion().displayName, null, listOf()),
        Crumb.Impl(
            null,
            logGroup,
            null,
            object : AbstractAction(message("cloudwatch.logs.view_log_streams")), DumbAware {
                override fun actionPerformed(e: ActionEvent?): Unit = runBlocking {
                    CloudWatchLogWindow.getInstance(project).showLogGroup(logGroup)
                }
            }
        ),
        logStream?.let {
            Crumb.Impl(
                null,
                it,
                null,
                object : AbstractAction(message("cloudwatch.logs.view_log_stream")), DumbAware {
                    override fun actionPerformed(e: ActionEvent?) {
                        CloudWatchLogWindow.getInstance(project).showLogStream(logGroup, it)
                    }
                }
            )
        }
    )
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.editor

import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.Constraints
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.PopupHandler
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.TableSpeedSearch
import com.intellij.ui.table.JBTable
import com.intellij.ui.table.TableView
import com.intellij.util.ui.ListTableModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.amazon.awssdk.services.cloudwatchlogs.model.LogStream
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogsActor
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogGroupActor
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogGroupSearchActor
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions.ExportActionGroup
import software.aws.toolkits.jetbrains.utils.ui.bottomReached
import software.aws.toolkits.resources.message
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
import javax.swing.JComponent
import javax.swing.SortOrder

class LogGroupTable(
    private val project: Project,
    private val client: CloudWatchLogsClient,
    private val logGroup: String,
    type: TableType
) : Disposable {
    private val coroutineScope = disposableCoroutineScope(this)
    val component: JComponent
    val channel: Channel<CloudWatchLogsActor.Message>
    private val groupTable: TableView<LogStream>
    private val logGroupActor: CloudWatchLogsActor<LogStream>

    enum class TableType {
        LIST,
        FILTER
    }

    init {
        val (sortColumn, sortOrder) = when (type) {
            TableType.LIST -> 1 to SortOrder.DESCENDING // Sort by event time, most recent first
            TableType.FILTER -> 0 to SortOrder.ASCENDING // Sort by name alphabetically
        }
        val tableModel = ListTableModel(
            arrayOf(LogStreamsStreamColumn(sortable = type == TableType.FILTER), LogStreamsDateColumn(sortable = type == TableType.LIST)),
            mutableListOf<LogStream>(),
            sortColumn,
            sortOrder
        )
        groupTable = TableView(tableModel).apply {
            setPaintBusy(true)
            autoscrolls = true
            cellSelectionEnabled = true
            emptyText.text = message("loading_resource.loading")
            tableHeader.reorderingAllowed = false
            tableHeader.resizingAllowed = false
        }
        TableSpeedSearch(groupTable)
        addTableMouseListener(groupTable)
        addKeyListener(groupTable)
        addActions(groupTable)

        logGroupActor = when (type) {
            TableType.LIST -> LogGroupActor(project, client, groupTable, logGroup)
            TableType.FILTER -> LogGroupSearchActor(project, client, groupTable, logGroup)
        }
        Disposer.register(this@LogGroupTable, logGroupActor)

        channel = logGroupActor.channel

        component = ScrollPaneFactory.createScrollPane(groupTable).also {
            it.bottomReached {
                if (groupTable.rowCount != 0) {
                    coroutineScope.launch { logGroupActor.channel.send(CloudWatchLogsActor.Message.LoadForward) }
                }
            }
        }
    }

    private fun addKeyListener(table: JBTable) {
        table.addKeyListener(
            object : KeyAdapter() {
                override fun keyTyped(e: KeyEvent) {
                    val logStream = table.getSelectedRowLogStream() ?: return
                    if (!e.isConsumed && e.keyCode == KeyEvent.VK_ENTER) {
                        e.consume()
                        val window = CloudWatchLogWindow.getInstance(project)
                        window.showLogStream(logGroup, logStream)
                    }
                }
            }
        )
    }

    private fun addTableMouseListener(table: JBTable) {
        object : DoubleClickListener() {
            override fun onDoubleClick(e: MouseEvent): Boolean {
                val logStream = table.getSelectedRowLogStream() ?: return false
                val window = CloudWatchLogWindow.getInstance(project)
                window.showLogStream(logGroup, logStream)
                return true
            }
        }.installOn(table)
    }

    private fun addActions(table: JBTable) {
        val actionGroup = DefaultActionGroup()
        actionGroup.addAction(
            ExportActionGroup(project, client, logGroup) {
                val row = groupTable.selectedRow.takeIf { it >= 0 } ?: return@ExportActionGroup null
                table.getValueAt(row, 0) as? String
            },
            Constraints.FIRST
        )
        PopupHandler.installPopupHandler(
            table,
            actionGroup,
            ActionPlaces.EDITOR_POPUP,
            ActionManager.getInstance()
        )
    }

    private fun JBTable.getSelectedRowLogStream(): String? {
        val row = selectedRow.takeIf { it >= 0 } ?: return null
        return getValueAt(row, 0) as? String
    }

    override fun dispose() {}
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.editor

import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.PopupHandler
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.TableSpeedSearch
import com.intellij.ui.table.TableView
import com.intellij.util.ui.ListTableModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogsActor
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamEntry
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamFilterActor
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamListActor
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions.ShowLogsAroundActionGroup
import software.aws.toolkits.jetbrains.utils.ui.bottomReached
import software.aws.toolkits.jetbrains.utils.ui.topReached
import software.aws.toolkits.resources.message
import javax.swing.JComponent
import javax.swing.JTable

class LogStreamTable(
    val project: Project,
    client: CloudWatchLogsClient,
    private val logGroup: String,
    private val logStream: String,
    type: TableType
) : Disposable {
    private val coroutineScope = disposableCoroutineScope(this)

    enum class TableType {
        LIST,
        FILTER
    }

    val component: JComponent
    val channel: Channel<CloudWatchLogsActor.Message>
    val logsTable: TableView<LogStreamEntry>
    private val logStreamActor: CloudWatchLogsActor<LogStreamEntry>

    init {
        val model = ListTableModel(
            arrayOf(LogStreamDateColumn(), LogStreamMessageColumn()),
            mutableListOf<LogStreamEntry>(),
        )
        logsTable = TableView(model).apply {
            autoscrolls = true
            tableHeader.reorderingAllowed = false
            tableHeader.resizingAllowed = false
            autoResizeMode = JTable.AUTO_RESIZE_LAST_COLUMN
            cellSelectionEnabled = true
            setPaintBusy(true)
            emptyText.text = message("loading_resource.loading")
            // Set the row height to 20. This is a magic number, so let me explain. This is
            // The height of a JLabel (16) + 1 inner border (top and bottom) + 1 outer border
            // (top and bottom) = 20 total. If we don't do this, as we scroll to the bottom,
            // it will spring back to the top in a very comical but very not great way
            setRowHeight(20)
        }

        // TODO this also searches the date column which we don't want to do.
        // The converter for TableSpeedSearch takes a string which we can't do much with
        // unless we want to detect it's a timestamp but it might detect messages too
        TableSpeedSearch(logsTable)

        logStreamActor = when (type) {
            TableType.LIST -> LogStreamListActor(project, client, logsTable, logGroup, logStream)
            TableType.FILTER -> LogStreamFilterActor(project, client, logsTable, logGroup, logStream)
        }
        Disposer.register(this@LogStreamTable, logStreamActor)
        channel = logStreamActor.channel

        component = ScrollPaneFactory.createScrollPane(logsTable).also {
            it.topReached {
                if (logsTable.rowCount != 0) {
                    coroutineScope.launch { logStreamActor.channel.send(CloudWatchLogsActor.Message.LoadBackward) }
                }
            }
            it.bottomReached {
                if (logsTable.rowCount != 0) {
                    coroutineScope.launch { logStreamActor.channel.send(CloudWatchLogsActor.Message.LoadForward) }
                }
            }
        }

        addActionsToTable()
    }

    private fun addActionsToTable() {
        val actionGroup = DefaultActionGroup().apply {
            add(ShowLogsAroundActionGroup(project, logGroup, logStream, logsTable))
        }
        PopupHandler.installPopupHandler(
            logsTable,
            actionGroup,
            ActionPlaces.EDITOR_POPUP,
            ActionManager.getInstance()
        )
    }

    override fun dispose() {}
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.editor

import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.speedSearch.SpeedSearchUtil
import com.intellij.util.text.DateFormatUtil
import com.intellij.util.text.SyncDateFormat
import com.intellij.util.ui.ColumnInfo
import software.amazon.awssdk.services.cloudwatchlogs.model.LogStream
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.LogStreamEntry
import software.aws.toolkits.jetbrains.utils.ui.ResizingTextColumnRenderer
import software.aws.toolkits.jetbrains.utils.ui.WrappingCellRenderer
import software.aws.toolkits.jetbrains.utils.ui.setSelectionHighlighting
import software.aws.toolkits.resources.message
import java.awt.Component
import java.text.SimpleDateFormat
import javax.swing.JTable
import javax.swing.table.TableCellRenderer

class LogStreamsStreamColumn(private val sortable: Boolean) : ColumnInfo<LogStream, String>(message("cloudwatch.logs.log_streams")) {
    private val renderer = LogStreamsStreamColumnRenderer()
    override fun valueOf(item: LogStream?): String? = item?.logStreamName()
    override fun isCellEditable(item: LogStream?): Boolean = false
    override fun getRenderer(item: LogStream?): TableCellRenderer = renderer
    override fun getComparator(): Comparator<LogStream>? = if (sortable) Comparator.comparing { it.logStreamName() } else null
}

class LogStreamsStreamColumnRenderer : TableCellRenderer {
    override fun getTableCellRendererComponent(table: JTable?, value: Any?, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int): Component {
        val component = SimpleColoredComponent()
        component.append((value as? String)?.trim() ?: "")
        if (table == null) {
            return component
        }
        component.setSelectionHighlighting(table, isSelected)
        SpeedSearchUtil.applySpeedSearchHighlighting(table, component, true, isSelected)

        return component
    }
}

class LogStreamsDateColumn(
    private val sortable: Boolean,
    val format: SyncDateFormat? = null
) : ColumnInfo<LogStream, String>(message("cloudwatch.logs.last_event_time")) {
    private val renderer = ResizingTextColumnRenderer()
    override fun valueOf(item: LogStream?): String? = TimeFormatConversion.convertEpochTimeToStringDateTime(
        item?.lastEventTimestamp(),
        showSeconds = false,
        format = format
    )

    override fun isCellEditable(item: LogStream?): Boolean = false
    override fun getRenderer(item: LogStream?): TableCellRenderer = renderer
    override fun getComparator(): Comparator<LogStream>? = if (sortable) Comparator.comparing { it.lastEventTimestamp() ?: Long.MIN_VALUE } else null
}

class LogStreamDateColumn(private val format: SyncDateFormat? = null) : ColumnInfo<LogStreamEntry, String>(message("general.time")) {
    private val renderer = ResizingTextColumnRenderer()
    override fun valueOf(item: LogStreamEntry?): String? = TimeFormatConversion.convertEpochTimeToStringDateTime(
        item?.timestamp,
        showSeconds = true,
        format = format
    )

    override fun isCellEditable(item: LogStreamEntry?): Boolean = false
    override fun getRenderer(item: LogStreamEntry?): TableCellRenderer = renderer
}

class LogStreamMessageColumn : ColumnInfo<LogStreamEntry, String>(message("general.message")) {
    private val renderer = WrappingCellRenderer(wrapOnSelection = true, wrapOnToggle = true)
    fun wrap() {
        renderer.wrap = true
    }

    fun unwrap() {
        renderer.wrap = false
    }

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

object TimeFormatConversion {
    fun convertEpochTimeToStringDateTime(epochTime: Long?, showSeconds: Boolean, format: SyncDateFormat? = null): String? {
        val formatter = format ?: if (showSeconds) {
            SyncDateFormat(SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"))
        } else {
            DateFormatUtil.getDateTimeFormat()
        }
        return epochTime?.let { formatter.format(it) }
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.ui.TableSpeedSearch
import com.intellij.ui.components.breadcrumbs.Breadcrumbs
import com.intellij.ui.table.TableView
import com.intellij.util.ExceptionUtil
import com.intellij.util.ui.ListTableModel
import com.intellij.util.ui.StatusText
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.editor.LocationCrumbs
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudwatchinsightsTelemetry
import software.aws.toolkits.telemetry.Result
import javax.swing.JPanel

class DetailedLogRecord(
    private val project: Project,
    private val client: CloudWatchLogsClient,
    private val logRecordPointer: String
) : Disposable {
    private val coroutineScope = disposableCoroutineScope(this)
    val title = message("cloudwatch.logs.log_record", logRecordPointer)

    private lateinit var breadcrumbHolder: JPanel
    private lateinit var locationInformation: Breadcrumbs
    lateinit var basePanel: JPanel
        private set
    lateinit var tableView: TableView<LogRecordFieldPair>
        private set
    private val recordLoadTask: Deferred<LogRecord>

    private fun createUIComponents() {
        val model = ListTableModel<LogRecordFieldPair>(
            LogRecordFieldColumn(),
            LogRecordValueColumn()
        )
        tableView = TableView(model).apply {
            setPaintBusy(true)
            emptyText.text = message("loading_resource.loading")
        }
        tableView.emptyText.text = StatusText.getDefaultEmptyText()
        TableSpeedSearch(tableView)
    }

    init {
        recordLoadTask = loadLogRecordAsync()
        locationInformation.isVisible = false
        coroutineScope.launch {
            val record = recordLoadTask.await()
            val items = record.map { it.key to it.value }

            runInEdt {
                tableView.listTableModel.items = items
                tableView.setPaintBusy(false)
            }

            if (items.isNotEmpty()) {
                val logGroup = record["@log"] ?: return@launch
                val logStream = record["@logStream"] ?: return@launch
                val locationCrumbs = LocationCrumbs(project, extractLogGroup(logGroup), logStream)
                locationInformation.crumbs = locationCrumbs.crumbs
                breadcrumbHolder.border = locationCrumbs.border
                locationInformation.isVisible = true
            }
        }
    }

    private fun loadLogRecordAsync() = coroutineScope.async<LogRecord> {
        var result = Result.Succeeded
        try {
            return@async client.getLogRecord {
                it.logRecordPointer(logRecordPointer)
            }.logRecord()
        } catch (e: Exception) {
            LOG.warn(e) { "Exception thrown while loading log record $logRecordPointer" }
            notifyError(
                project = project,
                title = message("cloudwatch.logs.exception"),
                content = ExceptionUtil.getThrowableText(e)
            )
            result = Result.Failed
        } finally {
            CloudwatchinsightsTelemetry.openDetailedLogRecord(project, result)
        }

        return@async mapOf()
    }

    override fun dispose() {
    }

    fun getComponent() = basePanel

    @TestOnly
    internal fun isLoaded() = recordLoadTask.isCompleted

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

        // Log group names can be between 1 and 512 characters long.
        // Log group names consist of the following characters: a-z, A-Z, 0-9, '_' (underscore), '-' (hyphen), '/' (forward slash), '.' (period), and '#' (number sign)
        // https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateLogGroup.html
        private val logGroupRegex = Regex("\\d{12}:(.{1,512})")

        fun extractLogGroup(log: String) =
            // conveniently, the @log field has the account ID prepended, which we don't want
            // @log is a log group identifier in the form of account-id:log-group-name. This can be useful in queries of multiple log groups, to identify which log group a particular event belongs to.
            // https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData-discoverable-fields.html
            logGroupRegex.find(log)?.groups?.get(1)?.value
                ?: throw IllegalStateException("$log format does not appear to be in a valid format (<account-id>:<log-group-name>)")
    }
}


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

import javax.swing.JPanel
import javax.swing.JTextField

class EnterQueryName {
    lateinit var queryName: JTextField
        private set
    lateinit var saveQueryPanel: JPanel
        private set
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights

import com.intellij.ui.SimpleColoredComponent
import com.intellij.util.ui.ColumnInfo
import software.aws.toolkits.jetbrains.utils.ui.ResizingTextColumnRenderer
import software.aws.toolkits.jetbrains.utils.ui.setSelectionHighlighting
import software.aws.toolkits.resources.message
import java.awt.Component
import javax.swing.JTable
import javax.swing.table.TableCellRenderer

class LogResultColumnInfo(private val fieldName: String) : ColumnInfo<LogResult, String>(fieldName) {
    override fun valueOf(item: Map<String, String>?): String? {
        if (item != null) {
            return item[fieldName]
        }
        return null
    }

    override fun isCellEditable(item: Map<String, String>?): Boolean = false
    override fun getRenderer(item: Map<String, String>?) = LogResultColumnRenderer()
}

class LogResultColumnRenderer : TableCellRenderer {
    override fun getTableCellRendererComponent(table: JTable?, value: Any?, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int): Component {
        val component = SimpleColoredComponent()
        component.append((value as? String)?.trim() ?: "")
        if (table == null) {
            return component
        }
        component.setSelectionHighlighting(table, isSelected)
        return component
    }
}

class LogRecordFieldColumn : ColumnInfo<LogRecordFieldPair, String>(message("cloudwatch.logs.log_record_field")) {
    override fun getRenderer(item: LogRecordFieldPair?) = ResizingTextColumnRenderer()
    override fun valueOf(item: LogRecordFieldPair?) = item?.first
}

class LogRecordValueColumn : ColumnInfo<LogRecordFieldPair, String>(message("cloudwatch.logs.log_record_value")) {
    override fun valueOf(item: LogRecordFieldPair?) = item?.second
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights

import software.amazon.awssdk.services.cloudwatchlogs.model.ResultField

/**
 * Mapped from response from the GetQueryResults call
 */
typealias LogResult = Map<String, String>

/**
 * Returned from the GetLogRecordResponse call
 */
typealias LogRecord = Map<String, String>
typealias LogRecordFieldPair = Pair<String, String>

fun List<ResultField>.toLogResult() = this.map { it.field() to it.value() }.toMap()

// @ptr is a unique identifier for each resultant log event which is used here to ensure results are not repeatedly displayed
fun LogResult.identifier(): String = this["@ptr"] ?: throw IllegalStateException("CWL GetQueryResults returned record without @ptr field")


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

import com.intellij.ui.TableSpeedSearch
import com.intellij.ui.TableUtil
import com.intellij.ui.table.TableView
import com.intellij.util.ui.ColumnInfo
import com.intellij.util.ui.ListTableModel
import software.aws.toolkits.resources.message

data class LogGroup(
    var selected: Boolean = false,
    var name: String
)

class LogGroupSelectorTable : TableView<LogGroup>(model) {
    init {
        listTableModel.items = emptyList()
        TableSpeedSearch(this)
        emptyText.text = message("loading_resource.loading")
    }

    fun populateLogGroups(selectedLogGroups: Set<String>, availableLogGroups: List<String>) {
        val (selected, modelItems) = availableLogGroups.mapIndexed { index, logGroup ->
            if (logGroup in selectedLogGroups) {
                index to LogGroup(true, logGroup)
            } else {
                null to LogGroup(false, logGroup)
            }
        }.unzip()

        listTableModel.items = modelItems
        TableUtil.setupCheckboxColumn(this, 0)
        TableUtil.updateScroller(this)
        TableUtil.selectRows(this, selected.filterNotNull().toIntArray())
        TableUtil.scrollSelectionToVisible(this)
    }

    /**
     * Assumes that the model has already been populated
     */
    internal fun selectLogGroups(selectedLogGroups: Set<String>) {
        val selected = listTableModel.items.mapIndexed { index, logGroup ->
            logGroup.selected = logGroup.name in selectedLogGroups

            if (logGroup.selected) index else null
        }

        TableUtil.selectRows(this, selected.filterNotNull().toIntArray())
        TableUtil.scrollSelectionToVisible(this)
    }

    fun getSelectedLogGroups(): List<String> =
        listTableModel.items
            .filter { it.selected }
            .mapNotNull { it.name }

    companion object {
        private val model = ListTableModel<LogGroup>(
            SelectedColumnInfo(),
            LogGroupNameColumnInfo()
        )

        private class SelectedColumnInfo : ColumnInfo<LogGroup, Boolean>("Selected") {
            override fun getColumnClass() = Boolean::class.java

            override fun isCellEditable(item: LogGroup) = true

            override fun valueOf(item: LogGroup) = item.selected

            override fun setValue(item: LogGroup, value: Boolean?) {
                item.selected = (value ?: false)
            }
        }

        private class LogGroupNameColumnInfo : ColumnInfo<LogGroup, String>("Log Group Name") {
            override fun isCellEditable(item: LogGroup) = false

            override fun valueOf(item: LogGroup) = item.name

            override fun setValue(item: LogGroup, value: String?) {
                item.name = (value ?: "")
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights

import software.aws.toolkits.core.ConnectionSettings
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import java.util.Date

data class QueryDetails(
    val connectionSettings: ConnectionSettings,
    val logGroups: List<String>,
    val timeRange: TimeRange,
    val query: QueryString
) {
    fun getQueryRange(clock: Clock = Clock.systemUTC()) =
        when (timeRange) {
            is TimeRange.AbsoluteRange -> {
                StartEndInstant(timeRange.startDate.toInstant(), timeRange.endDate.toInstant())
            }
            is TimeRange.RelativeRange -> {
                val now = Instant.now(clock)
                StartEndInstant(
                    // Instant doesn't support minus(Week), so we need to explicitly use the ISO calendar system
                    // ZonedDateTime must be based off an temporal instance with a ZoneId
                    ZonedDateTime.from(now.atZone(ZoneId.systemDefault())).minus(timeRange.relativeTimeAmount, timeRange.relativeTimeUnit).toInstant(),
                    now
                )
            }
        }

    fun getQueryString() =
        when (query) {
            is QueryString.SearchTermQueryString -> {
                val regexTerm = query.searchTerm.replace("/", "\\/")

                "fields @timestamp, @message | filter @message like /$regexTerm/"
            }

            is QueryString.InsightsQueryString -> query.query
        }
}

sealed class QueryString {
    data class SearchTermQueryString(
        val searchTerm: String
    ) : QueryString()
    data class InsightsQueryString(
        val query: String
    ) : QueryString()
}

sealed class TimeRange {
    data class AbsoluteRange(
        val startDate: Date,
        val endDate: Date
    ) : TimeRange()
    data class RelativeRange(
        val relativeTimeAmount: Long,
        val relativeTimeUnit: ChronoUnit
    ) : TimeRange()
}

data class StartEndInstant(
    val start: Instant,
    val end: Instant
)


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

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.ui.EnumComboBoxModel
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.michaelbaranov.microba.calendar.DatePicker
import software.aws.toolkits.jetbrains.utils.ui.find
import software.aws.toolkits.resources.message
import java.text.NumberFormat
import java.time.temporal.ChronoUnit
import javax.swing.JButton
import javax.swing.JComboBox
import javax.swing.JFormattedTextField
import javax.swing.JPanel
import javax.swing.JRadioButton
import javax.swing.JTextArea
import javax.swing.JTextField

class QueryEditor internal constructor(
    private val project: Project,
    private val initialQueryDetails: QueryDetails
) {
    lateinit var absoluteTimeRadioButton: JRadioButton
        private set
    lateinit var relativeTimeRadioButton: JRadioButton
        private set
    lateinit var searchTerm: JRadioButton
        private set
    lateinit var querySearchTerm: JTextField
        private set
    lateinit var queryLogGroupsRadioButton: JRadioButton
        private set
    lateinit var saveQueryButton: JButton
        private set
    lateinit var retrieveSavedQueriesButton: JButton
        private set
    lateinit var tablePanel: SimpleToolWindowPanel
        private set
    lateinit var queryBox: JTextArea
        private set
    lateinit var endDate: DatePicker
        private set
    lateinit var queryEditorBasePanel: JPanel
        private set
    lateinit var relativeTimeUnit: JComboBox<TimeUnit>
        private set
    lateinit var relativeTimeNumber: JFormattedTextField
        private set
    lateinit var startDate: DatePicker
        private set
    lateinit var queryGroupScrollPane: JBScrollPane
        private set
    lateinit var logGroupTable: LogGroupSelectorTable
        private set
    private lateinit var comboBoxModel: EnumComboBoxModel<TimeUnit>
    private lateinit var numberFormat: NumberFormat
    private lateinit var timePanel: JPanel
    private lateinit var searchPanel: JPanel

    private fun createUIComponents() {
        tablePanel = SimpleToolWindowPanel(false, true)
        logGroupTable = LogGroupSelectorTable()
        tablePanel.setContent(logGroupTable.component)
        // lateinit since this method runs before the standard initialization flow
        numberFormat = NumberFormat.getIntegerInstance()
        relativeTimeNumber = JFormattedTextField(numberFormat)
        // arbitrary length
        relativeTimeNumber.columns = 5
        comboBoxModel = EnumComboBoxModel(TimeUnit::class.java)
        relativeTimeUnit = ComboBox(comboBoxModel)
        relativeTimeUnit.renderer = timeUnitComboBoxRenderer
    }

    init {
        absoluteTimeRadioButton.addActionListener {
            setAbsolute()
        }

        relativeTimeRadioButton.addActionListener {
            setRelative()
        }

        queryLogGroupsRadioButton.addActionListener {
            setQueryLanguage()
        }

        searchTerm.addActionListener {
            setSearchTerm()
        }

        saveQueryButton.addActionListener {
            val query = if (queryBox.text.isNotEmpty()) queryBox.text else DEFAULT_INSIGHTS_QUERY_STRING
            SaveQueryDialog(project, initialQueryDetails.connectionSettings, query, logGroupTable.getSelectedLogGroups()).show()
        }

        retrieveSavedQueriesButton.addActionListener {
            RetrieveSavedQueryDialog(this, project, initialQueryDetails.connectionSettings).show()
        }

        startDate.isEnabled = false
        endDate.isEnabled = false
        relativeTimeNumber.isEnabled = true
        relativeTimeUnit.isEnabled = true
        querySearchTerm.isEnabled = true
        queryBox.isEnabled = false
        saveQueryButton.isEnabled = false
        queryLogGroupsRadioButton.isSelected = true
        queryBox.text = DEFAULT_INSIGHTS_QUERY_STRING

        queryGroupScrollPane.border = IdeBorderFactory.createTitledBorder(message("cloudwatch.logs.log_groups"), false, JBUI.emptyInsets())
        timePanel.border = JBUI.Borders.emptyTop(UIUtil.DEFAULT_VGAP)
        searchPanel.border = JBUI.Borders.emptyTop(UIUtil.DEFAULT_VGAP)
    }

    fun setAbsolute() {
        absoluteTimeRadioButton.isSelected = true
        startDate.isEnabled = true
        endDate.isEnabled = true
        relativeTimeNumber.isEnabled = false
        relativeTimeUnit.isEnabled = false
    }

    fun setRelative() {
        relativeTimeRadioButton.isSelected = true
        startDate.isEnabled = false
        endDate.isEnabled = false
        relativeTimeNumber.isEnabled = true
        relativeTimeUnit.isEnabled = true
    }

    fun setSearchTerm() {
        searchTerm.isSelected = true
        queryBox.isEnabled = false
        querySearchTerm.isEnabled = true
        saveQueryButton.isEnabled = false
    }

    fun setQueryLanguage() {
        queryLogGroupsRadioButton.isEnabled = true
        queryBox.isEnabled = true
        querySearchTerm.isEnabled = false
        saveQueryButton.isEnabled = true
    }

    fun getRelativeTimeAmount() = numberFormat.parse(relativeTimeNumber.text).toLong()

    fun getSelectedTimeUnit(): ChronoUnit = comboBoxModel.selectedItem.unit

    fun setSelectedTimeUnit(unit: ChronoUnit) {
        comboBoxModel.setSelectedItem(comboBoxModel.find { it.unit == unit })
    }

    companion object {
        private val timeUnitComboBoxRenderer = SimpleListCellRenderer.create<TimeUnit>("") {
            it.text
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import kotlinx.coroutines.async
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.resources.CloudWatchResources
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudwatchinsightsTelemetry
import software.aws.toolkits.telemetry.InsightsQueryStringType
import software.aws.toolkits.telemetry.InsightsQueryTimeType
import software.aws.toolkits.telemetry.Result
import java.awt.event.ActionEvent
import java.time.temporal.ChronoUnit
import javax.swing.Action
import javax.swing.JComponent

class QueryEditorDialog internal constructor(
    // TODO: Exposed for testing only, should be refactored to be private
    private val project: Project,
    private val initialQueryDetails: QueryDetails
) : DialogWrapper(project) {
    private val coroutineScope = projectCoroutineScope(project)
    constructor(project: Project, connectionSettings: ConnectionSettings, logGroupName: String) :
        this(project, defaultQuery(connectionSettings, logGroupName))

    private val view = QueryEditor(project, initialQueryDetails)
    private val action: OkAction = QueryLogGroupOkAction()

    init {
        super.init()

        title = message("cloudwatch.logs.query_editor_title")
        coroutineScope.launch {
            setView(initialQueryDetails)
        }
    }

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

    override fun doValidate(): ValidationInfo? = validateEditorEntries(view)

    override fun getOKAction(): Action = action

    override fun doOKAction() {
        // Do nothing, close logic is handled separately
    }

    // TODO: Exposed for testing only, should be refactored to be private
    internal suspend fun setView(queryDetails: QueryDetails) {
        when (val timeRange = queryDetails.timeRange) {
            is TimeRange.AbsoluteRange -> {
                view.setAbsolute()
                view.startDate.date = timeRange.startDate
                view.endDate.date = timeRange.endDate
            }

            is TimeRange.RelativeRange -> {
                view.setRelative()
                view.relativeTimeNumber.text = timeRange.relativeTimeAmount.toString()
                view.setSelectedTimeUnit(timeRange.relativeTimeUnit)
            }
        }

        when (val query = queryDetails.query) {
            is QueryString.SearchTermQueryString -> {
                view.setSearchTerm()
                view.querySearchTerm.text = query.searchTerm
            }

            is QueryString.InsightsQueryString -> {
                view.setQueryLanguage()
                view.queryBox.text = query.query
            }
        }

        val availableLogGroups = AwsResourceCache.getInstance().getResource(
            CloudWatchResources.LIST_LOG_GROUPS,
            region = initialQueryDetails.connectionSettings.region,
            credentialProvider = initialQueryDetails.connectionSettings.credentials
        ).await().map { it.logGroupName() }
        withContext(getCoroutineUiContext()) {
            view.logGroupTable.populateLogGroups(initialQueryDetails.logGroups.toSet(), availableLogGroups)
        }
    }

    private fun beginQuerying() {
        if (!okAction.isEnabled) {
            return
        }

        close(OK_EXIT_CODE)
        val queryDetails = getQueryDetails()
        val fieldList = getFields(queryDetails.getQueryString())
        coroutineScope.launch {
            val queryId = startQueryAsync(queryDetails).await()
            CloudWatchLogWindow.getInstance(project).showQueryResults(queryDetails, queryId, fieldList)
        }
    }

    // TODO: Exposed for testing only, should be refactored to be private
    internal fun getQueryDetails(): QueryDetails {
        val timeRange = if (view.absoluteTimeRadioButton.isSelected) {
            TimeRange.AbsoluteRange(
                startDate = view.startDate.date,
                endDate = view.endDate.date
            )
        } else {
            TimeRange.RelativeRange(
                relativeTimeAmount = view.getRelativeTimeAmount(),
                relativeTimeUnit = view.getSelectedTimeUnit()
            )
        }

        val query = if (view.searchTerm.isSelected) {
            QueryString.SearchTermQueryString(
                searchTerm = view.querySearchTerm.text
            )
        } else {
            QueryString.InsightsQueryString(
                query = view.queryBox.text
            )
        }

        return QueryDetails(
            connectionSettings = initialQueryDetails.connectionSettings,
            logGroups = view.logGroupTable.getSelectedLogGroups(),
            timeRange = timeRange,
            query = query
        )
    }

    private inner class QueryLogGroupOkAction : OkAction() {
        init {
            putValue(Action.NAME, message("cloudwatch.logs.query.form.ok_Button"))
        }

        override fun doAction(e: ActionEvent?) {
            super.doAction(e)
            if (doValidateAll().isNotEmpty()) return
            beginQuerying()
        }
    }

    // TODO: Exposed for testing only, should be refactored to be private
    internal fun validateEditorEntries(view: QueryEditor): ValidationInfo? {
        if (!view.absoluteTimeRadioButton.isSelected && !view.relativeTimeRadioButton.isSelected) {
            return ValidationInfo(message("cloudwatch.logs.validation.timerange"), view.absoluteTimeRadioButton)
        }
        if (view.relativeTimeRadioButton.isSelected && view.relativeTimeNumber.text.isEmpty()) {
            return ValidationInfo(message("cloudwatch.logs.no_relative_time_number"), view.relativeTimeNumber)
        }
        if (view.absoluteTimeRadioButton.isSelected && view.startDate.date > view.endDate.date) {
            return ValidationInfo(message("cloudwatch.logs.compare.start.end.date"), view.startDate)
        }
        if (view.queryLogGroupsRadioButton.isSelected && view.queryBox.text.isEmpty()) {
            return ValidationInfo(message("cloudwatch.logs.no_query_entered"), view.queryBox)
        }
        if (view.searchTerm.isSelected && view.querySearchTerm.text.isEmpty()) {
            return ValidationInfo(message("cloudwatch.logs.no_term_entered"), view.querySearchTerm)
        }
        if (view.logGroupTable.getSelectedLogGroups().isEmpty()) {
            return ValidationInfo(message("cloudwatch.logs.no_log_group"), view.logGroupTable)
        }
        return null
    }

    // TODO: Exposed for testing only, should be refactored to be private
    internal fun startQueryAsync(queryDetails: QueryDetails) = coroutineScope.async {
        val (credentials, region) = queryDetails.connectionSettings
        val client = AwsClientManager.getInstance().getClient<CloudWatchLogsClient>(credentials, region)
        val timeRange = queryDetails.getQueryRange()
        val queryString = queryDetails.getQueryString()
        var result = Result.Succeeded
        try {
            val response = client.startQuery {
                it.logGroupNames(queryDetails.logGroups)
                it.startTime(timeRange.start.epochSecond)
                it.endTime(timeRange.end.epochSecond)
                it.queryString(queryString)
                // 1k is default
                it.limit(1000)
            }

            return@async response.queryId()
        } catch (e: Exception) {
            notifyError(message("cloudwatch.logs.query_result_completion_status"), e.toString())
            result = Result.Failed
            throw e
        } finally {
            val timeType = when (queryDetails.timeRange) {
                is TimeRange.AbsoluteRange -> InsightsQueryTimeType.Absolute
                is TimeRange.RelativeRange -> InsightsQueryTimeType.Relative
            }
            val searchStringType = when (queryDetails.query) {
                is QueryString.InsightsQueryString -> InsightsQueryStringType.Insights
                is QueryString.SearchTermQueryString -> InsightsQueryStringType.SearchTerm
            }
            CloudwatchinsightsTelemetry.executeQuery(project, result, timeType, searchStringType)
        }
    }

    companion object {
        private fun defaultQuery(connection: ConnectionSettings, logGroupName: String) = QueryDetails(
            connection,
            mutableListOf(logGroupName),
            TimeRange.RelativeRange(10, ChronoUnit.MINUTES),
            QueryString.InsightsQueryString(DEFAULT_INSIGHTS_QUERY_STRING)
        )

        // TODO: Exposed for testing only, should be refactored to be private
        internal fun getFields(query: String): List<String> {
            val fieldsIdentifier = "fields"
            val fieldList = mutableListOf<List<String>>()
            query.replace("\\|", "")
            val queries = query.split("|")
            for (item in queries) {
                val splitQuery = item.trim()
                if (splitQuery.startsWith(fieldsIdentifier, ignoreCase = false)) {
                    val fields = splitQuery.substringAfter(fieldsIdentifier)
                    fieldList.add(fields.split(",").map { it.trim() })
                }
            }
            if (fieldList.isEmpty()) {
                return listOf("@timestamp", "@message")
            }
            return fieldList.flatten()
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights

/**
 * The default insights query string autofilled into the box. If this changes,
 * change the one in InsightsQueryTest as well
 */
const val DEFAULT_INSIGHTS_QUERY_STRING =
"""fields @timestamp, @message
| sort @timestamp desc
| limit 20
"""


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights

import com.intellij.icons.AllIcons
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.util.Disposer
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.InsightsQueryResultsActor
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudwatchinsightsTelemetry
import software.aws.toolkits.telemetry.InsightsDialogOpenSource

class QueryResultPanel(
    private val project: Project,
    fields: List<String>,
    queryId: String,
    private val queryDetails: QueryDetails
) : SimpleToolWindowPanel(false, true), Disposable {
    private val coroutineScope = disposableCoroutineScope(this)
    init {
        val resultsTable = QueryResultsTable(project, queryDetails.connectionSettings, fields, queryId)
        Disposer.register(this, resultsTable)
        setContent(resultsTable.component)
        toolbar = ActionManager.getInstance().createActionToolbar(
            ID,
            DefaultActionGroup(
                object : AnAction(message("cloudwatch.logs.open_query_editor"), null, AllIcons.Actions.EditSource) {
                    override fun actionPerformed(e: AnActionEvent) {
                        QueryEditorDialog(project, queryDetails).show()
                        CloudwatchinsightsTelemetry.openEditor(project, InsightsDialogOpenSource.ResultsWindow)
                    }
                }
            ),
            false
        ).component
        coroutineScope.launch { resultsTable.channel.send(InsightsQueryResultsActor.Message.StartLoadingAll) }
    }

    override fun dispose() {
    }

    private companion object {
        const val ID = "QueryResultPanel"
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights

import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.table.TableView
import com.intellij.util.ui.ListTableModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogWindow
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.InsightsQueryResultsActor
import software.aws.toolkits.resources.message
import java.awt.event.MouseEvent
import javax.swing.JComponent

class QueryResultsTable(
    private val project: Project,
    connectionSettings: ConnectionSettings,
    fields: List<String>,
    queryId: String
) : Disposable {
    private val coroutineScope = disposableCoroutineScope(this)
    private val client = let {
        val (credentials, region) = connectionSettings
        AwsClientManager.getInstance().getClient<CloudWatchLogsClient>(credentials, region)
    }
    private val insightsQueryActor: InsightsQueryResultsActor
    val component: JComponent
    val channel: Channel<InsightsQueryResultsActor.Message>
    val resultsTable: TableView<LogResult>

    init {
        val tableModel = ListTableModel<LogResult>(
            *fields.map {
                LogResultColumnInfo(it)
            }.toTypedArray()
        )

        resultsTable = TableView(tableModel).apply {
            setPaintBusy(true)
            autoscrolls = true
            emptyText.text = message("loading_resource.loading")
            tableHeader.reorderingAllowed = false
            tableHeader.resizingAllowed = true
        }

        installDetailedLogRecordOpenListener()
        insightsQueryActor = InsightsQueryResultsActor(project, client, resultsTable, queryId).also {
            Disposer.register(this, it)
        }
        channel = insightsQueryActor.channel
        component = ScrollPaneFactory.createScrollPane(resultsTable)
    }

    private fun installDetailedLogRecordOpenListener() {
        object : DoubleClickListener() {
            override fun onDoubleClick(e: MouseEvent): Boolean {
                // assume you can't double click multiple selection
                val identifier = resultsTable.selectedObject?.identifier() ?: return false
                coroutineScope.launch { CloudWatchLogWindow.getInstance(project).showDetailedEvent(client, identifier) }
                return true
            }
        }.installOn(resultsTable)
    }

    override fun dispose() {}
}


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

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import software.amazon.awssdk.services.cloudwatchlogs.model.QueryDefinition
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudwatchinsightsTelemetry
import software.aws.toolkits.telemetry.Result
import java.awt.event.ActionEvent
import javax.swing.Action
import javax.swing.JComponent

class RetrieveSavedQueryDialog(
    private val parentEditor: QueryEditor,
    private val project: Project,
    connectionSettings: ConnectionSettings
) : DialogWrapper(project) {
    private val view = SelectSavedQuery(connectionSettings)

    private val action: OkAction = object : OkAction() {
        override fun doAction(e: ActionEvent?) {
            super.doAction(e)
            if (doValidateAll().isNotEmpty()) return

            val selected = view.resourceSelector.selected() ?: throw IllegalStateException("No query definition was selected")
            populateParentEditor(parentEditor, selected)

            close(OK_EXIT_CODE)
            CloudwatchinsightsTelemetry.retrieveQuery(project, Result.Succeeded)
        }
    }

    init {
        super.init()
        title = message("cloudwatch.logs.select_saved_query_dialog_name")
    }

    override fun doCancelAction() {
        CloudwatchinsightsTelemetry.retrieveQuery(project, Result.Cancelled)
        super.doCancelAction()
    }

    override fun createCenterPanel(): JComponent? = view.getComponent()
    override fun getOKAction(): Action = action

    override fun doValidate(): ValidationInfo? {
        if (view.resourceSelector.selected() == null) {
            return ValidationInfo(message("cloudwatch.logs.no_query_entered"), view.resourceSelector)
        }

        return null
    }

    companion object {
        fun populateParentEditor(editor: QueryEditor, selection: QueryDefinition) {
            if (selection.hasLogGroupNames()) {
                editor.logGroupTable.selectLogGroups(selection.logGroupNames().toSet())
            }
            editor.setQueryLanguage()
            editor.queryBox.text = selection.queryString()
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.resources.CloudWatchResources
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudwatchinsightsTelemetry
import software.aws.toolkits.telemetry.Result
import java.awt.event.ActionEvent
import javax.swing.Action
import javax.swing.JComponent

class SaveQueryDialog(
    private val project: Project,
    private val connectionSettings: ConnectionSettings,
    private val query: String,
    private val logGroups: List<String>
) : DialogWrapper(project) {
    private val coroutineScope = projectCoroutineScope(project)
    val view = EnterQueryName()
    private val action: OkAction = object : OkAction() {
        init {
            putValue(Action.NAME, message("cloudwatch.logs.save_query"))
        }

        override fun doAction(e: ActionEvent?) {
            super.doAction(e)
            if (doValidateAll().isNotEmpty()) return
            saveQuery()

            close(OK_EXIT_CODE)
        }
    }
    private val client = let {
        val (credentials, region) = connectionSettings
        AwsClientManager.getInstance().getClient<CloudWatchLogsClient>(credentials, region)
    }
    private val resourceCache = AwsResourceCache.getInstance()

    init {
        super.init()
        title = message("cloudwatch.logs.save_query_dialog_name")
    }

    override fun doCancelAction() {
        CloudwatchinsightsTelemetry.saveQuery(project, Result.Cancelled)
        super.doCancelAction()
    }

    override fun createCenterPanel(): JComponent = view.saveQueryPanel
    override fun doValidate(): ValidationInfo? = validateQueryName(view)
    override fun getOKAction(): Action = action

    private suspend fun getExistingQueryId(queryName: String): String? {
        val definitions = withContext(getCoroutineBgContext()) {
            resourceCache.getResourceNow(
                CloudWatchResources.DESCRIBE_QUERY_DEFINITIONS,
                region = connectionSettings.region,
                credentialProvider = connectionSettings.credentials,
                forceFetch = true
            )
        }

        return definitions.find { it.name() == queryName }?.queryDefinitionId()
    }

    fun saveQuery() = coroutineScope.launch {
        var result = Result.Succeeded
        try {
            val queryName = view.queryName.text
            action.isEnabled = false

            val existingQueryId = getExistingQueryId(queryName)
            client.putQueryDefinition {
                it.queryDefinitionId(existingQueryId)
                it.name(queryName)
                it.logGroupNames(logGroups)
                it.queryString(query)
            }
            notifyInfo(message("cloudwatch.logs.saved_query_status"), message("cloudwatch.logs.query_saved_successfully"), project)
            // invalidate cache
            resourceCache.clear(
                CloudWatchResources.DESCRIBE_QUERY_DEFINITIONS,
                connectionSettings
            )
        } catch (e: Exception) {
            LOG.error(e) { "Failed to save insights query" }
            notifyError(message("cloudwatch.logs.failed_to_save_query"), e.toString())
            result = Result.Failed
        } finally {
            action.isEnabled = true
            CloudwatchinsightsTelemetry.saveQuery(project, result)
        }
    }

    fun validateQueryName(view: EnterQueryName): ValidationInfo? {
        if (view.queryName.text.isEmpty()) {
            return ValidationInfo(message("cloudwatch.logs.query_name_missing"), view.queryName)
        }
        return null
    }

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


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

import com.intellij.icons.AllIcons
import com.intellij.ui.SimpleListCellRenderer
import software.amazon.awssdk.services.cloudwatchlogs.model.QueryDefinition
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.resources.CloudWatchResources
import software.aws.toolkits.jetbrains.ui.ResourceSelector
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JTextArea

class SelectSavedQuery(
    private val connectionSettings: ConnectionSettings
) {
    lateinit var resourceSelector: ResourceSelector<QueryDefinition>
        private set
    private lateinit var basePanel: JPanel
    private lateinit var logGroups: JTextArea
    private lateinit var queryString: JTextArea
    private lateinit var refreshButton: JButton

    private fun createUIComponents() {
        resourceSelector = ResourceSelector.builder()
            .resource { CloudWatchResources.DESCRIBE_QUERY_DEFINITIONS }
            .awsConnection { connectionSettings }
            .customRenderer(SimpleListCellRenderer.create("") { it.name() })
            .build()

        // select the first entry, if applicable
        resourceSelector.selectedItem { true }

        resourceSelector.addActionListener {
            resourceSelector.selected()?.let {
                logGroups.text = it.logGroupNames().joinToString("\n")
                queryString.text = it.queryString()
                // reset to the start, since setting the text moves the cursor to the end,
                // which results in scrolling to the bottom right corner if there's enough text
                logGroups.caretPosition = 0
                queryString.caretPosition = 0
            }
        }
    }

    init {
        refreshButton.icon = AllIcons.Actions.Refresh
        refreshButton.addActionListener {
            logGroups.text = ""
            queryString.text = ""
            resourceSelector.reload(forceFetch = true)
        }
    }

    fun getComponent(): JComponent = basePanel
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights

import software.aws.toolkits.resources.message
import java.time.temporal.ChronoUnit

enum class TimeUnit(val unit: ChronoUnit, val text: String) {
    MINUTES(ChronoUnit.MINUTES, message("cloudwatch.logs.time_minutes")),
    HOURS(ChronoUnit.HOURS, message("cloudwatch.logs.time_hours")),
    DAYS(ChronoUnit.DAYS, message("cloudwatch.logs.time_days")),
    WEEKS(ChronoUnit.WEEKS, message("cloudwatch.logs.time_weeks"))
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights.actions

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.core.credentials.activeCredentialProvider
import software.aws.toolkits.jetbrains.core.credentials.activeRegion
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.CloudWatchLogsNode
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights.QueryEditorDialog
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudwatchinsightsTelemetry
import software.aws.toolkits.telemetry.InsightsDialogOpenSource

class QueryGroupAction : SingleResourceNodeAction<CloudWatchLogsNode>(message("cloudwatch.logs.open_query_editor")), DumbAware {
    override fun actionPerformed(selected: CloudWatchLogsNode, e: AnActionEvent) {
        val project = selected.nodeProject

        QueryEditorDialog(
            project,
            ConnectionSettings(project.activeCredentialProvider(), project.activeRegion()),
            selected.logGroupName
        ).show()
        CloudwatchinsightsTelemetry.openEditor(project, InsightsDialogOpenSource.Explorer)
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.resources

import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.amazon.awssdk.services.cloudwatchlogs.model.QueryDefinition
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource

object CloudWatchResources {
    val LIST_LOG_GROUPS =
        ClientBackedCachedResource(CloudWatchLogsClient::class, "cwl.log_groups") {
            describeLogGroupsPaginator().logGroups().filterNotNull().toList()
        }

    val DESCRIBE_QUERY_DEFINITIONS =
        ClientBackedCachedResource(CloudWatchLogsClient::class, "cwl.query_definitions") {
            // unfortunately there is no paginator for this
            sequence<QueryDefinition> {
                var token: String? = null
                do {
                    val result = describeQueryDefinitions {
                        it.nextToken(token)
                    }
                    token = result.nextToken()
                    yieldAll(result.queryDefinitions())
                } while (token != null)
            }.toList()
        }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs.toolwindow

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

class CloudWatchLogsToolWindowFactory : ToolWindowFactory, DumbAware {
    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        runInEdt {
            toolWindow.installWatcher(toolWindow.contentManager)
        }
    }

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

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

    companion object {
        const val TOOLWINDOW_ID = "aws.cloudwatchlogs"
    }
}