/*
 * Initscript for injection of Develocity into Gradle builds.
 * Version: v1.1
 */

import org.gradle.util.GradleVersion

initscript {
    // NOTE: there is no mechanism to share code between the initscript{} block and the main script, so some logic is duplicated
    def isTopLevelBuild = !gradle.parent
    if (!isTopLevelBuild) {
        return
    }

    def getInputParam = { Gradle gradle, String name ->
        def ENV_VAR_PREFIX = ''
        def envVarName = ENV_VAR_PREFIX + name.toUpperCase().replace('.', '_').replace('-', '_')
        return gradle.startParameter.systemPropertiesArgs[name] ?: System.getProperty(name) ?: System.getenv(envVarName)
    }

    def requestedInitScriptName = getInputParam(gradle, 'develocity.injection.init-script-name')
    def initScriptName = buildscript.sourceFile.name
    if (requestedInitScriptName != initScriptName) {
        return
    }

    // Plugin loading is only required for Develocity injection. Abort early if not enabled.
    def develocityInjectionEnabled = Boolean.parseBoolean(getInputParam(gradle, "develocity.injection-enabled"))
    if (!develocityInjectionEnabled) {
        return
    }

    def pluginRepositoryUrl = getInputParam(gradle, 'gradle.plugin-repository.url')
    def pluginRepositoryUsername = getInputParam(gradle, 'gradle.plugin-repository.username')
    def pluginRepositoryPassword = getInputParam(gradle, 'gradle.plugin-repository.password')
    def develocityPluginVersion = getInputParam(gradle, 'develocity.plugin.version')
    def ccudPluginVersion = getInputParam(gradle, 'develocity.ccud-plugin.version')

    def atLeastGradle5 = GradleVersion.current() >= GradleVersion.version('5.0')
    def atLeastGradle4 = GradleVersion.current() >= GradleVersion.version('4.0')

    if (develocityPluginVersion || ccudPluginVersion && atLeastGradle4) {
        pluginRepositoryUrl = pluginRepositoryUrl ?: 'https://plugins.gradle.org/m2'
        logger.lifecycle("Develocity plugins resolution: $pluginRepositoryUrl")

        repositories {
            maven {
                url pluginRepositoryUrl
                if (pluginRepositoryUsername && pluginRepositoryPassword) {
                    logger.lifecycle("Using credentials for plugin repository")
                    credentials {
                        username(pluginRepositoryUsername)
                        password(pluginRepositoryPassword)
                    }
                    authentication {
                        basic(BasicAuthentication)
                    }
                }
            }
        }
    }

    dependencies {
        if (develocityPluginVersion) {
            if (atLeastGradle5) {
                if (GradleVersion.version(develocityPluginVersion) >= GradleVersion.version("3.17")) {
                    classpath "com.gradle:develocity-gradle-plugin:$develocityPluginVersion"
                } else {
                    classpath "com.gradle:gradle-enterprise-gradle-plugin:$develocityPluginVersion"
                }
            } else {
                classpath "com.gradle:build-scan-plugin:1.16"
            }
        }

        if (ccudPluginVersion && atLeastGradle4) {
            classpath "com.gradle:common-custom-user-data-gradle-plugin:$ccudPluginVersion"
        }
    }
}

static getInputParam(Gradle gradle, String name) {
    def ENV_VAR_PREFIX = ''
    def envVarName = ENV_VAR_PREFIX + name.toUpperCase().replace('.', '_').replace('-', '_')
    return gradle.startParameter.systemPropertiesArgs[name] ?: System.getProperty(name) ?: System.getenv(envVarName)
}

def isTopLevelBuild = !gradle.parent
if (!isTopLevelBuild) {
    return
}

def requestedInitScriptName = getInputParam(gradle, 'develocity.injection.init-script-name')
def initScriptName = buildscript.sourceFile.name
if (requestedInitScriptName != initScriptName) {
    logger.quiet("Ignoring init script '${initScriptName}' as requested name '${requestedInitScriptName}' does not match")
    return
}

def develocityInjectionEnabled = Boolean.parseBoolean(getInputParam(gradle, "develocity.injection-enabled"))
if (develocityInjectionEnabled) {
    enableDevelocityInjection()
}

// To enable build-scan capture, a `captureBuildScanLink(String)` method must be added to `BuildScanCollector`.
def buildScanCollector = new BuildScanCollector()
def buildScanCaptureEnabled = buildScanCollector.metaClass.respondsTo(buildScanCollector, 'captureBuildScanLink', String)
if (buildScanCaptureEnabled) {
    enableBuildScanLinkCapture(buildScanCollector)
}

