/*
 * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
 * License v3.0 only", or the "Server Side Public License, v 1".
 */

import org.apache.tools.ant.filters.ReplaceTokens
import org.elasticsearch.gradle.LoggedExec
import org.elasticsearch.gradle.OS
import org.redline_rpm.header.Flags

import java.nio.file.Files
import java.nio.file.Path
import java.util.regex.Matcher
import java.util.regex.Pattern

/*****************************************************************************
 *                         Deb and rpm configuration                         *
 *****************************************************************************
 *
 * The general strategy here is to build a directory on disk that contains
 * stuff that needs to be copied into the distributions. This is
 * important for two reasons:
 * 1. ospackage wants to copy the directory permissions that it sees off of the
 *    filesystem. If you ask it to create a directory that doesn't already
 *    exist on disk it petulantly creates it with 0755 permissions, no matter
 *    how hard you try to convince it otherwise.
 * 2. Convincing ospackage to pick up an empty directory as part of a set of
 *    directories on disk is reasonably easy. Convincing it to just create an
 *    empty directory requires more wits than I have.
 * 3. ospackage really wants to suck up some of the debian control scripts
 *    directly from the filesystem. It doesn't want to process them through
 *    any copy-style action.
 *
 * The following commands are useful when it comes to check the user/group
 * and files permissions set within the RPM and DEB packages:
 *
 *    rpm -qlp --dump path/to/elasticsearch.rpm
 *    dpkg -c path/to/elasticsearch.deb
 */

plugins {
  alias(buildLibs.plugins.ospackage)
}

['deb', 'rpm'].each { type ->
  String packagingFiles = "build/packaging/${type}"

  String taskName = "process${type.capitalize()}Files"
  tasks.register(taskName, Copy) {
    into packagingFiles

    with copySpec {
      from 'src/common'
      from "src/${type}"
      filter("tokens": expansionsForDistribution(type, false), ReplaceTokens.class)
    }

    into('etc/elasticsearch') {
      with configFiles(type, false)
    }

    filter("tokens": expansionsForDistribution(type, false), ReplaceTokens.class)

    doLast {
      // create empty dirs, we set the permissions when configuring the packages
      mkdir "${packagingFiles}/var/log/elasticsearch"
      mkdir "${packagingFiles}/var/lib/elasticsearch"
      mkdir "${packagingFiles}/usr/share/elasticsearch/plugins"

      // bare empty dir for /etc/elasticsearch and /etc/elasticsearch/jvm.options.d
      mkdir "${packagingFiles}/elasticsearch"
      mkdir "${packagingFiles}/elasticsearch/jvm.options.d"
    }
  }
}

