@file:Suppress("HasPlatformType")

import com.jonnyzzz.mcpSteroid.gradle.*
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.net.HttpURLConnection
import java.net.URI
import java.net.URLEncoder
import java.util.*

plugins {
    kotlin("jvm")
    kotlin("plugin.serialization")
    id("org.jetbrains.intellij.platform")
}


val isReleaseBuild: Boolean by rootProject.extra

@Suppress("UNCHECKED_CAST")
val releaseNotesText: Provider<String>? = rootProject.extra["releaseNotesText"] as? Provider<String>

val releaseNotesVersion = providers.gradleProperty("mcp.release.notes.version")
    .orElse(rootProject.file("VERSION").readText().trim())
    .get()
val releaseNotesFile = rootProject.layout.projectDirectory.file("release/notes/$releaseNotesVersion.md")

val targetIdeProductRaw = providers.gradleProperty("mcp.platform.product").orElse("idea").get()
val targetIdeVersion = providers.gradleProperty("mcp.platform.version").orElse("2025.3").get()
val targetIdeProduct = when (targetIdeProductRaw.trim().lowercase()) {
    "idea", "iiu", "intellij", "intellijidea", "intellijideaultimate" -> JetBrainsIdeProduct.IntelliJIdeaUltimate
    "pycharm", "pcp", "python" -> JetBrainsIdeProduct.PyCharm
    else -> error(
        "Unsupported mcp.platform.product='$targetIdeProductRaw'. " +
                "Use one of: idea, pycharm."
    )
}

repositories {
    mavenCentral()

    intellijPlatform {
        defaultRepositories()
    }
}

// Libraries provided by IntelliJ platform - exclude from bundling
configurations.named("implementation") {
    exclude(group = "org.jetbrains.kotlin")
    exclude(group = "org.jetbrains.kotlinx")
    exclude(group = "org.jetbrains", module = "annotations")
    exclude(group = "org.slf4j")
}

// Consume kotlinc distribution from kotlin-cli subproject
val kotlincDist by configurations.creating {
    isCanBeConsumed = false
    isCanBeResolved = true
    attributes {
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class, "kotlinc-dist"))
    }
}

dependencies {
    intellijPlatform {
        when (targetIdeProduct) {
            JetBrainsIdeProduct.IntelliJIdeaUltimate -> intellijIdeaUltimate(targetIdeVersion)
            JetBrainsIdeProduct.PyCharm -> pycharm(targetIdeVersion)
            JetBrainsIdeProduct.GoLand,
            JetBrainsIdeProduct.WebStorm,
            -> error("Plugin build targets IntelliJ IDEA or PyCharm only. GoLand/WebStorm are for integration tests.")
        }
        // Required for IdeExamplesExecutionTest (Java refactoring APIs) and
        // LanguageSupportExecutionTest (Java/Kotlin language support actions)
        bundledPlugin("com.intellij.java")
        bundledPlugin("org.jetbrains.kotlin")
        // Direct (compile-time) access to IntelliJ's bundled MCP server plugin so
        // `IntelliJMcpServerProbe` can call McpServerService.getInstance() without
        // reflection. The plugin is bundled in IDEA 2025.3+ but the **user can
        // disable it at runtime**, so the probe still gates every call site behind
        // `PluginManagerCore.getPluginSet().enabledPlugins`.
        bundledPlugin("com.intellij.mcpServer")
        testFramework(TestFrameworkType.Platform)
    }

    implementation(project(":mcp-core"))
    // Ktor server + McpHttpTransport, transitively brings :mcp-core in
    implementation(project(":mcp-http"))
    // Transport-agnostic tool-handler metadata + registrations (empty for now —
    // classes will migrate across from :ij-plugin in the steps 6-13 refactor series).
    implementation(project(":mcp-steroid-server"))

    // IDE-free file-storage core for ExecutionStorage. The IntelliJ-side
    // IjExecutionStorage service wraps the generic class with project-scoped
    // path + identity providers.
    implementation(project(":execution-storage"))

    // Prompt base classes + generated prompt code
    implementation(project(":prompts"))

    // Kotlinc utility classes (KotlincArgFile, KotlincCommandLineBuilder)
    implementation(project(":kotlin-cli"))

    // Kotlinc binary distribution for plugin sandbox
    kotlincDist(project(":kotlin-cli"))

    // OCR common models shared with ocr-tesseract CLI
    implementation(project(":ocr-common"))

    // AI agent MCP server configuration helpers
    implementation(project(":ai-agents"))

    // PostHog analytics
    implementation("com.posthog:posthog-server:2.3.0")

    // Testing
    testImplementation("junit:junit:4.13.2")
    testImplementation(project(":intellij-downloader"))
    testImplementation(project(":test-helper"))

    // https://mvnrepository.com/artifact/org.testcontainers/testcontainers-bom
    testImplementation(platform("org.testcontainers:testcontainers-bom:2.0.2"))
    testImplementation("org.testcontainers:testcontainers")

    // Ktor client for MCP SSE transport tests
    val ktorVersion = "3.1.0"
    testImplementation("io.ktor:ktor-client-core:$ktorVersion")
    testImplementation("io.ktor:ktor-client-cio:$ktorVersion")
    testImplementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
    testImplementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
}

