//import org.jetbrains.kotlin.powerassert.gradle.PowerAssertGradleExtension
import java.time.Duration
import java.time.Instant
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters

plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.kotlin.serialization)
//    alias(libs.plugins.kotlin.power.assert)
    application
    `maven-publish`
    alias(libs.plugins.buildconfig)
    alias(libs.plugins.vanniktech.maven.publish)
    alias(libs.plugins.shadow)
    `java-test-fixtures`
    `jvm-test-suite`
}

repositories {
    mavenCentral()
    maven("https://repo.gradle.org/gradle/libs-releases")
}

application {
    mainClass.set("dev.rnett.gradle.mcp.Application")
    applicationDefaultJvmArgs = listOf(
        "-Xmx1024m",
        "-Xms256m"
    )
}

val updateToolsList = tasks.register<JavaExec>("updateToolsList") {
    inputs.files(sourceSets.main.get().runtimeClasspath).withPathSensitivity(PathSensitivity.NONE)
    inputs.files(sourceSets.main.get().kotlin).withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.dir(project.rootDir.resolve("docs/tools")).withPathSensitivity(PathSensitivity.RELATIVE)
    classpath = sourceSets.main.get().runtimeClasspath
    mainClass.set("dev.rnett.gradle.mcp.UpdateTools")
    args = listOf(project.rootDir.resolve("docs/tools").absolutePath)
}

val verifyToolsList = tasks.register<JavaExec>("verifyToolsList") {
    inputs.files(sourceSets.main.get().runtimeClasspath).withPathSensitivity(PathSensitivity.NONE)
    inputs.files(sourceSets.main.get().kotlin).withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.dir(project.rootDir.resolve("docs/tools")).withPathSensitivity(PathSensitivity.RELATIVE)
    classpath = sourceSets.main.get().runtimeClasspath
    mainClass.set("dev.rnett.gradle.mcp.UpdateTools")
    args = listOf(project.rootDir.resolve("docs/tools").absolutePath, "--verify")
}

tasks.check {
    dependsOn(verifyToolsList)
}

val generatedSkillsDir = layout.buildDirectory.dir("generated/skills/authoring-gradle-builds/references")

val zipSkills = tasks.register<Zip>("zipSkills") {
    dependsOn(generateBestPracticesDoc)
    from("src/main/skills")
    from(generatedSkillsDir) {
        into("authoring-gradle-builds/references")
    }
    archiveFileName.set("skills.zip")
    destinationDirectory.set(layout.buildDirectory.dir("generated/resources/skills"))
}

val bestPracticesGeneratorClasspath = configurations.create("bestPracticesGeneratorClasspath") {
    isCanBeConsumed = false
    isCanBeResolved = true
}

dependencies {
    add(bestPracticesGeneratorClasspath.name, project(":best-practices-generator"))
}

val generateBestPracticesDoc = tasks.register<JavaExec>("generateBestPracticesDoc") {
    val generatorProject = project(":best-practices-generator")
    val gradleDocsVersion = providers.gradleProperty("gradleDocsVersion").orElse(gradle.gradleVersion)

    classpath(bestPracticesGeneratorClasspath)
    dependsOn(generatorProject.tasks.named("classes"))
    mainClass.set("dev.rnett.gradle.mcp.bestpractices.GenerateBestPracticesDoc")
    args(
        generatedSkillsDir.get().asFile.absolutePath,
        gradleDocsVersion.get(),
    )
    inputs.property("gradleDocsVersion", gradleDocsVersion)
    inputs.dir(generatorProject.layout.projectDirectory.dir("src/main/kotlin"))
        .withPathSensitivity(PathSensitivity.RELATIVE)
    outputs.dir(generatedSkillsDir.map { it.dir("best-practices") })
}


val updateSkillsList = tasks.register<JavaExec>("updateSkillsList") {
    inputs.files(sourceSets.main.get().output.classesDirs).withPathSensitivity(PathSensitivity.NONE)
    inputs.dir(project.rootDir.resolve("src/main/skills")).withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.file(project.rootDir.resolve("docs/skills.md")).withPathSensitivity(PathSensitivity.NONE)
    classpath = sourceSets.main.get().output.classesDirs + files(configurations.named("compileClasspath"))
    mainClass.set("dev.rnett.gradle.mcp.UpdateSkills")
    args = listOf(project.rootDir.absolutePath)
}

