plugins {
    id "java"
    id "jacoco"
    id "io.spring.dependency-management" version "1.1.7"
    id "org.springframework.boot" version "3.5.7"
    id "org.springdoc.openapi-gradle-plugin" version "1.9.0"
    id "io.swagger.swaggerhub" version "1.3.2"
    id "com.diffplug.spotless" version "8.1.0"
    id "com.github.jk1.dependency-license-report" version "3.0.1"
    //id "nebula.lint" version "19.0.3"
    id "org.sonarqube" version "7.2.2.6593"
}

import com.github.jk1.license.render.*
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.xml.XmlSlurper
import org.gradle.api.tasks.testing.Test

ext {
    springBootVersion = "3.5.7"
    pdfboxVersion = "3.0.6"
    imageioVersion = "3.13.0"
    lombokVersion = "1.18.42"
    bouncycastleVersion = "1.83"
    springSecuritySamlVersion = "6.5.6"
    openSamlVersion = "4.3.2"
    commonmarkVersion = "0.27.0"
    googleJavaFormatVersion = "1.28.0"
    logback = "1.5.23"
    junitPlatformVersion = "1.12.2"
}

ext.isSecurityDisabled = { ->
    System.getenv('DOCKER_ENABLE_SECURITY') == 'false' ||
    System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true' ||
    (project.hasProperty('DISABLE_ADDITIONAL_FEATURES') &&
     System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')
}

jar {
    enabled = false
    manifest {
        attributes "Implementation-Title": "Stirling-PDF",
            "Implementation-Version": project.version
    }
}

bootJar {
    enabled = false
}

// Configure main class for the root project
springBoot {
    mainClass = 'stirling.software.SPDF.SPDFApplication'
}

repositories {
    mavenCentral()
    maven { url = 'https://build.shibboleth.net/maven/releases' }
}

allprojects {
    group = 'stirling.software'
    version = '2.2.1'

    configurations.configureEach {
        exclude group: 'commons-logging', module: 'commons-logging'
        exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
    }
}

def writeIfChanged(File targetFile, String newContent) {
    if (targetFile.getText('UTF-8') != newContent) {
        targetFile.write(newContent, 'UTF-8')
    }
}

def updateTauriConfigVersion(String version) {
    File tauriConfig = file('frontend/src-tauri/tauri.conf.json')
    def parsed = new JsonSlurper().parse(tauriConfig)
    parsed.version = version

    def formatted = JsonOutput.prettyPrint(JsonOutput.toJson(parsed)) + System.lineSeparator()
    writeIfChanged(tauriConfig, formatted)
}

def updateSimulationVersion(File fileToUpdate, String version) {
    def content = fileToUpdate.getText('UTF-8')
    def matcher = content =~ /(appVersion:\s*')([^']*)(')/

    if (!matcher.find()) {
        throw new GradleException("Could not locate appVersion in ${fileToUpdate} for synchronization")
    }

    def updatedContent = matcher.replaceFirst("${matcher.group(1)}${version}${matcher.group(3)}")
    writeIfChanged(fileToUpdate, updatedContent)
}

tasks.register('syncAppVersion') {
    group = 'versioning'
    description = 'Synchronizes app version across desktop and simulation configs.'

    doLast {
        def appVersion = project.version.toString()
        println "Synchronizing application version to ${appVersion}"
        updateTauriConfigVersion(appVersion)

        [
            'frontend/src/core/testing/serverExperienceSimulations.ts',
            'frontend/src/proprietary/testing/serverExperienceSimulations.ts'
        ].each { path ->
            updateSimulationVersion(file(path), appVersion)
        }
    }
}

tasks.register('writeVersion', WriteProperties) {
    destinationFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties')
    println "Writing version.properties to ${destinationFile.get().asFile.path}"
    comment = "${new Date()}"
    property 'version', project.provider { project.version.toString() }
}

subprojects {
    apply plugin: 'java'
    apply plugin: 'java-library'
    apply plugin: 'com.diffplug.spotless'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'jacoco'

    java {
        // 17 is lowest but we support and recommend 21
        sourceCompatibility = JavaVersion.VERSION_17
    }

     if (project.name != "stirling-pdf") {
        bootJar {
            enabled = false
        }
     }

    repositories {
        mavenCentral()
    }

    configurations.configureEach {
        exclude group: 'commons-logging', module: 'commons-logging'
        exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
        // Exclude vulnerable BouncyCastle version used in tableau
        exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
        exclude group: 'org.bouncycastle', module: 'bcutil-jdk15on'
        exclude group: 'org.bouncycastle', module: 'bcmail-jdk15on'
    }

    dependencyManagement {
        imports {
            mavenBom "org.springframework.boot:spring-boot-dependencies:$springBootVersion"
        }
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-actuator'
        implementation 'io.github.pixee:java-security-toolkit:1.2.2'

        //tmp for security bumps
        implementation "ch.qos.logback:logback-core:$logback"
        implementation "ch.qos.logback:logback-classic:$logback"
        compileOnly "org.projectlombok:lombok:$lombokVersion"
        annotationProcessor "org.projectlombok:lombok:$lombokVersion"

        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
        testRuntimeOnly "org.junit.platform:junit-platform-launcher"

        testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.3.2")
        testImplementation "com.squareup.okhttp3:mockwebserver"
    }

    tasks.withType(JavaCompile).configureEach {
        options.encoding = "UTF-8"
        if (!project.hasProperty("noSpotless")) {
            dependsOn "spotlessApply"
        }
    }

    tasks.named("compileJava", JavaCompile).configure {
        options.compilerArgs.add("-parameters")
    }

    def jacocoReport = tasks.named("jacocoTestReport")

    tasks.withType(Test).configureEach {
        useJUnitPlatform()
        finalizedBy(jacocoReport)
    }

    jacocoReport.configure {
        dependsOn(tasks.named("test"))
        reports {
            xml.required.set(true)
            csv.required.set(false)
            html.required.set(true)
        }
        doLast {
            def xmlReport = reports.xml.outputLocation.get().asFile
            if (!xmlReport.exists()) {
                logger.lifecycle("Jacoco coverage report not found at ${xmlReport}")
                return
            }

            def xmlContent = xmlReport.getText("UTF-8")
            xmlContent = xmlContent.replaceFirst('(?s)<!DOCTYPE.*?>', '')
            def report = new XmlSlurper(false, false).parseText(xmlContent)
            def counters = report.counter.collectEntries { counter ->
                def type = counter.@type.text()
                def covered = counter.@covered.text() as BigDecimal
                def missed = counter.@missed.text() as BigDecimal
                [(type): [covered: covered, missed: missed]]
            }

            def thresholds = [
                    LINE       : 0.16,
                    INSTRUCTION: 0.14,
                    BRANCH     : 0.09
            ]

            def types = ["LINE", "INSTRUCTION", "BRANCH"]
            def headers = ["Metric", "Coverage", "Covered/Total", "Status", "Target"]

            def rows = types.collect { String type ->
                def data = counters[type]
                if (!data) {
                    return [type, "—", "—", "No data", ""]
                }

                def total = data.covered + data.missed
                if (total == 0) {
                    return [type, "—", "0/${total.toBigInteger()}", "No executions", ""]
                }

                def ratio = data.covered / total * 100
                def coverageText = String.format(Locale.ROOT, "%.2f%%", ratio)
                def coveredText = String.format(Locale.ROOT, "%d/%d",
                        data.covered.toBigInteger(),
                        total.toBigInteger())

                def threshold = thresholds[type]
                def thresholdPercent = threshold != null ? threshold * 100 : null
                def targetText = thresholdPercent != null ?
                        String.format(Locale.ROOT, ">= %.2f%%", thresholdPercent) : ""
                def passed = thresholdPercent != null ? ratio >= thresholdPercent : null
                def statusText = passed == null ? "" : (passed ? "PASS" : "FAIL")

                return [type, coverageText, coveredText, statusText, targetText]
            }

            def columnIndexes = (0..<headers.size())
            def columnWidths = columnIndexes.collect { idx ->
                Math.max(headers[idx].length(), rows.collect { row ->
                    row[idx] != null ? row[idx].toString().length() : 0
                }.max() ?: 0)
            }

            def formatRow = { List<String> values ->
                columnIndexes.collect { idx ->
                    def value = values[idx] ?: ""
                    value.padRight(columnWidths[idx])
                }.join(" | ")
            }

            def separator = columnIndexes.collect { idx ->
                ''.padRight(columnWidths[idx], '-')
            }.join("-+-")

            logger.lifecycle("")
            logger.lifecycle("==== JaCoCo Coverage Summary ====")
            logger.lifecycle(formatRow(headers))
            logger.lifecycle(separator)
            rows.each { row ->
                logger.lifecycle(formatRow(row))
            }
            logger.lifecycle(separator)

            def htmlReport = reports.html.outputLocation.get().asFile
            logger.lifecycle("Detailed HTML report available at: ${htmlReport}")
            if (rows.any { it[3] == "FAIL" }) {
                logger.lifecycle("Some coverage targets were missed. Please review the detailed report above.")
            } else if (rows.any { it[3] == "PASS" }) {
                logger.lifecycle("Great job! All tracked coverage metrics meet their targets.")
            }
            logger.lifecycle("=================================\n")
        }
    }

    tasks.named("build") {
        dependsOn jacocoReport
    }

    jacocoTestCoverageVerification {
        dependsOn jacocoReport
        violationRules {
            rule {
                enabled = true
                element = 'BUNDLE'
                // Bytecode-Anweisungen abgedeckt
                limit {
                    counter = 'INSTRUCTION'
                    value = 'COVEREDRATIO'
                    minimum = 0.14
                }
                // wie viele Quellcode-Zeilen abgedeckt
                limit {
                    counter = 'LINE'
                    value = 'COVEREDRATIO'
                    minimum = 0.16
                }
                // Verzweigungen (if/else, switch) abgedeckt; misst Logik-Abdeckung
                limit {
                    counter = 'BRANCH'
                    value = 'COVEREDRATIO'
                    minimum = 0.09
                }
            }
        }
    }

    tasks.named("processResources") {
        dependsOn(rootProject.tasks.writeVersion)
    }

    if (name == 'stirling-pdf') {
        apply plugin: 'org.springdoc.openapi-gradle-plugin'

        openApi {
            apiDocsUrl = "http://localhost:8080/v1/api-docs"
            outputDir = file("$projectDir")
            outputFileName = "SwaggerDoc.json"
            waitTimeInSeconds = 60 // Increase the wait time to 60 seconds
        }

        tasks.named("forkedSpringBootRun") {
            dependsOn(":common:jar")
            dependsOn(":proprietary:jar")
        }

        tasks.register("copySwaggerDoc", Copy) {
            doNotTrackState("Writes SwaggerDoc.json to project root")
            from(layout.projectDirectory.file("SwaggerDoc.json"))
            into(rootProject.projectDir)
            dependsOn("generateOpenApiDocs")
        }

        tasks.register("cleanSwaggerInBuild", Delete) {
            doNotTrackState("Cleans up SwaggerDoc.json in build directory")
            delete(layout.projectDirectory.file("SwaggerDoc.json"))
            dependsOn("copySwaggerDoc")
        }

        tasks.named("copySwaggerDoc") {
            finalizedBy("cleanSwaggerInBuild")
        }

        tasks.named("generateOpenApiDocs") {
            finalizedBy("copySwaggerDoc")
            doNotTrackState("OpenAPI plugin writes outside build directory")
        }
    }
}

tasks.withType(JavaCompile).configureEach {
    options.encoding = "UTF-8"
    if (!project.hasProperty("noSpotless")) {
        dependsOn "spotlessApply"
    }
}

gradle.taskGraph.whenReady { graph ->
    if (project.hasProperty("noSpotless")) {
        tasks.matching { it.name.startsWith("spotless") }.configureEach {
            enabled = false
        }
    }
}

def allProjects = ((subprojects as Set<Project>) + project) as Set<Project>

licenseReport {
    projects = allProjects
    renderers = [new JsonReportRenderer()]
    allowedLicensesFile = project.layout.projectDirectory.file("app/allowed-licenses.json").asFile
    outputDir = project.layout.buildDirectory.dir("reports/dependency-license").get().asFile.path
    configurations = [ "productionRuntimeClasspath", "runtimeClasspath" ]
}

// Configure the forked spring boot run task to properly delegate to the stirling-pdf module
tasks.named('forkedSpringBootRun') {
    dependsOn ':stirling-pdf:bootRun'
    doFirst {
        println "Delegating forkedSpringBootRun to :stirling-pdf:bootRun"
    }
}

spotless {
    yaml {
        target '*.yml', '*.yaml'
        trimTrailingWhitespace()
        leadingTabsToSpaces()
        endWithNewline()
    }
    format 'gradle', {
        target 'build.gradle', 'settings.gradle', 'gradle/*.gradle', 'gradle/**/*.gradle'
        trimTrailingWhitespace()
        leadingTabsToSpaces()
        endWithNewline()
    }
}

sonar {
    properties {
        property "sonar.projectKey", "Stirling-Tools_Stirling-PDF"
        property "sonar.organization", "stirling-tools"

        property "sonar.exclusions", "**/build-wrapper-dump.json, **/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
        property "sonar.coverage.exclusions", "**/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
        property "sonar.cpd.exclusions", "**/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
    }
}

swaggerhubUpload {
    // dependsOn = generateOpenApiDocs  // Depends on your task generating Swagger docs
    api = "Stirling-PDF"  // The name of your API on SwaggerHub
    owner = "${System.getenv().getOrDefault('SWAGGERHUB_USER', 'Frooodle')}"  // Your SwaggerHub username (or organization name)
    version = project.version  // The version of your API
    inputFile = file("SwaggerDoc.json")  // The path to your Swagger docs
    token = "${System.getenv("SWAGGERHUB_API_KEY")}"  // Your SwaggerHub API key, passed as an environment variable
    oas = "3.0.0"  // The version of the OpenAPI Specification you"re using
}

dependencies {
    implementation project(':stirling-pdf')
    implementation project(':common')
    if (rootProject.ext.isSecurityDisabled()) {
        implementation project(':proprietary')
    }

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"

    testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.3.2")
    testImplementation "com.squareup.okhttp3:mockwebserver"
}

tasks.named("test") {
    useJUnitPlatform()
}

// Make sure all relevant processes depend on writeVersion
processResources.dependsOn(writeVersion)

tasks.register('printVersion') {
    doLast {
        println project.version
    }
}

tasks.named('bootRun') {
    group = 'application'
    description = 'Delegates to :stirling-pdf:bootRun'
    dependsOn ':stirling-pdf:bootRun'

    doFirst {
        println "Delegating to :stirling-pdf:bootRun"
    }
}

tasks.named('build') {
    group = 'build'
    description = 'Delegates to :stirling-pdf:bootJar'
    dependsOn ':stirling-pdf:bootJar', 'buildRestartHelper', 'syncAppVersion'

    doFirst {
        println "Delegating to :stirling-pdf:bootJar"
    }
}

// Task to compile RestartHelper.java
tasks.register('compileRestartHelper', JavaCompile) {
    group = 'build'
    description = 'Compiles the RestartHelper utility'

    source = fileTree(dir: 'scripts', include: 'RestartHelper.java')
    classpath = files()
    destinationDirectory = file("${buildDir}/restart-helper-classes")
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

// Task to create restart-helper.jar
tasks.register('buildRestartHelper', Jar) {
    group = 'build'
    description = 'Builds the restart-helper.jar'
    dependsOn 'compileRestartHelper'

    from "${buildDir}/restart-helper-classes"
    archiveFileName = 'restart-helper.jar'
    destinationDirectory = file("${buildDir}/libs")

    manifest {
        attributes 'Main-Class': 'RestartHelper'
    }

    doLast {
        println "restart-helper.jar created at: ${destinationDirectory.get()}/restart-helper.jar"
    }
}