// Dedicated source set for Docker-based CLI integration tests (CliClaudeIntegrationTest,
// CliCodexIntegrationTest, CliGeminiIntegrationTest, etc.). These extend BasePlatformTestCase
// and need the full IntelliJ Platform test framework classpath, but they spin up Docker
// containers and require API keys, so they are NOT part of the default `:ij-plugin:test` run.
// Invoke explicitly via `./gradlew :ij-plugin:integrationTest`.
val integrationTest: SourceSet by sourceSets.creating {
    compileClasspath += sourceSets["main"].output + sourceSets["test"].output +
            sourceSets["test"].compileClasspath
    runtimeClasspath += output + compileClasspath + sourceSets["test"].runtimeClasspath
}

configurations["integrationTestImplementation"].extendsFrom(configurations["testImplementation"])
configurations["integrationTestRuntimeOnly"].extendsFrom(configurations["testRuntimeOnly"])

kotlin {
    jvmToolchain(21)
}

val generatedSourcesPath = layout.buildDirectory.dir("generated/kotlin")

// Generate metadata with encoded version
val generateMetadata by tasks.registering(GenerateMetadataTask::class) {
    group = "build"
    description = "Generate plugin metadata with encoded version"

    versionString.set(version.toString())
    inputs.property("version", version)
    outputFile.set(generatedSourcesPath.map { it.file("PluginMetadata.kt") })
}

// Add generated sources to main source set and make kotlin compilation depend on it
kotlin.sourceSets.main {
    kotlin.srcDir(generatedSourcesPath)
}

tasks.withType<KotlinCompile>().configureEach {
    dependsOn(generateMetadata)
}

intellijPlatform {
    projectName = rootProject.name
    caching {
        ides {
            enabled = true
        }
    }
    buildSearchableOptions = false
    pluginConfiguration {
        name = "MCP Steroid"
        version = project.version.toString()
        if (releaseNotesText != null) {
            changeNotes = releaseNotesText
        }

        ideaVersion {
            // KEEP IN SYNC with `MANAGED_BACKEND_MIN_SUPPORTED_BUILD` in
            // intellij-downloader/.../CompatibilityFloor.kt — enforced by
            // PluginCompatibilityFloorTest.
            sinceBuild = "252"
            untilBuild = null
        }
    }

    pluginVerification {
        ides {
            create(IntelliJPlatformType.IntellijIdeaUltimate, "2025.3") { useInstaller = true }
            create(IntelliJPlatformType.IntellijIdeaUltimate, "2026.1") { useInstaller = true }
            //TODO: Setup 262 tests
        }
    }
}

// Register a dedicated `integrationTest` task via the IntelliJ Platform testing DSL so it
// gets its own sandbox (prepareSandbox_integrationTest) and the proper IDE JVM argument
// providers, while compiling from `src/integrationTest/kotlin` instead of `src/test/kotlin`.
intellijPlatformTesting {
    testIde {
        register("integrationTest") {
            // Populates the suffixed intellijPlatformTestDependencies_integrationTest
            // configuration with opentest4j + IntelliJ test framework JARs. Without this
            // the test JVM hits NoClassDefFoundError on org.opentest4j.AssertionFailedError.
            testFramework(TestFrameworkType.Platform)

            task {
                group = "verification"
                description = "Runs Docker-based CLI integration tests (Claude/Codex/Gemini). " +
                        "Requires Docker and API keys. Not run by default `:ij-plugin:test`."
                useJUnit()

                // Replace (not append) testClassesDirs: the TestIdeTask default includes
                // the plugin's instrumented default-test-set classes — keeping them would
                // run every regular test alongside the Cli*IntegrationTest classes.
                testClassesDirs = integrationTest.output.classesDirs

                // Additive on classpath: preserve the IntelliJ Platform JARs +
                // test framework that TestIdeTask.configuration (registration-time lambda)
                // already wired up, then add this source set's classes and its runtime deps
                // on top for our own tests (testcontainers, ktor-client, :test-helper, …).
                classpath += integrationTest.output + integrationTest.runtimeClasspath
            }
        }
    }
}

