/*
 * Copyright 2003-2023 The IdeaVim authors
 *
 * Use of this source code is governed by an MIT-style
 * license that can be found in the LICENSE.txt file or at
 * https://opensource.org/licenses/MIT.
 */

import dev.feedforward.markdownto.DownParser
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.addJsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.RepositoryBuilder
import org.intellij.markdown.ast.getTextInNode
import org.jetbrains.changelog.Changelog
import org.jetbrains.changelog.exceptions.MissingVersionException
import org.kohsuke.github.GHUser
import java.net.HttpURLConnection
import java.net.URL

buildscript {
    repositories {
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }

    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21")
        classpath("com.github.AlexPl292:mark-down-to-slack:1.1.2")
        classpath("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")

        // This is needed for jgit to connect to ssh
        classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:6.7.0.202309050840-r")
        classpath("org.kohsuke:github-api:1.305")

        classpath("io.ktor:ktor-client-core:2.3.4")
        classpath("io.ktor:ktor-client-cio:2.3.4")
        classpath("io.ktor:ktor-client-auth:2.3.4")
        classpath("io.ktor:ktor-client-content-negotiation:2.3.4")
        classpath("io.ktor:ktor-serialization-kotlinx-json:2.3.4")

        // This comes from the changelog plugin
//        classpath("org.jetbrains:markdown:0.3.1")
    }
}

plugins {
    antlr
    java
    kotlin("jvm") version "1.8.21"
    application

    id("org.jetbrains.intellij") version "1.15.0"
    id("org.jetbrains.changelog") version "2.2.0"

    // ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle
//    id("org.jlleitschuh.gradle.ktlint") version "11.3.1"

    id("org.jetbrains.kotlinx.kover") version "0.6.1"
    id("com.dorongold.task-tree") version "2.1.1"

    id("com.google.devtools.ksp") version "1.8.21-1.0.11"
}

ksp {
  arg("generated_directory", "$projectDir/src/main/resources")
  arg("vimscript_functions_file", "intellij_vimscript_functions.json")
  arg("ex_commands_file", "intellij_ex_commands.json")
}

afterEvaluate {
//  tasks.named("kspKotlin").configure { dependsOn("clean") }
  tasks.named("kspKotlin").configure { dependsOn("generateGrammarSource") }
  tasks.named("kspTestKotlin").configure { enabled = false }
}

// Import variables from gradle.properties file
val javaVersion: String by project
val kotlinVersion: String by project
val ideaVersion: String by project
val downloadIdeaSources: String by project
val instrumentPluginCode: String by project
val remoteRobotVersion: String by project
val antlrVersion: String by project

val publishChannels: String by project
val publishToken: String by project

val slackUrl: String by project
val youtrackToken: String by project

