import java.util.concurrent.ConcurrentHashMap

description = "Apereo Central Authentication Service $version"
defaultTasks "clean", "build"

buildscript {
    repositories {
        mavenCentral()
        gradlePluginPortal()
        maven {
            url = "https://repo.spring.io/milestone"
            mavenContent { releasesOnly() }
        }
    }

    dependencies {
        classpath libs.gradle.plugin.spring.boot
        classpath libs.gradle.plugin.git.properties
        classpath libs.gradle.plugin.lombok
        classpath libs.gradle.plugin.versions
        classpath libs.gradle.plugin.native
        classpath libs.gradle.plugin.errorprone
        classpath libs.gradle.plugin.sonarqube
        classpath libs.gradle.plugin.nmcp
    }
}

ext {
    srcTagMap = new ConcurrentHashMap<File, Set<String>>()
    
    repositoryUsername = providers.systemProperty("repositoryUsername").getOrElse(System.getenv("REPOSITORY_USER"))
    repositoryPassword = providers.systemProperty("repositoryPassword").getOrElse(System.getenv("REPOSITORY_PWD"))

    snapshotVersion = version.toString().endsWith("-SNAPSHOT")
    publishSnapshots = providers.systemProperty("publishSnapshots").present && snapshotVersion
    publishReleases = providers.systemProperty("publishReleases").present && !snapshotVersion

    publishFlag = publishSnapshots || publishReleases

    publishMinimalArtifacts = providers.systemProperty("publishMinimalArtifacts").present

    skipBootifulArtifact = providers.systemProperty("skipBootifulArtifact").present
    skipErrorProneCompiler = providers.systemProperty("skipErrorProneCompiler").present
    skipSpringBootDevTools = providers.systemProperty("skipSpringBootDevTools").present

    skipArtifactSigning = providers.systemProperty("skipArtifactSigning").present
    terminateCompilerOnWarning = providers.systemProperty("terminateCompilerOnWarning").getOrElse("true") == "true"

    enableRemoteDebugging = providers.systemProperty("enableRemoteDebugging").present
    remoteDebuggingSuspend = providers.systemProperty("remoteDebuggingSuspend").getOrElse("false") == "true" ? "y" : "n"
    generateGitProperties = publishFlag || providers.systemProperty("generateGitProperties").present
    generateTimestamps = publishFlag || providers.systemProperty("generateTimestamps").present
    
    skipBootifulLaunchScript = providers.systemProperty("skipBootifulLaunchScript").present

    ci = System.getenv("CI") || providers.systemProperty("CI").present

    excludedFilesFromTestCoverage = [
        '**/docs/**',
        '**/org/springframework/**',
        '**/soap/generated/**',
        '**/com/duosecurity/**',
        '**/org/apereo/inspektr/**',
        '**/net/jradius/**',
        '**/com/yubico/**',
        '**/saml/sts/SamlToken**',
        '**/**ConfigurationMetadata**',
        '**/**NimbusOAuthJacksonModule**',
        '**/**Application**',
        '**/**Application$**',
        '**/**Exception$**',
        '**/**Properties**',
        '**/**Properties$**'
    ]
}

def isArtifactSigningRequired = {
    publishReleases && !skipArtifactSigning
}

if (publishReleases || publishSnapshots) {
    apply plugin: "com.gradleup.nmcp.aggregation"
    nmcpAggregation {
        centralPortal {
            username = "${repositoryUsername}"
            password = "${repositoryPassword}"
            publicationName = "Apereo CAS Release ${project.version}"
            publishingType = "AUTOMATIC"
            validationTimeout.set(Duration.ZERO)
            publishingTimeout.set(Duration.ZERO)
        }
        publishAllProjectsProbablyBreakingProjectIsolation()
    }
}

apply from: rootProject.file("gradle/dependencies.gradle")
apply from: rootProject.file("gradle/dependencyUpdates.gradle")

