/*
* Licensed Materials - Property of IBM* and/or HCL**
* UrbanCode Deploy
* UrbanCode Build
* UrbanCode Release
* AnthillPro
* (c) Copyright IBM Corporation 2014, 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 com.urbancode.air.AirPluginTool
import com.urbancode.air.XTrustProvider
import com.urbancode.commons.fileutils.digest.DigestUtil
import com.urbancode.commons.httpcomponentsutil.HttpClientBuilder
import com.urbancode.commons.util.IO
import com.urbancode.ud.client.ComponentClient
import com.urbancode.ud.client.VersionClient

import groovy.json.JsonSlurper

import org.apache.commons.io.FilenameUtils
import org.apache.commons.lang3.ObjectUtils
import org.apache.http.HttpStatus
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpRequestBase
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.util.EntityUtils

import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.nio.charset.Charset
import java.security.NoSuchAlgorithmException
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream

def tempDirs = new HashMap<String, File>()

// Get step properties
def apTool  = new AirPluginTool(args[0], args[1])
def props = apTool.getStepProperties()

final def agentProps = new Properties()
agentProps.load(new FileInputStream(new File(System.getenv("AGENT_HOME"),
        "conf/agent/installed.properties")))
String charsetName = agentProps.getProperty("system.default.encoding")
Charset charset = null
if (charsetName != null) {
    charset = Charset.forName(charsetName)
}

final def REPO_PATH_SEPARATOR = "/"
def exitVal = 0

boolean isUseVFS = Boolean.valueOf(props['isUseVFS'])
def componentName = props['componentName']
def username = props['user']
def password = props['password']
def repoPath = props['repoPath']
def groupId = props['groupId'] ?: ""
def artifactId = props['artifactId'] ?: ""
boolean recursive = Boolean.valueOf(props['recursive'])
def importCount = props['copyCount'] ?: ""
def versionPattern = props['versionPattern']
def description = props['description']
def specificVersionPattern = props['version']
def saveFileExecuteBits = props['saveFileExecuteBits']
String statuses = props['statuses']
String links = props['links']
boolean unzipArchive = Boolean.valueOf(props['unzipArchive'])
boolean allowInsecure = Boolean.valueOf(props['allowInsecure'])
boolean checkHash = Boolean.valueOf(props['checkHash'])
String apiKey = props['apiKey']?.trim()
String proxyHost = props['proxyHost']?.trim()
String proxyPort = props['proxyPort']?.trim()
String proxyPass = props['proxyPass']?.trim()
String proxyUser = props['proxyUser']?.trim()

def extensionsString = props['extensions']
String[] extensions = new String[0]
if (! isEmpty(extensionsString)) {
    extensions = extensionsString.split(",")
    for (int i = 0; i < extensions.length; i++) {
        extensions[i] = extensions[i].trim()
    }
}

// Used to split the includes and excludes Strings by commas and newlines
String regexp = "[,\\n]"
String includesStr = props['includes']
if (includesStr == null || includesStr.equals("")) {
    includesStr = "**/*"
}
String excludesStr = props['excludes']
if (excludesStr == null) {
    excludesStr = ""
}

if (description == null) {
    description = "";
}

String[] includes = includesStr.split(regexp)
String[] excludes = excludesStr.split(regexp)

//remove trailing slashes from url
def repoName = props['repoName']
String repoUrl = props['repoUrl']
while (repoUrl.endsWith(REPO_PATH_SEPARATOR)) {
    repoUrl = repoUrl.substring(0, repoUrl.length() - 1)
}
//remove trailing slashes from groupId
while (groupId.endsWith(REPO_PATH_SEPARATOR)) {
    groupId = groupId.substring(0, groupId.length() - 1)
}

boolean skipSettingProperties = false
String[] statusList = statuses?.split(',')*.trim()
String[] linkList = links?.split(',')*.trim()