repositories {
    mavenCentral()
    maven { url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies") }
}

dependencies {
    compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
    compileOnly("org.jetbrains:annotations:24.0.1")

    // https://mvnrepository.com/artifact/com.ensarsarajcic.neovim.java/neovim-api
    testImplementation("com.ensarsarajcic.neovim.java:neovim-api:0.2.3")
    testImplementation("com.ensarsarajcic.neovim.java:core-rpc:0.2.3")

    // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-test
    testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")

    // https://mvnrepository.com/artifact/org.mockito.kotlin/mockito-kotlin
    testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")

    testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion")
    testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion")
    testImplementation("com.automation-remarks:video-recorder-junit:2.0")
    runtimeOnly("org.antlr:antlr4-runtime:$antlrVersion")
    antlr("org.antlr:antlr4:$antlrVersion")

    api(project(":vim-engine"))

    ksp(project(":annotation-processors"))
    compileOnly(project(":annotation-processors"))

    testApi("com.squareup.okhttp3:okhttp:4.11.0")

    testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
    testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0")
    testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0")
}

configurations {
    runtimeClasspath {
        exclude(group = "org.antlr", module = "antlr4")
    }
}

// --- Compilation
// This can be moved to other test registration when issue with tests in gradle will be fixed
tasks.register<Test>("testWithNeovim") {
    group = "verification"
    systemProperty("ideavim.nvim.test", "true")
    exclude("/ui/**")
    exclude("**/longrunning/**")
    exclude("**/propertybased/**")
    useJUnitPlatform()
}

tasks.register<Test>("testPropertyBased") {
    group = "verification"
//    include("**/propertybased/**")
    useJUnitPlatform()
}

tasks.register<Test>("testLongRunning") {
    group = "verification"
//    include("**/longrunning/**")
    useJUnitPlatform()
}

tasks {
    // Issue in gradle 7.3
    val test by getting(Test::class) {
        isScanForTestClasses = false
        // Only run tests from classes that end with "Test"
        include("**/*Test.class")
        include("**/*test.class")
        include("**/*Tests.class")
        exclude("**/ParserTest.class")
    }

    val testWithNeovim by getting(Test::class) {
        isScanForTestClasses = false
        // Only run tests from classes that end with "Test"
        include("**/*Test.class")
        include("**/*test.class")
        include("**/*Tests.class")
        exclude("**/ParserTest.class")
        exclude("**/longrunning/**")
        exclude("**/propertybased/**")
    }

    val testPropertyBased by getting(Test::class) {
        isScanForTestClasses = false
        // Only run tests from classes that end with "Test"
        include("**/propertybased/*Test.class")
        include("**/propertybased/*test.class")
        include("**/propertybased/*Tests.class")
    }

    val testLongRunning by getting(Test::class) {
        isScanForTestClasses = false
        // Only run tests from classes that end with "Test"
        include("**/longrunning/**/*Test.class")
        include("**/longrunning/**/*test.class")
        include("**/longrunning/**/*Tests.class")
        exclude("**/longrunning/**/ParserTest.class")
    }

    compileJava {
        sourceCompatibility = javaVersion
        targetCompatibility = javaVersion

        options.encoding = "UTF-8"
    }

    compileKotlin {
        kotlinOptions {
            jvmTarget = javaVersion
            apiVersion = "1.6"
            freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
//            allWarningsAsErrors = true
        }
    }
    compileTestKotlin {
        kotlinOptions {
            jvmTarget = javaVersion
            apiVersion = "1.6"
//            allWarningsAsErrors = true
        }
    }
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(javaVersion))
    }
}

kotlin {
    explicitApi()
    jvmToolchain {
        languageVersion.set(JavaLanguageVersion.of(javaVersion))
    }
}

gradle.projectsEvaluated {
    tasks.compileJava {
//        options.compilerArgs.add("-Werror")
        options.compilerArgs.add("-Xlint:deprecation")
    }
}

// --- Intellij plugin

intellij {
    version.set(ideaVersion)
    pluginName.set("IdeaVim")

    updateSinceUntilBuild.set(false)

    downloadSources.set(downloadIdeaSources.toBoolean())
    instrumentCode.set(instrumentPluginCode.toBoolean())
    intellijRepository.set("https://www.jetbrains.com/intellij-repository")
    // Yaml is only used for testing. It's part of the IdeaIC distribution, but needs to be included as a reference
    plugins.set(listOf("java", "AceJump:3.8.11", "yaml"/*, "Pythonid:231.8109.2", "com.intellij.clion-swift:231.8109.4"*/))
}

