/*
* 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
*/

package com.urbancode.air.plugin.artifactory

import groovy.json.JsonException
import groovy.json.JsonSlurper

import java.security.NoSuchAlgorithmException
import java.util.zip.ZipFile

import org.apache.commons.lang3.ObjectUtils
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.HttpPut
import org.apache.http.client.methods.HttpRequestBase
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.entity.ContentType
import org.apache.http.entity.StringEntity
import org.apache.http.HttpStatus
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.util.EntityUtils

import com.urbancode.air.XTrustProvider
import com.urbancode.commons.fileutils.digest.DigestUtil
import com.urbancode.commons.fileutils.FileUtils
import com.urbancode.commons.httpcomponentsutil.HttpClientBuilder
import com.urbancode.commons.util.IO


public class ArtifactoryHelper {
    private CloseableHttpClient client
    private String apiKey

    public ArtifactoryHelper(username, password, apiKey, allowInsecure, proxyHost, proxyPort, proxyPass, proxyUser) {
        HttpClientBuilder builder = new HttpClientBuilder()
        if (apiKey) {
            this.apiKey = apiKey
        }
        else if(!username || !password) {
            println("[Warning] No Credentials provided.  If an anonymous user does not have view permission, Artifactory may return an empty list.")
            println("[Recommended] Please enter an API Key or Username and Password.")
        }
        else {
            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}"
            }
        }

        client = builder.buildClient()
    }

    public void verifyHash (File fileToVerify, storedDigest) {
        final ARTIFACT_FILE_HASH_ALGORITHM = "sha1"
        if (storedDigest != null) {
            String computedDigest
            try {
                computedDigest = DigestUtil.getHexDigest(fileToVerify, ARTIFACT_FILE_HASH_ALGORITHM)
                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 Maven remote artifacts not supported: " +
                ARTIFACT_FILE_HASH_ALGORITHM)
            }
            catch (IOException e) {
                throw new Exception("[Error] Error verifying downloaded Maven remote artifacts: ${e.getMessage()}", e)
            }
        }
    }


    public File downloadFileFromRepo(String url, String checkHash) {
        def response = executeHttpRequest(url, new HttpGet(), null)
        if (!response) {
            throw new Exception("[Error] Could not find file ${url} in Artifactory.")
        }
        int status = response.getStatusLine().getStatusCode()
        if (status == HttpStatus.SC_OK) {
            def jsonString = EntityUtils.toString(response.getEntity())
            def slurper = new JsonSlurper()
            def infoJSON = slurper.parseText(jsonString)
            def checksumMap = infoJSON.checksums
            def downloadUrl = infoJSON.downloadUri
            System.out.println("[Action] Downloading: ${downloadUrl}")
            response = executeHttpRequest(downloadUrl, new HttpGet(), null)
            if (!response) {
                throw new Exception("[Error] Could not download file : ${downloadUrl}\nResponse : ${response}")
            }
            status = response.getStatusLine().getStatusCode()
            if (status == HttpStatus.SC_OK) {
                String tempFileSuffix = ".maven2"
                File artifactFile = null
                try {
                    int extIndex = url.lastIndexOf(".")
                    if (extIndex >= 0) {
                        tempFileSuffix = url.substring(extIndex)
                    }
                    artifactFile = File.createTempFile("maven2-", tempFileSuffix)
                }
                catch (IOException ex) {
                    //If file contains faulty extension or has issues determining extension, set to default
                    tempFileSuffix = ".maven2"
                    println("[Info] Could not determine file extension. Setting temporary file extension to '${tempFileSuffix}'")
                    artifactFile = File.createTempFile("maven2-", tempFileSuffix)
                }
                FileUtils.writeInputToFile(response.getEntity().getContent(), artifactFile)
                if (checkHash && Boolean.valueOf(checkHash)) {
                    //verify checksum
                    verifyHash(artifactFile, checksumMap.sha1)
                    System.out.println("[Ok] Verification for file : ${artifactFile} succeeded!")
                }
                return artifactFile
            } else {
                throw new Exception("[Error] Exception downloading file : ${downloadUrl}\nErrorCode : ${status.toString()}")
            }
        } else {
            throw new Exception("[Error] Received non OK status from ${url} in Artifactory: ${response}")
        }
    }

    /**
    *  @param base The base URL of the Artifactory Server
    *  @param path The path to append to the base
    *  @return The combined base and path URL
    */
    public String getCombinedUrl(String base, String path) {
        if (base.endsWith('/')) {
            base = base.substring(0, base.length() - 1)
        }
        if (path.startsWith('/')) {
            path = path.substring(1)
        }
        return base + '/' + path
    }

    /**
    *  @param url The url to send the POST request
    *  @param json The JSON payload to add to the request
    */
    public def jsonPost(String url, String json) {
        StringEntity jsonEntity
        try  {
            jsonEntity = new StringEntity(json, ContentType.APPLICATION_JSON)
        }
        catch (IllegalArgumentException e) {
            println ("[Error] Could not create StringEntity for POST: ${e.getMessage()}")
            println ("POST data: ${json}.replace('\n', '')")
            return false
        }
        catch (UnsupportedEncodingException e) {
            println ("[Error] Could not create StringEntity for POST: ${e.getMessage()}")
            println ("POST data: ${json}.replace('\n', '')")
            return false
        }
        def response = executeHttpRequest(url, new HttpPost(), jsonEntity)
        if (!response) {
            println ("[Error] Could not execute POST")
            return false
        }
        int statusCode = response.getStatusLine().getStatusCode()
        String statusMessage = response.getStatusLine().getReasonPhrase()
        println("[Info] Response Status: " + statusCode.toString() + " - " + statusMessage)
        if (statusCode != HttpStatus.SC_OK) {
            println ("[Error] Bad response from server: ${statusCode} - ${statusMessage}")
            return false
        }
        return response
    }

    /**
    *  @param url The URL to execute the HTTP PUT against
    *  @return True if the put operation is successful, false otherwise
    */
    public boolean put(String url) {
        def response = executeHttpRequest(url, new HttpPut(), null)
        if (!response) {
            return false
        }
        def statusCode = response.getStatusLine().getStatusCode()
        def text = response.entity?.content?.getText("UTF-8")
        if (statusCode != HttpStatus.SC_NO_CONTENT) {
            println ("[Error] Received bad status from Artifactory: ${statusCode} - ${text}")
            return false
        }
        else {
            return true
        }
    }

    /**
    *  @param url The url of the artifact to download
    *  @param workDir The working directory to download the artifact to
    *  @return True if the artifact is downloaded successfully, false otherwise
    */
    public boolean download(String url, def workDir) {
        println ("[Action] Downloading ${url} to temporary file...")
        def response = executeHttpRequest(url, new HttpGet(), null)
        if (!response) {
            return false
        }
        def statusCode = response.getStatusLine().getStatusCode()
        if (statusCode != HttpStatus.SC_OK) {
            println ("[Error] Unable to download artifacts. Response from Artifactory: ${statusCode} - ${response}")
            return false
        }
        String tempFileSuffix = ".tmp"
        int extIndex = url.lastIndexOf(".")
        if (extIndex >= 0) {
            tempFileSuffix = url.substring(extIndex)
        }
        String fileName
        // Attempt to get file name from response header
        if (response.getFirstHeader('Content-Disposition')) {
            def headerValues = response.getFirstHeader('Content-Disposition').getValue().split(';')
            String searchTerm = ' filename='
            headerValues.each { value ->
                if (value.startsWith(searchTerm)) {
                    fileName = value.substring(searchTerm.length() + 1, value.length() - 1)
                }
            }
        }
        // If filename couldn't be determined, get it from url
        if (!fileName) {
            String[] filePath = url.split('/')
            fileName = filePath[filePath.length - 1]
        }
        return downloadAndMoveFile(response, tempFileSuffix, fileName, workDir)
    }

    /**
    *  @param url The url of the download archive endpoint
    *  @param postInfo the json string containing the info to send to the download endpoint
    *  @param suffix The suffix for the temporary file
    *  @param fileName The name to download the final file as
    *  @param workDir The working directory to download the file in
    *  @return True if the artifact is downloaded successfully, false otherwise
    */
    public boolean downloadArchive(String url, String postInfo, String suffix, String fileName, def workDir) {
        println ("[Action] Downloading archive from ${url} with payload ${postInfo.replace('\n', '')} to temporary file...")
        def response = jsonPost(url, postInfo)
        if (!response) {
            return false
        }
        return downloadAndMoveFile(response, suffix, fileName, workDir)
        response.close()
    }

    /**
    *  @param url The HTTP response to download the file from
    *  @param tmpSuffix The suffix for the temporary file
    *  @param fileName The name to download the final file as
    *  @param workDir The working directory to download the file in
    *  @return True if the artifact is downloaded successfully, false otherwise
    */
    private boolean downloadAndMoveFile(def response, String tmpSuffix, String fileName, def workDir) {
        File tempFile = File.createTempFile("ArtifactoryDownload-", tmpSuffix)
        tempFile.deleteOnExit()
        try {
            FileUtils.writeInputToFile(response.getEntity().getContent(), tempFile)
        }
        catch (IOException e) {
            println ("[Error] Failed to download artifact to temporary file: ${e.getMessage()}")
            return false
        }
        println ("[Ok] Downloaded to temporary file ${tempFile.getAbsolutePath()}")
        File finalFile = new File(workDir, fileName)
        println ("[Action] Moving downloaded artifact to: ${finalFile.getAbsolutePath()}")
        try {
            IO.move(tempFile, finalFile)
        }
        catch (IOException e) {
            println ("[Error] Failed to move downloaded temporary file: ${e.getMessage()}")
            return false
        }
        println ('[Ok] Artifact moved.')
        return true
    }

    /**
    *  @param uri The URI of the HTTP Request to make
    *  @param request HTTP request to make
    *  @param postInfo The information to set as an entity for an HTTP POST/PUT
    *  @return The response received from executing the request
    */
    private def executeHttpRequest(String uri, HttpRequestBase request, def postInfo) {
        try {
            request.setURI(encodeUri(uri))
        }
        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
    }

    /**
    *  @param response The response to parse for messages
    *  @param level The level of messages to print out
    */
    private void printMessages(String text, String level) {
        try {
            def messages = new JsonSlurper().parseText(text)."messages"
            if (messages instanceof ArrayList && messages.size() > 0) {
                messages.each { message ->
                    if (message.level.equals(level)) {
                        println ("[${level.toLowerCase()}] ${message.message}")
                    }
                }
            }
        }
        catch (JsonException jse) {
            println ("[Error] Returned JSON is not well-formed: ${jse.getMessage()}")
            println ("JSON: ${text.replace('\n', '')}")
        }
    }

    private URI encodeUri(String uri) {
        if (!uri.startsWith('http://') && !uri.startsWith('https://')) {
            uri = 'https://' + uri
        }
        URL rawUri = new URL(uri)
        return new URI(rawUri.getProtocol(),
                       rawUri.getAuthority(),
                       rawUri.getPath(),
                       rawUri.getQuery(),
                       rawUri.getRef())
    }
}