tasks {
    test {
        useJUnit()

        // Explicitly restrict to the main 'test' source set. Without this, the
        // IntelliJ Platform plugin's TestIdeTask includes instrumented classes from
        // ALL test-like source sets (including 'integrationTest'), causing Docker CLI
        // tests to run during the regular :ij-plugin:test task where they fail due
        // to missing API keys.
        testClassesDirs = sourceSets["test"].output.classesDirs
    }

    patchPluginXml {
        if (isReleaseBuild) {
            // Release builds require release notes — track the file as a
            // mandatory input so changes trigger a rebuild.
            inputs.file(releaseNotesFile)
        }
    }
}

val verifyBundledKotlinCompatibility by tasks.registering(VerifyBundledKotlinCompatibilityTask::class) {
    group = "verification"
    description = "Verify bundled kotlinc is close enough to IntelliJ-bundled kotlin-stdlib"
    dependsOn(kotlincDist)
    dependsOn(tasks.prepareSandbox)

    val sourceSets = project.extensions.getByType<SourceSetContainer>()
    mainRuntimeClasspath.from(sourceSets.getByName("main").runtimeClasspath)
    mainRuntimeClasspath.from(configurations.getByName("intellijPlatformDependency"))
    kotlincHome.set(kotlincDist.elements.map { files ->
        val dir = files.first().asFile.resolve("kotlinc")
        layout.projectDirectory.dir(dir.absolutePath)
    })
    kotlinPluginVersion.set(providers.provider {
        plugins.getPlugin(org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper::class.java).pluginVersion
    })
    reportFile.set(layout.buildDirectory.file("reports/kotlin-version-compatibility.txt"))
}

val ocrToolDist by configurations.creating {
    isCanBeConsumed = false
    isCanBeResolved = true
    attributes {
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class, "install-dist"))
    }
}

dependencies {
    ocrToolDist(project(":ocr-tesseract"))
}

// Apply the same plugin-content wiring (ocr-tesseract, kotlinc, EULA) to every sandbox
// that runs the plugin: production (prepareSandbox), default test (prepareTestSandbox),
// and the dedicated integrationTest sandbox created by
// `intellijPlatformTesting.testIde { register("integrationTest") }` above.
// Without the integrationTest entry, steroid_execute_code fails at runtime with
// "Kotlinc executable not found: .../plugins_integrationTest/mcp-steroid/kotlinc/bin/kotlinc".
val prepareSandbox_integrationTest = tasks.named<Sync>("prepareSandbox_integrationTest")

listOf(tasks.prepareSandbox, tasks.prepareTestSandbox, prepareSandbox_integrationTest).forEach { r ->
    r.configure {
        from(ocrToolDist) {
            into(intellijPlatform.projectName.map { "$it/ocr-tesseract" })
            filesMatching("bin/*") {
                if (!name.endsWith(".bat")) {
                    permissions { unix("rwxr-xr-x") }
                }
            }
        }
        from(kotlincDist) {
            into(intellijPlatform.projectName)
            filesMatching("kotlinc/bin/*") {
                if (!name.endsWith(".bat")) {
                    permissions { unix("rwxr-xr-x") }
                }
            }
        }
        // Include EULA file in plugin root
        from(rootProject.layout.projectDirectory.file("EULA")) {
            into(intellijPlatform.projectName)
        }
    }
}

// Expose plugin .zip for consumption by test-integration module
val pluginZipElements by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
    attributes {
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class, "plugin-zip"))
    }
}

tasks.buildPlugin {
    // Preserve executable permissions in the ZIP for scripts
    filesMatching("**/kotlinc/bin/*") {
        if (!name.endsWith(".bat")) {
            permissions { unix("rwxr-xr-x") }
        }
    }
    filesMatching("**/ocr-tesseract/bin/*") {
        if (!name.endsWith(".bat")) {
            permissions { unix("rwxr-xr-x") }
        }
    }

    doFirst {
        val outputDir = tasks.buildPlugin.get().archiveFile.get().asFile.parentFile
        val zips = outputDir
            .listFiles()
            ?.filter { it.name.endsWith(".zip") }
            ?.filter { file -> file.lastModified() < System.currentTimeMillis() - 1000L * 60 * 60 * 12 }
            ?: return@doFirst

        delete(zips)
    }
}