tasks {
    downloadRobotServerPlugin {
        version.set(remoteRobotVersion)
    }

    publishPlugin {
        channels.set(publishChannels.split(","))
        token.set(publishToken)
    }

    signPlugin {
        certificateChain.set(providers.environmentVariable("CERTIFICATE_CHAIN"))
        privateKey.set(providers.environmentVariable("PRIVATE_KEY"))
        password.set(providers.environmentVariable("PRIVATE_KEY_PASSWORD"))
    }

    runIdeForUiTests {
        systemProperty("robot-server.port", "8082")
        systemProperty("ide.mac.message.dialogs.as.sheets", "false")
        systemProperty("jb.privacy.policy.text", "<!--999.999-->")
        systemProperty("jb.consents.confirmation.enabled", "false")
    }

    runPluginVerifier {
        downloadDir.set("${project.buildDir}/pluginVerifier/ides")
        teamCityOutputFormat.set(true)
//        ideVersions.set(listOf("IC-2021.3.4"))
    }

    generateGrammarSource {
        maxHeapSize = "128m"
        arguments.addAll(listOf("-package", "com.maddyhome.idea.vim.vimscript.parser.generated", "-visitor"))
        outputDirectory = file("src/main/java/com/maddyhome/idea/vim/vimscript/parser/generated")
    }

    named("compileKotlin") {
        dependsOn("generateGrammarSource")
    }
    named("compileTestKotlin") {
        dependsOn("generateTestGrammarSource")
    }

    // Add plugin open API sources to the plugin ZIP
    val createOpenApiSourceJar by registering(Jar::class) {
        dependsOn("generateGrammarSource")
        // Java sources
        from(sourceSets.main.get().java) {
            include("**/com/maddyhome/idea/vim/**/*.java")
        }
        from(project(":vim-engine").sourceSets.main.get().java) {
            include("**/com/maddyhome/idea/vim/**/*.java")
        }
        // Kotlin sources
        from(kotlin.sourceSets.main.get().kotlin) {
            include("**/com/maddyhome/idea/vim/**/*.kt")
        }
        from(project(":vim-engine").kotlin.sourceSets.main.get().kotlin) {
            include("**/com/maddyhome/idea/vim/**/*.kt")
        }
        destinationDirectory.set(layout.buildDirectory.dir("libs"))
        archiveClassifier.set("src")
    }

    buildPlugin {
        dependsOn(createOpenApiSourceJar)
        from(createOpenApiSourceJar) { into("lib/src") }
    }

    val pluginVersion = version
    // Don't forget to update plugin.xml
    patchPluginXml {
        sinceBuild.set("231.7515.13")

        // Get the latest available change notes from the changelog file
        changeNotes.set(
            provider {
                with(changelog) {
                    val log = try {
                        getUnreleased()
                    } catch (e: MissingVersionException) {
                        getOrNull(pluginVersion.toString()) ?: getLatest()
                    }
                    renderItem(
                        log,
                        org.jetbrains.changelog.Changelog.OutputType.HTML,
                    )
                }
            },
        )
    }
}

// --- Linting

//ktlint {
//    version.set("0.48.2")
//}

// --- Tests

tasks {
    test {
        useJUnitPlatform()
        exclude("**/propertybased/**")
        exclude("**/longrunning/**")
        exclude("/ui/**")
    }
}

tasks.register<Test>("testUi") {
    group = "verification"
    useJUnitPlatform()
    include("/ui/**")
}

// --- Changelog

changelog {
    groups.set(listOf("Features:", "Changes:", "Deprecations:", "Fixes:", "Merged PRs:"))
    itemPrefix.set("*")
    path.set("${project.projectDir}/CHANGES.md")
    unreleasedTerm.set("To Be Released")
    headerParserRegex.set("(\\d\\.\\d+(.\\d+)?)".toRegex())
//    header = { "${project.version}" }
//    version = "0.60"
}

// --- Kover

koverMerged {
    enable()
}

kover {
    instrumentation {
        // set of test tasks names to exclude from instrumentation. The results of their execution will not be presented in the report
        excludeTasks += "testPropertyBased"
        excludeTasks += "testLongRunning"
        excludeTasks += "testWithNeovim"
        excludeTasks += "testUi"
    }
}

// --- Slack notification

