/*
 * 
 * 
 * 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, 2020. 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 DockerTrustedRegistryClient {

    def registry
    def username
    def password
    def slurper
    def client
    def imageName
    def allowInsecure
    def protocol

    public DockerTrustedRegistryClient( 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.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()
		
		// Confirm registry does exist with the given information
		println "[Ok] Connecting to docker trusted registry..."
		pingRegistry()
		println "[Ok] Connected successfully."
		println ""
		
    }


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

    private def getPingUri() {
		
        return new URIBuilder(getBaseUri()).setPath("api/v0/repositories").build()
    }

    private def getTagsUri() {
	
        return new URIBuilder(getBaseUri()).setPath("api/v0/repositories/${imageName}/tags").build()
    }

    private def getManifestsUri() {
	
        return new URIBuilder(getBaseUri()).setPath("api/v0/repositories/${imageName}/manifests").build()
    }

    private pingRegistry() {

		// Assumes using protocol of https://
		
        def response
        int status
        StatusLine statusLine
        String body
		println("[Info] Ping url :" + getPingUri().toString())
        try {
            response = requestWithAuth(getPingUri())
            status = response.statusLine.statusCode
			println("Ping url status :" + status)
            statusLine = response.statusLine
            body = response.entity?.content?.getText("UTF-8")
        }
        catch (Exception ex) {
            println "[Error] Response from docker trusted registry server: ${ex.getMessage()}"
            // Continue and try to connect with insecure
        }
        finally {
            HttpClientUtils.closeQuietly(response)
        }
        if (status != 200) {
            if (status) {
                println "[Warning] Unexpected response from docker trusted 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?.getText("UTF-8")
                    if (status != 200) {
                        println "[Ok] Response from docker trusted registry server:"
                        println statusLine.toString()
                        println body
                        throw new java.net.ConnectException("[Error] Unable to ping registry using http and https protocols. Please check credentials and make sure that registry supports v0 api.")
                    }
                    println "[Warning] Connected to docker trusted registry using the unsecure http protocol."
                }
                catch (Exception ex) {
                    println "[Error] Response from docker trusted registry server: ${ex.getMessage()}"
                    throw ex
                }
                finally {
                    HttpClientUtils.closeQuietly(response)
                }
            }
            else {
                throw new java.net.ConnectException('[Possible Solution] Allow Insecure to attempt the connection over HTTP protocol.')
            }
        }
        
    }
	
	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 docker trusted 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()
		println("[Info] Tags url :" + tagsUri.toString())
		boolean morePages = true

		while (morePages) {
			CloseableHttpResponse response = requestWithAuth(tagsUri)
			int statusCode = response.statusLine.statusCode
			println("[Info] Tags url status:" + 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")
			tagsJson = slurper.parseText(tagsJson)
			tagsJson.each{ tag ->
				tags.add(tag.name)
			}
			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
			}
		}
		
		println "[Info] Tags retrieved : ${tags} "
		
		if (tags) {
			println "[Ok] Retrieved ${tags?.size()} tag(s)."
			println ""
		} else {
			println "[Info] No tags found"
		}
		
		return tags
	}

	public def getManifest() {

		Map result
		println("[Info] Manifest details ---------------------")
		println("[Info] Mnanifest url :" + getManifestsUri().toString())
		CloseableHttpResponse response = requestWithAuth(getManifestsUri())
		int statusCode = response.statusLine.statusCode
		println("[Info] Mnanifest url status: " + statusCode)
		String manifestJson = response.entity?.content?.getText("UTF-8")
		result = slurper.parseText(manifestJson)
		println("[Info] Manifest manifest : " + result)
		println("[Info] ---------------------------------------")
		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
	}

}
