/**
 * 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 org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.utils.HttpClientUtils
import org.apache.http.entity.StringEntity
import org.apache.http.client.utils.URIBuilder
import org.apache.http.util.EntityUtils
import com.urbancode.air.CommandHelper
import com.urbancode.air.ExitCodeException
import com.urbancode.air.XTrustProvider
import com.urbancode.commons.httpcomponentsutil.CloseableHttpClientBuilder

public class IBMContainerRegistryClient {
    CommandHelper helper
    final def workDir = new File('.').canonicalFile
    def registry
    def username
    def password
    def slurper
    def client
    def imageName
    def allowInsecure
    def api
    def apiKey
    def bearerToken
    def protocol
    def space
    def org
    def oauthAccessToken
    def bxHome

    public IBMContainerRegistryClient( def props) {
        this.registry = props['dockerRegistryName']?.trim()
        this.username = props['dockerRegistryUsername']?.trim()
        this.password = props['dockerRegistryPassword']?.trim()
        this.imageName = props['dockerImageName']?.trim()
        this.allowInsecure = (props['allowInsecure']?:"false").toBoolean()
        this.api = props['api']?.trim()
        this.apiKey = props['dockerRegistryAPIKey']?.trim()
        this.space = props['space']?.trim()
        this.org = props['org']?.trim()
        this.oauthAccessToken = "";
        this.bxHome = props['dockerHome']?.trim()
        if (bxHome.endsWith("/") || bxHome.endsWith("\\")) {
          bxHome = bxHome.substring(0,  bxHome.length() - 1)
        }

        this.protocol = "https://"

        this.slurper = new JsonSlurper()

        helper = new CommandHelper(workDir)

        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()
        this.client = builder.buildClient()

        bxLogin()
    }

    // Login to IBM Container Registry by running 'bx login' and 'bx cr login' processes
    void bxLogin() {
        def command = ["bx", "login"]

        if (api) {
            command << "-a"
            command << api
        }
        if (space) {
            command << "-s"
            command << space
            }
            if (org) {
                command << "-o"
                command << org
            }

        // Need to add support for '-c AccountID' at some point.
        // Would need changes to ImageRegistry resource role, Promote Image step,
        // and Docker source plugin.

        // Are we using API Key or user/password?
        if (apiKey) {
            command << "--apikey"
            command << apiKey
        }
        else {
            command << "-u"
            command << username
            command << "-p"
            command << password
        }

        CommandHelper bxLoginCH = new CommandHelper(new File("."))
        def raw
        bxLoginCH.runCommand("[Action] Logging into IBM Cloud", command, { proc ->
            proc.out.close() // close stdin
            proc.consumeProcessErrorStream(System.out) // forward stderr
            raw = proc.text.trim();
            def exitVal = proc.exitValue();
            if (exitVal > 0 )
                throw new java.net.ConnectException("Failed to connect to IBM Cloud Platform.  Please check Credentials, IBM Cloud API Endpoint, IBM Cloud Space, and IBM Cloud Organization.\n")
        })
    }

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

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

    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, Image Name, Registry, IBM Cloud API Endpoint, IBM Cloud Space, IBM Cloud Organization, and Bluemix Home. \n")
        }
        tagsJson = response.entity?.content?.getText("UTF-8")
        tags = slurper.parseText(tagsJson).tags

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

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

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

    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 requestWithAuth = { uri ->
        CloseableHttpResponse result
        CloseableHttpResponse initialResponse = null
        CloseableHttpResponse oauthResponse = null
        try {
            // if we already have an oauth token, use it to make the request
            if (oauthAccessToken) {
                result = requestWithOauth(uri, oauthAccessToken)
            }
            else {
                // make a request without an oauth token
                HttpGet initialRequest = new HttpGet(uri)
                // need to get the IAMRefreshToken for this registry from the .bluemix/config.json file
                def bxConfigFile = new File(bxHome + File.separator + "config.json")
                def bxConfigInfo = slurper.parseText(bxConfigFile.text)
                def iamRefreshToken = bxConfigInfo.IAMRefreshToken
                // token may start with prefix bearer:
                if (iamRefreshToken.startsWith("bearer:")) {
                    iamRefreshToken = iamRefreshToken.substring(7).trim();
                }
                // use the token from the .bluemix/config.json file to make a request
                initialRequest.addHeader("Content-Type", "application/x-www-form-urlencoded")
                initialRequest.addHeader("Authorization", "Bearer " + iamRefreshToken)
                println "[Action] Sending request to container registry using IAMRefreshToken from Bluemix config.json file"
                initialResponse = client.execute(initialRequest)
                def status = initialResponse.getStatusLine().getStatusCode()
                if (status == 401) {
                    // oauth is probably enabled; need to get oauth token
                    println "[Warning] Request returned 401 - Unauthorized. OAuth2 authentication may be enabled"
                    // the Www-Authenticate header in the 401 response has info on how to request oauth token
                    def String wwwAuthenticateHeader = initialResponse.getFirstHeader("Www-Authenticate")
                    println "[Info] The Www-Authenticate header is " + wwwAuthenticateHeader

                    // get the realm from the Www-Authenticate header
                    def realmIndexStart = wwwAuthenticateHeader.indexOf("realm=\"") + 7;
                    def realmIndexEnd = wwwAuthenticateHeader.indexOf("\"", realmIndexStart);
                    def String realm = wwwAuthenticateHeader.substring(realmIndexStart, realmIndexEnd);

                    // get the service from the Www-Authenticate header
                    def serviceIndexStart = wwwAuthenticateHeader.indexOf("service=\"") + 9;
                    def serviceIndexEnd = wwwAuthenticateHeader.indexOf("\"", serviceIndexStart);
                    def String service = wwwAuthenticateHeader.substring(serviceIndexStart, serviceIndexEnd);

                    // get the scope from the Www-Authenticate header
                    def scopeIndexStart = wwwAuthenticateHeader.indexOf("scope=\"") + 7;
                    def scopeIndexEnd = wwwAuthenticateHeader.indexOf("\"", scopeIndexStart);
                    def String scope = wwwAuthenticateHeader.substring(scopeIndexStart, scopeIndexEnd);

                    // build the request to get an oauth token
                    def URI oauthTokenURI = new URIBuilder(realm).build()
                    HttpPost oauthRequest = new HttpPost(oauthTokenURI)
                    // note the username is iamrefreshtoken since we are using the iamrefreshtoken
                    def newPayload = "service=" + service + "&scope=" + scope + "&client_id=ucd&grant_type=password&username=iamrefresh&password=" + iamRefreshToken;
                    oauthRequest.setEntity(new StringEntity(newPayload, "UTF-8"));
                    oauthRequest.setHeader("Content-Type", "application/x-www-form-urlencoded")

                    // make the request to get an oauth token
                    println "[Action] Making request to " + oauthTokenURI.toString() + " to obtain an oauth token"
                    oauthResponse = client.execute(oauthRequest);

                    // get the oauth token from the response
                    def entity = oauthResponse.getEntity();
                    def String responseString = EntityUtils.toString(entity, "UTF-8");
                    def responseJson = slurper.parseText(responseString)
                    oauthAccessToken = responseJson.access_token
                    println "[Ok] OAuth token obtained"
                    println "[Action] Sending request to the container registry using an oauth token"
                    result = requestWithOauth(uri, oauthAccessToken)

                }
                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 complete REST call to '${uri.toString()}': \n ${ex.getMessage()}")
        }
        finally {
           HttpClientUtils.closeQuietly(initialResponse)
           HttpClientUtils.closeQuietly(oauthResponse)
        }

        return result
    }

    def requestWithOauth(uri, oauthAccessToken) {
        HttpGet oauthRequest = new HttpGet(uri)
        oauthRequest.addHeader("Content-Type", "application/x-www-form-urlencoded")
        oauthRequest.addHeader("Authorization", "Bearer " + oauthAccessToken)
        def oauthResponse = client.execute(oauthRequest)
        return oauthResponse;
    }
}
