/*
 * Licensed Materials - Property of IBM Corp.
 * IBM UrbanCode Deploy
 * (c) Copyright IBM Corporation 2017. 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.artifactory

import java.io.File

import groovy.json.JsonSlurper

import java.security.NoSuchAlgorithmException

import org.apache.commons.lang.ObjectUtils
import org.apache.http.HttpResponse
import org.apache.http.HttpStatus
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPut
import org.apache.http.entity.StringEntity
import org.apache.http.entity.FileEntity
import org.apache.http.impl.client.CloseableHttpClient

import com.urbancode.commons.fileutils.FileUtils
import com.urbancode.commons.fileutils.AntStylePatternMatcher
import com.urbancode.commons.fileutils.digest.DigestUtil
import com.urbancode.commons.httpcomponentsutil.CloseableHttpClientBuilder
import com.urbancode.commons.util.IO

public class ArtifactoryHelper {

    private final CloseableHttpClientBuilder builder = new CloseableHttpClientBuilder()
    private final ARTIFACT_FILE_HASH_ALGORITHM = "sha1"

    private String username
    private String password

    private List<String> findFiles(File baseDir, List<String> includes, List<String> excludes) {
        if ((!baseDir.exists()) || baseDir.isFile()) {
            throw new Exception("The base directory '" + baseDir.getPath() + "' does not exist or is a file")
        }
        // FileUtils#getFileArray trims the baseDir part when returning data
        String[] filePaths = FileUtils.getFileArray(baseDir, [] as String[], [] as String[])
        String[] includesArray = includes as String[]
        String[] excludesArray = excludes as String[]
        AntStylePatternMatcher antMatcher = new AntStylePatternMatcher(includesArray, excludesArray)
        List<String> matchingFiles = new ArrayList<File>()
        for (String filePath : filePaths) {
            filePath = trimLeadingSlashes(filePath)
            filePath = normalizePath(filePath)
            File file = new File(baseDir, filePath)
            if (antMatcher.accept(file.getPath())) {
                println "Path $file.path matches the include/exclude patterns"
                matchingFiles.add(filePath)
            }
            else {
                println "Path $file.path does not match the include/exclude patterns. Skipping"
            }
        }
        return matchingFiles
    }

    private String calculateSha1(File file) {
        return DigestUtil.getHexDigest(file, ARTIFACT_FILE_HASH_ALGORITHM)
    }

    private boolean verifyChecksum(File fileToVerify, String checksum) {
        if (checksum) {
            String computedDigest
            try {
                computedDigest = calculateSha1(fileToVerify)
                return ObjectUtils.equals(checksum, computedDigest)
            }
            catch (NoSuchAlgorithmException e) {
                throw new Exception("Algorithm not supported: " + ARTIFACT_FILE_HASH_ALGORITHM)
            }
            catch (IOException e) {
                throw new Exception("Error verifying downloaded artifact: " +
                e.getMessage(), e)
            }
        }
    }

    private String parseProperties(Properties props, String delimiter) {
        props.inject(['']) { result, key, value ->
            if (value.class.isArray()) {
                value.each {
                    result << "$key$delimiter$it"
                }
            } else {
                result << "$key$delimiter${value}"
            }
            result
        }.join(';')
    }

    //----------------------------------------------------------------------------------------------

    public ArtifactoryHelper(String username, String password) {
        this.username = username
        this.password = password
    }

    public CloseableHttpClient createClient() {
        builder.setUsername(username)
        builder.setPassword(password)
        builder.setPreemptiveAuthentication(true)
        CloseableHttpClient client = builder.buildClient()
        return client
    }

    public File downloadFileFromRepo(CloseableHttpClient client,
                                     String repoUrl, String repoName, File baseDir, String targetPath,
                                     Boolean verifyHash) {
        String downloadUrl = repoUrl + "/" + repoName + "/" + targetPath
        HttpGet get = new HttpGet(downloadUrl)
        HttpResponse response = client.execute(get)
        int status = response.getStatusLine().getStatusCode()
        if (status != HttpStatus.SC_OK) {
            throw new Exception("Exception downloading file : " + downloadUrl + "\nErrorCode : " + status.toString());
        }
        File tempArtifactFile
        File targetFile
        try {
            tempArtifactFile = File.createTempFile("temp", null)
            tempArtifactFile << response.entity.content
            if (verifyHash) {
                String fileInfoUrl = repoUrl + "/api/storage/" + repoName + "/" + targetPath
                HttpGet fileInfoGet = new HttpGet(fileInfoUrl)
                HttpResponse fileInfoRes = client.execute(fileInfoGet)
                int fileInfoStatus = response.getStatusLine().getStatusCode()
                if (fileInfoStatus != HttpStatus.SC_OK) {
                    throw new Exception("Exception downloading file info from $fileInfoUrl: Response: "
                        + status.toString())
                }
                String fileInfoEntity = fileInfoRes.entity.content.text
                JsonSlurper slurper = new JsonSlurper()
                def fileInfoResult = slurper.parseText(fileInfoEntity)
                def sha1Sum = fileInfoResult?.checksums?.sha1
                if (!sha1Sum) {
                    throw new Exception("The response entity does not seem to have a sha1 checksum:\n$fileInfoEntity")
                }
                if (!verifyChecksum(tempArtifactFile, sha1Sum)) {
                    throw new Exception("Verification for file : " + tempArtifactFile + " failed")
                }
                println("Verification for file " + tempArtifactFile + " succeeded")
            }
            targetFile = new File(baseDir, targetPath)
            IO.mkdirs(targetFile.getParentFile())
            IO.move(tempArtifactFile, targetFile)
        }
        finally {
            if (tempArtifactFile != null && tempArtifactFile.exists()) {
                IO.delete(tempArtifactFile)
            }
        }
        return targetFile
    }

    public void uploadFileToRepo(CloseableHttpClient client,
                                 String baseUrl, String repoName, String targetPath, File baseDir,
                                 List<String> includes, List<String> excludes, Boolean preserveDirs,
                                 Properties properties) {
        List<String> fileList = findFiles(baseDir, includes, excludes);
        println "Found " + fileList.size() + " file(s) matching the include/exclude patterns";
        for (String filePath : fileList) {
            String artifactUploadPath = targetPath
            if (preserveDirs) {
                artifactUploadPath += "/" + filePath
            }
            else {
                File file = new File(filePath)
                artifactUploadPath += "/" + file.getName()
            }
            File fileToUpload = new File(baseDir, filePath)

            String parsedProperties = parseProperties(properties, "=")
            String uploadUrl = baseUrl + "/" + repoName + "/" + artifactUploadPath + parsedProperties

            HttpPut request = new HttpPut(uploadUrl)
            FileEntity fileEntity = new FileEntity(fileToUpload, "application/octet-stream")
            request.setEntity(fileEntity)

            String sha1 = calculateSha1(fileToUpload)
            request.setHeader("X-Checksum-Sha1", sha1)

            println "Uploading file '$fileToUpload.path' to path '$artifactUploadPath' of the Artifactory server"
            HttpResponse response = client.execute(request)
            int responseCode = response.getStatusLine().getStatusCode()
            if (responseCode == HttpStatus.SC_OK || responseCode == 201) {
                String responseEntity = response.entity.content.text
                JsonSlurper slurper = new JsonSlurper()
                def entityJson = slurper.parseText(responseEntity)
                String downloadUri = entityJson?.downloadUri
                println "Upload done. Download url: $downloadUri"
            }
            else {
                println response
                throw new Exception("Failed to upload file $fileToUpload.path: Error " + responseCode.toString()
                    + "\nMessage: " + response?.getEntity()?.getContent()?.text)
            }
        }
    }

    public void uploadBuildInfoJson(CloseableHttpClient client, String repoUrl, String buildInfo) {
        repoUrl = trimTrailingSlashes(repoUrl)
        String url = repoUrl + "/api/build/"
        HttpPut request = new HttpPut(url)
        StringEntity buildInfoEntity = new StringEntity(buildInfo)
        buildInfoEntity.setContentType("application/json")
        request.addHeader("Content-Type", "application/json")
        request.setEntity(buildInfoEntity)
        HttpResponse response = client.execute(request)
        int responseCode = response.getStatusLine().getStatusCode()
        if (responseCode == HttpStatus.SC_OK || responseCode == 204) {
            println "Build info has been successfully uploaded"
        }
        else {
            println response
            throw new Exception("Failed to upload build information: Error " + responseCode.toString()
                + "\nMessage: " + response?.getEntity()?.getContent()?.text)
        }
    }

    public String trimTrailingSlashes(String path) {
        String result = path.replaceAll("/+\$", "")
        result = result.replaceAll("\\\\+\$", "")
        return result
    }

    public String trimLeadingSlashes(String path) {
        String result = path.replaceAll("^/+", "")
        result = result.replaceAll("^\\\\+", "")
        return result
    }

    public String normalizePath(String path) {
        return path.replace("\\", "/")
    }
}