void enableDevelocityInjection() {
    def BUILD_SCAN_PLUGIN_ID = 'com.gradle.build-scan'
    def BUILD_SCAN_PLUGIN_CLASS = 'com.gradle.scan.plugin.BuildScanPlugin'

    def GRADLE_ENTERPRISE_PLUGIN_ID = 'com.gradle.enterprise'
    def GRADLE_ENTERPRISE_PLUGIN_CLASS = 'com.gradle.enterprise.gradleplugin.GradleEnterprisePlugin'

    def DEVELOCITY_PLUGIN_ID = 'com.gradle.develocity'
    def DEVELOCITY_PLUGIN_CLASS = 'com.gradle.develocity.agent.gradle.DevelocityPlugin'

    def CI_AUTO_INJECTION_CUSTOM_VALUE_NAME = 'CI auto injection'
    def CCUD_PLUGIN_ID = 'com.gradle.common-custom-user-data-gradle-plugin'
    def CCUD_PLUGIN_CLASS = 'com.gradle.CommonCustomUserDataGradlePlugin'

    def develocityUrl = getInputParam(gradle, 'develocity.url')
    def develocityAllowUntrustedServer = Boolean.parseBoolean(getInputParam(gradle, 'develocity.allow-untrusted-server'))
    def develocityEnforceUrl = Boolean.parseBoolean(getInputParam(gradle, 'develocity.enforce-url'))
    def buildScanUploadInBackground = Boolean.parseBoolean(getInputParam(gradle, 'develocity.build-scan.upload-in-background'))
    def develocityCaptureFileFingerprints = getInputParam(gradle, 'develocity.capture-file-fingerprints') ? Boolean.parseBoolean(getInputParam(gradle, 'develocity.capture-file-fingerprints')) : true
    def develocityPluginVersion = getInputParam(gradle, 'develocity.plugin.version')
    def ccudPluginVersion = getInputParam(gradle, 'develocity.ccud-plugin.version')
    def buildScanTermsOfUseUrl = getInputParam(gradle, 'develocity.terms-of-use.url')
    def buildScanTermsOfUseAgree = getInputParam(gradle, 'develocity.terms-of-use.agree')
    def ciAutoInjectionCustomValueValue = getInputParam(gradle, 'develocity.auto-injection.custom-value')

    def atLeastGradle5 = GradleVersion.current() >= GradleVersion.version('5.0')
    def atLeastGradle4 = GradleVersion.current() >= GradleVersion.version('4.0')
    def shouldApplyDevelocityPlugin = atLeastGradle5 && develocityPluginVersion && isAtLeast(develocityPluginVersion, '3.17')

    def dvOrGe = { def dvValue, def geValue ->
        if (shouldApplyDevelocityPlugin) {
            return dvValue instanceof Closure<?> ? dvValue() : dvValue
        }
        return geValue instanceof Closure<?> ? geValue() : geValue
    }

    def printEnforcingDevelocityUrl = {
        logger.lifecycle("Enforcing Develocity: $develocityUrl, allowUntrustedServer: $develocityAllowUntrustedServer")
    }

    def printAcceptingGradleTermsOfUse = {
        logger.lifecycle("Accepting Gradle Terms of Use: $buildScanTermsOfUseUrl")
    }

    // finish early if configuration parameters passed in via system properties are not valid/supported
    if (ccudPluginVersion && isNotAtLeast(ccudPluginVersion, '1.7')) {
        logger.warn("Common Custom User Data Gradle plugin must be at least 1.7. Configured version is $ccudPluginVersion.")
        return
    }

    // Conditionally apply and configure the Develocity plugin
    if (GradleVersion.current() < GradleVersion.version('6.0')) {
        rootProject {
            buildscript.configurations.getByName("classpath").incoming.afterResolve { ResolvableDependencies incoming ->
                def resolutionResult = incoming.resolutionResult

                if (develocityPluginVersion) {
                    def scanPluginComponent = resolutionResult.allComponents.find {
                        it.moduleVersion.with { group == "com.gradle" && ['build-scan-plugin', 'gradle-enterprise-gradle-plugin', 'develocity-gradle-plugin'].contains(name) }
                    }
                    if (!scanPluginComponent) {
                        def pluginClass = dvOrGe(DEVELOCITY_PLUGIN_CLASS, BUILD_SCAN_PLUGIN_CLASS)
                        def pluginVersion = atLeastGradle5 ? develocityPluginVersion : "1.16"
                        applyPluginExternally(pluginManager, pluginClass, pluginVersion)
                        def rootExtension = dvOrGe(
                            { develocity },
                            { buildScan }
                        )
                        def buildScanExtension = dvOrGe(
                            { rootExtension.buildScan },
                            { rootExtension }
                        )
                        if (develocityUrl) {
                            logger.lifecycle("Connection to Develocity: $develocityUrl, allowUntrustedServer: $develocityAllowUntrustedServer, captureFileFingerprints: $develocityCaptureFileFingerprints")
                            rootExtension.server = develocityUrl
                            rootExtension.allowUntrustedServer = develocityAllowUntrustedServer
                        }
                        if (!shouldApplyDevelocityPlugin) {
                            // Develocity plugin publishes scans by default
                            buildScanExtension.publishAlways()
                        }
                        // uploadInBackground not available for build-scan-plugin 1.16
                        if (buildScanExtension.metaClass.respondsTo(buildScanExtension, 'setUploadInBackground', Boolean)) buildScanExtension.uploadInBackground = buildScanUploadInBackground
                        buildScanExtension.value CI_AUTO_INJECTION_CUSTOM_VALUE_NAME, ciAutoInjectionCustomValueValue
                        if (isAtLeast(develocityPluginVersion, '2.1') && atLeastGradle5) {
                            logger.lifecycle("Setting captureFileFingerprints: $develocityCaptureFileFingerprints")
                            if (isAtLeast(develocityPluginVersion, '3.17')) {
                                buildScanExtension.capture.fileFingerprints.set(develocityCaptureFileFingerprints)
                            } else if (isAtLeast(develocityPluginVersion, '3.7')) {
                                buildScanExtension.capture.taskInputFiles = develocityCaptureFileFingerprints
                            } else {
                                buildScanExtension.captureTaskInputFiles = develocityCaptureFileFingerprints
                            }
                        }
                    }
                }

                eachDevelocityProjectExtension(project,
                    { develocity ->
                        afterEvaluate {
                            if (develocityUrl && develocityEnforceUrl) {
                                printEnforcingDevelocityUrl()
                                develocity.server = develocityUrl
                                develocity.allowUntrustedServer = develocityAllowUntrustedServer
                            }
                        }

                        if (buildScanTermsOfUseUrl && buildScanTermsOfUseAgree) {
                            printAcceptingGradleTermsOfUse()
                            develocity.buildScan.termsOfUseUrl = buildScanTermsOfUseUrl
                            develocity.buildScan.termsOfUseAgree = buildScanTermsOfUseAgree
                        }
                    },
                    { buildScan ->
                        afterEvaluate {
                            if (develocityUrl && develocityEnforceUrl) {
                                printEnforcingDevelocityUrl()
                                buildScan.server = develocityUrl
                                buildScan.allowUntrustedServer = develocityAllowUntrustedServer
                            }
                        }

                        if (buildScanTermsOfUseUrl && buildScanTermsOfUseAgree) {
                            printAcceptingGradleTermsOfUse()
                            if (buildScan.metaClass.respondsTo(buildScan, 'setTermsOfServiceUrl', String)) {
                                buildScan.termsOfServiceUrl = buildScanTermsOfUseUrl
                                buildScan.termsOfServiceAgree = buildScanTermsOfUseAgree
                            } else {
                                buildScan.licenseAgreementUrl = buildScanTermsOfUseUrl
                                buildScan.licenseAgree = buildScanTermsOfUseAgree
                            }
                        }
                    }
                )

                if (ccudPluginVersion && atLeastGradle4) {
                    def ccudPluginComponent = resolutionResult.allComponents.find {
                        it.moduleVersion.with { group == "com.gradle" && name == "common-custom-user-data-gradle-plugin" }
                    }
                    if (!ccudPluginComponent) {
                        logger.lifecycle("Applying $CCUD_PLUGIN_CLASS with version $ccudPluginVersion via init script")
                        pluginManager.apply(initscript.classLoader.loadClass(CCUD_PLUGIN_CLASS))
                    }
                }
            }
        }
    } else {
        gradle.settingsEvaluated { settings ->
            if (develocityPluginVersion) {
                if (!settings.pluginManager.hasPlugin(GRADLE_ENTERPRISE_PLUGIN_ID) && !settings.pluginManager.hasPlugin(DEVELOCITY_PLUGIN_ID)) {
                    def pluginClass = dvOrGe(DEVELOCITY_PLUGIN_CLASS, GRADLE_ENTERPRISE_PLUGIN_CLASS)
                    applyPluginExternally(settings.pluginManager, pluginClass, develocityPluginVersion)
                    if (develocityUrl) {
                        logger.lifecycle("Connection to Develocity: $develocityUrl, allowUntrustedServer: $develocityAllowUntrustedServer, captureFileFingerprints: $develocityCaptureFileFingerprints")
                        eachDevelocitySettingsExtension(settings) { ext ->
                            // server and allowUntrustedServer must be configured via buildScan extension for gradle-enterprise-plugin 3.1.1 and earlier
                            if (ext.metaClass.respondsTo(ext, 'getServer')) {
                                ext.server = develocityUrl
                                ext.allowUntrustedServer = develocityAllowUntrustedServer
                            } else {
                                ext.buildScan.server = develocityUrl
                                ext.buildScan.allowUntrustedServer = develocityAllowUntrustedServer
                            }
                        }
                    }

                    eachDevelocitySettingsExtension(settings) { ext ->
                        ext.buildScan.uploadInBackground = buildScanUploadInBackground
                        ext.buildScan.value CI_AUTO_INJECTION_CUSTOM_VALUE_NAME, ciAutoInjectionCustomValueValue
                    }

                    eachDevelocitySettingsExtension(settings,
                        { develocity ->
                            logger.lifecycle("Setting captureFileFingerprints: $develocityCaptureFileFingerprints")
                            develocity.buildScan.capture.fileFingerprints = develocityCaptureFileFingerprints
                        },
                        { gradleEnterprise ->
                            gradleEnterprise.buildScan.publishAlways()
                            if (isAtLeast(develocityPluginVersion, '2.1')) {
                                logger.lifecycle("Setting captureFileFingerprints: $develocityCaptureFileFingerprints")
                                if (isAtLeast(develocityPluginVersion, '3.7')) {
                                    gradleEnterprise.buildScan.capture.taskInputFiles = develocityCaptureFileFingerprints
                                } else {
                                    gradleEnterprise.buildScan.captureTaskInputFiles = develocityCaptureFileFingerprints
                                }
                            }
                        }
                    )
                }
            }

            eachDevelocitySettingsExtension(settings,
                { develocity ->
                    if (develocityUrl && develocityEnforceUrl) {
                        printEnforcingDevelocityUrl()
                        develocity.server = develocityUrl
                        develocity.allowUntrustedServer = develocityAllowUntrustedServer
                    }

                    if (buildScanTermsOfUseUrl && buildScanTermsOfUseAgree) {
                        printAcceptingGradleTermsOfUse()
                        develocity.buildScan.termsOfUseUrl = buildScanTermsOfUseUrl
                        develocity.buildScan.termsOfUseAgree = buildScanTermsOfUseAgree
                    }
                },
                { gradleEnterprise ->
                    if (develocityUrl && develocityEnforceUrl) {
                        printEnforcingDevelocityUrl()
                        // server and allowUntrustedServer must be configured via buildScan extension for gradle-enterprise-plugin 3.1.1 and earlier
                        if (gradleEnterprise.metaClass.respondsTo(gradleEnterprise, 'getServer')) {
                            gradleEnterprise.server = develocityUrl
                            gradleEnterprise.allowUntrustedServer = develocityAllowUntrustedServer
                        } else {
                            gradleEnterprise.buildScan.server = develocityUrl
                            gradleEnterprise.buildScan.allowUntrustedServer = develocityAllowUntrustedServer
                        }
                    }

                    if (buildScanTermsOfUseUrl && buildScanTermsOfUseAgree) {
                        printAcceptingGradleTermsOfUse()
                        gradleEnterprise.buildScan.termsOfServiceUrl = buildScanTermsOfUseUrl
                        gradleEnterprise.buildScan.termsOfServiceAgree = buildScanTermsOfUseAgree
                    }
                }
            )

            if (ccudPluginVersion) {
                if (!settings.pluginManager.hasPlugin(CCUD_PLUGIN_ID)) {
                    logger.lifecycle("Applying $CCUD_PLUGIN_CLASS with version $ccudPluginVersion via init script")
                    settings.pluginManager.apply(initscript.classLoader.loadClass(CCUD_PLUGIN_CLASS))
                }
            }
        }
    }
}