if (!providers.systemProperty("skipSonarqube").present) {
    apply plugin: "org.sonarqube"

    sonar {
        def exclusions = rootProject.excludedFilesFromTestCoverage.join(",")
        def token = providers.systemProperty("SONARCLOUD_TOKEN")
                .getOrElse(System.getenv("SONARCLOUD_TOKEN"))

        properties {
            property "sonar.host.url", "$sonarHostUrl"
            property "sonar.organization", "$sonarOrganization"
            property "sonar.projectName", "$sonarProjectName"
            property "sonar.sourceEncoding", "UTF-8"
            property "sonar.java.source", project.targetCompatibility
            property "sonar.token", token
            property "sonar.gradle.skipCompile", true
            property "sonar.coverage.exclusions", exclusions
            property "sonar.java.coveragePlugin", "jacoco"
        }
    }
}

allprojects {
    apply plugin: "idea"
    apply plugin: "eclipse"
    apply plugin: "java-library"
    apply plugin: "jacoco"

    jacoco {
        toolVersion = "${libs.versions.jacoco.get()}"
    }

    if (gradle.startParameter.taskNames.any { task -> task.contains("publishToMavenLocal") }) {
        apply plugin: "maven-publish"
    }

    apply from: rootProject.file("gradle/tasks.gradle")
    def arch = System.getProperty("os.arch")
    if (arch == "aarch64") {
        System.setProperty("os.arch", "x86_64")
        arch = System.getProperty("os.arch")
        def name = System.getProperty("os.name")
        logger.info("The OS platform architecture for this Gradle build is reset to ${arch} for ${name}")
    }

    project.ext["forceBootifulArtifact"] = null

    if (rootProject.generateGitProperties) {
        apply plugin: "io.hndrs.git-properties"
        tasks.named("generateGitProperties") {
            def propertiesFile = project.layout.buildDirectory.file("resources/main/git.properties").get().asFile
            def projectGroup = project.group
            def projectName = project.name
            def projectVersion = project.version
            def projectDescription = project.description
            
            it.dotGitDirectory.set(rootProject.layout.projectDirectory.dir(".git"))
            it.output.set(propertiesFile)
            doLast {
                propertiesFile.withWriterAppend('UTF-8') { writer ->
                    writer << "project.group=${projectGroup}"
                    writer << "\nproject.name=${projectName}"
                    writer << "\nproject.version=${projectVersion}"
                    writer << "\nproject.description=${projectDescription}\n"
                }
            }
        }
    }

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(project.targetCompatibility)
        }
    }

    idea {
        module {
            downloadSources = false
            downloadJavadoc = false
            jdkName = project.targetCompatibility
            excludeDirs += file(".gradle")
            [".settings", "classes", "bin", "out", "docs", "dependency-cache", "libs",
             "reports", "resources", "test-results", "tmp", "gh-pages"].each {
                excludeDirs += project.layout.buildDirectory.files(it)
            }
        }
    }

    tasks.named('javadoc') {
        options.memberLevel = JavadocMemberLevel.PUBLIC
        options.addBooleanOption('html5', true)
        options.addStringOption('Xdoclint:html,reference,syntax', '-quiet')
        options.addStringOption('encoding', 'UTF-8')
        failOnError = providers.systemProperty("ignoreJavadocFailures").present
        excludes = ['**/generated/**', '**/com/duosecurity/**']
    }

}

