#!/usr/bin/env groovy
/*
* Licensed Materials - Property of IBM* and/or HCL**
* UrbanCode Deploy
* UrbanCode Build
* UrbanCode Release
* AnthillPro
* (c) Copyright IBM Corporation 2011, 2017. All Rights Reserved.
* (c) Copyright HCL Technologies Ltd. 2018. All Rights Reserved.
*
* U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
* GSA ADP Schedule Contract with IBM Corp.
*
* * Trademark of International Business Machines
* ** Trademark of HCL Technologies Limited
*/

import java.io.IOException
import java.net.URLEncoder
import java.nio.charset.Charset
import java.util.UUID

import com.urbancode.air.AirPluginTool
import com.urbancode.commons.httpcomponentsutil.HttpClientBuilder
import com.ibm.uclab.csrepl.client.ops.Download
import com.ibm.uclab.csrepl.client.ops.DownloadSync
import com.ibm.uclab.csrepl.client.ops.Sync

import org.apache.http.HttpResponse
import org.apache.http.HttpStatus
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpUriRequest
import org.apache.http.client.utils.URIBuilder
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler
import org.apache.http.util.EntityUtils
import org.codehaus.jettison.json.JSONObject
import org.codehaus.jettison.json.JSONArray

def apTool = new AirPluginTool(args[0], args[1])

def PROXY_HOST = System.env['PROXY_HOST']
def PROXY_PORT = System.env['PROXY_PORT']
if (PROXY_PORT != null) {
    PROXY_PORT = Integer.valueOf(PROXY_PORT)
}

def workDir = new File('.').canonicalFile
def props = apTool.getStepProperties()

def MAX_TRIES = 5

def artifactSetBaseDir = props['artifactSetBaseDir']
def versionId = Util.parseUUID(props['versionId'])
def directoryOffset = props['directoryOffset']
def fileIncludePatterns = props['fileIncludePatterns']
def fileExcludePatterns = props['fileExcludePatterns']
def syncMode = props['syncMode']
def fullVerification = props['fullVerification'] == 'true'
def handleIncrementalVersions = props['handleIncrementalVersions'] == 'true'
def versionIsIncremental = props['versionType'] == 'incremental'
def resId = props['resId']
def compId = props['compId']
def envId = props['envId']
def label = props['label']
def serverURL = props['serverUrl']
def setFileExecuteBits = (props['setFileExecuteBits'] != null && Boolean.valueOf(props['setFileExecuteBits']))
def verifyFileIntegrity = (props['verifyFileIntegrity'] != null && props['verifyFileIntegrity'] == "true")
def charset = Util.getCharset(props)

def baseDirectory = workDir
if (directoryOffset) {
    baseDirectory = new File(workDir, directoryOffset).canonicalFile
}

def includes = fileIncludePatterns?.readLines()
def excludes = fileExcludePatterns?.readLines()

def useSync = syncMode != 'false'
def fullClean = syncMode == 'FULL'
def syncCleanup = useSync
def stagedFiles = []
def currentDirs = []
def apiHttpClient = null

while (serverURL.endsWith("/")) {
    serverURL = serverURL.substring(0, serverURL.length() - 1);
}

class HttpResult {
    int code;
    String content;
    HttpResult(int code, String content) {
        this.code = code;
        this.content = content;
    }
}

class DateOrderedVersionId implements Comparable<DateOrderedVersionId> {
    UUID versionId;
    long date;

    DateOrderedVersionId(UUID versionId, long date) {
        this.versionId = versionId;
        this.date = date;
    }

    @Override
    public int compareTo(DateOrderedVersionId rhs) {
        if (date < rhs.date) {
            return -1;
        }
        else if (date == rhs.date) {
            return 0;
        }
        else {
            return 1;
        }
    }
}