void applyPluginExternally(def pluginManager, String pluginClassName, String pluginVersion) {
    logger.lifecycle("Applying $pluginClassName with version $pluginVersion via init script")

    def externallyApplied = 'develocity.externally-applied'
    def externallyAppliedDeprecated = 'gradle.enterprise.externally-applied'
    def oldValue = System.getProperty(externallyApplied)
    def oldValueDeprecated = System.getProperty(externallyAppliedDeprecated)
    System.setProperty(externallyApplied, 'true')
    System.setProperty(externallyAppliedDeprecated, 'true')
    try {
        pluginManager.apply(initscript.classLoader.loadClass(pluginClassName))
    } finally {
        if (oldValue == null) {
            System.clearProperty(externallyApplied)
        } else {
            System.setProperty(externallyApplied, oldValue)
        }
        if (oldValueDeprecated == null) {
            System.clearProperty(externallyAppliedDeprecated)
        } else {
            System.setProperty(externallyAppliedDeprecated, oldValueDeprecated)
        }
    }
}

/**
 * Apply the `dvAction` to all 'develocity' extensions.
 * If no 'develocity' extensions are found, apply the `geAction` to all 'gradleEnterprise' extensions.
 * (The develocity plugin creates both extensions, and we want to prefer configuring 'develocity').
 */
