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

import com.urbancode.air.ExitCodeException
import com.urbancode.air.XTrustProvider
import com.urbancode.commons.httpcomponentsutil.CloseableHttpClientBuilder

public class RegistryClient {

    def registry
    def username
    def password
    def proxyHost
    def proxyPortString
    def proxyUsername
    def proxyPassword
    def slurper
    def client
    def imageName
    def allowInsecure
    def protocol

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

        this.proxyHost = props['proxyHost']
        this.proxyPortString = props['proxyPort']
        this.proxyUsername = props['proxyUsername']
        this.proxyPassword = props['proxyPassword']

        this.protocol = "https://"

        this.slurper = new JsonSlurper()

        CloseableHttpClientBuilder builder = new CloseableHttpClientBuilder()
        if (username) {
            builder.setPreemptiveAuthentication(true)
            builder.setUsername(username)
            builder.setPassword(password)
        }
        if (proxyHost) {
            def proxyPort = Integer.parseInt(proxyPortString)
            builder.setProxyHost(proxyHost)
            builder.setProxyPort(proxyPort)
            if (proxyUsername) {
                builder.setProxyUsername(proxyUsername)
                builder.setProxyPassword(proxyPassword)
            }
        }

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

        this.client = builder.buildClient()

        // Confirm registry does exist with the given information
        println "[Ok] Connecting to registry..."
        pingRegistry()
        println "[Ok] Connected successfully."
        println ""
    }

    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
    }

    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?.text
            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
    }

    def getBaseUri() {
        return new URIBuilder("${protocol}${registry}").build()
    }

    def getPingUri() {
        return new URIBuilder(getBaseUri()).setPath("/v2").build()
    }

    def getTagsUri() {
        return new URIBuilder(getBaseUri()).setPath("/v2/${imageName}/tags/list").build()
    }

    def getManifestsUri = { tag ->
        return new URIBuilder(getBaseUri()).setPath("/v2/${imageName}/manifests/${tag}").build()
    }

    void pingRegistry() {
        // Assumes using protocol of https://
        def response
        int status
        StatusLine statusLine
        String body
        try {
            response = requestWithAuth(getPingUri())
            status = response.statusLine.statusCode
            statusLine = response.statusLine
            body = response.entity?.content?.text
        }
        catch (SSLException ex) {
            println "[Error] Response from registry server: ${ex.getMessage()}"
            // Continue and try to connect with insecure
        }
        if (status != 200) {
            if (status) {
                println "[Warning] Unexpected response from registry server:"
                println statusLine.toString()
                println body
            }

            // Try using http:// instead
            if (allowInsecure) {
                try {
                    this.protocol = "http://"
                    response = requestWithAuth(getPingUri())
                    status = response.statusLine.statusCode
                    statusLine = response.statusLine
                    body = response.entity?.content?.text
                    if (status != 200) {
                        println "[Ok] Response from registry server:"
                        println statusLine.toString()
                        println body
                        throw new RuntimeException("[Error] Unable to ping registry using http and https protocols. Please check credentials and make sure that registry supports v2 api.")
                    }
                    println "[Warning] Connected to registry using the unsecure http protocol."
                }
                catch (SSLException ex) {
                    println "[Error] Response from registry server: ${ex.getMessage()}"
                    throw ex
                }
            }
            else {
                throw new java.net.ConnectException('[Possible Solution] Allow Insecure to attempt the connection over HTTP protocol.')
            }
        }
    }

    public def getTags() {
        def tags = []
        def tagsJson
        println "[Ok] Listing tags for ${imageName}..."
        CloseableHttpResponse response = requestWithAuth(getTagsUri())
        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. \nResponse Error: ${response.entity?.content?.text}\n")
        }
        tagsJson = response.entity?.content?.text
        tags = slurper.parseText(tagsJson).tags

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

        return tags
    }

    private def getManifest = { def tag ->
        Map result
        CloseableHttpResponse response = requestWithAuth(getManifestsUri(tag))
        String manifestJson = response.entity?.content?.text
        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
    }

    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.\n" +
                "Error Response: ${response.entity?.content?.text}")
            }
            else {
                result = initialResponse
            }
        }
        catch (ConnectException ex) {
            throw new ConnectException("[Error] Could not connect to the registry: '${registry}'. " +
            "Please verify the registry is reachable and the credentials are valid.")
        }
        catch (UnknownHostException ex) {
            throw new UnknownHostException("[Error] Could not connect to the registry: '${registry}'. " +
            "Please verify the registry is valid.")
        }
        catch (Exception ex) {
            throw new Exception("[Error] Unable to complete REST call: ${ex.getMessage()}")
        }
        return result
    }
}
