import org.gradle.process.internal.ExecException

// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

plugins {
  id "org.flywaydb.flyway" version "6.0.1"
  id 'maven-publish'
}

ext {
  Set restrictedDbEnv =
      [ 'sandbox', 'production' ].asUnmodifiable()

  def dbServerProperty = 'dbServer'
  def dbNameProperty = 'dbName'

  def dbServer = findProperty(dbServerProperty).toString().toLowerCase()
  def dbName = findProperty(dbNameProperty)

  isRestricted = {
    return restrictedDbEnv.contains(dbServer)
  }

  getAccessInfoByHostPort = { hostAndPort ->
    println "Database set to ${hostAndPort}."
    return [
        url: "jdbc:postgresql://${hostAndPort}/${dbName}",
        user: findProperty('dbUser'),
        password: findProperty('dbPassword')]
  }

  getSocketFactoryAccessInfo = { env ->
    def cred = getCloudSqlCredential(env).split(' ')
    def sqlInstance = cred[0]
    println "Database set to Cloud SQL instance ${sqlInstance}."
    return [
        url: """\
                   jdbc:postgresql://google/${dbName}?cloudSqlInstance=
                   ${sqlInstance}&socketFactory=
                   com.google.cloud.sql.postgres.SocketFactory"""
                   .stripIndent()
                   .replaceAll(System.lineSeparator(), '') ,
        user: cred[1],
        password: cred[2]]
  }

  getJdbcAccessInfo = {
    if (rootProject.projects.keySet().contains(dbServer)) {
      return getSocketFactoryAccessInfo(dbServer)
    } else if (!dbServer.isEmpty()) {
      return getAccessInfoByHostPort(dbServer)
    } else {
      // Not running flyway tasks. Return a dummy object for Flyway config.
      return [ url: '', user: '', password: '' ]
    }
  }

  // Retrieves the Cloud SQL credential for the schema deployer. Result is in
  // the form of 'instancename username password'.
  //
  // The env parameter may be one of the following: alpha, crash, sandbox, or
  // production.
  //
  // User must make sure that the nomulus tool can be found on PATH. An alias
  // will not work.
  getCloudSqlCredential = { env ->
    try {
      execInBash('which nomulus', '/tmp')
    } catch (ExecException e) {
      throw new IllegalStateException(
          'nomulus not found. Make sure it is on PATH, not just an alias.')
    }
    def command =
        "nomulus -e ${env} get_sql_credential --user schema_deployer"

    return execInBash(command, project.rootDir)
  }
}

task schemaJar(type: Jar) {
  archiveBaseName = 'schema'
  from(sourceSets.main.resources) {
    include 'sql/flyway/**'
    include 'sql/schema/nomulus.golden.sql'
  }
}

// Expose NomulusPostgreSql class to ':core' for implementation, without
// leaking unnecessary dependencies to the release artifacts through ':core'.
// Jar is put in the 'implementationApi' configuration.
task implementationApiJar(type: Jar) {
  archiveBaseName = 'implementation'
  from(sourceSets.main.output) {
    include 'google/registry/persistence/NomulusPostgreSql**'
  }
}

configurations {
  implementationApi
  schema
  integration
}

artifacts {
  implementationApi implementationApiJar
  schema schemaJar
}

publishing {
  repositories {
    maven {
      url project.publish_repo
    }
  }
  publications {
    sqlSchemaPublication(MavenPublication) {
      groupId 'google.registry'
      artifactId 'schema'
      version project.schema_version
      artifact schemaJar
    }
  }
}

// Adds flyway tasks such as: flywayInfo, flywayValidate, flywayMigrate (
// deploying the schema in local repository), and flywayClean (dropping all data
// in the database). The latter two commands are disallowed in environments
// listed in ext.restrictedDbEnv.
//
// Examples:
// Get info in alpha: nom_build :db:flywayInfo --dbServer=alpha
// Deploy schema to a local test instance and override the database name:
// nom_build :db:flywayMigrate --dbServer=localhost:5432 --dbName=not-default \
//    --dbUser=... --dbPassword=...