tasks.register("slackNotification") {
    doLast {
        if (slackUrl.isBlank()) {
            println("Slack Url is not defined")
            return@doLast
        }

        val changeLog = changelog.renderItem(changelog.getLatest(), Changelog.OutputType.PLAIN_TEXT)
        val slackDown = DownParser(changeLog, true).toSlack().toString()

        //language=JSON
        val message = """
            {
                "text": "New version of IdeaVim",
                "blocks": [
                    {
                        "type": "section",
                        "text": {
                            "type": "mrkdwn",
                            "text": "IdeaVim $version has been released\n$slackDown"
                        }
                    }
                ]
            }
        """.trimIndent()

        println("Parsed data: $slackDown")
        val post = URL(slackUrl)
        with(post.openConnection() as HttpURLConnection) {
            requestMethod = "POST"
            doOutput = true
            setRequestProperty("Content-Type", "application/json")

            outputStream.write(message.toByteArray())

            val postRc = responseCode
            println("Response code: $postRc")
            if (postRc == 200) {
                println(inputStream.bufferedReader().use { it.readText() })
            } else {
                println(errorStream.bufferedReader().use { it.readText() })
            }
        }
    }
}

// Uncomment to enable FUS testing mode
// tasks {
//    withType<org.jetbrains.intellij.tasks.RunIdeTask> {
//        jvmArgs("-Didea.is.internal=true")
//        jvmArgs("-Dfus.internal.test.mode=true")
//    }
// }

// --- Update authors
tasks.register("updateAuthors") {
    doLast {
        val uncheckedEmails = setOf(
            "aleksei.plate@jetbrains.com",
            "aleksei.plate@teamcity",
            "aleksei.plate@TeamCity",
            "alex.plate@192.168.0.109",
            "nikita.koshcheev@TeamCity",
            "TeamCity@TeamCity",
        )
        updateAuthors(uncheckedEmails)
    }
}

val prId: String by project

tasks.register("updateMergedPr") {
    doLast {
        if (project.hasProperty("prId")) {
            println("Got pr id: $prId")
            updateMergedPr(prId.toInt())
        } else {
            error("Cannot get prId")
        }
    }
}

tasks.register("updateChangelog") {
    doLast {
        updateChangelog()
    }
}

tasks.register("updateYoutrackOnCommit") {
    doLast {
        updateYoutrackOnCommit()
    }
}

val vimProjectId = "22-43"
val fixVersionsFieldId = "123-285"
val fixVersionsFieldType = "VersionProjectCustomField"
val fixVersionsElementType = "VersionBundleElement"

tasks.register("releaseActions") {
    group = "other"
    doLast {
        val tickets = getYoutrackTicketsByQuery("%23%7BReady+To+Release%7D")
        if (tickets.isNotEmpty()) {
            println("Updating statuses for tickets: $tickets")
            setYoutrackStatus(tickets, "Fixed")
            if (getVersionIdByName(version.toString()) != null) {
                addReleaseToYoutrack(version.toString())
            } else {
                println("Version $version is already exists in YouTrack")
            }
            setYoutrackFixVersion(tickets, version.toString())
        } else {
            println("No tickets to update statuses")
        }
    }
}