//use repo path instead of groupId and artifactId if given
if (repoPath?.isEmpty() && groupId?.isEmpty()){
    println("[Error] Either Repo Path or Group ID must be given.")
    System.exit(1)
}
else if (repoPath) {
    println("[Info] Repo Path $repoPath given, ignoring Group ID and Artifact ID fields.")
}
else {
    //replace dots with slashes in groupId for rest calls
    String encodedGroupId = groupId.replaceAll("\\.", "\\/")
    if (artifactId) {
        repoPath = encodedGroupId + REPO_PATH_SEPARATOR + artifactId
    }
    else {
        repoPath = encodedGroupId
    }
    println("[Info] Setting Repo Path to $repoPath.")
}

//remove trailing slashes from path
while (repoPath.endsWith(REPO_PATH_SEPARATOR)) {
    repoPath = repoPath.substring(0, repoPath.length() - 1)
}

int versionsCreated = 0

String UDUsername = "PasswordIsAuthToken"
String UDPassword = String.format("{\"token\": \"%s\"}", System.getenv("AUTH_TOKEN"))

String webUrl = System.getenv("AH_WEB_URL")
URI url = new URI(webUrl)

VersionClient versionClient = new VersionClient(url, UDUsername, UDPassword)
ComponentClient componentClient = new ComponentClient(url, UDUsername, UDPassword)

HttpClientBuilder builder = new HttpClientBuilder()
if (apiKey) {
    println ("[Info] API Key given, ignoring username and password.")
}
else if (username) {
    builder.setPreemptiveAuthentication(true)
    builder.setUsername(username)
    builder.setPassword(password)
}
if (allowInsecure) {
    println ("[Info] Allowing insecure connections...")
    XTrustProvider.install()
    builder.setTrustAllCerts(true)
}
if (proxyHost) {
    println "[Info] Using Proxy Host ${proxyHost}"
    println "[Info] Using Proxy Port ${proxyPort}"

    builder.setProxyHost(proxyHost)
    builder.setProxyPassword(proxyPass)
    builder.setProxyPort(Integer.valueOf(proxyPort))
    builder.setProxyUsername(proxyUser)
    if (proxyUser) {
      println "[Info] Using Proxy User ${proxyUser}"
    }
}
DefaultHttpClient client = builder.buildClient()

def getTempDir = { version ->
    File result = tempDirs.get(version)
    if (result == null) {
        result = new File(".", UUID.randomUUID().toString())
        IO.mkdirs(result)
        tempDirs.put(version, result)
    }

    return result
}

def isEmpty(value) {
    return value == null || value.equals("")
}

def executeHttpRequest = { uri, request, postInfo ->
    try {
        if (!uri.startsWith('http://') && !uri.startsWith('https://')) {
            uri = 'https://' + uri
        }
        URL rawUri = new URL(uri)
        def encodedUri = new URI(rawUri.getProtocol(),
                rawUri.getAuthority(),
                rawUri.getPath(),
                rawUri.getQuery(),
                rawUri.getRef())
        request.setURI(encodedUri)
    }
    catch (IllegalArgumentException e) {
        println ("[Error] Invalid URL: ${e.getMessage()}.")
        println ('[Possible Solution] Please update the step configuration with a valid URL.')
        return false
    }
    if (postInfo) {
        try {
            request.setEntity(postInfo)
        }
        catch (IOException e) {
            println ("[Error] Could not set entity: ${e.getMessage()}.")
            return false
        }
    }
    if (apiKey) {
        request.setHeader('X-JFrog-Art-API', apiKey)
    }
    CloseableHttpResponse response
    try {
        response = client.execute(request)
    }
    catch (IOException e) {
        println ("[Error] Problem executing HTTP request: ${e.getMessage()}.")
        println ("Response: ${response}")
        return false
    }
    return response
}