// Common configuration that is package dependent. This can't go in ospackage
// since we have different templated files that need to be consumed, but the structure
// is the same
def commonPackageConfig(String type, String architecture) {
  return {
    onlyIf("not running on windows") {
      OS.current().equals(OS.WINDOWS) == false
    }
    dependsOn "process${type.capitalize()}Files"
    packageName = "elasticsearch"
    if (type == 'deb') {
      if (architecture == 'x64') {
        arch = 'amd64'
      } else {
        assert architecture == 'aarch64': architecture
        arch = 'arm64'
      }
    } else {
      assert type == 'rpm': type
      if (architecture == 'x64') {
        arch = 'X86_64'
      } else {
        assert architecture == 'aarch64': architecture
        arch = 'aarch64'
      }
    }
    // Follow elasticsearch's file naming convention
    String prefix = "${architecture == 'aarch64' ? 'aarch64-' : ''}${type}"
    destinationDirectory = file("${prefix}/build/distributions")
    archiveFileName.value(project.provider({ "${packageName}-${project.version}-${archString}.${type}" }))
    String packagingFiles = "build/packaging/${type}"

    String scripts = "${packagingFiles}/scripts"
    preInstall file("${scripts}/preinst")
    postInstall file("${scripts}/postinst")
    preUninstall file("${scripts}/prerm")
    postUninstall file("${scripts}/postrm")
    if (type == 'rpm') {
      postTrans file("${scripts}/posttrans")
    }

    // top level "into" directive is not inherited from ospackage for some reason, so we must
    // specify it again explicitly for copying common files
    String platform = 'linux-' + ((architecture == 'x64') ? 'x86_64' : architecture)
    into('/usr/share/elasticsearch') {
      into('bin') {
        with binFiles(type, false)
      }
      from(rootProject.projectDir) {
        include 'README.asciidoc'
        filePermissions {
          unix 0644
        }
      }
      into('lib') {
        with libFiles('linux', architecture)
      }
      into('modules') {
        with modulesFiles('linux', architecture)
      }
      into('jdk') {
        with jdkFiles(project, 'linux', architecture)
      }
      // we need to specify every intermediate directory in these paths so the package managers know they are explicitly
      // intended to manage them; otherwise they may be left behind on uninstallation. duplicate calls of the same
      // directory are fine
      eachFile { FileCopyDetails fcp ->
        String[] segments = fcp.relativePath.segments
        for (int i = segments.length - 2; i > 2; --i) {
          if (type == 'rpm') {
            directory('/' + segments[0..i].join('/'), 0755)
          }
          if (segments[-2] == 'bin' || segments[-1] == 'jspawnhelper') {
            fcp.permissions {
              unix(0755)
            }
          } else {
            fcp.permissions {
              unix(0644)
            }
          }
        }
      }
    }

    // license files
    if (type == 'deb') {
      into("/usr/share/doc/${packageName}") {
        from "${packagingFiles}/copyright"
        filePermissions {
          unix(0644)
        }
      }
    } else {
      assert type == 'rpm'
      into('/usr/share/elasticsearch') {
        from(layout.settingsDirectory.file('licenses').asFile) {
          include 'ELASTIC-LICENSE-2.0.txt'
          rename { 'LICENSE.txt' }
        }
        filePermissions {
          unix(0644)
        }
      }
    }

    // ========= config files =========
    configurationFile '/etc/elasticsearch/elasticsearch.yml'
    configurationFile '/etc/elasticsearch/elasticsearch-plugins.example.yml'
    configurationFile '/etc/elasticsearch/jvm.options'
    configurationFile '/etc/elasticsearch/log4j2.properties'
    configurationFile '/etc/elasticsearch/role_mapping.yml'
    configurationFile '/etc/elasticsearch/roles.yml'
    configurationFile '/etc/elasticsearch/users'
    configurationFile '/etc/elasticsearch/users_roles'
    from("${packagingFiles}") {
      dirPermissions {
        unix(0750)
      }
      into('/etc')
      permissionGroup 'elasticsearch'
      setgid true
      includeEmptyDirs = true
      createDirectoryEntry true
      include("elasticsearch") // empty dir, just to add directory entry
      include("elasticsearch/jvm.options.d") // empty dir, just to add directory entry
    }
    from("${packagingFiles}/etc/elasticsearch") {
      into('/etc/elasticsearch')
      dirPermissions {
        unix(0750)
      }
      setgid = true
      filePermissions {
        unix(0660)
      }
      permissionGroup 'elasticsearch'
      includeEmptyDirs = true
      createDirectoryEntry true
      fileType CONFIG | NOREPLACE
    }
    String envFile = expansionsForDistribution(type, false)['path.env']
    configurationFile envFile
    into(new File(envFile).getParent()) {
      fileType CONFIG | NOREPLACE
      permissionGroup 'elasticsearch'
      filePermissions {
        unix(0660)
      }
      from "${packagingFiles}/env/elasticsearch"
    }

    // ========= systemd =========
    into('/usr/lib/tmpfiles.d') {
      from "${packagingFiles}/systemd/elasticsearch.conf"
      filePermissions {
        unix(0644)
      }
    }
    into('/usr/lib/systemd/system') {
      fileType CONFIG | NOREPLACE
      from "${packagingFiles}/systemd/elasticsearch.service"
      filePermissions {
        unix(0644)
      }
    }
    into('/usr/lib/sysctl.d') {
      fileType CONFIG | NOREPLACE
      from "${packagingFiles}/systemd/sysctl/elasticsearch.conf"
      filePermissions {
        unix(0644)
      }
    }
    into('/usr/share/elasticsearch/bin') {
      from "${packagingFiles}/systemd/systemd-entrypoint"
      filePermissions {
        unix(0755)
      }
    }

    // ========= empty dirs =========
    // NOTE: these are created under packagingFiles as empty, but the permissions are set here
    Closure copyEmptyDir = { path, u, g, gid, mode ->
      File file = new File(path)
      into(file.parent) {
        from "${packagingFiles}/${file.parent}"
        include file.name
        includeEmptyDirs = true
        createDirectoryEntry true
        user u
        permissionGroup g
        dirPermissions {
          unix(mode)
        }
        setgid(gid)
      }
    }
    copyEmptyDir('/var/log/elasticsearch', 'elasticsearch', 'elasticsearch', true, 0750)
    copyEmptyDir('/var/lib/elasticsearch', 'elasticsearch', 'elasticsearch', true, 0750)
    copyEmptyDir('/usr/share/elasticsearch/plugins', 'root', 'root', false, 0755)

    // the oss package conflicts with the default distribution and vice versa
    conflicts('elasticsearch-oss')

    into '/usr/share/elasticsearch'
    with noticeFile(false)
  }
}

