/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */

import com.avast.gradle.dockercompose.tasks.ComposePull
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin
import de.thetaphi.forbiddenapis.gradle.ForbiddenApisPlugin
import org.elasticsearch.gradle.internal.BuildPlugin
import org.elasticsearch.gradle.Version
import org.elasticsearch.gradle.VersionProperties
import org.elasticsearch.gradle.internal.BwcVersions
import org.elasticsearch.gradle.internal.info.BuildParams
import org.elasticsearch.gradle.plugin.PluginBuildPlugin
import org.gradle.plugins.ide.eclipse.model.AccessRule
import org.gradle.util.DistributionLocator
import org.gradle.util.GradleVersion
import org.elasticsearch.gradle.util.GradleUtils

import static org.elasticsearch.gradle.util.GradleUtils.maybeConfigure
import org.gradle.plugins.ide.eclipse.model.ProjectDependency
import org.elasticsearch.gradle.internal.InternalPluginBuildPlugin
import org.elasticsearch.gradle.internal.ResolveAllDependencies
import java.nio.file.Files
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING

plugins {
  id 'lifecycle-base'
  id 'elasticsearch.docker-support'
  id 'elasticsearch.global-build-info'
  id 'elasticsearch.build-scan'
  id 'elasticsearch.build-complete'
  id 'elasticsearch.jdk-download'
  id 'elasticsearch.internal-distribution-download'
  id 'elasticsearch.runtime-jdk-provision'
  id 'elasticsearch.ide'
  id 'elasticsearch.forbidden-dependencies'
  id 'elasticsearch.local-distribution'
  id 'elasticsearch.fips'
  id 'elasticsearch.internal-testclusters'
  id 'elasticsearch.run'
  id 'elasticsearch.release-tools'
  id 'elasticsearch.versions'
}

/**
 * This is a convenient method for declaring test artifact dependencies provided by the internal
 * test artifact plugin. It replaces basically the longer dependency notation with explicit capability
 * declaration like this:
 *
 * testImplementation(project(xpackModule('repositories-metering-api'))) {
 *    capabilities {
 *         requireCapability("org.elasticsearch.gradle:repositories-metering-api-test-artifacts")
 *    }
 * }
 *
 * */
ext.testArtifact = { p, String name = "test" ->
  def projectDependency = p.dependencies.create(p)
  projectDependency.capabilities {
    requireCapabilities("org.elasticsearch.gradle:${projectDependency.name}-${name}-artifacts")
  };
}

tasks.register("updateCIBwcVersions") {
  doLast {
    File yml = file(".ci/bwcVersions")
    yml.text = ""
    yml << "BWC_VERSION:\n"
    BuildParams.bwcVersions.indexCompatible.each {
      yml << "  - \"$it\"\n"
    }
  }
}

tasks.register("verifyVersions") {
  doLast {
    if (gradle.startParameter.isOffline()) {
      throw new GradleException("Must run in online mode to verify versions")
    }
    // Read the list from maven central.
    // Fetch the metadata and parse the xml into Version instances because it's more straight forward here
    // rather than bwcVersion ( VersionCollection ).
    new URL('https://repo1.maven.org/maven2/org/elasticsearch/elasticsearch/maven-metadata.xml').openStream().withStream { s ->
      BuildParams.bwcVersions.compareToAuthoritative(
        new XmlParser().parse(s)
          .versioning.versions.version
          .collect { it.text() }.findAll { it ==~ /\d+\.\d+\.\d+/ }
          .collect { Version.fromString(it) }
      )
    }
    String ciYml = file(".ci/bwcVersions").text
    BuildParams.bwcVersions.indexCompatible.each {
      if (ciYml.contains("\"$it\"\n") == false) {
        throw new Exception(".ci/bwcVersions is outdated, run `./gradlew updateCIBwcVersions` and check in the results");
      }
    }

    // Make sure backport bot config file is up to date
    JsonNode backportConfig = new ObjectMapper().readTree(file(".backportrc.json"))
    List<BwcVersions.UnreleasedVersionInfo> unreleased = BuildParams.bwcVersions.unreleased.collect { BuildParams.bwcVersions.unreleasedInfo(it) }
    unreleased.each { unreleasedVersion ->
      boolean valid = backportConfig.get("targetBranchChoices").elements().any { branchChoice ->
        if (branchChoice.isObject()) {
          return branchChoice.get("name").textValue() == unreleasedVersion.branch
        } else {
          return branchChoice.textValue() == unreleasedVersion.branch
        }
      }
      if (valid == false) {
        throw new GradleException("No branch choice exists for development branch ${unreleasedVersion.branch} in .backportrc.json.")
      }
    }
    BwcVersions.UnreleasedVersionInfo nextMinor = unreleased.find { it.branch.endsWith("x") }
    String versionMapping = backportConfig.get("branchLabelMapping").fields().find { it.value.textValue() == nextMinor.branch }.key
    if (versionMapping != "^v${nextMinor.version}\$") {
      throw new GradleException("Backport label mapping for branch ${nextMinor.branch} is '${versionMapping}' but should be " +
        "'^v${nextMinor.version}\$'. Update .backportrc.json.")
    }
  }
}