def verifyHash = { fileToVerify, storedDigest, artifactFileHashAlgorithm ->
    if (storedDigest != null) {
        String computedDigest
        try {
            computedDigest = DigestUtil.getHexDigest(fileToVerify, artifactFileHashAlgorithm)
            if (!ObjectUtils.equals(storedDigest, computedDigest)) {
                throw new Exception("[Error] Artifact file verification of ${fileToVerify.getName()} failed. Expected "
                + " digest of ${storedDigest} but the downloaded file was ${computedDigest}")
            }
        }
        catch (NoSuchAlgorithmException e) {
            throw new Exception("[Error] Algorithm to verify remote artifacts not supported: " +
            artifactFileHashAlgorithm)
        }
        catch (IOException e) {
            throw new Exception("[Error] Error verifying downloaded remote artifacts: ${e.getMessage()}", e)
        }
    }
}

def filterVersions = { versionList ->
    def resultMap = new HashMap<Object, Date>()
    println("[Action] Filtering list of versions based on version name pattern '${versionPattern}'")

    for (vObject in versionList) {
        // If we still need versions (Cannot break in Groovy)
        version = vObject.key
        boolean shouldAdd = false
        // Make sure the overall component source configuration pattern is matched
        def versionPatternMatcher = version =~ versionPattern
        if (versionPatternMatcher.matches()) {

            // If they specified a specific version on the import dialog, only get that one
            if (specificVersionPattern != null && !specificVersionPattern.isEmpty()) {
                def specificVersionMatcher = version =~ specificVersionPattern
                if (specificVersionMatcher.matches()) {
                    shouldAdd = true
                }
            } else {
                // They want all the versions that match the source config pattern
                shouldAdd = true
            }
        }

        if (shouldAdd) {
            resultMap.put(vObject.key, vObject.value)
        }
    }

    return resultMap
}

def sortVersions = { filteredList ->
    println("[Action] Sorting versions based on creation date in Artifactory.")
    //If sorting versions fails, print warning and move on.
    try {
        def sortedMap = filteredList.sort { it.value }
        def sortedList = new ArrayList(sortedMap.keySet())
        return sortedList
    }
    catch (Exception e) {
        println("[Warning] Failed to sort list of versions. Returning unsorted list.")
    }

    return filteredList
}

def getVersionList = {
    String versionUrl = (repoUrl + REPO_PATH_SEPARATOR + "api/storage/" + repoName + REPO_PATH_SEPARATOR
            + repoPath).toString()
    println ("[Action] Getting list of all versions for: " + versionUrl)

    CloseableHttpResponse response
    try {
        response = executeHttpRequest(versionUrl, new HttpGet(), null)
        int status = response.getStatusLine().getStatusCode()
        if (status == HttpStatus.SC_OK) {
            Map< Object, Date> versionMap = new HashMap< Object, Date >()
            def jsonString = EntityUtils.toString(response.getEntity())
            def slurper = new JsonSlurper()
            def children = slurper.parseText(jsonString).children
            for (child in children) {
                def isFolder = child.folder.toString()
                if (isFolder.equals("true")) {
                    def currVersion = child.uri.toString()
                    def dateCreated
                    String getDateUrl = versionUrl + currVersion
                    CloseableHttpResponse dateResponse
                    try {
                        dateResponse = executeHttpRequest(getDateUrl, new HttpGet(), null)
                        int dateStatus = dateResponse.getStatusLine().getStatusCode()
                        if (dateStatus == HttpStatus.SC_OK) {
                            def dateJson = EntityUtils.toString(dateResponse.getEntity())
                            dateCreated = slurper.parseText(dateJson).created
                        }
                        else {
                            println("[Warning] Could not determine creation date of version: " + currVersion)
                        }
                        versionMap.put(currVersion.substring(1, currVersion.length()), dateCreated)
                    }
                    finally {
                        dateResponse.close()
                    }
                }
            }
            return versionMap
        }
        else {
            throw new Exception("Exception searching: " + versionUrl.toString() + "\nErrorCode : " + response.getStatusLine().toString())
        }
    }
    finally {
        response.close()
    }
}

def getLatestVersions = {
    def versionList = getVersionList()

    if (isEmpty(versionPattern)) {
        versionPattern = ".*"
    }
    def filteredList = filterVersions(versionList)

    return sortVersions(filteredList)
}