// this is package independent configuration
ospackage {
  maintainer = 'Elasticsearch Team <info@elastic.co>'
  summary = 'Distributed RESTful search engine built for the cloud'
  packageDescription = '''
    Reference documentation can be found at
    https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
    and the 'Elasticsearch: The Definitive Guide' book can be found at
    https://www.elastic.co/guide/en/elasticsearch/guide/current/index.html
  '''.stripIndent().trim()
  url = 'https://www.elastic.co/'

  // signing setup
  if (project.hasProperty('signing.password') && buildParams.snapshotBuild == false) {
    signingKeyId = project.hasProperty('signing.keyId') ? project.property('signing.keyId') : 'D88E42B4'
    signingKeyPassphrase = project.property('signing.password')
    signingKeyRingFile = project.hasProperty('signing.secretKeyRingFile') ?
      project.file(project.property('signing.secretKeyRingFile')) :
      new File(new File(project.providers.systemProperty('user.home').orElse('.gnupg').get()), 'secring.gpg')
  }

  // version found on oldest supported distro, centos-6
  requires('coreutils', '8.4', GREATER | EQUAL)

  fileMode = 0644
  dirMode = 0755
  user = 'root'
  permissionGroup = 'root'

  into '/usr/share/elasticsearch'
}

Closure commonDebConfig(String architecture) {
  return {
    configure(commonPackageConfig('deb', architecture))

    // jdeb does not provide a way to set the License control attribute, and
    // ospackage silently ignores setting it. This is probably because `License`
    // is not actually a field in the Debian control file. So instead, we set
    // the license as "custom field".
    customFields['License'] = 'Elastic-License'

    archiveVersion = project.version.replace('-', '~')
    packageGroup = 'web'

    // versions found on oldest supported distro, centos-6
    requires('bash', '4.1', GREATER | EQUAL)
    requires 'libc6'
    requires 'adduser'

    into('/usr/share/lintian/overrides') {
      from('src/deb/lintian/elasticsearch')
      filePermissions {
        unix(0644)
      }
    }
  }
}

tasks.register('buildAarch64Deb', Deb) {
  configure(commonDebConfig('aarch64'))
}

tasks.register('buildDeb', Deb) {
  configure(commonDebConfig('x64'))
}

