/*
* Licensed Materials - Property of IBM Corp.
* IBM UrbanCode Build
* IBM UrbanCode Deploy
* IBM UrbanCode Release
* IBM AnthillPro
* (c) Copyright IBM Corporation 2002, 2017. All Rights Reserved.
*
* U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
* GSA ADP Schedule Contract with IBM Corp.
*/
import org.codehaus.jettison.json.JSONArray
import org.codehaus.jettison.json.JSONObject
import org.yaml.snakeyaml.Yaml

import com.urbancode.air.AirPluginTool
import com.urbancode.air.XTrustProvider
import com.urbancode.air.plugin.docker.DockerUtils
import com.urbancode.ud.client.ApplicationClient
import com.urbancode.ud.client.ComponentClient
import com.urbancode.ud.client.EnvironmentClient
import com.urbancode.ud.client.ResourceClient
import com.urbancode.ud.client.VersionClient
import com.urbancode.ud.client.PropertyClient
import com.urbancode.ud.client.UDRestClient

XTrustProvider.install()
int MAX_RETRIES = 3
def COMPONENT_TAG='compose.service'

// Get step properties
def airTool = new AirPluginTool(this.args[0], this.args[1])
def props = airTool.getStepProperties()
String serverUrl                    = props['serverUrl']
String username                     = props['username']
String password                     = props['password']
String resourceId                   = props['resource']
String applicationId                = props['application']
String applicationProcessRequestId  = props['applicationProcessRequest']
String componentProcessRequestId    = props['componentProcessRequest']
String environmentId                = props['environment']
String environmentName              = props['environmentName']
String composeComponentId           = props['component']
String composeFilesRaw              = props['composeFiles']
String sourceConfigType             = props['sourceConfigType']
String componentTemplate            = props['componentTemplate']

// Sanitize properties
def serverUri = DockerUtils.stringToUri(serverUrl)
if (serverUri.equals(new URI(''))) {
    System.exit(1)
}
if (!username) {
    username = airTool.getAuthTokenUsername()
}
if (!password) {
    password = airTool.getAuthToken()
}
def composeFilePaths = DockerUtils.toTrimmedList(composeFilesRaw, '\n')
if (!composeFilePaths) {
    composeFilePaths << 'docker-compose.yml'
}

def composeFiles = []
def envPropReferences = [] as Set
Yaml yaml = new Yaml()
ApplicationClient applicationClient = new ApplicationClient(serverUri, username, password)
ComponentClient componentClient = new ComponentClient(serverUri, username, password)
EnvironmentClient environmentClient = new EnvironmentClient(serverUri, username, password)
ResourceClient resourceClient = new ResourceClient(serverUri,  username, password)
VersionClient versionClient = new VersionClient(serverUri, username, password)
PropertyClient propertyClient = new PropertyClient(serverUri, username, password)
UDRestClient restClient = new UDRestClient(serverUri, username, password)

def applicationName = DockerUtils.parseTextAsJson(applicationClient.getApplication(applicationId)).name
String deploymentRequestId = componentClient.getComponentProcessRequest(componentProcessRequestId)?.deploymentRequestId

// Add all compose files and their env props
composeFilePaths.each { path ->
    def composeFile = new File(path)
    if (!composeFile.exists()) {
        throw new RuntimeException("Could not find compose file, ${path}")
    }
    composeFiles << composeFile
    envPropReferences.addAll(DockerUtils.getEnvReferences(composeFile.text))
}

// Get compose file version
def baseComposeFile = composeFiles[0]
println "Loading ${baseComposeFile}"
def composeYml = yaml.load(baseComposeFile.text)
def serviceList
def composeFileVersion
try {
    composeFileVersion = composeYml.version.isInteger() ? composeYml.version.toInteger() : null
    println "Found compose file format version ${composeFileVersion}"
    if (composeFileVersion >= 2) {
        serviceList = composeYml.services
    }
    else {
        serviceList = composeYml
    }
}
catch (MissingPropertyException e) {
    println 'Found compose file format version 1'
    composeFileVersion = 1
    serviceList = composeYml
}

// Environment property definitions
JSONArray propDefs = new JSONArray()
String propSheetDefPath = "components/${composeComponentId}/environmentPropSheetDef"
if (envPropReferences.size()) {
    println 'Creating environment property definitions for the following properties:'
    envPropReferences.each { propReference ->
        println "\t${propReference}"
        JSONObject propDef = new JSONObject()
        propDef.put('name', propReference)
        propDef.put('label', propReference)
        propDef.put('type', 'TEXT')
        propDefs.put(propDef)
    }
}