tasks.register("integrationsTest") {
    group = "other"
    doLast {
        val testTicketId = "VIM-2784"

        // YouTrack set to Ready To Release on Fix commit
        setYoutrackStatus(listOf(testTicketId), "Ready To Release")
        if ("Ready To Release" != getYoutrackStatus(testTicketId)) {
            error("Ticket status was not updated")
        }
        setYoutrackStatus(listOf(testTicketId), "Open")

        // Check YouTrack requests
        val prevStatus = getYoutrackStatus(testTicketId)
        setYoutrackStatus(listOf(testTicketId), "Ready To Release")
        val tickets = getYoutrackTicketsByQuery("%23%7BReady+To+Release%7D")
        if (testTicketId !in tickets) {
            error("Test ticket is not found in request")
        }
        setYoutrackStatus(listOf(testTicketId), prevStatus)

        // Check adding and removing release
        val existingVersionId = getVersionIdByName("TEST_VERSION")
        if (existingVersionId != null) {
            deleteVersionById(existingVersionId)
        }
        val versionId = addReleaseToYoutrack("TEST_VERSION")
        guard(getVersionIdByName("TEST_VERSION") != null) { "Test version isn't created" }
        setYoutrackStatus(listOf(testTicketId), "Fixed")
        setYoutrackFixVersion(listOf(testTicketId), "TEST_VERSION")
        deleteVersionById(versionId)
        setYoutrackStatus(listOf(testTicketId), "Open")
        guard(getVersionIdByName("TEST_VERSION") == null) { "Test version isn't deleted" }

        updateMergedPr(525)
        // TODO: test Ticket parsing
        // TODO: test Update CHANGES
        // TODO: test Update AUTHORS
        // TODO: test Slack notification
        // TODO: Add a comment on EAP release
    }
}

fun guard(check: Boolean, ifWrong: () -> String) {
    if (!check) {
        error(ifWrong())
    }
}

tasks.register("testUpdateChangelog") {
    group = "verification"
    description = "This is a task to manually assert the correctness of the update tasks"
    doLast {
        val changesFile = File("$projectDir/CHANGES.md")
        val changes = changesFile.readText()

        val changesBuilder = StringBuilder(changes)
        val insertOffset = setupSection(changes, changesBuilder, "### Changes:")

        changesBuilder.insert(insertOffset, "--Hello--\n")

        changesFile.writeText(changesBuilder.toString())
    }
}

fun addReleaseToYoutrack(name: String): String {
    val client = httpClient()
    println("Creating new release version in YouTrack: $name")

    return runBlocking {
        val response = client.post("https://youtrack.jetbrains.com/api/admin/projects/$vimProjectId/customFields/$fixVersionsFieldId/bundle/values?fields=id,name") {
            contentType(ContentType.Application.Json)
            accept(ContentType.Application.Json)
            val request = buildJsonObject {
                put("name", name)
                put("\$type", fixVersionsElementType)
            }
            setBody(request)
        }
        response.body<JsonObject>().getValue("id").jsonPrimitive.content
    }
}

fun getVersionIdByName(name: String): String? {
    val client = httpClient()

    return runBlocking {
        val response = client.get("https://youtrack.jetbrains.com/api/admin/projects/$vimProjectId/customFields/$fixVersionsFieldId/bundle/values?fields=id,name&query=$name")
        response.body<JsonArray>().singleOrNull()?.jsonObject?.get("id")?.jsonPrimitive?.content
    }
}

fun deleteVersionById(id: String) {
    val client = httpClient()

    runBlocking {
        client.delete("https://youtrack.jetbrains.com/api/admin/projects/$vimProjectId/customFields/$fixVersionsFieldId/bundle/values/$id")
    }
}

fun updateYoutrackOnCommit() {
    println("Start updating youtrack")
    println(projectDir)

    val newFixes = changes()
    val newTickets = newFixes.map { it.id }
    println("Set new status for $newTickets")
    setYoutrackStatus(newTickets, "Ready To Release")
}

fun getYoutrackTicketsByQuery(query: String): Set<String> {
    val client = httpClient()

    return runBlocking {
        val response = client.get("https://youtrack.jetbrains.com/api/issues/?fields=idReadable&query=project:VIM+$query")
        response.body<JsonArray>().mapTo(HashSet()) { it.jsonObject.getValue("idReadable").jsonPrimitive.content }
    }
}

