import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import org.apache.tools.ant.filters.ReplaceTokens
import org.sonar.build.LicenseReader

plugins {
  id "com.github.hierynomus.license-report"
  id "com.github.johnrengelman.shadow"
  id "de.undercouch.download"
  id "org.cyclonedx.bom"
}

sonar {
  properties {
    property 'sonar.projectName', "${projectTitle} :: Application"
  }
}

configurations {
  zipDist
  zip
  scanner
  web
  shutdowner
  jdbc_mssql {
    transitive = false
  }
  jdbc_postgresql {
    transitive = false
  }
  jdbc_h2 {
    transitive = false
  }
  bundledPlugin {
    transitive = false
  }
  bundledPlugin_deps {
    extendsFrom bundledPlugin
    transitive = true
  }
  appLicenses.extendsFrom(api, web, scanner, jdbc_mssql, jdbc_postgresql, jdbc_h2, bundledPlugin_deps)
  cyclonedx
}

jar.enabled = false
shadowJar {
  archiveBaseName = 'sonar-application'
  archiveClassifier = null
  mergeServiceFiles()
  zip64 true
  manifest {
    attributes('Main-Class': 'org.sonar.application.App')
  }
}

dependencies {
    // please keep list ordered
    api 'org.slf4j:slf4j-api'

    api 'org.elasticsearch.client:elasticsearch-rest-high-level-client'
    api 'org.sonarsource.api.plugin:sonar-plugin-api'
    api project(':server:sonar-ce')
    api project(':server:sonar-main')
    api project(':server:sonar-process')
    api project(':server:sonar-webserver')
    api project(':sonar-core')
    api project(':sonar-plugin-api-impl')

    compileOnlyApi 'com.github.spotbugs:spotbugs-annotations'

    scanner project(path: ':sonar-scanner-engine-shaded', configuration: 'shadow')
    cyclonedx project(path: ':sonar-scanner-engine-shaded')
    web project(':server:sonar-web')
    shutdowner project(':sonar-shutdowner')

    jdbc_h2 'com.h2database:h2'
    jdbc_mssql 'com.microsoft.sqlserver:mssql-jdbc'
    jdbc_postgresql 'org.postgresql:postgresql'

    def artifactoryUsername = System.env.'ARTIFACTORY_PRIVATE_USERNAME' ?: (project.hasProperty('artifactoryUsername') ? project.getProperty('artifactoryUsername') : '')
    def artifactoryPassword = System.env.'ARTIFACTORY_PRIVATE_PASSWORD' ?: (project.hasProperty('artifactoryPassword') ? project.getProperty('artifactoryPassword') : '')
    if (artifactoryUsername && artifactoryPassword) {
      zipDist "sonarqube:elasticsearch:${elasticSearchServerVersion}-no-jdk-linux-x86_64@tar.gz"
    } else {
      zipDist "elasticsearch:elasticsearch:${elasticSearchServerVersion}-linux-x86_64@tar.gz"
    }

}

// declare dependencies in configuration bundledPlugin to be packaged in lib/extensions
apply from: 'bundled_plugins.gradle'

//verify if sonar.properties files does not have any external input
task verifySonarProperties(type: Verify) {
  def propertiesFile = file('src/main/assembly/conf/sonar.properties')
  propertiesFile.withReader { reader ->
    def line
    while ((line = reader.readLine()) != null) {
      if (!line.startsWith('#') && !line.isEmpty()) {
        throw new GradleException('sonar.properties file by default must not provide any user configuration.')
      }
    }
  }
}


downloadLicenses {
  dependencyConfiguration = 'appLicenses'
}

tasks.register('downloadJres') {
  doLast {
    def jresMetadata = new JsonSlurper().parse(file(layout.projectDirectory.dir('src/main/resources/jres-metadata.json').asFile))
    jresMetadata.each { jre ->
      downloadJreFromAdoptium(jre.os, jre.arch, jre.filename, jre.sha256)
    }
  }
}

