package com.hcl.devops.test.util

import java.security.KeyManagementException
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateException

import javax.net.ssl.SSLContext
import javax.net.ssl.SSLException
import javax.net.ssl.TrustManagerFactory

import org.apache.http.Header
import org.apache.http.NameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.HttpUriRequest
import org.apache.http.conn.ssl.NoopHostnameVerifier
import org.apache.http.conn.ssl.TrustAllStrategy
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.message.BasicNameValuePair
import org.apache.http.ssl.SSLContextBuilder
import org.apache.http.util.EntityUtils

import groovy.json.JsonSlurper

/**
 * Utility class to interact with the OneTest Hub REST API handling the token management.
 */
class TestHubClient {

   private static final String TRUSTSTORE_DEFAULT_PASSWORD = 'changeit'
   private final CloseableHttpClient client
   private final String serverUrl
   private final String offlineToken
   String accessToken
   String refreshToken

   // Initialize with blind trust SSLContext that doesn't do hostname validation.
   TestHubClient(String serverUrl, String offlineToken) {
      this.serverUrl = serverUrl
      this.offlineToken = offlineToken
      println '[Info]  Building HTTP client with no SSL checking.'
      client = HttpClientBuilder.create()
            .setSSLContext(new SSLContextBuilder().loadTrustMaterial(null, TrustAllStrategy.INSTANCE).build())
            .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
            .build()
      verify()
   }

   // Initialize with default or custom SSLContext
   TestHubClient(String serverUrl, String offlineToken, String trustStorePath, String trustStorePassword)
   throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
      this.serverUrl = serverUrl
      this.offlineToken = offlineToken
      if (trustStorePath) {
         println "[Info]  Building HTTP client using custom trust store \"${trustStorePath}\"."
         if (trustStorePassword) {
            client = HttpClientBuilder.create().setSSLContext(trustStore(loadKeyStore(trustStorePath, trustStorePassword))).build()
         } else {
            client = HttpClientBuilder.create().setSSLContext(trustStore(loadKeyStore(trustStorePath, TRUSTSTORE_DEFAULT_PASSWORD))).build()
         }
      } else {
         println '[Info]  Building HTTP client using JRE trust store.'
         client = HttpClientBuilder.create().build()
      }
      verify()
   }

   // Initialize SSLContext from keystore
   SSLContext trustStore(KeyStore store) throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
      TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.defaultAlgorithm)
      tmf.init(store)
      SSLContext sslContext = SSLContext.getInstance('TLS') //$NON-NLS-1$
      sslContext.init(null, tmf.trustManagers, null)
      return sslContext
   }

   // Initialize Keystore and loading certificates
   KeyStore loadKeyStore(String trustStorePath, String password) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
      KeyStore keyStore = KeyStore.getInstance(KeyStore.defaultType)
      keyStore.load(new FileInputStream(trustStorePath), password.toCharArray())
      return keyStore
   }

   // Check that the base URL can be contacted via our client instance.
   void verify() {
      try {
         updateTokens()
         return
      } catch (UnknownHostException e) {
         println "Could not establish connection to the server ${serverUrl}.  Please check the server URL and server connectivity. Error: ${e.message}"
      }
      catch (SSLException e) {
         println "Could not establish secure connection to the server ${serverUrl}. Please validate the SSL certificate of the server and if it is self-signed either append '#insecure-skip-tls-verify' to the URL to disable checking, or place it in a Java KeyStore and reference it in Custom Trust Store Path. Error: ${e.message}"
      }
      catch (Exception e) {
         println "Could not establish connection to the server ${serverUrl}. Error: ${e.message}"
      }
      System.exit(1)
   }

   //Generates the access token
   void updateTokens() {
      HttpPost getTokenMethod = new HttpPost("${serverUrl}/rest/tokens/")
      getTokenMethod.addHeader('Accept', 'application/json')
      getTokenMethod.addHeader('Content-Type', 'application/x-www-form-urlencoded')
      List<NameValuePair> postParameters = []
      postParameters.add(new BasicNameValuePair('refresh_token', "${offlineToken}"))
      getTokenMethod.entity = new UrlEncodedFormEntity(postParameters, 'UTF-8')
      println "[API]   getToken: '${getTokenMethod}'"
      CloseableHttpResponse resp = client.execute(getTokenMethod)

      int statusCode = resp.statusLine.statusCode
      if (statusCode < 200 || statusCode >= 300) {
         println "[Error] Token request failed with status ${statusCode}. Exiting Failure."
         println('Response:\n' + resp.entity?.content?.getText('UTF-8'))
         System.exit(1)
      }
      if (statusCode == 403) {
         println "[Error] Error during retrieval of access token. Please check the license, request is unauthorized, Request returned response code ${statusCode}"
         System.exit(1)
      }
      String entity = EntityUtils.toString(resp.entity)

      try {
         Map tokenResponse = new JsonSlurper().parseText(entity)
         accessToken = tokenResponse.access_token
         refreshToken = tokenResponse.refresh_token
      }
      catch (groovy.json.JsonException e) {
         println 'Failed to parse response body. Printing useful debugging information and exiting.'
         println "Response status code: ${statusCode}."
         println 'Header:'
         Header[] headers = resp.allHeaders
         for (Header header : headers) {
            println "${header.name}:${header.value}"
         }
         println 'Body:'
         println entity
         println 'Stacktrace:'
         e.printStackTrace()
         System.exit(1)
      }
   }

   CloseableHttpResponse execute(HttpUriRequest request) {
      if (!request.getFirstHeader('Content-Type')) {
         request.addHeader('Content-Type', 'application/json')
      }
      request.addHeader('Authorization', "Bearer ${accessToken}")
      CloseableHttpResponse response = client.execute(request)
      if (response.statusLine.statusCode == 401) {
         updateTokens()
         request.setHeader('Authorization', "Bearer ${accessToken}")
         response = client.execute(request)
      }
      return response
   }
}