// Create compose.service.scale environment property
JSONArray componentEnvironmentProperties = propertyClient.getPropSheetDefPropDefs(propSheetDefPath)
String composeServiceScaleStr = ""
for(int i = 0; i < componentEnvironmentProperties.length(); i++) {
    JSONObject compEnvProp = componentEnvironmentProperties.getJSONObject(i);
    if (compEnvProp["name"] == "compose.service.scale") {
        composeServiceScaleStr = compEnvProp["value"]
        break
    }
}

def scaleValsArr = composeServiceScaleStr.split("\n|,")*.trim() - ""
def splitScaleValsMap = scaleValsArr*.split("=", 2)
// Get first elements (service names)
def currentServiceNames = []
splitScaleValsMap.each { it ->
    currentServiceNames << it[0]
}

serviceList.each {service ->
    if (!currentServiceNames.contains(service.getKey())) {
        scaleValsArr << "${service.getKey()}=1"
    }
}
JSONObject propDef = new JSONObject()
propDef.put('name', 'compose.service.scale')
propDef.put('label', 'compose.service.scale')
propDef.put('description', "Specify the scale container amount for each Docker service.")
propDef.put('type', 'TEXTAREA')
propDef.put('value', scaleValsArr.join("\n"))
propDefs.put(propDef)
propertyClient.updatePropDefs(propSheetDefPath, propDefs, false)


// Get the current inventory for existing components and set as overrides
// We need this in case the user did not specify a version for the component
def overrides = [:]
components = DockerUtils.parseTextAsJson(applicationClient.getApplicationComponents(applicationId));
components.each { component ->
    // UDRestClient currently does not encode the component name for getLatestEnvironmentInventoryByComponent, so we need to
    sanitizedComponentName = restClient.sanitizePathSegment(component.name);
    inventory = DockerUtils.parseTextAsJson(environmentClient.getLatestEnvironmentInventoryByComponent(environmentName, applicationId, sanitizedComponentName));
    if ((inventory)) {
        componentProps = DockerUtils.parseTextAsJson(componentClient.getComponent(component.name)).properties
        imageName = getComponentProperty(componentProps, "docker.image.name", "template")
        registry = getComponentProperty(componentProps, "docker.registry.name", "custom")
        serviceName = getComponentProperty(componentProps, "compose.service", "custom")
        if (imageName) {
          String imageSpec = DockerUtils.buildImageSpec(registry, imageName, inventory.version.name)
          overrides.put(serviceName, ['image' : imageSpec]) //Docker compose format
        }
    }
}


// Build image specs, get versions
def manifest = [:]
def userSelectedVersions = DockerUtils.parseTextAsJson(applicationClient.getApplicationProcessRequestVersions(applicationProcessRequestId))
userSelectedVersions.each{ version ->
    def versionProps = versionClient.getVersionProperties(version.name, version.component.name)
    if (versionProps.containsKey('dockerImageTag')) {
        manifest.put(version.component.name, [version.name]) //uDeployRestClient format
        componentProps = DockerUtils.parseTextAsJson(componentClient.getComponent(version.component.name)).properties
        imageName = getComponentProperty(componentProps, "docker.image.name", "template")
        registry = getComponentProperty(componentProps, "docker.registry.name", "custom")
        serviceName = getComponentProperty(componentProps, "compose.service", "custom")
        def tag = versionProps.get('dockerImageTag')

        if (imageName) {
            String imageSpec = DockerUtils.buildImageSpec(registry, imageName, tag)
            overrides.put(serviceName, ['image' : imageSpec]) //Docker compose format
        }
        else {
            println "Skipping override for ${version.component.name}"
        }
    }
}