task zip(type: Zip, dependsOn: [configurations.compileClasspath]) {
  duplicatesStrategy DuplicatesStrategy.EXCLUDE
  def archiveDir = "sonarqube-$project.version"

  dependsOn tasks.downloadJres
  into("${archiveDir}/jres") {
    from(layout.buildDirectory.dir('jres'))
  }

  if(release) {
    dependsOn tasks.downloadLicenses
    into("${archiveDir}/") {
      from(tasks.downloadLicenses.outputs) {
        include 'dependency-license.json'
        filter(LicenseReader)
      }
    }
  }

  into("${archiveDir}/") {
    from(file('src/main/assembly')) {
      exclude 'conf/sonar.properties'
      exclude 'bin/windows-x86-64/lib/SonarServiceWrapperTemplate.xml'
      exclude 'bin/windows-x86-64/StartSonar.bat'
      exclude 'bin/linux-x86-64/sonar.sh'
      exclude 'bin/macosx-universal-64/sonar.sh'
    }
  }

  ResolvedArtifact elasticSearchArtifact = configurations.zipDist.resolvedConfiguration.resolvedArtifacts.find {
    it.moduleVersion.id.name == "elasticsearch"
  }
  from(tarTree(elasticSearchArtifact.file)) {
    eachFile { fcd ->
      def path = fcd.relativePath.segments - fcd.relativeSourcePath.segments + fcd.relativeSourcePath.segments.drop(1)
      fcd.relativePath = new RelativePath(true, *path)
    }
    into("${archiveDir}/elasticsearch")
    exclude '**/bin/elasticsearch-certgen'
    exclude '**/bin/elasticsearch-certutil'
    exclude '**/bin/elasticsearch-create-enrollment-token'
    exclude '**/bin/elasticsearch-croneval'
    exclude '**/bin/elasticsearch-env-from-file'
    exclude '**/bin/elasticsearch-geoip'
    exclude '**/bin/elasticsearch-keystore'
    exclude '**/bin/elasticsearch-node'
    exclude '**/bin/elasticsearch-plugin'
    exclude '**/bin/elasticsearch-reconfigure-node'
    exclude '**/bin/elasticsearch-reset-password'
    exclude '**/bin/elasticsearch-saml-metadata'
    exclude '**/bin/elasticsearch-service-tokens'
    exclude '**/bin/elasticsearch-setup-passwords'
    exclude '**/bin/elasticsearch-shard'
    exclude '**/bin/elasticsearch-sql-cli'
    exclude '**/bin/elasticsearch-sql-cli-8.6.1.jar'
    exclude '**/bin/elasticsearch-syskeygen'
    exclude '**/bin/elasticsearch-users'
    exclude '**/jdk/**'
    exclude '**/lib/tools/ansi-console'
    exclude '**/lib/tools/geoip-cli'
    exclude '**/lib/tools/plugin-cli'
    exclude '**/modules/aggs-matrix-stats/**'
    exclude '**/modules/blob-cache/**'
    exclude '**/modules/constant-keyword/**'
    exclude '**/modules/data-streams/**'
    exclude '**/modules/frozen-indices/**'
    exclude '**/modules/inference/**'
    exclude '**/modules/ingest-attachment/**'
    exclude '**/modules/ingest-common/**'
    exclude '**/modules/ingest-geoip/**'
    exclude '**/modules/ingest-user-agent/**'
    exclude '**/modules/kibana/**'
    exclude '**/modules/lang-expression/**'
    exclude '**/modules/lang-mustache/**'
    exclude '**/modules/legacy-geo/**'
    exclude '**/modules/mapper-extras/**'
    exclude '**/modules/mapper-version/**'
    exclude '**/modules/ml-package-loader/**'
    exclude '**/modules/percolator/**'
    exclude '**/modules/rank-eval/**'
    exclude '**/modules/rank-rrf/**'
    exclude '**/modules/repositories-metering-api/**'
    exclude '**/modules/repository-azure/**'
    exclude '**/modules/repository-encrypted/**'
    exclude '**/modules/repository-gcs/**'
    exclude '**/modules/repository-s3/**'
    exclude '**/modules/repository-url/**'
    exclude '**/modules/runtime-fields-common/**'
    exclude '**/modules/searchable-snapshots/**'
    exclude '**/modules/search-business-rules/**'
    exclude '**/modules/snapshot-based-recoveries/**'
    exclude '**/modules/snapshot-repo-test-kit/**'
    exclude '**/modules/spatial/**'
    exclude '**/modules/transform/**'
    exclude '**/modules/unsigned-long/**'
    exclude '**/modules/vectors/**'
    exclude '**/modules/vector-tile/**'
    exclude '**/modules/wildcard/**'
    exclude '**/modules/x-pack-aggregate-metric'
    exclude '**/modules/x-pack-aggregate-metric/**'
    exclude '**/modules/x-pack-analytics/**'
    exclude '**/modules/x-pack-async-search/**'
    exclude '**/modules/x-pack-async/**'
    exclude '**/modules/x-pack-autoscaling/**'
    exclude '**/modules/x-pack-ccr/**'
    exclude '**/modules/x-pack-deprecation/**'
    exclude '**/modules/x-pack-downsample/**'
    exclude '**/modules/x-pack-enrich/**'
    exclude '**/modules/x-pack-ent-search/**'
    exclude '**/modules/x-pack-eql/**'
    exclude '**/modules/x-pack-esql/**'
    exclude '**/modules/x-pack-fleet/**'
    exclude '**/modules/x-pack-graph/**'
    exclude '**/modules/x-pack-identity-provider/**'
    exclude '**/modules/x-pack-ilm/**'
    exclude '**/modules/x-pack-logstash/**'
    exclude '**/modules/x-pack-ml/**'
    exclude '**/modules/x-pack-monitoring/**'
    exclude '**/modules/x-pack-profiling/**'
    exclude '**/modules/x-pack-ql/**'
    exclude '**/modules/x-pack-rollup/**'
    exclude '**/modules/x-pack-shutdown/**'
    exclude '**/modules/x-pack-slm/**'
    exclude '**/modules/x-pack-sql/**'
    exclude '**/modules/x-pack-stack/**'
    exclude '**/modules/x-pack-text-structure/**'
    exclude '**/modules/x-pack-voting-only-node/**'
    exclude '**/modules/x-pack-watcher/**'
    exclude '**/modules/x-pack-write-load-forecaster/**'
    includeEmptyDirs = false
  }

  into("${archiveDir}/conf/") {
    from file('src/main/assembly/conf/sonar.properties')
    filter(ReplaceTokens, tokens: [
            'searchDefaultHeapSize': '512MB',
            'searchJavaOpts'       : '-Xmx512m -Xms512m -XX:MaxDirectMemorySize=256m -XX:+HeapDumpOnOutOfMemoryError',
            'ceDefaultHeapSize'    : '512MB',
            'ceJavaOpts'           : '-Xmx512m -Xms128m -XX:+HeapDumpOnOutOfMemoryError',
            'webDefaultHeapSize'   : '512MB',
            'webJavaOpts'          : '-Xmx512m -Xms128m -XX:+HeapDumpOnOutOfMemoryError'
    ])
  }

  into("${archiveDir}/bin/linux-x86-64/") {
    from file('src/main/assembly/bin/linux-x86-64/sonar.sh')
    filter(ReplaceTokens, tokens: [
            'sqversion': version
    ])
  }

  into("${archiveDir}/bin/macosx-universal-64/") {
    from file('src/main/assembly/bin/macosx-universal-64/sonar.sh')
    filter(ReplaceTokens, tokens: [
            'sqversion': version
    ])
  }

  into("${archiveDir}/bin/windows-x86-64/") {
    from file('src/main/assembly/bin/windows-x86-64/StartSonar.bat')
    filter(ReplaceTokens, tokens: [
            'sqversion': version
    ])
  }

  into("${archiveDir}/bin/windows-x86-64/lib/") {
    from file('src/main/assembly/bin/windows-x86-64/lib/SonarServiceWrapperTemplate.xml')
    filter(ReplaceTokens, tokens: [
            'sqversion': version
    ])
  }
  // Create the empty dir (plugins) required by elasticsearch
  into("${archiveDir}/elasticsearch/") {
    // Create the empty dir required by elasticsearch
    from {
      new File(buildDir, 'elasticsearch/plugins').mkdirs()
      "$buildDir/elasticsearch"
    }
  }
  into("${archiveDir}/lib/extensions/") {
    from configurations.bundledPlugin
  }
  into("${archiveDir}/lib/scanner/") {
    from configurations.scanner
  }
  into("${archiveDir}/lib/") {
    from shadowJar
  }
  into("${archiveDir}/web/") {
    duplicatesStrategy DuplicatesStrategy.FAIL
    // FIXME use configurations.web with correct artifacts
    from(tasks.getByPath(':server:sonar-web:yarn_run').outputs) { a ->
      if (official) {
         project(':private:branding').fileTree('src').visit { b ->
           if (!b.isDirectory()) {
             a.exclude b.relativePath.toString()
           }
         }
      }
    }
    if (official) {
      from project(':private:branding').file('src')
    }
  }
  into("${archiveDir}/lib/jdbc/mssql/") {
    from configurations.jdbc_mssql
  }
  into("${archiveDir}/lib/jdbc/postgresql/") {
    from configurations.jdbc_postgresql
  }
  into("${archiveDir}/lib/jdbc/h2/") {
    from configurations.jdbc_h2
  }
  into("${archiveDir}/lib/") {
    from configurations.shutdowner
  }
}