fun setYoutrackStatus(tickets: Collection<String>, status: String) {
    val client = httpClient()

    runBlocking {
        for (ticket in tickets) {
            println("Try to set $ticket to $status")
            val response = client.post("https://youtrack.jetbrains.com/api/issues/$ticket?fields=customFields(id,name,value(id,name))") {
                contentType(ContentType.Application.Json)
                accept(ContentType.Application.Json)
                val request = buildJsonObject {
                    putJsonArray("customFields") {
                        addJsonObject {
                            put("name", "State")
                            put("\$type", "SingleEnumIssueCustomField")
                            putJsonObject("value") {
                                put("name", status)
                            }
                        }
                    }
                }
                setBody(request)
            }
            println(response)
            println(response.body<String>())
            if (!response.status.isSuccess()) {
                error("Request failed. $ticket, ${response.body<String>()}")
            }
            val finalState = response.body<JsonObject>()["customFields"]!!.jsonArray
                .single { it.jsonObject["name"]!!.jsonPrimitive.content == "State" }
                .jsonObject["value"]!!
                .jsonObject["name"]!!
                .jsonPrimitive.content
            if (finalState != status) {
                error("Ticket $ticket is not updated! Expected status $status, but actually $finalState")
            }
        }
    }
}

fun setYoutrackFixVersion(tickets: Collection<String>, version: String) {
    val client = httpClient()

    runBlocking {
        for (ticket in tickets) {
            println("Try to set fix version $version for $ticket")
            val response = client.post("https://youtrack.jetbrains.com/api/issues/$ticket?fields=customFields(id,name,value(id,name))") {
                contentType(ContentType.Application.Json)
                accept(ContentType.Application.Json)
                val request = buildJsonObject {
                    putJsonArray("customFields") {
                        addJsonObject {
                            put("name", "Fix versions")
                            put("\$type", "MultiVersionIssueCustomField")
                            putJsonArray("value") {
                                addJsonObject { put("name", version) }
                            }
                        }
                    }
                }
                setBody(request)
            }
            println(response)
            println(response.body<String>())
            if (!response.status.isSuccess()) {
                error("Request failed. $ticket, ${response.body<String>()}")
            }
            val finalState = response.body<JsonObject>()["customFields"]!!.jsonArray
                .single { it.jsonObject["name"]!!.jsonPrimitive.content == "Fix versions" }
                .jsonObject["value"]!!
                .jsonArray[0]
                .jsonObject["name"]!!
                .jsonPrimitive.content
            if (finalState != version) {
                error("Ticket $ticket is not updated! Expected fix version $version, but actually $finalState")
            }
        }
    }
}

fun getYoutrackStatus(ticket: String): String {
    val client = httpClient()

    return runBlocking {
        val response = client.get("https://youtrack.jetbrains.com/api/issues/$ticket/customFields/123-129?fields=value(name)")
        response.body<JsonObject>()["value"]!!.jsonObject.getValue("name").jsonPrimitive.content
    }
}

fun updateChangelog() {
    println("Start update authors")
    println(projectDir)
    val newFixes = changes()

    // Update changes file
    val changesFile = File("$projectDir/CHANGES.md")
    val changes = changesFile.readText()

    val changesBuilder = StringBuilder(changes)
    val insertOffset = setupSection(changes, changesBuilder, "### Fixes:")

    if (insertOffset < 50) error("Incorrect offset: $insertOffset")

    val firstPartOfChanges = changes.take(insertOffset)
    val actualFixes = newFixes
        .filterNot { it.id in firstPartOfChanges }
    val newUpdates = actualFixes
        .joinToString("") { "* [${it.id}](https://youtrack.jetbrains.com/issue/${it.id}) ${it.text}\n" }

    changesBuilder.insert(insertOffset, newUpdates)
    if (actualFixes.isNotEmpty()) {
        changesFile.writeText(changesBuilder.toString())
    }
}