val verifySkillsList = tasks.register<JavaExec>("verifySkillsList") {
    inputs.files(sourceSets.main.get().output.classesDirs).withPathSensitivity(PathSensitivity.NONE)
    inputs.dir(project.rootDir.resolve("src/main/skills")).withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.file(project.rootDir.resolve("docs/skills.md")).withPathSensitivity(PathSensitivity.NONE)
    classpath = sourceSets.main.get().output.classesDirs + files(configurations.named("compileClasspath"))
    mainClass.set("dev.rnett.gradle.mcp.UpdateSkills")
    args = listOf(project.rootDir.absolutePath, "--verify")
    dependsOn(generateBestPracticesDoc)
}


sourceSets {
    main {
        resources {
            srcDir(zipSkills.map { it.destinationDirectory.get() })
        }
    }
}

dependencies {
    api(project(":repl-shared"))
    api(libs.gradle.tooling.api)

    api(libs.caffeine)
    api(libs.commons.io)

    api(libs.kotlin.reflect)
    api(libs.schema.kenerator.core)
    api(libs.schema.kenerator.serialization)
    api(libs.schema.kenerator.jsonschema)
    api(libs.kotlinx.serialization.json)

    api(libs.flexmark.html2md)

    api(libs.kotlinx.coroutines.core)
    api(libs.mcp.sdk)
    api(libs.jsoup)
    api(libs.koin.core)
    api(libs.koin.ktor)
    api(libs.koin.logger.slf4j)
    api(libs.ktor.server.netty)
    api(libs.ktor.server.di)
    api(libs.ktor.server.sse)
    api(libs.ktor.serialization.kotlinx.json)
    api(libs.ktor.server.config.yaml)
    api(libs.ktor.client.cio)
    api(libs.ktor.client.content.negotiation)
    api(libs.ktor.serialization.kotlinx.xml)
    api(libs.ktor.serialization.kotlinx.json)
    api(libs.logback.classic)

    api(libs.lucene.core)
    api(libs.lucene.codecs)
    api(libs.lucene.analysis.common)
    api(libs.lucene.queryparser)
    api(libs.lucene.highlighter)

    api(libs.ktreesitter)
    api(libs.zstd.jni)
    api(libs.commons.compress)

    testFixturesApi(project)
    testFixturesApi(libs.kotlin.test)
    testFixturesApi(libs.kotlinx.coroutines.test)
    testFixturesApi(libs.junit.jupiter)
    testFixturesApi(libs.mockk)
    testFixturesApi(libs.koin.test)
    testFixturesApi(libs.koin.test.junit5)
    testFixturesApi(libs.ktor.client.cio)
    testFixturesApi(libs.ktor.client.content.negotiation)
    testFixturesApi(libs.ktor.client.mock)
    testFixturesApi(libs.mcp.sdk.testing)

    testImplementation(testFixtures(project))
}

kotlin {
    jvmToolchain(25)

    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
        freeCompilerArgs.addAll(
            "-Xannotation-default-target=param-property",
            "-Xcontext-parameters",
            "-opt-in=kotlin.concurrent.atomics.ExperimentalAtomicApi"
        )
    }
}

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

val isCI = providers.environmentVariable("CI").orNull != null

testing {
    suites {
        val integrationTest by registering(JvmTestSuite::class) {
            dependencies {
                implementation(project())
                implementation(dependencies.testFixtures(project()))
            }


            targets {
                all {
                    testTask.configure {
                        // must keep to ensure compatability with jbang catalog entry
                        javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(21) }
                        maxParallelForks = if (isCI) 3 else 8
                        if (isCI) {
                            mustRunAfter("test")
                        } else {
                            shouldRunAfter("test")
                        }
                        maxHeapSize = "1g"
                    }
                }
            }
        }

        val treeSitterTest by registering(JvmTestSuite::class) {
            dependencies {
                implementation(project())
                implementation(project.dependencies.testFixtures(project()))
            }

            targets {
                all {
                    testTask.configure {
                        maxParallelForks = if (isCI) 3 else 8
                        if (isCI) {
                            mustRunAfter("test")
                        } else {
                            shouldRunAfter("test")
                        }
                        maxHeapSize = "1g"
                    }
                }
            }
        }
    }
}

