/*
 * Licensed Materials - Property of IBM Corp.
 * IBM UrbanCode Build
 * IBM UrbanCode Deploy
 * IBM UrbanCode Release
 * IBM AnthillPro
 * (c) Copyright IBM Corporation 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 com.urbancode.air.AirPluginTool
import com.urbancode.air.XTrustProvider
import com.urbancode.air.plugin.kubernetes.KubernetesHelper
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
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.HttpResponse;

XTrustProvider.install()
def COMPONENT_TAG='kubernetes.image'

// Get step properties
def airTool = new AirPluginTool(this.args[0], this.args[1])
def props = airTool.getStepProperties()
String username                     = props['username']?.trim()
String password                     = props['password']?.trim()
String resourceId                   = props['resource'].trim()
String resourcePath                 = props['resourcePath'].trim()
String applicationId                = props['application'].trim()
String applicationProcessRequestId  = props['applicationProcessRequest'].trim()
String componentProcessRequestId    = props['componentProcessRequest'].trim()
String environmentId                = props['environment'].trim()
String environmentName              = props['environmentName'].trim()
String kubeFilePath                 = props['yamlFile'].trim()
String sourceConfigType             = props['sourceConfigType'].trim()
String componentTemplate            = props['componentTemplate'].trim()
String alwaysVersionFromYaml        = props['alwaysVersionFromYaml'].trim()

// Sanitize properties
def serverUrl = System.getenv("AH_WEB_URL");
def serverUri = KubernetesHelper.stringToUri(serverUrl)
if (serverUri.equals(new URI(''))) {
    System.exit(1)
}
if (!username) {
    username = airTool.getAuthTokenUsername()
}
if (!password) {
    password = airTool.getAuthToken()
}


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)

applicationName = KubernetesHelper.parseTextAsJson(applicationClient.getApplication(applicationId)).name
String deploymentRequestId = componentClient.getComponentProcessRequest(componentProcessRequestId)?.deploymentRequestId
def existingResources = KubernetesHelper.parseTextAsJson(resourceClient.getResourceChildren(resourceId))
resource = resourceClient.getResourceById(resourceId);

// Read the yaml file, find all images used in it, and create a list of their potential component names
// Component names are in the format ApplicationName-ImageName
if (!kubeFilePath) {
    // if the user did not specify a yaml file, assume one or more files exist in the working dir
    String currentDir = new File(".").getAbsoluteFile().getParent();
    baseDir = new File(currentDir);
    allFilesInDir = baseDir.listFiles();
    if (allFilesInDir.size() == 0) {
        throw new Exception("No Kubernetes YAML file was specified and no files were found in " + baseDir);
    }
    if (allFilesInDir.size() == 1) {
        kubeFilePath = allFilesInDir[0];
    }
    if (allFilesInDir.size() > 1) {
        kubeFilePath = mergeYamlFiles(currentDir, allFilesInDir);
    }
}
else {
    // the user may have specified a directory instead of a file
    def specifiedKubeFile = new File(kubeFilePath);
    if (specifiedKubeFile.isDirectory()) {
        allFilesInDir = specifiedKubeFile.listFiles();
        kubeFilePath = mergeYamlFiles(kubeFilePath, allFilesInDir);
    }
}


def kubeFile = new File(kubeFilePath);
println "Loading ${kubeFile}"
imageComponentList = [];
def List newYamlLines = [];
yamlLines = kubeFile.readLines();
yamlLines.each { String line ->
    trimmedLine = line.trim();
    if (trimmedLine.contains("image: ") && (trimmedLine.indexOf("#") != 0)) {
        // get everything after "image: "
        image = trimmedLine.substring(trimmedLine.indexOf("image: ")+7);
        // there could be comments after the image name
        indexOfComment = image.indexOf("#");
        if (indexOfComment != -1) {
            image = image.substring(0, indexOfComment).trim();
        }
        // remove possible leading/trailing quotes
        firstChar = image.substring(0,1);
        lastChar = image.substring(image.length()-1, image.length());
        if (firstChar == "\"" || firstChar == "'") {
            image = image.substring(1);
        }
        if (lastChar == "\"" || lastChar == "'") {
            image = image.substring(0, image.length()-1);
        }
        parsedImage = KubernetesHelper.parseImageSpec(image);
        imageName = parsedImage.image;
        imageNameWithoutNameSpace = getImageNameWithoutNamespace(imageName);
        imageComponentName = applicationName + "-" + imageNameWithoutNameSpace;
        imageComponentList.add(imageComponentName);
    }
}


// Get the current inventory for any existing image components
// We need this in case the user did not specify a version for the component
// (to ensure we deploy the version UCD says is deployed)
Map<String, String> desiredInventory = new HashMap<String,String>()
components = KubernetesHelper.parseTextAsJson(applicationClient.getApplicationComponents(applicationId));
components.each { component ->
    componentId = componentClient.getComponentUUID(component.name).toString();
    if (imageComponentList.contains(component.name)) {
        // UDRestClient currently does not encode the component name for getLatestEnvironmentInventoryByComponent, so we need to
        sanitizedComponentName = restClient.sanitizePathSegment(component.name);
        inventory = KubernetesHelper.parseTextAsJson(environmentClient.getLatestEnvironmentInventoryByComponent(environmentName, applicationId, sanitizedComponentName));
        if (inventory) {
            imageName = component.name.substring(applicationName.length()+1);
            desiredInventory.put(imageName, inventory.version.name);
        }

        // if the component exists but is not mapped to our resource, map it
        if (!existingResources?.find{component.name.equals(it.name)}) {
            println "Mapping " + component.name + " to base resource"
            try {
                createdResource = KubernetesHelper.parseTextAsJson(resourceClient.createResource(component.name, null, null, resourceId, component.name))
                // need to set desired/actual inventory for these new resources
                if (inventory) {
                    if (inventory.version.name) {
                        def existingVersions = KubernetesHelper.parseTextAsJson(componentClient.getComponentVersionsJsonArray(component.name, false))
                        def versionId
                        existingVersions.each { def existingVersion ->
                            if (inventory.version.name.equals(existingVersion.name)) {
                                versionId = existingVersion.id
                            }
                        }
                        if (versionId) {
                            println "Updating desired/actual inventory for " + component.name
                            environmentClient.createDesiredInventoryEntry(deploymentRequestId, environmentId, componentId, versionId.toString(), 'Active')
                            resourceClient.createResourceInventoryEntry(deploymentRequestId, createdResource.id, componentId, versionId.toString(), 'Active')
                        }
                    }
                }
            }
            catch (IOException e){
                // do nothing; the resource probably exists
            }
        }
    }
}

// Get the user selected component versions
userSelectedVersions = KubernetesHelper.parseTextAsJson(applicationClient.getApplicationProcessRequestVersions(applicationProcessRequestId));
userSelectedVersions.each{ version ->
    componentName = version.component.name;
    component = KubernetesHelper.parseTextAsJson(componentClient.getComponent(componentName));
    // if this component has the yaml file (call it root resource), we don't want to create components/update inventory
    // if following the out-of-the box tutorial, the resource is a tag ("Kubernetes")
    // So, check components to see if they have the resource tag. If not using tags,
    // determine if component name = resource name.
    hasResourceNameAsTag = false
    if (component.tags?.find{resource.name.equals(it.name)}) {
        hasResourceNameAsTag = true;
    }
    isRootResourceComponent = true
    if (!hasResourceNameAsTag && !component.name.equals(resource.name)) {
        isRootResourceComponent = false
    }
    // components (for images) names are in the format ApplicationName-imageName
    if (!isRootResourceComponent && componentName.startsWith(applicationName)) {
        imageName = componentName.substring(applicationName.length()+1);
        desiredInventory.put(imageName, version.name)
    }
}

if (alwaysVersionFromYaml.equalsIgnoreCase("true")) {
  println "********************************************************************************************************************************************************"
  println "***** Applying container image tags as they are specified in the yaml. Tags selected from the UrbanCode Deploy user interface will NOT be applied. *****"
  println "********************************************************************************************************************************************************"
}
else {
  println "The desired versions for existing image components is " + desiredInventory
}


// Read the yaml file, replacing image versions as needed
imagesToCreateComponentsFor = [];
imagesNeedingInventoryUpdates = [];
replacedImages = [];
originalYamlLines = kubeFile.readLines();
originalYamlLines.each { String line ->
    trimmedLine = line.trim();
    if (trimmedLine.contains("image: ")) {
        // get everything after "image: "
        image = trimmedLine.substring(trimmedLine.indexOf("image: ")+7);
        // there could be comments after the image name
        indexOfComment = image.indexOf("#");
        if (indexOfComment != -1) {
          image = image.substring(0, indexOfComment).trim();
        }
        // remove possible leading/trailing quotes
        firstChar = image.substring(0,1);
        lastChar = image.substring(image.length()-1, image.length());
        if (firstChar == "\"" || firstChar == "'") {
          image = image.substring(1);
        }
        if (lastChar == "\"" || lastChar == "'") {
          image = image.substring(0, image.length()-1);
        }
        parsedImage = KubernetesHelper.parseImageSpec(image);
        imageName = parsedImage.image;
        imageVersion = parsedImage.version;
        if (desiredInventory != null) {
            imageNameWithoutNameSpace = getImageNameWithoutNamespace(imageName)
            userSelectedVersionForThisImage = desiredInventory.get(imageNameWithoutNameSpace);
            if (userSelectedVersionForThisImage != null) {
                // we found a user selected version for this image
                line = line.replace(":" + imageVersion, ":" + userSelectedVersionForThisImage);
                // keep track of any images we've replaced (so we may determine if any images
                // have been removed from the yaml)
                replacedImages.add(imageNameWithoutNameSpace);
                // if always applying the container image versions specified in yaml
                // (and ignoring current inventory and user selected versions),
                // we need to ensure UCD inventory is updated
                if (alwaysVersionFromYaml.equalsIgnoreCase("true")) {
                    imagesNeedingInventoryUpdates.add(parsedImage);
                }
            }
            else {
                // we need to create a component for this image
                imagesToCreateComponentsFor.add(parsedImage);
            }
        }
        else {
            // we need to create a component for this image
            imagesToCreateComponentsFor.add(parsedImage);
        }
  }
  newYamlLines << line;
}


// Create an new yaml file containing user selected versions for images
println "Creating ibm-ucd-kubernetes.yaml"
new File("ibm-ucd-kubernetes.yaml").withWriter { out ->
    if (alwaysVersionFromYaml.equalsIgnoreCase("true")) {
        originalYamlLines.each {
            out.println it
        }
    }
    else {
        newYamlLines.each {
            out.println it
        }
    }
}

// If the user specified a version for a container image component,
// but the image was not found in the yaml file, delete the resource and
// the desired inventory
for (replacedImage in replacedImages) {
    foundInDesiredInventory = desiredInventory.get(replacedImage);
    if (foundInDesiredInventory != null) {
        desiredInventory.remove(replacedImage);
    }
}

for (e in desiredInventory) {
    // get the component name
    componentName = applicationName + "-" + e.getKey();

    // delete the resource
    try {
        // get the resource by path
        path = resourcePath + "/" + componentName
        targetResource = resourceClient.getResourceByPath(path);
        println "Deleting resource " + path + " because the image it represents was not in the yaml file."
        resourceClient.deleteResource(targetResource.id)
    }
    catch (IOException ex){
          // do nothing; the resource probably does not exist
    }

    // delete the desired inventory
    try {
        componentId = componentClient.getComponentUUID(componentName).toString();
        environmentClient.deleteAllDesiredInventoryVersions(environmentId, componentId);
        println "Deleted desired inventory for " + componentName
    }
    catch (IOException ex){
        // do nothing
    }
}

// Create components
def applicationComponents = KubernetesHelper.parseTextAsJson(applicationClient.getApplicationComponents(applicationId))
imagesToCreateComponentsFor.each{ entry ->
    // image names may contain a slash (such as google-samples/gb-frontend)
    // however, having slashes in a component name will prevent us from using
    // the uDeployRestClient. So, we will remove the namespace
    // (everything before and including the slash) when naming our component
    imageNameWithoutNameSpace = getImageNameWithoutNamespace(entry.image);
    componentName = applicationName + "-" + imageNameWithoutNameSpace;
    sanitizedComponentName = restClient.sanitizePathSegment(componentName);
    def registry = entry.registry
    def image = entry.image
    def tag
    if (entry.version) {
        tag = entry.version
    }
    else {
        tag = 'latest'
    }

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

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

    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 to base reource ${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)

    createVersionAndUpdateDesiredInventory(componentClient, componentName, tag, deploymentRequestId, environmentId, environmentClient, versionClient, registry);
}

imagesNeedingInventoryUpdates.each{ entry ->
    // image names may contain a slash (such as google-samples/gb-frontend)
    // however, having slashes in a component name will prevent us from using
    // the uDeployRestClient. So, we will remove the namespace
    // (everything before and including the slash) when naming our component
    imageNameWithoutNameSpace = getImageNameWithoutNamespace(entry.image);
    componentName = applicationName + "-" + imageNameWithoutNameSpace;
    sanitizedComponentName = restClient.sanitizePathSegment(componentName);
    def registry = entry.registry
    def image = entry.image
    def tag
    if (entry.version) {
        tag = entry.version
    }
    else {
        tag = 'latest'
    }
    createVersionAndUpdateDesiredInventory(componentClient, componentName, tag, deploymentRequestId, environmentId, environmentClient, versionClient, registry);
}

// Update inventory for any components that were created
if (imagesToCreateComponentsFor.size() > 0 || imagesNeedingInventoryUpdates.size() > 0) {
    println "Updating inventory..."
    def children = KubernetesHelper.parseTextAsJson(resourceClient.getResourceChildren(resourceId))
    children.each { childResource ->
        def roles = KubernetesHelper.parseTextAsJson(resourceClient.getResourceRoles(childResource.id))
        roles.each { role ->
            // if child resource has role, update inventory
            def componentName = role.name
            def componentId = componentClient.getComponentUUID(componentName).toString();
            if (componentName.startsWith(applicationName + "-")) {
                imageName = componentName.substring(applicationName.length()+1);
                // get desired version
                desiredVersion = "";
                imagesToCreateComponentsFor.each{ entry ->
                    entryImageWithoutNamespace = getImageNameWithoutNamespace(entry.image)
                    if (entryImageWithoutNamespace == imageName) {
                        desiredVersion = entry.version;
                    }
                }
                // also need to update inventory for existing component if using applying
                // versions specified in the yaml
                imagesNeedingInventoryUpdates.each{ entry ->
                    entryImageWithoutNamespace = getImageNameWithoutNamespace(entry.image)
                    if (entryImageWithoutNamespace == imageName) {
                        desiredVersion = entry.version;
                    }
                }
                if (desiredVersion != "") {
                    // need to get the id of the version
                    def existingVersions = KubernetesHelper.parseTextAsJson(componentClient.getComponentVersionsJsonArray(componentName, false))
                    def versionId
                    existingVersions.each { def existingVersion ->
                        if (desiredVersion.equals(existingVersion.name)) {
                            versionId = existingVersion.id
                        }
                    }
                    if (versionId) {
                        println "Updating inventory for " + componentName
                        resourceClient.createResourceInventoryEntry(deploymentRequestId, childResource.id, componentId, versionId, 'Active')
                    }
                }
            }
        }
    }
}

/**
 *  @param imageName Name of a container image (which may be in the format namespace/image)
 *  @return The image name with the namespace and slash removed, if they were present
 */