def setVersionProperties = { version ->
    //Get Artifactory version properties and add to UCD version properties
    String propertiesUrl = (repoUrl + REPO_PATH_SEPARATOR + "api/storage/" + repoName + REPO_PATH_SEPARATOR
            + repoPath + REPO_PATH_SEPARATOR + version + "?properties").toString()

    CloseableHttpResponse response
    try {
        response = executeHttpRequest(propertiesUrl, new HttpGet(), null)
        int status = response.getStatusLine().getStatusCode()
        if (status == HttpStatus.SC_OK) {
            def properties = []
            def jsonString = EntityUtils.toString(response.getEntity())
            def slurper = new JsonSlurper()
            def children = slurper.parseText(jsonString)?.properties
            for (prop in children) {
                // Set all Artifactory properties as component version properties
                versionClient.setVersionProperty(version.toString(), componentName, prop.getKey().toString(), prop.getValue().join(","), false)

                // If statuses are specified and exist as Artifactory properties, add them to version
                if (statuses) {
                    for (versStatus in statusList) {
                        if (versStatus.equals(prop.getKey().toString())) {
                            try {
                                println ("[Action] Property for status: '${versStatus}' found, adding status: '" + prop.getValue()[0].toString() + "'")
                                componentClient.addComponentVersionStatus(componentName, version.toString(), prop.getValue()[0].toString())
                            }
                            catch (IOException e) {
                                println ("[Warning] Property not set because UCD status '" + prop.getValue()[0].toString() + "' does not exist.")
                                println ("[Possible Solution] Create version status for '" + prop.getValue()[0].toString() + "' on UCD server.")
                            }
                        }
                    }
                }
                // If links are specified and exist as Artifactory properties, add them to version
                if (links) {
                    for (link in linkList) {
                        if (link.equals(prop.getKey().toString())) {
                            try {
                                println ("[Action] Property for link: '${link}' found, adding link: '" + prop.getValue()[0].toString() + "'")
                                componentClient.addComponentVersionLink(componentName, version.toString(), prop.getKey().toString(), prop.getValue()[0].toString())
                            }
                            catch (IOException e) {
                                println ("[Warning] Property '" + prop.getKey().toString() + "' not set as UCD link")
                            }
                        }
                    }
                }
            }
        }
        else if (status == HttpStatus.SC_NOT_FOUND || status == HttpStatus.SC_NO_CONTENT) {
            println ("[Info] No Artifactory properties found for artifact.")
        }
        else {
            println ("[ERROR] A problem was encountered while setting Artifactory properties as version properties")
            throw new Exception("Exception searching: " + propertiesUrl.toString() + "\nErrorCode : " + response.getStatusLine().toString())
        }
    }
    finally {
        response.close()
    }
}

def downloadFile = { fileUrl, version ->
    CloseableHttpResponse response
    try {
        response = executeHttpRequest(fileUrl, new HttpGet(), null)
        if (!response) {
            throw new Exception("[Error] Could not find file ${url} in Artifactory.")
        }
        int status = response.getStatusLine().getStatusCode()
        def checksumMap = null
        if (status == HttpStatus.SC_OK) {
            def jsonString = EntityUtils.toString(response.getEntity())
            def slurper = new JsonSlurper()
            def infoJSON = slurper.parseText(jsonString)
            checksumMap = checkHash ? infoJSON.checksums : ""
        }
        fileUrl = fileUrl.replace("api/storage/", "")
        println "[Action] Downloading file: " + fileUrl
        response = executeHttpRequest(fileUrl, new HttpGet(), null)
        status = response.getStatusLine().getStatusCode()
        if (status == HttpStatus.SC_OK) {
            String[] currFile = null
            if (recursive){
                currFile = fileUrl.split(REPO_PATH_SEPARATOR + version + REPO_PATH_SEPARATOR)
            }
            else {
                currFile = fileUrl.split(REPO_PATH_SEPARATOR)
            }
            File file = new File(getTempDir(version), currFile[currFile.length - 1])
            IO.copy(response.getEntity().getContent(), file)
            if (checksumMap) {
                //verify checksum
				if(checksumMap.sha256) {
					verifyHash(file, checksumMap.sha256, "SHA-256")
                    			System.out.println("[Ok] SHA-256 Verification for file : ${file} succeeded!")
				}else if(checksumMap.sha1){
					verifyHash(file, checksumMap.sha1, "SHA-1")
                    			System.out.println("[Ok] SHA-1 Verification for file : ${file} succeeded!")
				}else{
					verifyHash(file, checksumMap.md5, "MD5")
                    			System.out.println("[Ok] MD5 Verification for file : ${file} succeeded!")
				}
				
            }
            return getTempDir(version)
        }
        else {
            throw new Exception("Exception downloading file : " + fileUrl + "\nErrorCode : " + response.getStatusLine())
        }
    }
    finally {
        response.close()
    }
}