artifacts {
    add(pluginZipElements.name, tasks.buildPlugin.map { it.archiveFile }) {
        builtBy(tasks.buildPlugin)
    }
}

// Verify bundled libraries in plugin/lib folder
val pluginVersion = version.toString()
val verifyBundledLibraries by tasks.registering {
    group = "verification"
    description = "List and verify libraries bundled in plugin lib folder"
    dependsOn(tasks.buildPlugin)
    doLast {
        val zip = tasks.buildPlugin.get().outputs.files.singleFile

        // Read ZIP entries with executable flag detection (:X suffix)
        var allFiles: SortedSet<String> = run {
            val allFiles = mutableListOf<String>()
            zipTree(zip).visit {
                if (!isDirectory) {
                    val isExec = permissions.user.execute
                    val path = relativePath.pathString

                    allFiles += when {
                        isExec ->  "$path:X"
                        else -> path
                    }
                }
            }
            allFiles
        }.toSortedSet()

        val pluginPrefix = intellijPlatform.projectName.get() + "/"
        check(allFiles.all { it.startsWith(pluginPrefix) }) {
            "files must be under plugin roots: " + allFiles.map { it.substringBefore('/') }.toSortedSet()
        }
        allFiles = allFiles.map { it.removePrefix(pluginPrefix) }.toSortedSet()

        check(allFiles.isNotEmpty()) { "no libraries found in ${allFiles.joinToString { "\n  - $it" }}" }

        val kotlincFiles = allFiles.filter { it.startsWith("kotlinc/") }.toSortedSet()
        check(kotlincFiles.contains("kotlinc/bin/kotlinc:X")) { "Kotlinc must be included in " + kotlincFiles.joinToString { "\n $it" } }
        check(kotlincFiles.contains("kotlinc/bin/kotlinc.bat")) { "Kotlinc must be included in " + kotlincFiles.joinToString { "\n $it" } }
        allFiles = (allFiles - kotlincFiles).toCollection(sortedSetOf())


        val ocrFiles = allFiles.filter { it.startsWith("ocr-tesseract/") }.toSortedSet()
        check(ocrFiles.contains("ocr-tesseract/bin/ocr-tesseract:X")) { "ocr-tesseract must be included in " + ocrFiles.joinToString { "\n $it" } }
        check(ocrFiles.contains("ocr-tesseract/bin/ocr-tesseract.bat")) { "ocr-tesseract must be included in " + ocrFiles.joinToString { "\n $it" } }
        check(ocrFiles.contains("ocr-tesseract/tessdata/eng.traineddata")) { "ocr-tesseract must be included in " + ocrFiles.joinToString { "\n $it" } }
        check(ocrFiles.contains("ocr-tesseract/tessdata/osd.traineddata")) { "ocr-tesseract must be included in " + ocrFiles.joinToString { "\n $it" } }
        check(ocrFiles.any { it.startsWith("ocr-tesseract/lib/ocr-common-$pluginVersion.jar") }) { "ocr-common jar must be included in ocr-tesseract" }
        check(ocrFiles.any { it.startsWith("ocr-tesseract/lib/ocr-tesseract-$pluginVersion.jar") }) { "ocr-tesseract jar must be included in ocr-tesseract" }

        allFiles = (allFiles - ocrFiles).toCollection(sortedSetOf())

        // Assert expected libraries - update this list when dependencies change
        val expectedFiles = sortedSetOf(
            // EULA file
            "EULA",

            //our binaires
            "lib/ai-agents-$pluginVersion.jar",
            "lib/ij-plugin-$pluginVersion.jar",
            "lib/kotlin-cli-$pluginVersion.jar",
            "lib/ocr-common-$pluginVersion.jar",
            "lib/execution-storage-$pluginVersion.jar",
            "lib/mcp-core-$pluginVersion.jar",
            "lib/mcp-http-$pluginVersion.jar",
            "lib/mcp-steroid-server-$pluginVersion.jar",
            "lib/prompts-api-$pluginVersion.jar",
            "lib/prompts-$pluginVersion.jar",

            //libraries
            "lib/config-1.4.3.jar",
            "lib/gson-2.10.1.jar",
            "lib/jansi-2.4.1.jar",

            "lib/ktor-events-jvm-3.1.0.jar",
            "lib/ktor-http-cio-jvm-3.1.0.jar",
            "lib/ktor-http-jvm-3.1.0.jar",
            "lib/ktor-io-jvm-3.1.0.jar",
            "lib/ktor-network-jvm-3.1.0.jar",
            "lib/ktor-serialization-jvm-3.1.0.jar",
            "lib/ktor-server-cio-jvm-3.1.0.jar",
            "lib/ktor-server-core-jvm-3.1.0.jar",
            "lib/ktor-server-sse-jvm-3.1.0.jar",
            "lib/ktor-sse-jvm-3.1.0.jar",
            "lib/ktor-utils-jvm-3.1.0.jar",
            "lib/ktor-websockets-jvm-3.1.0.jar",

            "lib/okhttp-4.11.0.jar",
            "lib/okio-jvm-3.2.0.jar",
            "lib/posthog-6.4.0.jar",
            "lib/posthog-server-2.3.0.jar",

        ).toSortedSet()

        if (allFiles != expectedFiles) {
            val missing = expectedFiles - allFiles
            val unexpected = allFiles - expectedFiles
            throw GradleException(buildString {
                appendLine("Bundled libraries mismatch!")
                if (missing.isNotEmpty()) {
                    appendLine("Missing libraries:")
                    missing.forEach { appendLine("  - $it") }
                }
                if (unexpected.isNotEmpty()) {
                    appendLine("Unexpected libraries:")
                    unexpected.forEach { appendLine("  - $it") }
                }
                appendLine()
                appendLine("Actual libraries: ")
                allFiles.forEach { appendLine("  - $it") }
                appendLine()
                appendLine("Update expectedLibraries in build.gradle.kts if this change is intentional.")
            })
        }
    }
}