subprojects {
    if (projectShouldBePublished(project)) {
        apply plugin: "maven-publish"
        apply plugin: "signing"
    }

    if (!rootProject.skipErrorProneCompiler) {
        apply plugin: "net.ltgt.errorprone"
    }
    apply from: rootProject.file("gradle/tests.gradle")

    ext.libraries = rootProject.ext.libraries

    configurations {
        tests
        provided
        jvmAgents
        javadocClasspath
    }

    apply from: rootProject.file("gradle/overrides.gradle")

    /**
     * Apply publishing rules after the build has had a chance to
     * evaluate sub-projects to apply plugins, etc.
     */
    afterEvaluate { project ->
        if (projectShouldBePublished(project)) {
            logger.info "Project ${project.name} should be published"
            apply from: rootProject.file("gradle/maven.gradle")

            publishing {
                publications {
                    def isWarPluginApplied = project.plugins.hasPlugin("war")
                    if (isWarPluginApplied) {
                        logger.info "Project ${project.name} should be published as a WAR"
                        mavenWeb(MavenPublication) {
                            from components.web
                            pom {
                                project.ext.createPom(it, project)
                            }
                            pom.withXml {
                                project.ext.createPomRepositories(asNode())
                            }
                        }
                    } else if (project.hasProperty("skipPublication") && project.skipPublication) {
                        logger.info "Skipping artifact publication for project ${project.name}"
                    } else if (project.name.contains("cas-server-support-bom")) {
                        logger.info "Project ${project.name} is published as a BOM"
                    } else {
                        logger.info "Project ${project.name} should be published as a JAR"
                        mavenJava(MavenPublication) {

                            logger.info "Project ${project.name} does not have a bootJar task, using default JAR"
                            from components.java
                            
                            if (project.hasProperty("publishMinimalArtifacts") && project.publishMinimalArtifacts) {
                                logger.info "Publishing minimal artifacts for project ${project.name}"
                            } else {
                                artifact tasks['sourcesJar']
                                artifact tasks['resourcesJar']
                                artifact tasks['javadocJar']
                                artifact tasks['testJar']
                            }

                            pom {
                                project.ext.createPom(it, project)
                            }
                            pom.withXml {
                                project.ext.createPomRepositories(asNode())
                            }
                        }
                    }
                }
            }

            signing {
                required = isArtifactSigningRequired
                if (System.getenv("PGP_PRIVATE_KEY") && System.getenv("PGP_PASSPHRASE")) {
                    useInMemoryPgpKeys(System.getenv("PGP_PRIVATE_KEY"), System.getenv("PGP_PASSPHRASE"))
                }
                sign publishing.publications
            }
        }
    }

    tasks.named("assemble") {
        if (rootProject.publishFlag && !rootProject.publishMinimalArtifacts) {
            dependsOn(sourcesJar, javadocJar, resourcesJar, testJar)
        } else {
            dependsOn(testJar)
        }
    }
    
    artifacts {
        tests testJar
    }

    sourceSets {
        main {
            compileClasspath += configurations.provided
            runtimeClasspath += configurations.provided

            java {
                srcDirs = ["${projectDir}/src/main/java"]
            }
            resources {
                srcDirs = ["${projectDir}/src/main/resources"]
            }
        }
        test {
            java {
                srcDirs = ["${projectDir}/src/test/java"]
            }
            resources {
                srcDirs = ["${projectDir}/src/test/resources"]
            }
        }
    }

    ext.buildJarFile = new File(project.layout.buildDirectory.getAsFile().get(), "libs/${project.name}-${project.version}.jar")

    [compileTestJava, compileJava].each {
        if (rootProject.generateGitProperties) {
            it.dependsOn("generateGitProperties")
        }

        it.options.fork = true
        it.options.incremental = true

        def casCompilerArgs = ["-parameters", '-Xlint:-removal']
        if (rootProject.ext.terminateCompilerOnWarning) {
            casCompilerArgs.add("-Werror")
        }

        if (!rootProject.skipErrorProneCompiler) {
            it.options.errorprone.disableAllChecks = !rootProject.ext.terminateCompilerOnWarning
            it.options.errorprone.allErrorsAsWarnings = false
            it.options.errorprone.disableWarningsInGeneratedCode = true
            it.options.errorprone.errorproneArgs = [
                                                    '-Xep:JavaUtilDate:OFF',
                                                    '-Xep:BooleanLiteral:OFF',
                                                    '-Xep:StringConcatToTextBlock:OFF',
                                                    '-Xep:AddressSelection:OFF']
            casCompilerArgs.add("-XDcompilePolicy=byfile")
            casCompilerArgs.add("-XDshould-stop.ifError=FLOW")
        }
        it.options.compilerArgs += casCompilerArgs
    }

    if (projectRequiresLombok(project)) {
        apply plugin: "io.freefair.lombok"
        lombok {
            version = "${libs.versions.lombok.get()}"
        }
    }

    tasks.named('jar') {
        def projectName = project.name
        def projectGroup = project.group
        def projectVersion = project.version

        def propertiesFile = project.layout.buildDirectory.file("resources/main/git.properties").get().asFile
        def properties = new Properties()
        if (propertiesFile.exists()) {
            propertiesFile.withInputStream { stream ->
                properties.load(stream)
            }
        }
        def projectSpecVersion = properties.getProperty('git.commit.id', 'N/A')
        
        manifest {
            attributes(
                    "Automatic-Module-Name": project.name.replace("-", "."),
                    "Implementation-Title": projectName,
                    "Implementation-Vendor": projectGroup,
                    "Created-By": projectGroup
            )
            if (generateTimestamps) {
                attributes("Implementation-Date": java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC))
            }
            attributes(
                    "Specification-Version": projectSpecVersion,
                    "Implementation-Version": projectVersion)
        }
    }

    if (!providers.systemProperty("skipCheckstyle").present && projectShouldBePublished(project)) {
        apply plugin: "checkstyle"
        checkstyle {
            configProperties = [
                    "checkstyle.suppressions.file" : new File(rootDir, "style/checkstyle-suppressions.xml"),
                    "checkstyle.importcontrol.file": new File(rootDir, "style/import-control.xml")
            ]
            configFile = new File(rootDir, "style/checkstyle-rules.xml")
            ignoreFailures = false
            showViolations = true
            toolVersion = "${libs.versions.checkstyle.get()}"
        }
    }

    dependencies {
        implementation libraries.aspectj

        api libraries.swaggerannotations
        api libraries.slf4j
        api libraries.guava
        api libraries.commons
        api libraries.spring
        api libraries.jackson
        api libraries.httpclient
        api libraries.quartz
        api libraries.caffein
        api libraries.springcloud
        api libraries.springboot
        api libraries.springsecurity
        api libraries.springintegration
        api libraries.springwebflow
        api libraries.jakarta
        
        compileOnly libraries.jakartapersistence

        annotationProcessor libraries.springindexer

        if (!providers.systemProperty("skipErrorProneCompiler").present) {
            errorprone "com.google.errorprone:error_prone_core:${libs.versions.errorprone.get()}"
        }

        testImplementation libraries.tests
        testImplementation libraries.log4j
        testImplementation libraries.hibernatevalidator
        testImplementation libraries.jakartapersistence

        runtimeOnly libraries.httpclient4

        jvmAgents(libs.mockito.core) {
            transitive = false
        }
        jvmAgents(libs.mockito.inline) {
            transitive = false
        }

        javadocClasspath project(path: project.path, configuration: 'compileClasspath')
    }
}