tasks.test {
    if (isCI) {
        systemProperty("junit.jupiter.execution.parallel.config.fixed.parallelism", "4")
    } else {
        systemProperty("junit.jupiter.execution.parallel.config.fixed.parallelism", "10")
    }
    maxHeapSize = "2g"
}

tasks.withType<Test>().configureEach {
    javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(21) }
    jvmArgs("--add-modules", "jdk.incubator.vector")
    useJUnitPlatform()
    systemProperty("GRADLE_MCP_LOG_DIR", layout.buildDirectory.dir("test-logs").get().asFile.absolutePath)
    systemProperty("GRADLE_MCP_TEST_SHARED_DISTS_DIR", layout.buildDirectory.dir("test-shared-gradle-dists").get().asFile.absolutePath)
    systemProperty("docs.updateSnapshots", System.getProperty("docs.updateSnapshots"))
}

tasks.named("check") {
    dependsOn(testing.suites.named("integrationTest"))
    dependsOn(testing.suites.named("treeSitterTest"))
}

/**
 * Shared, build-scoped state for the baseline probe. A shared build service is the
 * configuration-cache-safe way to coordinate state between task actions: the configuration cache
 * gives every task its own copy of captured script state, so plain captured objects cannot be
 * shared across tasks. Build services are recreated once per build and shared by all tasks that
 * use them (see Gradle's "Using Shared Build Services").
 *
 * The two real-Gradle suites record their start/finish from `doFirst`/`doLast` actions; the
 * baselineProbe task reads the aggregated report and writes it to `build/probes/`.
 */
abstract class BaselineProbeService : BuildService<BuildServiceParameters.None>, AutoCloseable {
    private val startedSuites = AtomicInteger(0)
    private val finishedSuites = AtomicInteger(0)
    private val peakDaemons = AtomicInteger(0)
    private val suiteStart = AtomicReference<Instant?>(null)
    private val report = AtomicReference<String?>(null)
    private val sampler = AtomicReference<ScheduledExecutorService?>(null)

    /** Called from each suite task's `doFirst`. Starts the wall clock and the daemon sampler once. */
    fun onSuiteStart() {
        if (startedSuites.incrementAndGet() == 1) {
            suiteStart.set(Instant.now())
            val scheduler = Executors.newSingleThreadScheduledExecutor()
            sampler.set(scheduler)
            scheduler.scheduleAtFixedRate(
                { peakDaemons.accumulateAndGet(countLiveGradleDaemons()) { a, b -> maxOf(a, b) } },
                0,
                2,
                TimeUnit.SECONDS
            )
        }
    }

    /**
     * Called from each suite task's `doLast`. When both suites have finished, stops the sampler and
     * aggregates the report. Returns the report, or null if measurement never completed.
     */
    fun onSuiteFinish(): String? {
        if (finishedSuites.incrementAndGet() == 2) {
            sampler.getAndSet(null)?.shutdownNow()
            val start = suiteStart.get()
            if (start != null) {
                val wall = Duration.between(start, Instant.now())
                report.set(
                    buildString {
                        appendLine("baselineProbe (reduce-test-build-volume)")
                        appendLine("timestamp: $start")
                        appendLine("suites: integrationTest, treeSitterTest")
                        appendLine("maxParallelForks: local 8 / CI 3 (fork reduction is baseline-gated)")
                        appendLine("suiteWallTime: $wall")
                        appendLine("peakLiveDaemons: ${peakDaemons.get()}")
                        appendLine("note: live-daemon sampling includes the daemon running this build; compare relative runs, not absolute counts.")
                    }
                )
            }
        }
        return report.get()
    }

    /** The aggregated report, or null if it was never produced. */
    fun report(): String? = report.get()

    override fun close() {
        // Safety net: make sure the sampler is stopped even if the suites never both completed.
        sampler.getAndSet(null)?.shutdownNow()
    }

