/*
 * Licensed Materials - Property of IBM Corp.
 * IBM UrbanCode Deploy
 * (c) Copyright IBM Corporation 2014, 2017. All Rights Reserved.
 *
 * U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
 * GSA ADP Schedule Contract with IBM Corp.
 */

import com.urbancode.air.AirPluginTool
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.http.HttpResponse
import org.apache.http.HttpStatus
import org.apache.http.client.methods.HttpGet
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.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']
boolean recursive = Boolean.valueOf(props['recursive'])
def extension = props['extension']
def copyCount = props['copyCount']
def versionPattern = props['versionPattern']
def specificVersionPattern = props['version']
def saveFileExecuteBits = props['saveFileExecuteBits']
String statuses = props['statuses']
String links = props['links']
Boolean unzipArchive = Boolean.valueOf(props['unzipArchive'])
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()
    }
}

def repoName = props['repoName']
String repoUrl = props['repoUrl']
while (repoUrl.endsWith(REPO_PATH_SEPARATOR)) {
    repoUrl = repoUrl.substring(0, repoUrl.length() - 1)
}

String[] statusList = statuses?.split(',')*.trim()
String[] linkList = links?.split(',')*.trim()
String[] extensionList = extension?.split(',')*.trim()

//replace dots with slashes for rest calls
String encodedRepoPath = repoPath.replaceAll("\\.", "\\/")
int versionImportCount = 0
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 (username) {
    builder.setPreemptiveAuthentication(true)
    builder.setUsername(username)
    builder.setPassword(password)
}
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 filterVersions = { versionList ->
    def result = []
    for (version in versionList) {
        // If we still need versions (Cannot break in Groovy)
        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) {
            result.add(version)
        }
    }

    return result
}

def getVersionList = {
    String versionUrl = (repoUrl + REPO_PATH_SEPARATOR + "api/storage/" + repoName + REPO_PATH_SEPARATOR
            + encodedRepoPath).toString().replace(' ', '%20')
    URI versionUri = new URI(versionUrl)
    println ("[Action] Getting list of all versions for: " + versionUri.toString())
    HttpGet get = new HttpGet(versionUri.toString())
    HttpResponse response = client.execute(get)
    int status = response.getStatusLine().getStatusCode()
    if (status == HttpStatus.SC_OK) {
        def versions = []
        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()
                versions.add(0, currVersion.substring(1, currVersion.length()))
            }
        }
        return versions
    }
    else {
        throw new Exception("Exception searching: " + versionUri.toString() + "\nErrorCode : " + status.toString())
    }
}

def getLatestVersions = {
    def versionList = getVersionList()

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

def setVersionProperties = { version ->
    println ("[Action] Setting version properties for version: " + version)
    //Set unzipped boolean value
    try {
        versionClient.setVersionProperty(version.toString(), componentName, "isUnzipped", unzipArchive.toString(), false)
    }
    catch (IOException ex) {
        println ""
        println "[Warning] Unable to assign component version properties. " +
            "Create these version properties manually in the $componentName or component template's configuration tab."
        println ("    Property 'isUnzipped' not set on version")
    }

    //Get Artifactory version properties and add to UCD version properties
    String propertiesUrl = (repoUrl + REPO_PATH_SEPARATOR + "api/storage/" + repoName + REPO_PATH_SEPARATOR
        + encodedRepoPath + REPO_PATH_SEPARATOR + version + "?properties").toString().replace(' ', '%20')
    URI propertiesUri = new URI(propertiesUrl)
    HttpGet get = new HttpGet(propertiesUri)
    HttpResponse response = client.execute(get)
    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
            try {
                versionClient.setVersionProperty(version.toString(), componentName, prop.getKey().toString(), prop.getValue()[0].toString(), false)
            }
            catch (IOException e) {
                println ("    Property '" + prop.getKey().toString() + "' not set on version")
            }
            // 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: " + propertiesUri.toString() + "\nErrorCode : " + status.toString())
    }
}

def downloadFile = { fileUrl, version ->
    fileUrl = fileUrl.replace("api/storage/", "")
    String fileDownloadUrl = fileUrl.replace(' ', '%20')
    println "[Action] Downloading file: " + fileUrl
    HttpGet get = new HttpGet(fileDownloadUrl)
    HttpResponse response = client.execute(get)
    int 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)
        return getTempDir(version)
    }
    else {
        throw new Exception("Exception downloading file : " + fileUrl + "\nErrorCode : " + status.toString())
    }
}

def download = { version ->
    String downloadUrl = (repoUrl + REPO_PATH_SEPARATOR + "api/storage/" + repoName + REPO_PATH_SEPARATOR
            + encodedRepoPath + REPO_PATH_SEPARATOR + version).toString().replace(' ', '%20')
    URI downloadUri = null
    if (recursive) {
        downloadUri = new URI(downloadUrl + "?list&deep=1")
    }
    else {
        downloadUri = new URI(downloadUrl)
    }
    println "[Action] Downloading version: " + downloadUri.toString()
    HttpGet get = new HttpGet(downloadUri)
    HttpResponse response = client.execute(get)
    int status = response.getStatusLine().getStatusCode()
    if (status == HttpStatus.SC_OK) {
        def files = []
        def jsonString = EntityUtils.toString(response.getEntity())
        def slurper = new JsonSlurper()
        def children = null
        if (recursive) {
            children = slurper.parseText(jsonString).files
        }
        else {
            children = slurper.parseText(jsonString).children
        }
        for (child in children) {
            def isFolder = child.folder.toString()
            if (isFolder.equals("false")) {
                def currChild = child.uri.toString()
                for (fileType in extensionList) {
                    if (currChild =~ fileType + "\$") {
                        downloadFile(downloadUrl + currChild, version)
                    }
                }
            }
        }

        return getTempDir(version)
    }
}

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("Aborting: " + e.printStackTrace())
            }
        }
    }
}

def integrate = { version ->
    File tempDir = null
    String[] includes = ["**/*"] as String[]
    String[] excludes = [] as String[]
    try {
        System.out.println() // Put a new line in the output to separate versions

        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)

        println ("[Info] Version from artifactory: " + version)

        //count versions even if they've been imported to avoid cascading import of older versions
        versionImportCount++
        if (!hasVersion) {
            versionsCreated++
            println ("[Action] Creating version: " + version)
            String versionId = versionClient.createVersion(componentName.toString(), version.toString(), " ").toString()
            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))
            }
            setVersionProperties(version)
            apTool.setOutputProperty("VersionID", versionId)
            println ("[Info] Version '${version}' import complete.")
        } else {
            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 creating a new version: %s", e.toString()))
        exitVal = 1
    } finally {
        try {
            if (tempDir != null && tempDir.exists()) {
                IO.delete(tempDir)
            }
        } catch (IOException e) {
            System.err.println(String.format("Unable to delete download directory",
                    e.getMessage()))
            exitVal = 1
        }
    }
}

try {
    def latestVersions = getLatestVersions()
    for (version in latestVersions) {
        if (versionImportCount >= Integer.valueOf(copyCount)) {
            println("[Info] Skipping Version '${version}': Reached Latest Build Count max for import")
        }
        else {
            integrate(version)
        }
    }
    System.out.println() // Put a new line in the output to separate the end of the imports
    apTool.storeOutputProperties()
    System.out.println("[Info] Versions Created: " + versionsCreated.toString())
    System.out.println("[Ok] Finished")
}
catch (Exception e) {
    e.printStackTrace()
    exitVal=1
}
System.exit(exitVal)