tasks.test {
    dependsOn(verifyBundledLibraries)
}

tasks.buildPlugin {
    finalizedBy(verifyBundledLibraries)
}

tasks.verifyPlugin {
    dependsOn(verifyBundledKotlinCompatibility)
    dependsOn(verifyBundledLibraries)
}

// Deploy plugin to running IDEs with hot-reload support
val deployPlugin by tasks.registering {
    group = "intellij platform"
    description = "Deploy plugin to running IDEs"
    dependsOn(verifyBundledLibraries)
    doLast {
        val zip = tasks.buildPlugin.get().outputs.files.singleFile
        val home = File(System.getProperty("user.home"))
        val endpoints = home.listFiles { f -> f.name.matches(Regex("\\.\\d+\\.hot-reload")) }
            ?.mapNotNull { f ->
                val pid = Regex("\\.(\\d+)\\.").find(f.name)?.groupValues?.get(1)?.toLongOrNull() ?: return@mapNotNull null
                if (!ProcessHandle.of(pid).isPresent) return@mapNotNull null
                val lines = f.readLines().takeIf { it.size >= 2 } ?: return@mapNotNull null
                lines[0] to lines[1]
            }?.distinctBy { it.first } ?: emptyList()

        require(endpoints.isNotEmpty()) { "No running IDEs found" }

        endpoints.forEach { (url, token) ->
            val encodedPath = URLEncoder.encode(zip.absolutePath, Charsets.UTF_8)
            val fileUrl = "$url?local-disk-file=$encodedPath"
            println("\n→ $fileUrl")
            val conn = (URI(fileUrl).toURL().openConnection() as HttpURLConnection).apply {
                requestMethod = "POST"; doOutput = false; doInput = true
                setRequestProperty("Authorization", token)
                connectTimeout = 5000; readTimeout = 300000
            }
            val responseLines = if (conn.responseCode in 200..299) {
                conn.inputStream.bufferedReader().readLines()
            } else {
                conn.errorStream?.bufferedReader()?.readLines().orEmpty()
            }
            responseLines.forEach { println("  $it") }
            require(conn.responseCode in 200..299 && responseLines.lastOrNull() == "SUCCESS") {
                "Deploy failed (HTTP ${conn.responseCode}): ${responseLines.lastOrNull()}"
            }
        }
    }
}


val deployPluginLocallyTo253 by tasks.registering(Sync::class) {
    dependsOn(tasks.buildPlugin)
    dependsOn(verifyBundledLibraries)
    group = "intellij platform"
    outputs.upToDateWhen { false }

    val targetName = "" + rootProject.name
    val targetDir = "${System.getenv("HOME")}/intellij-253/config/plugins/$targetName"

    this.destinationDir = file(targetDir)
    from(
        tasks.buildPlugin
            .map { it.archiveFile }
            .map { zipTree(it) }
    ) {
        includeEmptyDirs = false
        eachFile {
            println(this)
            this.path = this.path.substringAfter("/")
        }
    }
}