static def eachDevelocitySettingsExtension(def settings, def dvAction, def geAction = dvAction) {
    def GRADLE_ENTERPRISE_EXTENSION_CLASS = 'com.gradle.enterprise.gradleplugin.GradleEnterpriseExtension'
    def DEVELOCITY_CONFIGURATION_CLASS = 'com.gradle.develocity.agent.gradle.DevelocityConfiguration'

    def dvExtensions = settings.extensions.extensionsSchema.elements
        .findAll { it.publicType.concreteClass.name == DEVELOCITY_CONFIGURATION_CLASS }
        .collect { settings[it.name] }
    if (!dvExtensions.empty) {
        dvExtensions.each(dvAction)
    } else {
        def geExtensions = settings.extensions.extensionsSchema.elements
            .findAll { it.publicType.concreteClass.name == GRADLE_ENTERPRISE_EXTENSION_CLASS }
            .collect { settings[it.name] }
        geExtensions.each(geAction)
    }
}

/**
 * Apply the `dvAction` to the 'develocity' extension.
 * If no 'develocity' extension is found, apply the `bsAction` to the 'buildScan' extension.
 * (The develocity plugin creates both extensions, and we want to prefer configuring 'develocity').
 */
static def eachDevelocityProjectExtension(def project, def dvAction, def bsAction = dvAction) {
    def BUILD_SCAN_PLUGIN_ID = 'com.gradle.build-scan'
    def DEVELOCITY_PLUGIN_ID = 'com.gradle.develocity'

    def configureDvOrBsExtension = {
        if (project.extensions.findByName("develocity")) {
            dvAction(project.develocity)
        } else {
            bsAction(project.buildScan)
        }
    }

    project.pluginManager.withPlugin(BUILD_SCAN_PLUGIN_ID, configureDvOrBsExtension)

    project.pluginManager.withPlugin(DEVELOCITY_PLUGIN_ID) {
        // Proper extension will be configured by the build-scan callback.
        if (project.pluginManager.hasPlugin(BUILD_SCAN_PLUGIN_ID)) return
        configureDvOrBsExtension()
    }
}