fun updateAuthors(uncheckedEmails: Set<String>) {
    println("Start update authors")
    println(projectDir)
    val repository = RepositoryBuilder().setGitDir(File("$projectDir/.git")).build()
    val git = Git(repository)
    val lastSuccessfulCommit = System.getenv("SUCCESS_COMMIT")!!
    val hashesAndEmailes = git.log().call()
        .takeWhile {
            !it.id.name.equals(lastSuccessfulCommit, ignoreCase = true)
        }
        .associate { it.authorIdent.emailAddress to it.name }

    println("Last successful commit: $lastSuccessfulCommit")
    println("Amount of commits: ${hashesAndEmailes.size}")
    println("Emails: ${hashesAndEmailes.keys}")
    val gitHub = org.kohsuke.github.GitHub.connect()
    val ghRepository = gitHub.getRepository("JetBrains/ideavim")
    val users = mutableSetOf<Author>()
    println("Start emails processing")
    for ((email, hash) in hashesAndEmailes) {
        println("Processing '$email'...")
        if (email in uncheckedEmails) {
            println("Email '$email' is in unchecked emails. Skip it")
            continue
        }
        if ("dependabot[bot]@users.noreply.github.com" in email) {
            println("Email '$email' is from dependabot. Skip it")
            continue
        }
        if ("tcuser" in email) {
            println("Email '$email' is from teamcity. Skip it")
            continue
        }
        val user: GHUser? = ghRepository.getCommit(hash).author
        if (user == null) {
            println("Cant get the commit author. Email: $email. Commit: $hash")
            continue
        }
        val htmlUrl = user.htmlUrl.toString()
        val name = user.name ?: user.login
        users.add(Author(name, htmlUrl, email))
    }

    println("Emails processed")
    val authorsFile = File("$projectDir/AUTHORS.md")
    val authors = authorsFile.readText()
    val parser =
        org.intellij.markdown.parser.MarkdownParser(org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor())
    val tree = parser.buildMarkdownTreeFromString(authors)

    val contributorsSection = tree.children[24]
    val existingEmails = mutableSetOf<String>()
    for (child in contributorsSection.children) {
        if (child.children.size > 1) {
            existingEmails.add(
                child.children[1].children[0].children[2].children[2].getTextInNode(authors).toString(),
            )
        }
    }

    val newAuthors = users.filterNot { it.mail in existingEmails }
    if (newAuthors.isEmpty()) return

    val authorNames = newAuthors.joinToString(", ") { it.name }
    println("::set-output name=authors::$authorNames")

    val insertionString = newAuthors.toMdString()
    val resultingString = StringBuffer(authors).insert(contributorsSection.endOffset, insertionString).toString()

    authorsFile.writeText(resultingString)
}

fun List<Author>.toMdString(): String {
    return this.joinToString {
        """
          |
          |* [![icon][mail]](mailto:${it.mail})
          |  [![icon][github]](${it.url})
          |  &nbsp;
          |  ${it.name}
        """.trimMargin()
    }
}

data class Author(val name: String, val url: String, val mail: String)
data class Change(val id: String, val text: String)

fun updateMergedPr(number: Int) {
    val token = System.getenv("GITHUB_OAUTH")
    println("Token size: ${token.length}")
    val gitHub = org.kohsuke.github.GitHubBuilder().withOAuthToken(token).build()
    println("Connecting to the repo...")
    val repository = gitHub.getRepository("JetBrains/ideavim")
    println("Getting pull requests...")
    val pullRequest = repository.getPullRequest(number)
    if (pullRequest.user.login == "dependabot[bot]") return

    val changesFile = File("$projectDir/CHANGES.md")
    val changes = changesFile.readText()

    val changesBuilder = StringBuilder(changes)
    val insertOffset = setupSection(changes, changesBuilder, "### Merged PRs:")

    if (insertOffset < 50) error("Incorrect offset: $insertOffset")
    if (pullRequest.user.login == "dependabot[bot]") return

    val prNumber = pullRequest.number
    val userName = pullRequest.user.name ?: pullRequest.user.login
    val login = pullRequest.user.login
    val title = pullRequest.title
    val section =
        "* [$prNumber](https://github.com/JetBrains/ideavim/pull/$prNumber) by [$userName](https://github.com/$login): $title\n"
    changesBuilder.insert(insertOffset, section)

    changesFile.writeText(changesBuilder.toString())
}