def getApiHttpClient = {
    if (apiHttpClient == null) {
        HttpClientBuilder builder = new HttpClientBuilder();
        if (PROXY_HOST != null) {
            builder.setProxyHost(PROXY_HOST);
            builder.setProxyPort(Integer.parseInt(PROXY_PORT));
        }
        builder.setUsername(apTool.getAuthTokenUsername());
        builder.setPassword(apTool.getAuthToken());
        builder.setPreemptiveAuthentication(true);

        String verifyServerIdentityString = System.getenv().get("UC_TLS_VERIFY_CERTS");
        Boolean trustAllCerts = Boolean.valueOf(verifyServerIdentityString);
        builder.setTrustAllCerts(!trustAllCerts);

        apiHttpClient = builder.buildClient();
    }
    return apiHttpClient;
}

def callHttp = { HttpUriRequest req, String callDescription ->
    DefaultHttpClient client = getApiHttpClient();

    int tries = 0;
    for (;;) {
        try {
            int code;
            String content;
            CloseableHttpResponse res = client.execute(req);
            try {
                code = res.getStatusLine().getStatusCode();
                content = EntityUtils.toString(res.getEntity());
            }
            finally {
                res.close();
            }
            return new HttpResult(code, content);
        }
        catch (Exception e) {
            tries++;
            if (tries >= MAX_TRIES) {
                throw e;
            }
            System.out.println("Error " + callDescription + " : " + e.getMessage());
        }
    }
}

// Legacy: convert version name/label to version ID
def resolveVersionId = {
    URI uri = new URIBuilder(serverURL + "/cli/version/getVersionId")
        .addParameter("component", compId)
        .addParameter("version", label)
        .build();
    HttpResult httpResult = callHttp(new HttpGet(uri), "resolving version ID");

    switch (httpResult.code) {
    case 200:
        return UUID.fromString(httpResult.content.trim())
    default:
        println "Could not resolve version ID. Method returned code ${httpResult.code}"
        return null
    }
}

// Legacy: lookup single, immediately previous version ID,
// meaning what is currently deployed in resource inventory.
def getPreviousVersionId = {
    String reqUrl = "${serverURL}/rest/inventory/versionByResourceAndComponent/${resId}/${compId}";
    HttpResult httpResult = callHttp(new HttpGet(reqUrl), "getting previous version ID for resource");

    switch (httpResult.code) {
    case 200:
        def res = new JSONObject(httpResult.content)
        if (!res.isNull("version")) {
            JSONObject version = res.getJSONObject("version")
            if (!version.isNull("id")) {
                return UUID.fromString(version.getString("id"))
            }
        }
        return null
    case 404:
        println "Server returned 404 for getting the previous label"
        return null
    default:
        println "Could not get previous label. Method returned code ${httpResult.code}"
        return null
    }
}

// Lookup previously deployed version IDs, meaning what is currently
// deployed in resource inventory. The API is quite slow as it is essentially dumping
// resource inventory for the entire environment.
def getPreviousVersionIdsSlowest = {
    String reqUrl = "${serverURL}/rest/inventory/environmentInventoryByResource/${envId}";
    HttpResult httpResult = callHttp(new HttpGet(reqUrl), "getting previous version IDs for resource");

    switch (httpResult.code) {
    case 200:
        // Response structure: [{id:string,children:[component:{id:string},version:{id:string},date:number]}]
        JSONArray resources = new JSONArray(httpResult.content);

        // keep IDs ordered by entry creation date so that metadata tables
        // are merged in the right order (FULL INC1 INC2 INC3...) to ensure
        // newer files replace older ones
        List<DateOrderedVersionId> ids = new ArrayList<DateOrderedVersionId>();
        for (int i = 0; i < resources.length(); i++) {
            JSONObject resource = resources.getJSONObject(i);
            Object childrenObj;
            if (resId.equalsIgnoreCase(resource.getString("id")) &&
                (childrenObj = resource.opt("children")) instanceof JSONArray)
            {
                JSONArray children = (JSONArray)childrenObj;
                for (int j = 0; j < children.length(); j++) {
                    JSONObject entry = children.getJSONObject(j);
                    if (compId.equalsIgnoreCase(entry.getJSONObject("component").getString("id"))) {
                        ids.add(new DateOrderedVersionId(
                            UUID.fromString(entry.getJSONObject("version").getString("id")),
                            entry.getLong("date")));
                    }
                }
            }
        }
        Collections.sort(ids);
        List<UUID> result = new ArrayList<UUID>(ids.size());
        for (DateOrderedVersionId id : ids) {
            result.add(id.versionId);
        }
        return result;
    case 404:
        System.out.println("Server returned 404 for getting the previous version IDs.");
        return null;
    default:
        System.out.println("Could not get previous version IDs. Method returned code " + httpResult.code);
        return null;
    }
}