def download = { version ->
    String downloadUrl = (repoUrl + REPO_PATH_SEPARATOR + "api/storage/" + repoName + REPO_PATH_SEPARATOR
            + repoPath + REPO_PATH_SEPARATOR + version).toString()
    String versionListUrl = downloadUrl
    if (recursive) {
        versionListUrl = versionListUrl + "?list&deep=1"
    }
    println "[Action] Downloading version: " + downloadUrl
    CloseableHttpResponse response
    try {
        response = executeHttpRequest(versionListUrl, new HttpGet(), null)
        int status = response.getStatusLine().getStatusCode()
        if (status == HttpStatus.SC_OK) {
            def files = []
            def jsonString = EntityUtils.toString(response.getEntity())
            def slurper = new JsonSlurper()
            def infoJSON = slurper.parseText(jsonString)
            def children = null
            if (recursive) {
                children = infoJSON.files
            }
            else {
                children = infoJSON.children
            }
            for (child in children) {
                def isFolder = child.folder.toString()
                if (isFolder.equals("false")) {
                    def currChild = child.uri.toString()
                    downloadFile(downloadUrl + currChild, version)
                }
            }

            return getTempDir(version)
        }
        else {
            println("[Warning] Recursive download requires Artifactory PRO.")
            throw new Exception("Exception downloading version: " + version + "\nErrorCode : " + response.getStatusLine())
        }
    }
    finally {
        response.close()
    }
}

private boolean unzipFile(File zipFile, File destDir) throws IOException {
    boolean unzipped = false

    final int BUFFER_SIZE = 4096
    ZipInputStream zipInput = new ZipInputStream(new FileInputStream(zipFile))
    ZipEntry zipEntry = zipInput.getNextEntry()

    if (!destDir.exists()) {
        destDir.mkdir()
    }

    while (zipEntry != null) {
        unzipped = true
        File entry = new File(destDir, zipEntry.getName())
        entry.deleteOnExit()

        if (zipEntry.isDirectory()) {
            entry.mkdir()
        }
        else {
            try {
                FileOutputStream fileOutput = new FileOutputStream(entry)
                BufferedOutputStream output = new BufferedOutputStream(fileOutput)
                byte[] buffer = new byte[BUFFER_SIZE]
                int read = 1

                while ((read = zipInput.read(buffer)) > 0) {
                    //write $read number of bytes from the buffer into the file
                    output.write(buffer, 0, read)
                }
                output.close()
            }
            catch (Exception e) {
                println ("[Error] + " + e.getMessage())
                System.exit(1)
            }
        }

        zipInput.closeEntry()
        zipEntry = zipInput.getNextEntry()
    }

    zipInput.close()
    return unzipped
}