if (!gradle.startParameter.excludedTaskNames.contains("javadoc") || rootProject.publishFlag) {
    tasks.withType(Javadoc).configureEach {
        source subprojects.collect { project -> project.sourceSets.main.allJava }
        destinationDir = new File(project.layout.buildDirectory.getAsFile().get(), "javadoc")
        if (project != rootProject) {
            classpath = configurations.javadocClasspath
        }
        options.addBooleanOption('html5', true)
        failOnError = providers.systemProperty("ignoreJavadocFailures").present
    }

    tasks.register('aggregateJavadocsIntoJar', Jar) {
        dependsOn javadoc
        description = "Aggregate all Javadoc into a single artifact"
        archiveClassifier.set("javadoc")
        from javadoc
    }
}

if (rootProject.publishFlag) {
    tasks.register('rootSourcesJar', Jar) {
        description = "Build JAR for the root CAS module"
        archiveBaseName.set("${project.name}-sources")
        from rootProject.file("src")
    }
}

tasks.register('gradleHome') {
    description = "Display GRADLE_HOME environment variable"
    doFirst {
        ansi.green "\nexport GRADLE_HOME=" + gradle.gradleHomeDir
    }
}

if (rootProject.publishFlag && !rootProject.publishMinimalArtifacts) {
    artifacts {
        archives tasks.named('aggregateJavadocsIntoJar')
        archives tasks.named('rootSourcesJar')
    }
}

