/**
 * Licensed Materials - Property of IBM* and/or HCL**
 * UrbanCode Deploy
 * (c) Copyright IBM Corporation 2011, 2018. All Rights Reserved.
 * (c) Copyright HCL Technologies Ltd. 2018, 2019. 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.docker

import groovy.json.JsonSlurper
import javax.net.ssl.SSLException
import java.util.regex.Matcher
import java.util.regex.Pattern
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.utils.HttpClientUtils
import org.apache.http.client.utils.URIBuilder
import com.urbancode.air.ExitCodeException
import com.urbancode.air.XTrustProvider
import com.urbancode.commons.httpcomponentsutil.CloseableHttpClientBuilder

public class Docker {

    def registry
    def username
    def password
    def slurper
    def client
    def imageName
    def allowInsecure
    def protocol
    def registryType
    def importCustomPropertiesJFrog
    def digest
    def harborProject
    def harborRepository

    public Docker( def props) {
        this.registry = props['dockerRegistryName']?.trim()
        this.username = props['dockerRegistryUsername']?.trim()
        this.password = props['dockerRegistryPassword']
        this.imageName = props['dockerImageName']?.trim()
        this.harborProject = props['harborProject']?.trim()
        this.harborRepository = props['harborRepository']?.trim()
        this.allowInsecure = (props['allowInsecure']?:"false").toBoolean()
        this.registryType  = props["registryType"]
        this.importCustomPropertiesJFrog = (props['importCustomPropertiesJFrog']?:"false").toBoolean()
        this.protocol = "https://"
        this.slurper = new JsonSlurper()

        CloseableHttpClientBuilder builder = new CloseableHttpClientBuilder()
        if (username) {
            builder.setPreemptiveAuthentication(true)
            builder.setUsername(username)
            builder.setPassword(password)
        }

        if (allowInsecure) {
            XTrustProvider.install()
            builder.setTrustAllCerts(true)
        }

        this.client = builder.buildClient()
    }

    public def parseAuthHeader = { def header ->
        def result = new Expando()
        Pattern p = Pattern.compile("(\\S+)=(\\S+)")
        String [] chunks = header.value.split("[,\\s]+")

        chunks.each { chunk ->
            Matcher m = p.matcher(chunk)
            if (m.matches())
            {
                if (m.group(1).equalsIgnoreCase("realm")) {
                    result.realm = m.group(2).replaceAll("\\\"", "")
                }
                else if (m.group(1).equalsIgnoreCase("service")) {
                    result.service = m.group(2).replaceAll("\\\"", "")
                }
                else if (m.group(1).equalsIgnoreCase("scope")) {
                    result.scope = m.group(2).replaceAll("\\\"", "")
                }
            }
        }

        return result
    }

    public def getToken = { def authHeader ->
        def result = null
        def authConfig = parseAuthHeader(authHeader)
        def uriBuilder = new URIBuilder(authConfig.realm)
        if (authConfig.scope) {
            uriBuilder.setParameter("scope", authConfig.scope)
        }
        if (authConfig.service) {
            uriBuilder.setParameter("service", authConfig.service)
        }
        def tokenUri = uriBuilder.build()
        CloseableHttpResponse tokenResponse = null
        try {
            HttpGet tokenRequest = new HttpGet(tokenUri)
            tokenResponse = client.execute(tokenRequest)
            String responseBody = tokenResponse.entity?.content?.getText("UTF-8")
            if (responseBody) {
                result = slurper.parseText(responseBody).token
            } else {
                println "[Warning] Authorization Token was not found."
            }
        }
        catch (SSLException ex) {
            println "[Error] Response from registry server: ${ex.getMessage()}"
            throw ex
        }
        finally {
            HttpClientUtils.closeQuietly(tokenResponse)
        }
        return result
    }

    public def getTags() {
        List<String> tags = new ArrayList<String>()
        def tagsJson
        println "[Ok] Listing tags for ${imageName}..."

        URI tagsUri = getTagsUri()
        boolean morePages = true
        while (morePages) {
            CloseableHttpResponse response = null
            try {
                response = requestWithAuth(tagsUri)
                int statusCode = response.statusLine.statusCode
                if (statusCode != 200) {
                    throw new java.net.ConnectException("[Error] Unable to find image: '${imageName}'. Please confirm Credentials and Image Name. \n")
                }
                tagsJson = response.entity?.content?.getText("UTF-8")
                tags.addAll(slurper.parseText(tagsJson).tags)
                String linkVal = response.getFirstHeader("link")?.getValue()

                if (linkVal?.contains('<') && linkVal?.contains('>')) {
                    String nextPage = linkVal.substring(linkVal.indexOf('<') + 1, linkVal.indexOf('>'))
                    tagsUri = getLinkUri(nextPage)
                }
                else {
                    morePages = false
                }
            }
            finally {
                HttpClientUtils.closeQuietly(response)
            }
        }
        if (tags) {
            println "[Ok] Retrieved ${tags?.size()} tag(s)."
            println ""
        } else {
            println "No tags found"
        }

        return tags
    }

    public def getTagsHarbor() {
        List<String> tags = new ArrayList<String>()
        println "Listing tags for Harbor image: ${harborProject}/${harborRepository}"
        URI tagsUri = new URIBuilder(getBaseUri())
                .setPath("/api/v2.0/projects/${harborProject}/repositories/${harborRepository}/artifacts").build()

        CloseableHttpResponse response = null
        try {
            response = requestWithAuth(tagsUri)
            def artifactsJson = response.entity?.content?.getText("UTF-8")
            def artifacts = slurper.parseText(artifactsJson)
            artifacts.each { artifact ->
                artifact.tags.each { tagObj ->
                    tags.add(tagObj.name)
                }
            }
        }
        finally {
            HttpClientUtils.closeQuietly(response)
        }
        return tags
    }

    public def getManifest = { def tag ->
        Map result
        CloseableHttpResponse response = null
        try {
            response = requestWithAuth(getManifestsUri(tag))
            String manifestJson = response.entity?.content?.getText("UTF-8")
            result = slurper.parseText(manifestJson)
            if (response.containsHeader("Docker-Content-Digest")) {
                this.digest = response.getFirstHeader("Docker-Content-Digest").getValue()
                printf("[Info] getManifest(%s) received Docker-Content-Digest response header: %s\n", tag, this.digest)
            }
        }
        finally {
            HttpClientUtils.closeQuietly(response)
        }
        return result
    }

    public def getArtifacts = { def tag ->
        Map result
        CloseableHttpResponse response = null
        try {
            response = requestWithAuth(getArtifactsUri(tag))
            String artifactsJson = response.entity?.content?.getText("UTF-8")
            result = slurper.parseText(artifactsJson)
        }
        finally {
            HttpClientUtils.closeQuietly(response)
        }
        return result
    }

    public def getCustomProp = { def tag ->
        CloseableHttpResponse response = null
        try {
            Map result
            response = requestWithAuth(getCustomPropsUri(tag))
            String manifestJson = response.entity?.content?.getText("UTF-8")
            result = slurper.parseText(manifestJson)
            return result
        }
        catch (Exception e){
            println "[warning] Unable to find custom properties for '${tag.toString()}'."
            return null
        }
        finally {
            HttpClientUtils.closeQuietly(response)
        }
    }

    public def getLabelsForTag = { def tag ->
        Map<String,String> result = new HashMap<String,String>()
        def manifest = getManifest(tag)
        def v1 = null
        def history = manifest.history
        if (history) {
            v1 = history[0]?.v1Compatibility
        }
        if (v1) {
            if (slurper.parseText(v1).config.Labels != null) {
                result = slurper.parseText(v1).config.Labels
                // println '[Debug] Result: ' + result
            }
        }
        else {
            println '[Warning] While getting labels, found manifest for tag, "' + tag + '" but unable to find labels.'
            manifest?.errors.each { println "Response from registry: $it.message" }
        }
        if (importCustomPropertiesJFrog) {
            def customPropResults = getCustomProp(tag)
            // println '[Debug] customPropResults: ' + customPropResults
            if (customPropResults != null && customPropResults.properties) {
                def v2 = customPropResults.properties
                // println '[Debug] v2: ' + v2
                v2.each { key, value ->
                    if (key != null && value != null) {
                        // println '[Debug] Key: ' + key.toString() + '  Vaule: ' + value[0].toString()
                        result.put(key.toString(), value[0].toString())
                    }
                }
            }
        }
        return result
    }
    public def getLabelsForTagHarbor = { def tag ->
        Map<String,String> result = new HashMap<String,String>()
        def artifacts = getArtifacts(tag)
        def labels = artifacts.tags.name[0];
        result["name"] = labels
        return result;
    }

    public def getIdForTag = { def tag ->
        def result
        result = null
        def manifest = getManifest(tag)
        def v1 = null
        def history = manifest.history
        if (manifest.schemaVersion.equals(2)) {
            // If response header Docker-Content-Digest is present,
            // it should be the digest value we should return
            if (this.digest) {
                return this.digest
            }
        }
        if (history) {
            v1 = history[0]?.v1Compatibility
        }
        if (v1) {
            result = slurper.parseText(v1).id
        } else {
            println '[Warning] While getting Ids, found listing for tag, "' + tag + '" in registry but no manifest. Unable to return digest value.'
            manifest?.errors.each { println "Response from registry: $it.message" }
        }

        return result
    }
    public def getIdForTagHarbor = { def tag ->
        def result
        result = null
        def artifacts = getArtifacts(tag)
        return artifacts.id;
    }

    public def getDigestForTagHarbor = { def tag ->
        def artifacts = getArtifacts(tag)
        return artifacts.digest;
    }
    public def getSignatureForTag = { def tag ->
        def result
        def manifest = getManifest(tag)
        result = manifest.signatures[0]?.signature
        return result
    }

    public def requestWithAuth = { uri ->
        CloseableHttpResponse result
        CloseableHttpResponse initialResponse = null
        CloseableHttpResponse authenticatedResponse = null
        try {
            HttpGet initialRequest = new HttpGet(uri)
            if (uri.toString().contains("/manifests")) {
                /* Accept OCI and schemaVersion 2 manifests and indexes */
                initialRequest.setHeader("Accept", "application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json")
            }
            initialResponse = client.execute(initialRequest)
            def status = initialResponse.getStatusLine().getStatusCode()
            if (status == 401) {
                String token = getToken(initialResponse.getFirstHeader("Www-Authenticate"))
                HttpGet authenticatedRequest = new HttpGet(uri)
                authenticatedRequest.setHeader("Authorization", "Bearer $token")
                if (uri.toString().contains("/manifests")) {
                    /* Accept OCI and schemaVersion 2 manifests and indexes */
                    authenticatedRequest.setHeader("Accept", "application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json")
                }
                authenticatedResponse = client.execute(authenticatedRequest)
                result = authenticatedResponse
                HttpClientUtils.closeQuietly(initialResponse)
            }
            else if (status < 200 || status >= 300) {
                try {
                    throw new ExitCodeException("[Error] Unable to access the specified Docker Registry '${uri.toString()}'.\n" +
                            "Error Response: " + initialResponse?.entity?.content?.getText("UTF-8"))
                }
                finally {
                    HttpClientUtils.closeQuietly(initialResponse)
                }
            }
            else {
                result = initialResponse
            }
        }
        catch (ConnectException ex) {
            HttpClientUtils.closeQuietly(initialResponse)
            throw new ConnectException("[Error] Could not connect to the registry: '${uri.toString()}'. " +
                    "Please verify the registry is reachable and the credentials are valid.")
        }
        catch (UnknownHostException ex) {
            HttpClientUtils.closeQuietly(initialResponse)
            throw new UnknownHostException("[Error] Could not connect to the registry: '${uri.toString()}'. " +
                    "Please verify the registry is valid.")
        }
        catch (ExitCodeException ex) {
            HttpClientUtils.closeQuietly(initialResponse)
            throw ex
        }
        catch (Exception ex) {
            HttpClientUtils.closeQuietly(initialResponse)
            throw new Exception("[Error] Unable to connect to '${uri.toString()}': \n Please check credentials, image name, and Registry. \n")
        }
        return result
    }
}