/*
 * When adding backcompat behavior that spans major versions, temporarily
 * disabling the backcompat tests is necessary. This flag controls
 * the enabled state of every bwc task. It should be set back to true
 * after the backport of the backcompat code is complete.
 */

boolean bwc_tests_enabled = true
// place a PR link here when committing bwc changes:
String bwc_tests_disabled_issue = ""
/*
 * FIPS 140-2 behavior was fixed in 7.11.0. Before that there is no way to run elasticsearch in a
 * JVM that is properly configured to be in fips mode with BCFIPS. For now we need to disable
 * all bwc testing in fips mode.
 */

if ( BuildParams.inFipsJvm ) {
  bwc_tests_enabled = false
  bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/issues/66772"
}
if (bwc_tests_enabled == false) {
  if (bwc_tests_disabled_issue.isEmpty()) {
    throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false")
  }
  println "========================= WARNING ========================="
  println "         Backwards compatibility tests are disabled!"
  println "See ${bwc_tests_disabled_issue}"
  println "==========================================================="
}
if (project.gradle.startParameter.taskNames.find { it.startsWith("checkPart") } != null) {
  // Disable BWC tests for checkPart* tasks as it's expected that this will run un it's own check
  bwc_tests_enabled = false
}

subprojects {
  apply plugin: 'elasticsearch.base'
}

