/**
 * 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 org.apache.http.Header
import org.apache.http.StatusLine

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

    public Docker( def props) {
        this.registry = props['dockerRegistryName']?.trim()
        this.username = props['dockerRegistryUsername']?.trim()
        this.password = props['dockerRegistryPassword']
        this.imageName = props['dockerImageName']?.trim()
        this.allowInsecure = (props['allowInsecure']?:"false").toBoolean()
        this.registryType  = props["registryType"]

        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
        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
        }
        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 = 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()

            /* Checks if link exists and if it contains the correct delimiters */
            if (linkVal?.contains('<') && linkVal?.contains('>')) {
                String nextPage = linkVal.substring(linkVal.indexOf('<') + 1, linkVal.indexOf('>'))
                tagsUri = getLinkUri(nextPage)
            }
            else {
                morePages = false
            }
        }

        if (tags) {
            println "[Ok] Retrieved ${tags?.size()} tag(s)."
            println ""
        } else {
            println "No tags found"
        }

        return tags
    }

    public def getManifest = { def tag ->
        Map result
        CloseableHttpResponse response = requestWithAuth(getManifestsUri(tag))
        String manifestJson = response.entity?.content?.getText("UTF-8")
        result = slurper.parseText(manifestJson)
        return result
    }

    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) {
            result = slurper.parseText(v1).config.Labels
        }
        else {
            println '[Warning] Found listing for tag, "' + tag + '" in registry but no manifest. Skipping...'
            manifest?.errors.each { println "Response from registry: $it.message" }
        }
        return result
    }

    public def getIdForTag = { def tag ->
        def result
        result = null
        def manifest = getManifest(tag)
        def v1 = null
        def history = manifest.history
        if (history) {
            v1 = history[0]?.v1Compatibility
        }
        if (v1) {
            result = slurper.parseText(v1).id
        } else {
            println '[Warning] Found listing for tag, "' + tag + '" in registry but no manifest. Skipping...'
            manifest?.errors.each { println "Response from registry: $it.message" }
        }

        return result
    }

    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)

            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")
                authenticatedResponse = client.execute(authenticatedRequest)
                result = authenticatedResponse
                HttpClientUtils.closeQuietly(initialResponse)
            }
            else if (status < 200 || status >= 300) {
                throw new ExitCodeException("[Error] Unable to access the specified Docker Registry '${uri.toString()}'.\n" +
                "Error Response: " + initialResponse?.entity?.content?.getText("UTF-8"))
            }
            else {
                result = initialResponse
            }
        }
        catch (ConnectException ex) {
            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) {
            throw new UnknownHostException("[Error] Could not connect to the registry: '${uri.toString()}'. " +
            "Please verify the registry is valid.")
        }
        catch (ExitCodeException ex) {
            throw ex
        }
        catch (Exception ex) {
            throw new Exception("[Error] Unable to connect to '${uri.toString()}': \n Please check credentials, image name, and Registry. \n")
        }
        return result
    }
}