if (gradle.startParameter.taskNames.any { task -> task.startsWith("jacoco") }) {
    tasks.register('jacocoRootReport', JacocoReport) {
        outputs.cacheIf { true }

        subprojects.each { subproject ->
            {
                if (!subproject.path.contains(":docs")) {
                    subproject.tasks.withType(Test).each { task ->
                        dependsOn(task)
                        mustRunAfter(task)
                    }
                }
            }
        }

        def executions = subprojects.collect { subproject ->
            def execFiles = subproject.layout.buildDirectory.asFileTree.matching { include "**/jacoco/jacoco-*.exec" }
            execFiles.each {
                logger.info "${subproject.name} with execution file ${it}"
                inputs.file(it).withPropertyName("${subproject.name}-jacoco").withPathSensitivity(PathSensitivity.RELATIVE)

            }
            execFiles.files
        }.flatten()

        ansi.green("Coverage execution data files: ${executions}")
        executionData.from(executions)
        inputs.files(executions)
                .withPropertyName('jacocoRootReport')
                .withPathSensitivity(PathSensitivity.RELATIVE)
        outputs.files(rootProject.layout.buildDirectory.file('reports/jacoco/jacocoRootReport/jacocoRootReport.xml').get().asFile)

        reports {
            html.required.set(true)
            xml.required.set(true)
            csv.required.set(true)
        }
        additionalSourceDirs.setFrom(files(subprojects.sourceSets.main.allSource.srcDirs))
        sourceDirectories.setFrom(files(subprojects.sourceSets.main.allSource.srcDirs))
        classDirectories.setFrom(
                files(subprojects.sourceSets.main.output).collect {
                    fileTree(dir: it,
                            include: ['**/*.*'],
                            exclude: rootProject.excludedFilesFromTestCoverage
                    )
                })
    }
}

tasks.register('casVersion') {
    description = "Display current CAS version"
    def casVersion = rootProject.version
    doLast {
        println casVersion
    }
}

tasks.register('tomcatVersion') {
    description = "Display current Apache Tomcat version"
    doLast {
        println "${tomcatVersion}"
    }
}

tasks.register('signingKey') {
    description = "Display CAS signing key id"
    doLast {
        println "Signing key: " + project.findProperty("signing.keyId")
    }
}

tasks.register('verifyRequiredJavaVersion') {
    def requiredVersion = JavaVersion.toVersion(project.targetCompatibility)
    if (!JavaVersion.current().isCompatibleWith(requiredVersion)) {
        throw new GradleException("Current Java version ${JavaVersion.current()} does not match required Java version ${requiredVersion}")
    }
}

tasks.register('generateProjectModulesMetadata') {
    def outputFile = new File(project.layout.buildDirectory.getAsFile().get(), "modules.json")
    def results = []
    subprojects.each {
        def publishMetadata = it.hasProperty("publishMetadata") ? it.property("publishMetadata") : false
        if (publishMetadata) {
            def metadata = it.ext.projectMetadata
            if (!metadata.containsKey("title") || !metadata.containsKey("category")) {
                throw new GradleException("Missing required project metadata for ${it.name}")
            }
            if (!metadata.containsKey("selectable")) {
                metadata.put("selectable", true)
            }
            results += [
                    name       : it.name,
                    version    : it.version.replaceAll("-SNAPSHOT", ""),
                    group      : it.group,
                    description: it.description,
                    details    : metadata
            ]
        }
    }
    doLast {
        outputFile.getParentFile().mkdirs()
        def json = groovy.json.JsonOutput.toJson(results)
        outputFile.write json
        println "Published project modules to ${outputFile}"
    }
}

boolean projectShouldBePublished(Project project) {
    def publishable = project.hasProperty("publishable") ? project.publishable : true
    project.ext.publishable = publishable
    logger.info "Project ${project.name} is publishable: ${publishable}"
    return publishable
}

boolean projectRequiresLombok(Project project) {
    return !["api", "core", "docs", "support", "webapp"].contains(project.name) && project.name != "cas-server-documentation"
}