private void unzipRecursive(File unzipDir) {
    for (File zipFile : unzipDir.listFiles()) {
        String fileExtension = FilenameUtils.getExtension(zipFile.getPath())
        if (zipFile.isDirectory()) {
            unzipRecursive(zipFile)
        }
        else if (fileExtension == 'zip' || fileExtension == 'jar' || fileExtension == 'tar.gz' || fileExtension == '7z') {
            try {
                String folderName = zipFile.getName().substring(0, zipFile.getName().lastIndexOf('.'))
                File destDir = new File(unzipDir, folderName)
                boolean unzipped = unzipFile(zipFile, destDir)
                if (unzipped) {
                    println("[Info] File: '" + zipFile.getName() + "' unzipped, deleting archive...")
                    zipFile.delete()
                }
            }
            catch (Exception e) {
                println("[Error] Failed to unzip file: " + zipFile)
                throw new Exception("[Error] Aborting: " + e.printStackTrace())
            }
        }
    }
}

def integrate = { version ->
    File tempDir = null
    String versionId
    try {
        boolean preserveExecutePermissions = Boolean.valueOf(props['saveFileExecuteBits'])
        List<String> versions = componentClient.getComponentVersions(componentName, false)
        List<String> archivedVersions = componentClient.getArchivedComponentVersions(componentName)
        versions.addAll(archivedVersions)
        boolean hasVersion = versions.contains(version)
        if (!hasVersion) {
            versionId = versionClient.createVersion(componentName.toString(), version.toString(), description, true).toString()

            // Try setting properties one time, if it fails, print warning and don't attempt again.
            if (!skipSettingProperties) {
                try {
                    //set property to tell if archives are being unzipped
                    versionClient.setVersionProperty(version.toString(), componentName, "isUnzipped", unzipArchive.toString(), false)
                }
                catch (IOException ex) {
                    println "[Warning] Unable to assign component version properties. " +
                            "Create these version properties manually in the $componentName or component template's configuration tab."
                    skipSettingProperties = true
                }
            }
            println() // Put a new line in the output to separate versions
            println ("[Action] Creating version: " + version)
            if (isUseVFS) {
                tempDir = download(version)
                // If expecting to unzip, perform this action.  Otherwise add files to version now
                if (unzipArchive) {
                    unzipRecursive(tempDir)
                }
                versionClient.addVersionFiles(componentName.toString(), versionId, tempDir, "", includes, excludes, preserveExecutePermissions, true, charset, extensions)
            }
            else {
                println (String.format("Not uploading version %s to VFS because using VFS was not selected.",
                        versionId))
            }
            if (!skipSettingProperties) {
                setVersionProperties(version)
            }
            versionClient.markImportFinished(componentName.toString(), versionId);
            apTool.setOutputProperty("VersionID", versionId)
            println ("[Info] Version '${version}' import complete.")
            versionsCreated++
        }
        else {
            println()
            println (String.format("[Info] UCD already contains version %s. A duplicate will not be created.",
                    version))
        }
    }
    catch (Exception e) {
        System.err.println(String.format("[Error] Error creating a new version: %s", e.toString()))
        if (versionId) {
            versionClient.deleteVersion(UUID.fromString(versionId))
        }
        exitVal = 1
    }
    finally {
        try {
            if (tempDir != null && tempDir.exists()) {
                IO.delete(tempDir)
            }
        }
        catch (IOException e) {
            System.err.println(String.format("[Error] Unable to delete download directory",
                    e.getMessage()))
            exitVal = 1
        }
    }
}

try {
    def latestVersions = getLatestVersions()
    def versions
    if(importCount) {
        importCount = Integer.valueOf(importCount)
        if (importCount < latestVersions.size()) {
            latestVersions = latestVersions.subList(latestVersions.size()-importCount, latestVersions.size())
            println("[Action] Importing the latest '${importCount}' version(s) from Artifactory: " + latestVersions)
        }
    }
    for (version in latestVersions) {
        integrate(version)
    }
    println() // Put a new line in the output to separate the end of the imports
    apTool.storeOutputProperties()
    println("[Info] Versions Created: " + versionsCreated.toString())
    println("[Ok] Finished")
}
catch (Exception e) {
    e.printStackTrace()
    exitVal=1
}
System.exit(exitVal)