flyway {
  def accessInfo = project.ext.getJdbcAccessInfo()

  url = accessInfo.url
  user = accessInfo.user
  password = accessInfo.password
  schemas = [ 'public' ]

  locations = [ "classpath:sql/flyway" ]
}

dependencies {
  def deps = rootProject.dependencyMap

  implementation deps['org.flywaydb:flyway-core']

  runtimeOnly deps['com.google.cloud.sql:postgres-socket-factory']
  runtimeOnly deps['org.postgresql:postgresql']

  testImplementation deps['com.google.flogger:flogger']
  testRuntimeOnly deps['com.google.flogger:flogger-system-backend']
  testImplementation deps['com.google.guava:guava']
  testImplementation deps['com.google.truth:truth']
  testImplementation deps['com.google.truth.extensions:truth-java8-extension']
  testRuntimeOnly deps['io.github.java-diff-utils:java-diff-utils']
  testImplementation deps['org.junit.jupiter:junit-jupiter-api']
  testImplementation deps['org.junit.jupiter:junit-jupiter-engine']
  testImplementation deps['org.testcontainers:postgresql']
  testImplementation deps['org.testcontainers:testcontainers']
  testImplementation deps['org.testcontainers:junit-jupiter']
  testImplementation deps['org.testcontainers:postgresql']
  testImplementation project(path: ':common', configuration: 'testing')
}

test {
  // When set, run FlywayDeadlockTest#validNewScript.
  def deadlock_check = 'do_flyway_deadlock_check'
  if (findProperty(deadlock_check)) {
    systemProperty deadlock_check, findProperty(deadlock_check)
  }
}

task generateFlywayIndex {
  def flywayBase = "$projectDir/src/main/resources/sql/flyway"
  def filenamePattern = /V(\d+)__.*\.sql/

  def getSeqNum = { file ->
    def match = file.getName() =~ filenamePattern
    if (match.size() != 1) {
      throw new IllegalArgumentException("Bad Flyway filename: $file")
    }
    return match[0][1] as int
  }

  doLast {
    def files = new File(flywayBase).listFiles()
    def indexFile = new File("${flywayBase}.txt")
    indexFile.write ''
    for (def file : files.sort{a, b -> getSeqNum(a) <=> getSeqNum(b)}) {
      indexFile << "${file.name}\n"
    }
  }
}

flywayInfo.dependsOn('buildNeeded')
flywayValidate.dependsOn('buildNeeded')

if (ext.isRestricted()) {
  // Disable dangerous Flyway tasks in sandbox and production. Only allow info and validate.
  tasks.findAll { task -> task.group.equals('Flyway')}.each {
    if (it.name == 'flywayMigrate') {
      it.doFirst {
        throw new UnsupportedOperationException(
            """ \
              FlywayMigrate is disabled. See README.md for schema deployment
              instructions.""".stripIndent())
      }
    } else if (it.name != 'flywayInfo' && it.name != 'flywayValidate') {
      it.doFirst {
        throw new UnsupportedOperationException(
            "${it.name} from commandline is not allowed.")
      }
    }
  }
}

if (project.baseSchemaTag != '') {
  repositories {
    maven {
      url project.publish_repo
    }
  }
  dependencies {
    integration "google.registry:schema:${project.baseSchemaTag}"
  }

  // Checks if Flyway scripts can be deployed to an existing database with
  // an older release. Please refer to SchemaTest.java for more information.
  task schemaIncrementalDeployTest(dependsOn: processResources, type: Test) {
    useJUnitPlatform()
    include 'google/registry/sql/flyway/SchemaTest.*'
    classpath = configurations.testRuntimeClasspath
        .plus(configurations.integration)
        .plus(files(sourceSets.test.output.classesDirs))
        .plus(files(sourceSets.test.output.resourcesDir))
        .plus(files(sourceSets.main.output.classesDirs))

    // Declare test-runtime dependency on Flyway scripts in the resources dir.
    // They are not on classpath since they conflict with the base schema.
    inputs.dir sourceSets.main.output.resourcesDir

    // Specifies which test to run using the following property
    systemProperty 'deploy_to_existing_db', 'true'
  }
}