// Lookup previously deployed version IDs, meaning what is currently
// deployed in resource inventory. The API is better than the slowest
// version, but it isn't usable in versions of the server with auth token
// restrictions through at least 7.1.0.3 because they reject multivalued
// query parameters such as 'filterFields' unconditionally.
def getPreviousVersionIdsSlower = {
    URI uri = new URIBuilder(serverURL + "/rest/inventory/resourceInventory/table")
        .addParameter("rowsPerPage", "1000000")
        .addParameter("pageNumber", "1")
        .addParameter("orderField", "dateCreated")
        .addParameter("sortType", "asc")
        .addParameter("filterFields", "resourceId")
        .addParameter("filterFields", "componentId")
        .addParameter("filterFields", "ghostedDate")
        .addParameter("filterValue_resourceId", resId)
        .addParameter("filterValue_componentId", compId)
        .addParameter("filterValue_ghostedDate", "0")
        .addParameter("filterType_resourceId", "eq")
        .addParameter("filterType_componentId", "eq")
        .addParameter("filterType_ghostedDate", "eq")
        .addParameter("filterClass_resourceId", "String")
        .addParameter("filterClass_componentId", "String")
        .addParameter("filterClass_ghostedDate", "Long")
        .build();
    HttpResult httpResult = callHttp(new HttpGet(uri), "getting previous version IDs for resource");

    switch (httpResult.code) {
    case 200:
        // Response structure: {records:[{version:{id:string}}]}
        List<UUID> versionIds = new ArrayList<UUID>();
        JSONObject response = new JSONObject(httpResult.content);
        JSONArray records = response.getJSONArray("records");
        for (int i = 0; i < records.length(); i++) {
            JSONObject inventoryEntry = records.getJSONObject(i);
            JSONObject version = inventoryEntry.getJSONObject("version");
            versionIds.add(UUID.fromString(version.getString("id")));
        }
        return versionIds;
    case 404:
        System.out.println("Server returned 404 for getting the previous version IDs.");
        return null;
    case 403:
        System.out.println("Server returned 403 for getting the previous version IDs.");
        System.out.println("The auth token restriction logic may be limiting multivalued query parameters.");
        return null;
    default:
        System.out.println("Could not get previous version IDs. Method returned code " + httpResult.code);
        return null;
    }
}

// Lookup previously deployed version IDs, meaning what is currently
// deployed in resource inventory. This is the fastest method as it
// use an API created for this purpose, but no server version before
// 7.1.1.0 implements it.
def getPreviousVersionIdsFast = {
    URI uri = new URIBuilder(serverURL + "/cli/inventory/getResourceInventoryEntriesForComponent")
        .addParameter("resource", resId)
        .addParameter("component", compId)
        .build();
    HttpResult httpResult = callHttp(new HttpGet(uri), "getting previous version IDs for resource");

    switch (httpResult.code) {
    case 200:
        List<UUID> versionIds = new ArrayList<UUID>();
        JSONArray response = new JSONArray(httpResult.content);
        for (int i = 0; i < response.length(); i++) {
            JSONObject entry = response.getJSONObject(i);
            versionIds.add(UUID.fromString(entry.getString("versionId")));
        }
        return versionIds;
    case 400:
        // In most versions of the server, /cli has an exception mapper that turns
        // almost every kind of error into a 400, even it has a type that would
        // would get mapped to a sensible status like 404.
        System.out.println("Server returned 400 for getting the previous versions.");
        System.out.println("The server may be too old to for this API.");
        return null;
    case 404:
        System.out.println("Server returned 404 for getting the previous versions.");
        System.out.println("The server may be too old to for this API.");
        return null;
    default:
        System.out.println("Could not get previous versions. Method returned code " + httpResult.code);
        return null;
    }
}