allprojects {
  // We disable this plugin for now till we shaked out the issues we see
  // e.g. see https://github.com/elastic/elasticsearch/issues/72169
  // apply plugin:'elasticsearch.internal-test-rerun'

  plugins.withType(InternalPluginBuildPlugin).whenPluginAdded {
    project.dependencies {
      compileOnly project(":server")
      testImplementation project(":test:framework")
    }
  }

  // injecting groovy property variables into all projects
  project.ext {
    // for ide hacks...
    isEclipse = providers.systemProperty("eclipse.launcher").forUseAtConfigurationTime().isPresent() ||   // Detects gradle launched from Eclipse's IDE
            providers.systemProperty("eclipse.application").forUseAtConfigurationTime().isPresent() ||    // Detects gradle launched from the Eclipse compiler server
            gradle.startParameter.taskNames.contains('eclipse') ||  // Detects gradle launched from the command line to do eclipse stuff
            gradle.startParameter.taskNames.contains('cleanEclipse')
  }

  ext.bwc_tests_enabled = bwc_tests_enabled

  // ignore missing javadocs
  tasks.withType(Javadoc).configureEach { Javadoc javadoc ->
    // the -quiet here is because of a bug in gradle, in that adding a string option
    // by itself is not added to the options. By adding quiet, both this option and
    // the "value" -quiet is added, separated by a space. This is ok since the javadoc
    // command already adds -quiet, so we are just duplicating it
    // see https://discuss.gradle.org/t/add-custom-javadoc-option-that-does-not-take-an-argument/5959
    javadoc.options.encoding = 'UTF8'
    javadoc.options.addStringOption('Xdoclint:all,-missing', '-quiet')
  }

  // eclipse configuration
  apply plugin: 'elasticsearch.eclipse'

  /*
   * Allow accessing com/sun/net/httpserver in projects that have
   * configured forbidden apis to allow it.
   */
  plugins.withType(ForbiddenApisPlugin) {
    eclipse.classpath.file.whenMerged { classpath ->
      if (false == forbiddenApisTest.bundledSignatures.contains('jdk-non-portable')) {
        classpath.entries
                .findAll { it.kind == "con" && it.toString().contains("org.eclipse.jdt.launching.JRE_CONTAINER") }
                .each {
                  it.accessRules.add(new AccessRule("accessible", "com/sun/net/httpserver/*"))
                }
      }
    }
  }

  tasks.register('resolveAllDependencies', ResolveAllDependencies) {
    configs = project.configurations
    if (project.path.contains("fixture")) {
      dependsOn tasks.withType(ComposePull)
    }
  }

  def checkPart1 = tasks.register('checkPart1')
  def checkPart2 = tasks.register('checkPart2')
  plugins.withId('lifecycle-base') {
    if (project.path.startsWith(":x-pack:")) {
      checkPart2.configure { dependsOn 'check' }
    } else {
      checkPart1.configure { dependsOn 'check' }
    }
  }

  project.ext.disableTasks = { String... tasknames ->
    for (String taskname : tasknames) {
      project.tasks.named(taskname).configure { enabled = false }
    }
  }

  /*
   * Remove assemble/dependenciesInfo on all qa projects because we don't
   * need to publish artifacts for them.
   */
  if (project.name.equals('qa') || project.path.contains(':qa:')) {
    maybeConfigure(project.tasks, 'assemble') {
      it.enabled = false
    }
    maybeConfigure(project.tasks, 'dependenciesInfo') {
      it.enabled = false
    }
    maybeConfigure(project.tasks, 'dependenciesGraph') {
      it.enabled = false
    }
  }

  project.afterEvaluate {
    // Ensure similar tasks in dependent projects run first. The projectsEvaluated here is
    // important because, while dependencies.all will pickup future dependencies,
    // it is not necessarily true that the task exists in both projects at the time
    // the dependency is added.
    if (project.path == ':test:framework') {
      // :test:framework:test cannot run before and after :server:test
      return
    }
    tasks.matching { it.name.equals('integTest')}.configureEach {integTestTask ->
      integTestTask.mustRunAfter tasks.matching { it.name.equals("test") }
    }

    configurations.matching { it.canBeResolved }.all { Configuration configuration ->
      dependencies.matching { it instanceof ProjectDependency }.all { ProjectDependency dep ->
        Project upstreamProject = dep.dependencyProject
        if (project.path != upstreamProject?.path) {
          for (String taskName : ['test', 'integTest']) {
            project.tasks.matching { it.name == taskName }.configureEach {task ->
              task.shouldRunAfter(upstreamProject.tasks.matching { upStreamTask -> upStreamTask.name == taskName })
            }
          }
        }
      }
    }


    // Handle javadoc dependencies across projects. Order matters: the linksOffline for
    // org.elasticsearch:elasticsearch must be the last one or all the links for the
    // other packages (e.g org.elasticsearch.client) will point to server rather than
    // their own artifacts.
    if (project.plugins.hasPlugin(BuildPlugin) || project.plugins.hasPlugin(PluginBuildPlugin)) {
      String artifactsHost = VersionProperties.elasticsearch.endsWith("-SNAPSHOT") ? "https://snapshots.elastic.co" : "https://artifacts.elastic.co"
      Closure sortClosure = { a, b -> b.group <=> a.group }
      Closure depJavadocClosure = { shadowed, dep ->
        if ((dep instanceof ProjectDependency) == false) {
          return
        }
        Project upstreamProject = dep.dependencyProject
        if (upstreamProject == null) {
          return
        }
        if (shadowed) {
          /*
           * Include the source of shadowed upstream projects so we don't
           * have to publish their javadoc.
           */
          project.evaluationDependsOn(upstreamProject.path)
          project.javadoc.source += upstreamProject.javadoc.source
          /*
           * Instead we need the upstream project's javadoc classpath so
           * we don't barf on the classes that it references.
           */
          project.javadoc.classpath += upstreamProject.javadoc.classpath
        } else {
          // Link to non-shadowed dependant projects
          project.javadoc.dependsOn "${upstreamProject.path}:javadoc"
          String externalLinkName = upstreamProject.archivesBaseName
          String artifactPath = dep.group.replaceAll('\\.', '/') + '/' + externalLinkName.replaceAll('\\.', '/') + '/' + dep.version
          project.javadoc.options.linksOffline artifactsHost + "/javadoc/" + artifactPath, "${upstreamProject.buildDir}/docs/javadoc/"
        }
      }
      boolean hasShadow = project.plugins.hasPlugin(ShadowPlugin)
      project.configurations.compileClasspath.dependencies
              .findAll()
              .toSorted(sortClosure)
              .each({ c -> depJavadocClosure(hasShadow, c) })
      project.configurations.compileOnly.dependencies
              .findAll()
              .toSorted(sortClosure)
              .each({ c -> depJavadocClosure(false, c) })
      if (hasShadow) {
        // include any dependencies for shadow JAR projects that are *not* bundled in the shadow JAR
        project.configurations.shadow.dependencies
                .findAll()
                .toSorted(sortClosure)
                .each({ c -> depJavadocClosure(false, c) })
      }
    }
  }

  apply plugin: 'elasticsearch.formatting'
}