static boolean isAtLeast(String versionUnderTest, String referenceVersion) {
    GradleVersion.version(versionUnderTest) >= GradleVersion.version(referenceVersion)
}

static boolean isNotAtLeast(String versionUnderTest, String referenceVersion) {
    !isAtLeast(versionUnderTest, referenceVersion)
}

void enableBuildScanLinkCapture(BuildScanCollector collector) {
    // Conditionally apply and configure the Develocity plugin
    if (GradleVersion.current() < GradleVersion.version('6.0')) {
        rootProject {
            eachDevelocityProjectExtension(project,
                { develocity -> buildScanPublishedAction(develocity.buildScan, collector) },
                { buildScan  -> buildScanPublishedAction(buildScan, collector) }
            )
        }
    } else {
        gradle.settingsEvaluated { settings ->
            eachDevelocitySettingsExtension(settings) { ext ->
                buildScanPublishedAction(ext.buildScan, collector)
            }
        }
    }
}

// Action will only be called if a `BuildScanCollector.captureBuildScanLink` method is present.
// Add `void captureBuildScanLink(String) {}` to the `BuildScanCollector` class to respond to buildScanPublished events
static buildScanPublishedAction(def buildScanExtension, BuildScanCollector collector) {
    if (buildScanExtension.metaClass.respondsTo(buildScanExtension, 'buildScanPublished', Action)) {
        buildScanExtension.buildScanPublished { scan ->
            collector.captureBuildScanLink(scan.buildScanUri.toString())
        }
    }
}

class BuildScanCollector {}