    private fun countLiveGradleDaemons(): Int {
        // A live Gradle daemon is a JVM whose main class is org.gradle.launcher.daemon.bootstrap.GradleDaemon.
        return try {
            ProcessHandle.allProcesses()
                .toList()
                .mapNotNull { handle -> runCatching { handle.info().commandLine().orElse(null) }.getOrNull() }
                .count { commandLine -> commandLine.contains("GradleDaemon") }
        } catch (e: Exception) {
            // Measurement unavailable — report 0 so the probe still records suite wall time.
            0
        }
    }
}

/**
 * Shared service instance for the baseline probe. Registered here so the suite tasks and the
 * baselineProbe task all use the same instance; created on first use, so unrelated builds never
 * touch it.
 */
val baselineProbeService = gradle.sharedServices.registerIfAbsent(
    "baselineProbeService",
    BaselineProbeService::class.java
) { }

// Attach the probe measurement to the two real-Gradle suites. The actions only run when a suite
// task executes, so ordinary builds (e.g. `test` or `check` without the probe) are unaffected.
listOf("integrationTest", "treeSitterTest").forEach { suiteTask ->
    // Bind to a local before the closures: referencing the script-level `baselineProbeService`
    // property from inside the task action closures captures the script instance (`this$0`),
    // which the configuration cache cannot serialize. A captured local Provider is fine.
    val service = baselineProbeService
    tasks.named(suiteTask) {
        usesService(service)
        doFirst { service.get().onSuiteStart() }
        doLast { service.get().onSuiteFinish() }
    }
}

/**
 * Baseline probe for the real-Gradle suites (see the `reduce-test-build-volume` change). Runs
 * `integrationTest` and `treeSitterTest` and records the combined suite wall time plus the peak
 * number of live Gradle daemons observed during the run to `build/probes/`. The recorded baseline
 * gates the fork-concurrency decision: local forks stay 8 / CI 3 unless a recorded baseline
 * justifies a reduction to 4 (CI stays 3 regardless).
 */
val baselineProbe = tasks.register("baselineProbe") {
    group = "verification"
    description = "Runs the real-Gradle suites and records a baseline of suite wall time and peak Gradle daemon count to build/probes/."

    // Same config-cache-safe local binding as above; the doLast closure must not capture the script.
    val service = baselineProbeService
    dependsOn(tasks.named("integrationTest"), tasks.named("treeSitterTest"))
    usesService(service)

    val probeDir = layout.buildDirectory.dir("probes")
    outputs.dir(probeDir)
    outputs.upToDateWhen { false }

    doLast {
        val report = service.get().report()
        if (report == null) {
            logger.warn("baselineProbe: suite timing was not captured (were the suites excluded?)")
            return@doLast
        }
        val file = probeDir.get().file("baseline-${Instant.now().toString().replace(":", "-")}.txt").asFile
        file.parentFile.mkdirs()
        file.writeText(report)
        logger.lifecycle("baselineProbe: baseline written to {}", file)
    }
}

tasks.named<UpdateDaemonJvm>("updateDaemonJvm") {
    languageVersion = JavaLanguageVersion.of(25)
}