// Lookup previously deployed version IDs, meaning what is currently
// deployed in resource inventory. Calls out to each API available
// from fastest to slowest until it finds one that is compatible.
def getPreviousVersionIds = {
    List<UUID> result = getPreviousVersionIdsFast();
    if (result == null) {
        System.out.println("Using slower fallback that is compatible with older servers to get previous version IDs.");
        result = getPreviousVersionIdsSlower();
    }
    if (result == null) {
        System.out.println("Using slowest fallback that is compatible with most servers to get previous version IDs.");
        result = getPreviousVersionIdsSlowest();
    }
    return result;
}

//
// Validation
//
baseDirectory.mkdirs()
if (baseDirectory.isFile()) {
    throw new IllegalArgumentException("Base directory ${baseDirectory} is a file!")
}

Util.configureLogging()

//
// Download the files
//

if (!versionId) {
    println "Cannot parse version ID - attempting to resolve from component ID and label..."
    println "  Component ID: ${compId}"
    println "  Label: ${label}"
    def resolvedVersionId = resolveVersionId()
    if (resolvedVersionId) {
        println "Resolved version ID ${resolvedVersionId}"
        versionId = resolvedVersionId
    }
    else {
        println "Resolution failed - terminating step."
        System.exit(1)
    }
}

def forceUseServerUrl = false
def csClient = Util.getCodestationClient(
    serverURL,
    forceUseServerUrl,
    apTool.getAuthTokenUsername(),
    apTool.getAuthToken())

def op
if (useSync) {
    if (handleIncrementalVersions) {
        def previousVersionIds = getPreviousVersionIds()
        if (versionIsIncremental) {
            println "Deploying incremental version"

            def allVersions = previousVersionIds.collect() + versionId
            op = new Sync(csClient, baseDirectory, allVersions)
            op.setSkipClean(true)
        }
        else {
            op = new DownloadSync(csClient, baseDirectory, versionId, previousVersionIds)
            op.setFullClean(fullClean)
        }
    }
    else {
        def previousVersionId = getPreviousVersionId()
        // Need to cast to a static type as previousVersionId can be null
        // and groovy will select an overload dynamically and may pick the
        // wrong overload as 'null' has no type. Giving previousVersionId
        // a static type is not sufficient.
        op = new DownloadSync(csClient, baseDirectory, versionId, previousVersionId as UUID)
        op.setFullClean(fullClean)
    }
}
else {
    op = new Download(csClient, baseDirectory, versionId)
}

op.setMaxDownloadAttempts(MAX_TRIES);
op.setIncludes(includes)
op.setExcludes(excludes)
op.setOutputCharset(charset)
op.setSetExecuteBits(setFileExecuteBits)
op.setSourcePathOffset(artifactSetBaseDir)
op.setVerifyFiles(verifyFileIntegrity)
op.setSlowVerification(fullVerification)

println "Artifact source: ${csClient.url}"
println "Working directory: ${baseDirectory.path}"
println "Including ${fileIncludePatterns}"
println "Excluding ${fileExcludePatterns}"
println "Offset ${directoryOffset}"
println "Artifact base directory ${artifactSetBaseDir}"
println "Set file bits are ${setFileExecuteBits}"
println ""

op.run()