// Create components
def existingResources = DockerUtils.parseTextAsJson(resourceClient.getResourceChildren(resourceId))
def applicationComponents = DockerUtils.parseTextAsJson(applicationClient.getApplicationComponents(applicationId))
serviceList.each{ service ->
    //create component for each service
    if (service.value.image) {
        String componentName = "${applicationName.replace(' ', '')}-${service.key}"

        def tagSpec = DockerUtils.parseImageSpec(service.value.image)

        def registry = tagSpec.registry
        def image = tagSpec.image
        def tag
        if (tagSpec.version) {
            tag = tagSpec.version
        }
        else {
            tag = 'latest'
        }

        def sourceProps = [:]
        if (registry) {
            sourceProps.put('DockerTagImport/dockerRegistryName', registry)
        }
        else {
            sourceProps.put('DockerTagImport/dockerRegistryName', '')
        }
        sourceProps.put('DockerTagImport/dockerImageName', image)

        def componentProps = new Properties()
        componentProps.setProperty('docker.image.name', image)
        if (registry) {
            componentProps.setProperty('docker.registry.name', registry)
        }
        else {
            componentProps.setProperty('docker.registry.name', '')
        }

        componentProps.setProperty('compose.service', service.key)

        String componentId = createComponent(componentClient, componentName, sourceProps, componentProps,
                                             sourceConfigType, componentTemplate).toString()

        //map components to this base resource
        //Currently assigns name componentName - if creating multiple instances of a container, should be unique

        if (!existingResources?.find{ componentName.equals(it.name) }) {
            println "Mapping compose service, ${componentName}"
            println(existingResources)
            try {
                resourceClient.createResource(componentName, null, null, resourceId, componentName)
            }
            catch (IOException e){
              // do nothing; the resource probably exists
            }
        }

        if (applicationComponents && !applicationComponents.find{ resourceId.equals(it.id) }) {
            applicationClient.addComponentToApplication(applicationId, componentId)
        }

        componentClient.addTagToComponent(componentId, COMPONENT_TAG)

        def existingVersions = DockerUtils.parseTextAsJson(componentClient.getComponentVersionsJsonArray(componentName, false))


        def versionId

        existingVersions.each { def existingVersion ->
            if (tag.equals(existingVersion.name)) {
                versionId = existingVersion.id
            }
        }

        if (!versionId) {
            versionId = versionClient.createVersion(componentId, tag, "Compose service for image: ${image}")

            versionClient.setVersionProperty(tag, componentId, 'dockerImageTag', tag, false)
            if (registry) {
                versionClient.setVersionProperty(tag, componentId, 'registry', registry, false)
            }
            else {
                versionClient.setVersionProperty(tag, componentId, 'registry', '', false)
            }
        }

        if (!manifest.containsKey(componentName)){
            manifest.put(componentName, [tag])
            try {
                environmentClient.createDesiredInventoryEntry(deploymentRequestId, environmentId, componentId, versionId.toString(), 'Active')
            }
            catch (IOException ex) {
                throw new IOException("[Error] Unable to update the Application manifest.\n " +
                    "[Possible Solution] IBM UrbanCode Deploy v6.2+ is required for this step to complete successfully.", ex)
            }
        }
    }
    else {
        println "Skipping component creation for ${service.key} due to build specification"
    }
}

// Set inventory versions
if (overrides) {
    println "The following versions will be set in the overrides file:\n${overrides}"
    def servicesYaml
    if (composeFileVersion < 2) {
        servicesYaml = yaml.dump(overrides)
    }
    else {
        servicesYaml = yaml.dump(['version': String.valueOf(composeFileVersion), 'services': overrides])
    }

    def composeOverrideFile = new File('ucd-version-overrides.yml')
    if (composeOverrideFile.exists()) {
        composeOverrideFile.delete()
    }
    composeOverrideFile << servicesYaml

    println ('Setting desired inventory to the following versions:')
    overrides.each { println "\t[${it.key}:\t\t${it.value}]" }
}

// End of main processing, set any output properties
airTool.setOutputProperty("composeFileVersion", String.valueOf(composeFileVersion))
airTool.storeOutputProperties()


/**
*  @param name The name of the component to create
*  @param sourceProps The list of source properties to set for the component
*  @param componentProps The list of properties to set for the component
*/
private def createComponent(def client, String name, def sourceProps, def componentProps, def configType, def template) {
    def result
    def image = sourceProps['DockerTagImport/dockerImageName']
    try {
        result = DockerUtils.parseTextAsJson(client.getComponent(name)).id
    }
    catch (IOException e){
        println "Creating component: ${name}"
        result = client.createComponent(
                name,
                "Compose service for image: ${image}",
                configType,
                'FULL',
                template,
                -1,
                true,
                false,
                sourceProps)
    }

    componentProps.each { componentProp ->
        client.setComponentProperty(result.toString(), componentProp.key, componentProp.value, false)
    }

    return result
}

/**
*  @param properties The JSON array of component properties. Each array element has id, name, value, description and secure
*                    fields.  The property names are prefixed with "custom/", "template/", "DockerTagImport/", etc to
*                    separate the various "types" of component properties.
*  @param propName   The name of the component property we are trying to retrieve.
*  @param prefix     The prefix/type of the property we are looking for.  If left blank, we will return the value of the
*                    first property that ends with the specified property name.
*
*  Elements in the JSON array of component properties have the following form
*    {
*      "id": "a342fa8b-9d7b-4f84-984e-ab66af8ba202",
*      "name": "template\/docker.image.name",
*      "value": "library\/mysql",
*      "description": "",
*      "secure": false
*    }
*
*/
String getComponentProperty(def componentProps, String propName, String prefix = null) {
    String retVal = null
    componentProps.find { componentProp ->
        if (prefix) {
            if (componentProp.name == "${prefix}/${propName}") {
                retVal = componentProp.value
                return true // returns from closure, ie. break
            }
        }
        else {
            if (componentProp.name.endsWith("/${propName}")) {
                retVal = componentProp.value
                return true // returns from closure, ie. break
            }
        }
    }
    return retVal
}