tasks.register("verifyBwcTestsEnabled") {
  doLast {
    if (bwc_tests_enabled == false) {
      throw new GradleException('Bwc tests are disabled. They must be re-enabled after completing backcompat behavior backporting.')
    }
  }
}

tasks.register("branchConsistency") {
  description 'Ensures this branch is internally consistent. For example, that versions constants match released versions.'
  group 'Verification'
  dependsOn ":verifyVersions", ":verifyBwcTestsEnabled"
}

tasks.named("wrapper").configure {
  distributionType = 'ALL'
  doLast {
    final DistributionLocator locator = new DistributionLocator()
    final GradleVersion version = GradleVersion.version(wrapper.gradleVersion)
    final URI distributionUri = locator.getDistributionFor(version, wrapper.distributionType.name().toLowerCase(Locale.ENGLISH))
    final URI sha256Uri = new URI(distributionUri.toString() + ".sha256")
    final String sha256Sum = new String(sha256Uri.toURL().bytes)
    wrapper.getPropertiesFile() << "distributionSha256Sum=${sha256Sum}\n"
    println "Added checksum to wrapper properties"
    // copy wrapper properties file to build-tools-internal to allow seamless idea integration
    def file = new File("build-tools-internal/gradle/wrapper/gradle-wrapper.properties")
    Files.copy(wrapper.getPropertiesFile().toPath(), file.toPath(), REPLACE_EXISTING)
    // Update build-tools to reflect the Gradle upgrade
    // TODO: we can remove this once we have tests to make sure older versions work.
    project.file('build-tools-internal/src/main/resources/minimumGradleVersion').text = gradleVersion
    println "Updated minimum Gradle Version"
  }
}

gradle.projectsEvaluated {
  // Having the same group and name for distinct projects causes Gradle to consider them equal when resolving
  // dependencies leading to hard to debug failures. Run a check across all project to prevent this from happening.
  // see: https://github.com/gradle/gradle/issues/847
  Map coordsToProject = [:]
  project.allprojects.forEach { p ->
    String coords = "${p.group}:${p.name}"
    if (false == coordsToProject.putIfAbsent(coords, p)) {
      throw new GradleException(
        "Detected that two projects: ${p.path} and ${coordsToProject[coords].path} " +
          "have the same name and group: ${coords}. " +
          "This doesn't currently work correctly in Gradle, see: " +
          "https://github.com/gradle/gradle/issues/847"
      )
    }
  }
}

tasks.named("precommit") {
  dependsOn gradle.includedBuild('build-tools').task(':precommit')
  dependsOn gradle.includedBuild('build-tools-internal').task(':precommit')
}

tasks.named("checkPart1").configure {
  dependsOn gradle.includedBuild('build-tools').task(':check')
  dependsOn gradle.includedBuild('build-tools-internal').task(':check')
}

tasks.named("assemble").configure {
  dependsOn gradle.includedBuild('build-tools').task(':assemble')
}

tasks.named("cleanEclipse").configure {
  dependsOn gradle.includedBuild('build-conventions').task(':cleanEclipse')
  dependsOn gradle.includedBuild('build-tools').task(':cleanEclipse')
  dependsOn gradle.includedBuild('build-tools-internal').task(':cleanEclipse')
}

tasks.named("eclipse").configure {
  dependsOn gradle.includedBuild('build-conventions').task(':eclipse')
  dependsOn gradle.includedBuild('build-tools').task(':eclipse')
  dependsOn gradle.includedBuild('build-tools-internal').task(':eclipse')
}

tasks.register("buildReleaseArtifacts").configure {
  group = 'build'
  description = 'Builds all artifacts required for release manager'

  dependsOn allprojects.findAll { it.path.startsWith(':distribution:docker') == false && it.path.startsWith(':ml-cpp') == false }
    .collect { GradleUtils.findByName(it.tasks, 'assemble') }
    .findAll { it != null }
}

tasks.named("spotlessApply").configure {
  dependsOn gradle.includedBuild('build-tools').task(':spotlessApply')
  dependsOn gradle.includedBuild('build-tools').task(':reaper:spotlessApply')
  dependsOn gradle.includedBuild('build-tools-internal').task(':spotlessApply')
}