/*
 * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.gradle.plugin.diagnostics

/**
 * Represents a diagnostic message generated by tooling within the Kotlin Gradle Plugin.
 *
 * @property identifier An identifier containing unique ID and a display name for the diagnostic.
 * @property message The core message of the diagnostic, offering details about the issue.
 * @property severity The severity level of the diagnostic (e.g., WARNING, ERROR, or FATAL).
 * @property solutions A list of potential solutions or steps to address the diagnostic issue.
 * @property documentation Optional documentation providing additional context or resources.
 * @property throwable Optional exception related to the diagnostic issue if applicable.
 */
internal class ToolingDiagnostic(
    val identifier: ID,
    val messageBuilder: () -> String,
    val severity: Severity,
    val solutions: List<String>,
    val documentation: Documentation? = null,
    val throwable: Throwable? = null,
) {
    constructor(
        identifier: ID,
        message: String,
        severity: Severity,
        solutions: List<String>,
        documentation: Documentation? = null,
        throwable: Throwable? = null,
    ) : this(
        identifier = identifier,
        messageBuilder = { message },
        severity = severity,
        solutions = solutions,
        documentation = documentation,
        throwable = throwable,
    )

    val message: String
        get() = messageBuilder()

    private data class ToolingDiagnosticSnapshot(
        val identifier: ID,
        val message: String,
        val severity: Severity,
        val solutions: List<String>,
        val documentation: Documentation?,
        val throwable: Throwable?,
    ) {
        constructor(diagnostic: ToolingDiagnostic) : this(
            identifier = diagnostic.identifier,
            message = diagnostic.message,
            severity = diagnostic.severity,
            solutions = diagnostic.solutions,
            documentation = diagnostic.documentation,
            throwable = diagnostic.throwable,
        )
    }

    override fun equals(other: Any?): Boolean = other is ToolingDiagnostic
            && ToolingDiagnosticSnapshot(this) == ToolingDiagnosticSnapshot(other)
    override fun hashCode(): Int = ToolingDiagnosticSnapshot(this).hashCode()

    /**
     * Represents an identifier associated with a [ToolingDiagnostic].
     *
     * @property id A unique string identifier for the diagnostic.
     * @property displayName A user-friendly name associated with the diagnostic.
     * @property group The diagnostic group to which this identifier belongs.
     */
    data class ID(val id: String, val displayName: String, val group: DiagnosticGroup) {
        override fun toString(): String {
            return "$group | $id | $displayName"
        }
    }

    /**
     * Represents the documentation associated with a particular issue or diagnostic in the system.
     *
     * @property url The URL pointing to the primary documentation resource.
     * @property additionalUrlContext The additional text to the [url] providing hints or context for better understanding.
     */
    data class Documentation(val url: String, val additionalUrlContext: String)

    enum class Severity {
        /**
         * More visible than most of the output (intuition: yellow-highlighting).
         * Doesn't prevent the build from running.
         *
         * Use for non-critical misconfigurations with low rate of false-positives
         */
        WARNING,

        /**
         * Heavily emphasized in the output (intuition: bold red highlighting).
         *
         * ATTENTION. If a diagnostic with this severity is reported, Kotlin compiler
         * will _not_ be invoked (build will appear failed, as with compilation error)
         * see [org.jetbrains.kotlin.gradle.plugin.diagnostics.CheckKotlinGradlePluginConfigurationErrors]
         *
         * However, Gradle IDE Sync and other tasks that are not connected with
         * any of the Kotlin Compiler and tools (e.g. 'help', 'clean'), will run successfully.
         *
         * Use for critical misconfigurations that need immediate addressing
         */
        ERROR,

        /**
         * Same display as [ERROR] but will not fail the compilation.
         *
         * See [org.jetbrains.kotlin.gradle.plugin.diagnostics.CheckKotlinGradlePluginConfigurationErrors]
         */
        STRONG_WARNING,

        /**
         * Aborts the progress of the current process (Gradle build/Import/...).
         *
         * Please use *extremely* sparingly, as failing the current process can:
         * - mask further errors (forcing users to make multiple runs before fixing all issues)
         *
         * - lead to unpleasant UX in IDE (if the failure happens during import, then depending
         *   on when it happened users might not have even basic IDE assistance, which makes fixing
         *   the root cause very annoying)
         *
         * Use for irreconcilable misconfigurations / malformed input which prevent further
         * configuration _and_ when the graceful degradation (allowing configuration phase to finish)
         * is too expensive.
         */
        FATAL,
    }

    val id: String get() = identifier.id
    val name: String get() = identifier.displayName
    val group: DiagnosticGroup get() = identifier.group

    internal fun parsableFormat(effectiveSeverity: ToolingDiagnostic.Severity) = buildString {
        append("[$id | $effectiveSeverity]")
        appendLine(" $name")
        appendLine(message)

        val subLines = solutions + listOfNotNull(documentation?.additionalUrlContext)

        subLines.filter { it.isNotBlank() }.forEach { subLine ->
            appendLine(subLine)
        }
    }.trimEnd()

    override fun toString() = parsableFormat(severity)
}