/*
 * 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.
 * (c) Copyright HCL Technologies Ltd. 2023. All Rights Reserved.
 *
 * U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
 * GSA ADP Schedule Contract with IBM Corp.
 */
package com.urbancode.air.plugin.argocd

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.argocd.ArgoCDHelper
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;

class Inventory {
    def COMPONENT_TAG='kubernetes.image'

    /**
    * Step Properties
    */
    private String username
    private String password
    private String resourceId
    private String resourcePath
    private String applicationId
    private String applicationProcessRequestId
    private String componentProcessRequestId
    private String environmentId
    private String environmentName
    private String sourceConfigType
    private String componentTemplate
    private String deleteMissingInventory

    /**
    * UDRestClient Objects
    */
    private	ApplicationClient applicationClient
	private	ComponentClient componentClient
	private	EnvironmentClient environmentClient
	private	ResourceClient resourceClient
	private	VersionClient versionClient
    private PropertyClient propertyClient
	private	UDRestClient restClient

    /**
    * Deployment-specific resources
    */
	private String applicationName
	private String deploymentRequestId
	private def existingResources
	private def resource

    /**
    *  @param airTool airTool instance to provide access to step properties
    */
    public Inventory(AirPluginTool airTool) {
        XTrustProvider.install()

        def props = airTool.getStepProperties()
        username                     = props['username']?.trim()
        password                     = props['password']?.trim()
        resourceId                   = props['resource']?.trim()
		resourcePath                 = props['resourcePath']?.trim()
		applicationId                = props['deployapplication']?.trim()
		applicationProcessRequestId  = props['applicationProcessRequest']?.trim()
		componentProcessRequestId    = props['componentProcessRequest']?.trim()
		environmentId                = props['environment']?.trim()
		environmentName              = props['environmentName']?.trim()
		sourceConfigType             = props['sourceConfigType']?.trim()
		componentTemplate            = props['componentTemplate']?.trim()
		deleteMissingInventory       = props['deleteMissingInventory']

	    def serverUrl = System.getenv("AH_WEB_URL");
		def serverUri = ArgoCDHelper.stringToUri(serverUrl)
		if (serverUri.equals(new URI(''))) {
		    System.exit(1)
		}
		def serverpassword = ""

        if (!username) {
		    username = airTool.getAuthTokenUsername()
		}
		if (!serverpassword) {
		    serverpassword = airTool.getAuthToken()
		}

        applicationClient = new ApplicationClient(serverUri, username, serverpassword)
	    componentClient = new ComponentClient(serverUri, username, serverpassword)
	    environmentClient = new EnvironmentClient(serverUri, username, serverpassword)
	    resourceClient = new ResourceClient(serverUri,  username, serverpassword)
	    versionClient = new VersionClient(serverUri, username, serverpassword)
        propertyClient = new PropertyClient(serverUri, username, serverpassword)
	    restClient = new UDRestClient(serverUri, username, serverpassword)

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

    /**
    *  @param imageSpecs images to create or update in the application inventory
    */
    public void update(String[] imageSpecs) {
        def imageComponentList = [];
        def parsedImageComponentList = [];
        if (imageSpecs) {
            for (String imageSpec in imageSpecs) {
                 def parsedImage = ArgoCDHelper.parseImageSpec(imageSpec);
			     def imageName = parsedImage.image;
			     def imageNameWithoutNameSpace = getImageNameWithoutNamespace(imageName);
			     def imageComponentName = applicationName + "-" + imageNameWithoutNameSpace;
			     imageComponentList.add(imageComponentName);
			     parsedImageComponentList.add(parsedImage);
			}
        }

		// 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)
		def inventoryToDelete = [];
		Map<String, String> desiredInventory = new HashMap<String,String>()
		def components = ArgoCDHelper.parseTextAsJson(applicationClient.getApplicationComponents(applicationId));
		components.each { component ->
		    def 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
		        def sanitizedComponentName = restClient.sanitizePathSegment(component.name);
		        def inventory = ArgoCDHelper.parseTextAsJson(environmentClient.getLatestEnvironmentInventoryByComponent(environmentName, applicationId, sanitizedComponentName));
		        if (inventory) {
		            def 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 {
		                def createdResource = ArgoCDHelper.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 = ArgoCDHelper.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
		            }
		        }
		    }
		    else {
                if (deleteMissingInventory.toBoolean()) {
		            inventoryToDelete.add(component.name)
                }
		    }

		}

		// Get the user selected component versions
		def userSelectedVersions = ArgoCDHelper.parseTextAsJson(applicationClient.getApplicationProcessRequestVersions(applicationProcessRequestId));
		userSelectedVersions.each{ version ->
		    def componentName = version.component.name;
		    def component = ArgoCDHelper.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.
		    def hasResourceNameAsTag = false
		    if (component.tags?.find{resource.name.equals(it.name)}) {
		        hasResourceNameAsTag = true;
		    }
		    def 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)) {
		        def imageName = componentName.substring(applicationName.length()+1);
		        desiredInventory.put(imageName, version.name)
		    }
		    // Remove root resource from inventory to delete
	        if (isRootResourceComponent) {
	            inventoryToDelete.remove(component.name)
	        }
		}

		println "The desired versions for existing image components is " + desiredInventory

		def imagesToCreateComponentsFor = [];
		def imagesNeedingInventoryUpdates = [];
		parsedImageComponentList.each { argoComponent ->
		   if (!desiredInventory.isEmpty() && desiredInventory.get(getImageNameWithoutNamespace(argoComponent.image))) {
		      imagesNeedingInventoryUpdates.add(argoComponent);
		   }
	       else {
	          // we need to create a component for this image
	          imagesToCreateComponentsFor.add(argoComponent);
           }
        }

        /**
         * If imagesspec is in the current desired inventory, but no longer in the imagespec list obtained from argocd, then it
         * will be removed from the desired inventory.
         */
        println "Inventory to delete: " + inventoryToDelete.toString()
        for (componentName in inventoryToDelete) {
		    // get the component name
		    // def componentName = applicationName + "-" + entry;
            println "componentName: " + componentName
		    // delete the resource
		    try {
		        // get the resource by path
		        def path = resourcePath + "/" + componentName
		        def targetResource = resourceClient.getResourceByPath(path);
		        println "Deleting resource " + path + " because the image is no longer part of the argocd deployment."
		        resourceClient.deleteResource(targetResource.id)
		    }
		    catch (IOException ex){
		          // do nothing; the resource probably does not exist
		    }

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

        println ""
        println "imagesNeedingInventoryUpdates: " + imagesNeedingInventoryUpdates.toString()
        println "imagesToCreateComponentsFor: " + imagesToCreateComponentsFor.toString()

		// Create components
		def applicationComponents = ArgoCDHelper.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
		    def imageNameWithoutNameSpace = getImageNameWithoutNamespace(entry.image);
		    def componentName = applicationName + "-" + imageNameWithoutNameSpace;
		    def 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
		    def imageNameWithoutNameSpace = getImageNameWithoutNamespace(entry.image);
		    def componentName = applicationName + "-" + imageNameWithoutNameSpace;
		    def 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 = ArgoCDHelper.parseTextAsJson(resourceClient.getResourceChildren(resourceId))
		    children.each { childResource ->
		        def roles = ArgoCDHelper.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 + "-")) {
		                def imageName = componentName.substring(applicationName.length()+1);
		                // get desired version
		                def desiredVersion = "";
		                imagesToCreateComponentsFor.each{ entry ->
		                    def 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 ->
		                    def entryImageWithoutNamespace = getImageNameWithoutNamespace(entry.image)
		                    if (entryImageWithoutNamespace == imageName) {
		                        desiredVersion = entry.version;
		                    }
		                }
		                if (desiredVersion != "") {
		                    // need to get the id of the version
		                    def existingVersions = ArgoCDHelper.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 = ArgoCDHelper.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 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 = ArgoCDHelper.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)
	    }
	}
}