// Check the size of the archive
zip {
  mustRunAfter cyclonedxBom
  doLast {
    //When the archive size increases due to dependencies, the expected size should be updated as well.
    //Bump the expected size by at least 10 more megabytes than what is strictly needed, this in conjunction with the
    //tolerance will allow for some growth in the archive size.
    def expectedSize = 760000000
    //We set a tolerance of 15MB to avoid failing the build for small differences in the archive size.
    def tolerance = 15000000
    def minArchiveSize = expectedSize - tolerance
    def maxArchiveSize = expectedSize + tolerance

    def archiveSize = archiveFile.get().asFile.length()
    if (archiveSize < minArchiveSize)
      throw new GradleException("${archiveFileName.get()} size ($archiveSize) too small. Min is $minArchiveSize")
    if (archiveSize > maxArchiveSize)
      throw new GradleException("${destinationDirectory.get()}/${archiveFileName.get()} size ($archiveSize) too large. Max is $maxArchiveSize")
  }
}
assemble.dependsOn zip

// the script start.sh unpacks OSS distribution into $buildDir/distributions/sonarqube-oss.
// This directory should be deleted when the zip is changed.
task cleanLocalUnzippedDir(dependsOn: zip) {
  def unzippedDir = file("$buildDir/distributions/sonarqube-$version")
  inputs.files(file("$buildDir/distributions/sonar-application-${version}.zip"))
  outputs.upToDateWhen { true }

  doLast {
    println("delete directory ${unzippedDir}")
    project.delete(unzippedDir)
  }
}
assemble.dependsOn cleanLocalUnzippedDir