Closure commonRpmConfig(String architecture) {
  return {
    configure(commonPackageConfig('rpm', architecture))

    license = 'Elastic License'

    packageGroup = 'Application/Internet'
    requires '/bin/bash'

    obsoletes packageName, '7.0.0', Flags.LESS

    prefix '/usr'
    packager = 'Elasticsearch'
    archiveVersion = project.version.replace('-', '_')
    release = '1'
    os = 'LINUX'
    distribution = 'Elasticsearch'
    vendor = 'Elasticsearch'
    // TODO ospackage doesn't support icon but we used to have one

    // without this the rpm will have parent dirs of any files we copy in, eg /etc/elasticsearch
    addParentDirs = false
  }
}

tasks.register('buildAarch64Rpm', Rpm) {
  configure(commonRpmConfig('aarch64'))
}

tasks.register('buildRpm', Rpm) {
  configure(commonRpmConfig('x64'))
}

Closure dpkgExists = { it -> new File('/bin/dpkg-deb').exists() || new File('/usr/bin/dpkg-deb').exists() || new File('/usr/local/bin/dpkg-deb').exists() }
Closure rpmExists = { it -> new File('/bin/rpm').exists() || new File('/usr/bin/rpm').exists() || new File('/usr/local/bin/rpm').exists() }

Closure debFilter = { f -> f.name.endsWith('.deb') }