fun setupSection(
    changes: String,
    authorsBuilder: StringBuilder,
    sectionName: String,
): Int {
    val parser =
        org.intellij.markdown.parser.MarkdownParser(org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor())
    val tree = parser.buildMarkdownTreeFromString(changes)

    var idx = -1
    for (index in tree.children.indices) {
        if (tree.children[index].getTextInNode(changes).startsWith("## ")) {
            idx = index
            break
        }
    }

    val hasToBeReleased = tree.children[idx].getTextInNode(changes).contains("To Be Released")
    return if (hasToBeReleased) {
        var mrgIdx = -1
        for (index in (idx + 1) until tree.children.lastIndex) {
            val textInNode = tree.children[index].getTextInNode(changes)
            val foundIndex = textInNode.startsWith(sectionName)
            if (foundIndex) {
                var filledPr = index + 2
                while (tree.children[filledPr].getTextInNode(changes).startsWith("*")) {
                    filledPr++
                }
                mrgIdx = tree.children[filledPr].startOffset + 1
                break
            } else {
                val currentSectionIndex = sections.indexOf(sectionName)
                val insertHere = textInNode.startsWith("## ") ||
                    textInNode.startsWith("### ") &&
                    sections.indexOfFirst { textInNode.startsWith(it) }
                        .let { if (it < 0) false else it > currentSectionIndex }
                if (insertHere) {
                    val section = """
                        $sectionName
                        
                        
                    """.trimIndent()
                    authorsBuilder.insert(tree.children[index].startOffset, section)
                    mrgIdx = tree.children[index].startOffset + (section.length - 1)
                    break
                }
            }
        }
        mrgIdx
    } else {
        val section = """
            ## To Be Released
            
            $sectionName
            
            
        """.trimIndent()
        authorsBuilder.insert(tree.children[idx].startOffset, section)
        tree.children[idx].startOffset + (section.length - 1)
    }
}

val sections = listOf(
    "### Features:",
    "### Changes:",
    "### Fixes:",
    "### Merged PRs:",
)

fun changes(): List<Change> {
    val repository = RepositoryBuilder().setGitDir(File("$projectDir/.git")).build()
    val git = Git(repository)
    val lastSuccessfulCommit = System.getenv("SUCCESS_COMMIT")!!
    val messages = git.log().call()
        .takeWhile {
            !it.id.name.equals(lastSuccessfulCommit, ignoreCase = true)
        }
        .map { it.shortMessage }

    // Collect fixes
    val newFixes = mutableListOf<Change>()
    println("Last successful commit: $lastSuccessfulCommit")
    println("Amount of commits: ${messages.size}")
    println("Start changes processing")
    for (message in messages) {
        println("Processing '$message'...")
        val lowercaseMessage = message.toLowerCase()
        val regex = "^fix\\((vim-\\d+)\\):".toRegex()
        val findResult = regex.find(lowercaseMessage)
        if (findResult != null) {
            println("Message matches")
            val value = findResult.groups[1]!!.value.toUpperCase()
            val shortMessage = message.drop(findResult.range.last + 1).trim()
            newFixes += Change(value, shortMessage)
        } else {
            println("Message doesn't match")
        }
    }
    return newFixes
}

fun httpClient(): HttpClient {
    return HttpClient(CIO) {
        expectSuccess = true
        install(Auth) {
            bearer {
                loadTokens {
                    val accessToken = youtrackToken.ifBlank { System.getenv("YOUTRACK_TOKEN")!! }
                    BearerTokens(accessToken, "")
                }
            }
        }
        install(ContentNegotiation) {
            json(
                Json {
                    prettyPrint = true
                    isLenient = true
                },
            )
        }
    }
}