artifacts { zip zip }

artifactoryPublish.skip = false

def bomFile = layout.buildDirectory.file('reports/bom.json')
cyclonedxBom {
  includeConfigs = ["runtimeClasspath", "web", "shutdowner", "jdbc_mssql", "jdbc_postgresql", "jdbc_h2", "bundledPlugin_deps",
                    "cyclonedx"]
  outputs.file bomFile
}
tasks.cyclonedxBom {
  inputs.files(configurations.runtimeClasspath, configurations.shutdowner, configurations.jdbc_mssql,
          configurations.jdbc_postgresql, configurations.jdbc_h2, configurations.bundledPlugin_deps, configurations.cyclonedx)
}
def bomArtifact = artifacts.add('archives', bomFile.get().asFile) {
  type 'json'
  classifier 'cyclonedx'
  builtBy 'cyclonedxBom'
}

publishing {
  publications {
    mavenJava(MavenPublication) {
      artifact zip
    }
    if (enableBom) {
      mavenJava(MavenPublication) {
        artifact bomArtifact
      }
    }
  }
}

String.metaClass.urlEncode = { -> URLEncoder.encode(delegate as String, 'UTF-8') }
ext {
  WINDOWS = 'windows'
  MAC = 'mac'
  ALPINE_ADOPTIUM = 'alpine-linux'
  ALPINE = 'alpine'
}

/**
 * Run this task only when you want to update embedded JREs metadata. Please double check the generated file jres-metadata.json before committing.
 */
tasks.register('updateJresMetadata') {
  doLast {
    // download JSON file from Adoptium API
    URL apiUrl = new URL("https://api.adoptium.net/v3/assets/release_name/eclipse/${jre_release_name.urlEncode()}?heap_size=normal&image_type=jre&project=jdk")
    def response = new JsonSlurper().parse(apiUrl)

    def metadataContent = []

    def supportedOsAndArch = [[os:'linux', arch:'x64'], [os:'linux', arch:'aarch64'], [os:ext.ALPINE_ADOPTIUM, arch:'x64'], [os: ext.WINDOWS, arch:'x64'], [os:ext.MAC, arch:'x64'], [os:'mac', arch:'aarch64']]

    for (jreFlavor in supportedOsAndArch) {
      var candidates = response.binaries.findAll {
        it.os == jreFlavor.os && it.architecture == jreFlavor.arch
      }
      assert candidates.size() == 1, "Expected one JRE package for ${jreFlavor.os} ${jreFlavor.arch} but got ${candidates.size()}"
      var jre = candidates[0]
      metadataContent <<  [
          'id': UUID.randomUUID(),
          'filename': jre.package.name,
          'sha256': jre.package.checksum,
          'javaPath': "${jre_release_name}-jre" + (jreFlavor.os == ext.MAC ? '/Contents/Home' : '') + '/bin/java' + (jreFlavor.os == ext.WINDOWS ? '.exe' : ''),
          'os': jreFlavor.os.replace(ext.ALPINE_ADOPTIUM, ext.ALPINE),
          'arch': jreFlavor.arch
        ]
    }

    def jresMetadata = file(layout.projectDirectory.dir('src/main/resources/jres-metadata.json').asFile)
    jresMetadata.text = new JsonBuilder(metadataContent).toPrettyString()
  }
}

def downloadJreFromAdoptium(os, arch, filename, sha256) {
  def jreFile = layout.buildDirectory.file("jres/${filename}")
  download.run {
    src "https://api.adoptium.net/v3/binary/version/${jre_release_name.urlEncode()}/${os.replace(ext.ALPINE, ext.ALPINE_ADOPTIUM).urlEncode()}/${arch.urlEncode()}/jre/hotspot/normal/eclipse?project=jdk"
    dest jreFile
    overwrite false
  }
  verifyChecksum.run {
    src jreFile
    algorithm 'SHA-256'
    checksum sha256
  }
}