buildConfig {
    sourceSets.named("testFixtures") {
        useKotlinOutput { internalVisibility = false }
        buildConfigField("KOTLIN_VERSION", libs.versions.kotlin.get())
        buildConfigField("KOTLINX_SERIALIZATION_VERSION", libs.versions.kotlinxSerializationJson.get())
        buildConfigField("SLF4J_VERSION", libs.versions.slf4j.get())
        buildConfigField("GUAVA_VERSION", libs.versions.guava.get())
        buildConfigField("JUNIT_JUPITER_VERSION", libs.versions.junit.jupiter.get())
        buildConfigField("COMPOSE_VERSION", libs.versions.jetbrains.compose.get())
        buildConfigField("GRADLE_VERSION", libs.versions.gradleToolingApi.get())
        buildConfigField("AGP_9_VERSION", libs.versions.agp9.get())
        buildConfigField("ANDROIDX_COMPOSE_VERSION", libs.versions.androidxCompose.get())
        buildConfigField("ANDROID_COMPILE_SDK", "35")
        buildConfigField("ANDROID_MIN_SDK", "24")
        buildConfigField("ANDROID_TARGET_SDK", "35")
        buildConfigField("ANDROIDX_ACTIVITY_COMPOSE_VERSION", libs.versions.androidxActivityCompose.get())
        buildConfigField("TREE_SITTER_LANGUAGE_PACK_VERSION", libs.versions.treeSitterLanguagePack.get())
    }
    packageName("dev.rnett.gradle.mcp")
    buildConfigField("GRADLE_VERSION", libs.versions.gradleToolingApi.get())
    buildConfigField("TREE_SITTER_LANGUAGE_PACK_VERSION", libs.versions.treeSitterLanguagePack.get())
    buildConfigField("APP_VERSION", provider { "${project.version}" })

    // need to manage manually
    buildConfigField("REPL_WORKER_JAR", "repl-worker.jar")
    buildConfigField("BUNDLED_JARS", "repl-worker.jar")
    buildConfigField("ALL_INIT_SCRIPTS", provider {
        layout.projectDirectory.dir("src/main/resources/init-scripts")
            .asFile.listFiles().map { it.name }
            .joinToString("|")
    })
// Ensure build is re-run if init scripts, skills, or bundled jars change
    project.tasks.matching { it.name == "generateBuildConfig" }.configureEach {
        inputs.dir("src/main/resources")
        inputs.dir("src/main/skills")
    }
}

tasks.processResources {
    from(project(":repl-worker").tasks.named("shadowJar")) {
        rename { "repl-worker.jar" }
    }
}

tasks.shadowJar {
    archiveClassifier = ""
    duplicatesStrategy = DuplicatesStrategy.INCLUDE
    mergeServiceFiles()
    manifest {
        attributes("Add-Modules" to "jdk.incubator.vector")
    }
}

tasks.jar {
    archiveClassifier = "single"
    manifest {
        attributes("Add-Modules" to "jdk.incubator.vector")
    }
}

mavenPublishing {
    publishToMavenCentral(automaticRelease = true)
    coordinates(group.toString(), project.name, project.version.toString())

    signAllPublications()

    pom {
        name.set("Gradle MCP")
        description.set("A MCP server for Gradle.")
        inceptionYear.set("2025")
        url.set("https://gradle-mcp.rnett.dev/")
        this.licenses {
            license {
                name.set("The Apache License, Version 2.0")
                url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
                distribution.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
            }
        }
        developers {
            developer {
                id.set("rnett")
                name.set("Ryan Nett")
                url.set("https://github.com/rnett/")
            }
        }
        scm {
            url.set("https://github.com/rnett/gradle-mcp/")
            connection.set("scm:git:git://github.com/rnett/gradle-mcp.git")
            developerConnection.set("scm:git:ssh://git@github.com/rnett/gradle-mcp.git")
        }
    }
}
java {
    withSourcesJar()
    withJavadocJar()
}

publishing {
    publications {
        register<MavenPublication>("shadow") {
            from(components["shadow"])
            artifact(tasks.kotlinSourcesJar)
            artifact(tasks.named("javadocJar"))
        }
    }
}

//allprojects {
//    @OptIn(ExperimentalKotlinGradlePluginApi::class)
//    plugins.withType<org.jetbrains.kotlin.powerassert.gradle.PowerAssertGradlePlugin> {
//        configure<PowerAssertGradleExtension> {
//            functions = listOf(
//                "kotlin.assert",
//                // kotlin.test
//                "kotlin.test.assertTrue",
//                "kotlin.test.assertEquals",
//                "kotlin.test.assertNotEquals",
//                "kotlin.test.assertNull",
//                "kotlin.test.assertNotNull",
//                "kotlin.test.assertFails",
//                "kotlin.test.assertFailsWith",
//                "kotlin.test.assertContains",
//                "kotlin.test.assertContentEquals",
//                "kotlin.test.assertIs",
//                "kotlin.test.assertIsNot",
//                "kotlin.test.assertSame",
//                "kotlin.test.assertNotSame",
//            )
//        }
//    }
//}