private String getImageNameWithoutNamespace(String imageName) {
    String imageNameWithoutNamespace = imageName;
    int indexOfSlash = imageName.indexOf("/");
    if (indexOfSlash != -1) {
        imageNameWithoutNamespace = imageName.substring(indexOfSlash+1);
    }
    return imageNameWithoutNamespace;
}

/**
 *  @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 = KubernetesHelper.parseTextAsJson(client.getComponent(name)).id
    }
    catch (IOException e){
        println "Creating component: ${name}"
        result = client.createComponent(
                 name,
                 "Kubernetes image: ${image}",
                 configType,
                 'FULL',
                 template,
                 -1,
                 true,
                 false,
                 sourceProps)
    }

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

    return result
}

/**
 *  @param baseDir Path and name of the directory containing the files to process
 *  @param allFilesInDir A list of all files in the directory
 *  @return The name of the resulting, merged file
 */
private String mergeYamlFiles(String baseDir, File[] allFilesInDir) {
    // merge all files in the directory
    mergedFileName = "ibm-ucd-merged.yaml"
    println "More than one file found in " + baseDir + ". Creating new file " + mergedFileName + " and appending files to it."
    mergedFile = new File(mergedFileName);
    mergedFilePath = mergedFile.getAbsolutePath();
    oneOrMoreFilesFound = "false"
    allFilesInDir.each {
        yFileName = it.toString();
        extension = yFileName.substring(yFileName.lastIndexOf(".") + 1);
        if (extension.equalsIgnoreCase("yaml") || extension.equalsIgnoreCase("yml")) {
            println "Appending " + it.toString();
            mergedFile.append(it.getText());
            oneOrMoreFilesFound = "true"
        }
        else {
            println "Not appending " + yFileName + " because it is not a .yaml or .yml file."
        }
    }
    if (oneOrMoreFilesFound.equals("false")) {
        throw new Exception("Did not find any .yaml or .yml files in " + baseDir);
    }
    return mergedFilePath;
}

/**
 *  @param componentClient UDeployRest client for working with components
 *  @param componentName Name of the component to create a version for
 *  @param tag Tag of the container image
 *  @param deploymentRequestId ID of this deployment request
 *  @param environmentId ID of the current environment
 *  @param environmentClient UDeployRest client for working with environments
 *  @param versionClient UDeployRest client for working with versions
 *  @param registry Registry of the container image
 */
private void createVersionAndUpdateDesiredInventory(def componentClient, def componentName, def tag, def deploymentRequestId, def environmentId, def environmentClient, def versionClient, def registry) {
    def componentId = componentClient.getComponentUUID(componentName).toString();
    def existingVersions = KubernetesHelper.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, "Imported from Kubernetes yaml")

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

    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)
    }
}