// This configures the default artifact for the distribution specific
// subprojects. We have subprojects because Gradle project substitutions
// can only bind to the default configuration of a project
subprojects {
  apply plugin: 'distribution'

  String buildTask = "build${it.name.replaceAll(/-[a-z]/) { it.substring(1).toUpperCase() }.capitalize()}"
  ext.buildDist = parent.tasks.named(buildTask)
  artifacts {
    'default' buildDist
  }

  if (dpkgExists() || rpmExists()) {

    // sanity checks if packages can be extracted
    final File extractionDir = new File(buildDir, 'extracted')
    File packageExtractionDir
    if (project.name.contains('deb')) {
      packageExtractionDir = new File(extractionDir, 'deb-extracted')
    } else {
      assert project.name.contains('rpm')
      packageExtractionDir = new File(extractionDir, 'rpm-extracted')
    }
    tasks.register('checkExtraction', LoggedExec) {
      dependsOn buildDist
      doFirst {
        delete(extractionDir)
        extractionDir.mkdirs()
      }
    }

    tasks.named("check").configure { dependsOn "checkExtraction" }
    if (project.name.contains('deb')) {
      tasks.named("checkExtraction").configure {
        onlyIf("dpkg exists", dpkgExists)
        commandLine 'dpkg-deb', '-x', "${-> buildDist.get().outputs.files.filter(debFilter).singleFile}", packageExtractionDir
      }
    } else {
      assert project.name.contains('rpm')
      tasks.named("checkExtraction").configure {
        onlyIf("rpm exists", rpmExists)
        final File rpmDatabase = new File(extractionDir, 'rpm-database')
        commandLine 'rpm',
          '--badreloc',
          '--ignorearch',
          '--ignoreos',
          '--nodeps',
          '--noscripts',
          '--notriggers',
          '--dbpath',
          rpmDatabase,
          '--relocate',
          "/=${packageExtractionDir}",
          '-i',
          "${-> buildDist.get().outputs.files.singleFile}"
      }
    }

    tasks.register("checkLicense") {
      dependsOn buildDist, "checkExtraction"
    }
    tasks.named("check").configure { dependsOn "checkLicense" }
    if (project.name.contains('deb')) {
      tasks.named("checkLicense").configure {
        onlyIf("dpkg exists", dpkgExists)
        doLast {
          Path copyrightPath
          String expectedLicense
          String licenseFilename
          copyrightPath = packageExtractionDir.toPath().resolve("usr/share/doc/elasticsearch/copyright")
          expectedLicense = "Elastic-License"
          licenseFilename = "ELASTIC-LICENSE-2.0.txt"
          final List<String> header = Arrays.asList(
            "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/",
            "Copyright: Elasticsearch B.V. <info@elastic.co>",
            "License: " + expectedLicense
          )
          final List<String> licenseLines = Files.readAllLines(rootDir.toPath().resolve("licenses/" + licenseFilename))
          final List<String> expectedLines = header + licenseLines.collect { " " + it }
          assertLinesInFile(copyrightPath, expectedLines)
        }
      }
    } else {
      assert project.name.contains('rpm')
      tasks.named("checkLicense").configure {
        onlyIf("rpm exists", rpmExists)
        doLast {
          String licenseFilename
          licenseFilename = "ELASTIC-LICENSE-2.0.txt"
          final List<String> licenseLines = Files.readAllLines(rootDir.toPath().resolve("licenses/" + licenseFilename))
          final Path licensePath = packageExtractionDir.toPath().resolve("usr/share/elasticsearch/LICENSE.txt")
          assertLinesInFile(licensePath, licenseLines)
        }
      }
    }

    tasks.register("checkNotice") {
      dependsOn buildDist, "checkExtraction"
      onlyIf("${project.name.contains('deb') ? 'dpkg' : 'rpm'} exists") {
        (project.name.contains('deb') && dpkgExists.call(it)) || (project.name.contains('rpm') && rpmExists.call(it))
      }
      doLast {
        final List<String> noticeLines = Arrays.asList("Elasticsearch", "Copyright 2009-2024 Elasticsearch")
        final Path noticePath = packageExtractionDir.toPath().resolve("usr/share/elasticsearch/NOTICE.txt")
        assertLinesInFile(noticePath, noticeLines)
      }
    }

    def checkLicenseMetadataTaskProvider = tasks.register('checkLicenseMetadata', LoggedExec) {
      dependsOn buildDist, "checkExtraction"
    }
    tasks.named("check").configure {
      dependsOn "checkNotice"
      dependsOn(checkLicenseMetadataTaskProvider)
    }

    if (project.name.contains('deb')) {
      checkLicenseMetadataTaskProvider.configure { LoggedExec exec ->
        onlyIf("dpkg exists", dpkgExists)
        exec.commandLine 'dpkg-deb', '--info', "${-> buildDist.get().outputs.files.filter(debFilter).singleFile}"
        exec.getCaptureOutput().set(true)
        doLast {
          String expectedLicense
          expectedLicense = "Elastic-License"
          final Pattern pattern = Pattern.compile("\\s*License: (.+)")
          final String[] actualLines = getOutput().split("\n")
          int count = 0
          for (final String actualLine : actualLines) {
            final Matcher matcher = pattern.matcher(actualLine)
            if (matcher.matches()) {
              count++
              final String actualLicense = matcher.group(1)
              if (expectedLicense != actualLicense) {
                throw new GradleException("expected license [${expectedLicense} for package info but found [${actualLicense}]")
              }
            }
          }
          if (count == 0) {
            throw new GradleException("expected license [${expectedLicense}] for package info but found none in:\n${info}")
          }
          if (count > 1) {
            throw new GradleException("expected a single license for package info but found [${count}] in:\n${info}")
          }
        }
      }
    } else {
      assert project.name.contains('rpm')
      checkLicenseMetadataTaskProvider.configure { LoggedExec exec ->
        onlyIf("rpm exists", rpmExists)
        exec.commandLine 'rpm', '-qp', '--queryformat', '%{License}', "${-> buildDist.get().outputs.files.singleFile}"
        exec.getCaptureOutput().set(true)
        doLast {
          String license = getOutput()
          String expectedLicense
          expectedLicense = "Elastic License"
          if (license != expectedLicense) {
            throw new GradleException("expected license [${expectedLicense}] for [${-> buildDist.get().outputs.files.singleFile}] but was [${license}]")
          }
        }
      }
    }
  }
}
