/*
* Licensed Materials - Property of IBM* and/or HCL**
* UrbanCode Deploy
* (c) Copyright IBM Corporation 2016, 2017. All Rights Reserved.
* (c) Copyright HCL Technologies Ltd. 2018. 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.odm

import groovy.json.JsonBuilder
import groovy.json.JsonException
import groovy.json.JsonSlurper

import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpDelete
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.HttpPut
import org.apache.http.NameValuePair
import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.client.methods.HttpRequestBase
import org.apache.http.entity.AbstractHttpEntity
import org.apache.http.entity.ContentType
import org.apache.http.entity.FileEntity
import org.apache.http.entity.StringEntity
import org.apache.http.HttpResponse
import org.apache.http.HttpStatus
import org.apache.http.util.EntityUtils
import org.xml.sax.SAXException

import com.urbancode.commons.fileutils.FileUtils
import com.urbancode.commons.httpcomponentsutil.HttpClientBuilder
import com.urbancode.commons.util.IO

public class OdmHelper {

    private URI uri
    private HttpClient client
    private String accessToken
    private String headerParam
    private String tokenUrlValue

    /**
     *  A class to assist in executing ODM operations via REST API
     *  @param username The username to authenicate with the Rule Execution Server
     *  @param password The password to authenicate with the Rule Execution Server
     */
    public OdmHelper(String username, String password, String tokenUrl,  String scope, String grantType, String clientID, String clientSecret, String oAuthusername, String oAuthpassword) {
        HttpClientBuilder builder = new HttpClientBuilder()
        builder.setTrustAllCerts(true)
        builder.setUsername(username)
        builder.setPassword(password)
        client = builder.buildClient()
        tokenUrlValue = tokenUrl
        if(tokenUrl !=null && tokenUrl.length()>0) {
            builder.setPreemptiveAuthentication(false)
            HttpPost getTokenMethod = new HttpPost(tokenUrl);
            getTokenMethod.addHeader("Accept", "application/json");
            getTokenMethod.addHeader("Content-Type", "application/x-www-form-urlencoded");

            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
            nameValuePairs.add(new BasicNameValuePair("scope", scope));
            nameValuePairs.add(new BasicNameValuePair("grant_type", grantType));
            nameValuePairs.add(new BasicNameValuePair("client_id", clientID));
            nameValuePairs.add(new BasicNameValuePair("client_secret", clientSecret));
            if(grantType != 'client_credentials') {
                nameValuePairs.add(new BasicNameValuePair("username", oAuthusername));
                nameValuePairs.add(new BasicNameValuePair("password", oAuthpassword));
            }
            
            getTokenMethod.setEntity(new UrlEncodedFormEntity(nameValuePairs, "UTF-8"));
            def responseToken = client.execute(getTokenMethod);
            def entity = EntityUtils.toString(responseToken.getEntity());
            def slurper = new JsonSlurper();
            def parsedJson = slurper.parseText(entity)
            accessToken = parsedJson.access_token;
            headerParam = "Bearer "+ accessToken
        } else {
            builder.setPreemptiveAuthentication(true)
        }
        
    }

    /**
     *  Set the URI endpoint of the REST call to execute
     *  @param hostname The hostname of the Rule Exectuion Server
     *  @param port The port that the Rule Execution Server is listening on
     *  @param path The path to the specific REST endpoint
     *  @return True if the URL is valid, false otherwise
     */
    public boolean setUri(String hostname, String port, String path) {
        if (!hostname.startsWith('http') && !hostname.startsWith('https')) {
            hostname = 'https://' + hostname
        }
        URL url
        try {
            //If using a load balancer, no port will be given
            if (port.length() > 0) {
                url = new URL("${hostname}:${port}/${path}")
            }
            else {
                url = new URL("${hostname}/${path}")
            }
        }
        catch (MalformedURLException ex) {
            println ("[Error] Invalid URL: ${ex.getMessage()}")
            return false
        }
        try {
            uri = new URI(url.getProtocol(),
                    url.getAuthority(),
                    url.getPath(),
                    url.getQuery(),
                    url.getRef())
        }
        catch (URISyntaxException ex) {
            println ("[Error] Could not create URI from URL: ${ex.getMessage()}")
            return false
        }
        return true
    }

    /**
     *  Deploy a XOM Resource (Jar or Zip) to a Rule Execution Server
     *  @param fileName The path to the XOM Resource file to deploy
     *  @param propertyName The name of the file property to tell the user to update if it doesn't exist
     *  @param fileType The type of ODM file to deploy to tell the user it could not deployed if there is any error
     *  @return True if the resource is sucessfully deployed to the Rule Execution Server, false otherwise
     */
    public boolean deployFile(String fileName, String propertyName, String fileType) {
        File file = new File(fileName)
        if (!file.isFile()) {
            println ("[Error] ${fileName} is not a file.")
            println ("[Possible Solution] Please update the step configuration with valid file for the ${propertyName} property.")
            return false
        }
        FileEntity entity = new FileEntity(file, ContentType.APPLICATION_OCTET_STREAM)
        def response = executeHttpRequest(new HttpPost(), entity)
        if (!response) {
            println ("[Error] Could not deploy ${fileType} - ${response}")
            return false
        }
        if (!response.getStatusLine().getStatusCode().equals(HttpStatus.SC_CREATED) &&
        !response.getStatusLine().getStatusCode().equals(HttpStatus.SC_OK) &&
        !response.getStatusLine().getStatusCode().equals(HttpStatus.SC_ACCEPTED)) {
            println ("[Error] Bad response code of ${response.getStatusLine().getStatusCode()} - ${response}.")
            println ('Response:\n' + response.entity?.content?.getText("UTF-8"))
            return false
        }
        return getStatus(response.entity?.content?.getText("UTF-8"))
    }

    /**
     *  Fetch a resource from a Rule Execution Server
     *  @param workDir The directory to download the resource to
     *  @return True if the resource is successfully fetched, false otherwise
     */
    public boolean fetchResource(def workDir,String fileName) {
        def response =  executeHttpRequest(new HttpGet(), null)
        if (!response) {
            println ("[Error] Could not fetch resource - ${response}")
            return false
        }
        if (!response.getStatusLine().getStatusCode().equals(HttpStatus.SC_OK) &&
        !response.getStatusLine().getStatusCode().equals(HttpStatus.SC_ACCEPTED)) {
            println ("[Error] Bad response code of ${response.getStatusLine().getStatusCode()} - ${response}.")
            println ('Response:\n' + response.entity?.content?.getText("UTF-8"))
            return false
        }
        String tempFileSuffix = ".tmp"
        File tempFile = File.createTempFile("RetrieveArtifact-", tempFileSuffix)
        tempFile.deleteOnExit()
        try {
            FileUtils.writeInputToFile(response.getEntity().getContent(), tempFile)
        }
        catch (IOException e) {
            println ("[Error] Failed to download resource to temporary file: ${e.getMessage()}")
            return false
        }
        println ("[Ok] Resource downloaded to temporary file ${tempFile.getAbsolutePath()}")
        File finalFile = new File(workDir, fileName)
        println ("[Action] Moving downloaded resource to: ${finalFile.getAbsolutePath()}")
        try {
            IO.move(tempFile, finalFile)
        }
        catch (IOException e) {
            println ("[Error] Failed to move downloaded temporary file: ${e.getMessage()}")
            return false
        }
        println ('[Ok] Resource moved to working directory.')

        return true
    }

    /**
     *  Delete a resource from a Rule Execution Server
     *  @param workDir The directory to download the resource to
     *  @return True if the resource is successfully fetched, false otherwise
     */
    public boolean deleteResource() {
        def response =  executeHttpRequest(new HttpDelete(), null)
        if (!response) {
            println ("[Error] Could not delete resource - ${response}")
            return false
        }
        if (!response.getStatusLine().getStatusCode().equals(HttpStatus.SC_OK)) {
            println ("[Error] Bad response code of ${response.getStatusLine().getStatusCode()} - ${response}.")
            println ('Response:\n' + response.entity?.content?.getText("UTF-8"))
            return false
        }

        println ('[Ok] Resource deleted from Rule Execution Server.')
        return true
    }

    /**
     *  Get the highest version of a resource from a Rule Execution Server
     *  @return returns version number if successful, false otherwise
     */
    public def getHighestVersion() {
        def method = new HttpGet()
        method.addHeader("Accept", "application/json")
        method.addHeader("Content-Type", "application/json")
        def response =  executeHttpRequest(method, null)
        if (!response) {
            println ("[Error] Could not get highest version - ${response}")
            return false
        }
        if (!response.getStatusLine().getStatusCode().equals(HttpStatus.SC_OK) &&
        !response.getStatusLine().getStatusCode().equals(HttpStatus.SC_ACCEPTED)) {
            println ("[Error] Bad response code of ${response.getStatusLine().getStatusCode()} - ${response}.")
            println ('Response:\n' + response.entity?.content?.getText("UTF-8"))
            return false
        }

        def jsonString = EntityUtils.toString(response.getEntity())
        def slurper = new JsonSlurper()
        String versionString = slurper.parseText(jsonString).version

        return versionString
    }

    /**
     *  Get the properties of a resource from a Rule Execution Server
     *  @param workDir The directory to download the resource to
     *  @return True if the resource properties are successfully fetched, false otherwise
     */
    public def getProperties() {
        def method = new HttpGet()
        method.addHeader("Accept", "application/json")
        method.addHeader("Content-Type", "application/json")
        def response =  executeHttpRequest(method, null)
        if (!response) {
            println ("[Error] Could not fetch properties - ${response}")
            return false
        }
        if (!response.getStatusLine().getStatusCode().equals(HttpStatus.SC_OK) &&
        !response.getStatusLine().getStatusCode().equals(HttpStatus.SC_ACCEPTED)) {
            println ("[Error] Bad response code of ${response.getStatusLine().getStatusCode()} - ${response}.")
            println ('Response:\n' + response.entity?.content?.getText("UTF-8"))
            return false
        }

        def jsonString = EntityUtils.toString(response.getEntity())
        def slurper = new JsonSlurper()
        def children = slurper.parseText(jsonString)

        return children
    }

    /**
     *  Update or Creates a property of a RuleApp or RuleSet.
     *  @param propName The property name to be added or updated
     *  @param propValue The new value of the property
     *  @param addNew Identify if property is new or existing
     *  @return True if the property is successfully created or updated, false otherwise
     */
    public boolean setProperty(String propName, String propValue, boolean addNew) {
        StringEntity entity = new StringEntity(propValue)
        def response = null
        if (!addNew) {
            println ("[Action] Attempting to update existing property: ${propName}")
            try {
                response = executeHttpRequest(new HttpPut(), entity)
            }
            catch (Exception e) {
                println ("[Error] Could not update property ${propName}: "+ e.getMessage())
                return false
            }
        }
        else {
            println ("[Action] Attempting to create new property: ${propName}")
            try {
                response = executeHttpRequest(new HttpPost(), entity)
            }
            catch (Exception e) {
                println ("[Error] Could not create property ${propName}: "+ e.getMessage())
                return false
            }
        }
        if (!response.getStatusLine().getStatusCode().equals(HttpStatus.SC_OK)) {
            println ("[Error] Bad status code of ${response.getStatusLine().getStatusCode()} - ${response}.")
            println ('Response:\n' + response.entity?.content?.getText("UTF-8"))
            return false
        }
        return getStatus(response.entity?.content?.getText("UTF-8"))
    }


    /**
     *  Execute an HTTP request to the class URL
     *  @param request The instance of the Http method to execute (HttpPost, HttpGet, etc)
     *  @param postInfo The entity to set for HttpPost or HttpPut
     *  @return The HttpResponse
     */
    private def executeHttpRequest(HttpRequestBase request, AbstractHttpEntity entity) {
        println ("[Action] Executing ${request.getClass().getSimpleName()} to ${uri.toString()}")
        try {
            if(tokenUrlValue !=null && tokenUrlValue.length()>0) {
                def headerDataBefore = request.getHeaders("Authorization")
                request.addHeader("Authorization", headerParam )
                def headerDataAfter = request.getHeaders("Authorization")
            }
            request.setURI(uri)
        }
        catch (IllegalArgumentException e) {
            println ("[Error] Invalid URL: ${e.getMessage()}.")
            println ('[Possible Solution] Please update the step configuration with a valid URL.')
            return false
        }
        if (entity) {
            try {
                request.setEntity(entity)
            }
            catch (IOException e) {
                println ("[Error] Could not set entity: ${e.getMessage()}.")
                return false
            }
        }
        HttpResponse response
        try {
            response = client.execute(request)
        }
        catch (IOException e) {
            println ("[Error] Problem executing HTTP request: ${e.getMessage()}.")
            println ("Response: ${response}")
            return false
        }
        return response
    }

    /**
     *  Retrieve whether or not the deployment was successful from the HTTP Response
     *  @param response The HTTP Response XML to check success status of
     *  @return True if the deployment was successful, false otherwise
     */
    private def getStatus(def response) {
        def parser
        try {
            parser = new XmlParser().parseText(response)
            return parser.succeeded
        }
        catch (IOException e) {
            println ('[Error] Could not process response.')
            println ("Response: ${parser.toString().replace('\n', '')}")
            return false
        }
        catch (SAXException e) {
            println ('[Error] XML is not well-formed.')
            println ("XML: ${parser.toString().replace('\n', '')}")
            return false
        }
        catch (MissingPropertyException e) {
            println ('[Error] Could not find status in response.')
            println ("Response: ${parser.toString().replace('\n', '')}")
            return false
        }
        catch (NullPointerException e) {
            println ('[Error] Could not get status from response.')
            println ("Response: ${parser.toString().replace('\n', '')}")
            return false
        }
    }

    /**
     *  Api : GET /v1/decisionservices?q=name:decisionServiceName Get the decisionServiceID given a particular name
     *  Get decision services from decision server
     *  @param decisionServiceName The name of the decision service on decision server
     *  @return list of Decision services available on decision server
     */
    public String getDecisionServiceIdByName(String decisionServiceName) {

        def method = new HttpGet()
        method.addHeader("Accept", "application/json")
        method.addHeader("Content-Type", "application/json")
        def response =  executeHttpRequest(method, null)
        if (!response) {
            println ("[Error] Could not get DecisionServices - ${response}")
            System.exit(1)
        }
        if (!response.getStatusLine().getStatusCode().equals(HttpStatus.SC_OK)) {
            println ("[Error] Bad response code of ${response.getStatusLine().getStatusCode()} - ${response}.")
            println ('Response:\n' + response.entity?.content?.getText("UTF-8"))
            System.exit(1)
        }

        def jsonString = EntityUtils.toString(response.getEntity())
        def slurper = new JsonSlurper()
        def decisionService = slurper.parseText(jsonString)

        println ("\nDecision service : ${decisionService}")

        if(decisionService.elements) {
            return decisionService.elements[0].id
        }
        return null
    }

    /**
     *  Api: GET /v1/decisionservices/{decisionServiceId}/testsuites?q=name:testSuiteName Get the testSuiteId given a particular name
     *  Get decision services from decision server
     *  @param testSuiteName The testSuite name belongs to decision service
     *  @param decisionId The id of the decision service to get testSuite
     *  @return testSuiteId of the testSuiteName falling under specified decision service.
     */
    public String getTestSuiteIdByDecisionIdAndTestSuiteName(String testSuiteName, String decisionId) {

        String path = "decisioncenter-api/v1/decisionservices/" + decisionId + "/testsuites?q=name:" + testSuiteName

        if (!this.setUri(uri.getHost(), uri.getPort().toString(), path)) {
            println ('[error]  Could not set URI.')
            System.exit(1)
        }
        def method = new HttpGet()
        method.addHeader("Accept", "application/json")
        method.addHeader("Content-Type", "application/json")
        def response =  executeHttpRequest(method, null)
        if (!response) {
            println ("[Error] Could not get TestSuites - ${response}")
            System.exit(1)
        }
        if (!response.getStatusLine().getStatusCode().equals(HttpStatus.SC_OK)) {
            println ("[Error] Bad response code of ${response.getStatusLine().getStatusCode()} - ${response}.")
            println ('Response:\n' + response.entity?.content?.getText("UTF-8"))
            System.exit(1)
        }

        def jsonString = EntityUtils.toString(response.getEntity())
        def slurper = new JsonSlurper()
        def testSuite = slurper.parseText(jsonString)
        println ("\nTest suites : ${testSuite.elements}")

        if(testSuite.elements) {
            return testSuite.elements[0].id
        }
        return null
    }

    /**
     *  Api : GET /v1/testreports/{testReportId}  Get the test result (pass/fail)
     *  Get the test Suite Result
     *  @param testReportId The id of the testReport
     *  @return PASS if the test Suite are successfully ran, FAIL otherwise
     */
    public boolean getTestSuiteResult(String testReportId) {


        String path = "decisioncenter-api/v1/testreports/" + testReportId

        if (!this.setUri(uri.getHost(), uri.getPort().toString(), path)) {
            println ('[error]  Could not set URI.')
            System.exit(1)
        }
        def method = new HttpGet()
        method.addHeader("Accept", "application/json")
        method.addHeader("Content-Type", "application/json")
        if(tokenUrlValue !=null && tokenUrlValue.length()>0) {
            method.addHeader("Authorization", headerParam )
        }
        String status = null
        def doneStatuses = ['ABORTED', 'COMPLETED', 'FAILED', 'STOPPED']
        int failures = 0
        boolean testResult
        def testSuiteReport
        while(!(status in doneStatuses)) {

            Thread.sleep(5000)
            def response =  executeHttpRequest(method, null)
            if (!response) {
                println ("[Error] Could not get TestSuiteResult - ${response}")
                System.exit(1)
            }
            if (!response.getStatusLine().getStatusCode().equals(HttpStatus.SC_OK)) {
                println ("[Error] Bad response code of ${response.getStatusLine().getStatusCode()} - ${response}.")
                println ('Response:\n' + response.entity?.content?.getText("UTF-8"))
                System.exit(1)
            }

            def jsonString = EntityUtils.toString(response.getEntity())
            def slurper = new JsonSlurper()
            testSuiteReport = slurper.parseText(jsonString)

            status = testSuiteReport.status

        }

        failures = testSuiteReport.failures

        return (status == 'COMPLETED' && !failures)
    }


    /**
     *  Api : POST    /v1/testsuites/{testSuiteId}/run    Run the test and get a testReportId
     *  Runs the test suites
     *  @param decisionServiceName The name of the decision service
     *  @param testSuiteNames The list of testSuites need to be run
     *  @return PASS if all test Suites are successfully ran, FAIL otherwise
     */
    public boolean runTestSuites(String decisionServiceName, def testSuiteNames) {

        boolean currentTestSuiteResultStatus

        String decisionId = getDecisionServiceIdByName(decisionServiceName)
        if (!decisionId) {
            println ("[error]  DecisionId By Name - ${decisionServiceName} does not exist.")
            System.exit(1)
        }


        String testSuiteId
        boolean allTestSuitesResultStatus = true
        int counter = 0
        String path
        testSuiteNames.each { testSuiteName ->

            testSuiteId = getTestSuiteIdByDecisionIdAndTestSuiteName(testSuiteName, decisionId)
            if (!testSuiteId) {
                println ("[error]  TestSuite by name - ${testSuiteName} not found.")
                System.exit(1)
            }

            path = "decisioncenter-api/v1/testsuites/" + testSuiteId + "/run"

            if (!this.setUri(uri.getHost(), uri.getPort().toString(), path)) {
                println ('[error]  Could not set URI.')
                System.exit(1)
            }
            def method = new HttpPost()
            method.addHeader("Accept", "application/json")
            method.addHeader("Content-Type", "application/json")
            def response =  executeHttpRequest(method, null)

            println ("\nTest suite started running : ")

            if (!response) {
                println ("[Error] Could not get TestSuite - ${response}")
                System.exit(1)
            }
            if (!response.getStatusLine().getStatusCode().equals(HttpStatus.SC_OK)) {
                println ("[Error] Bad response code of ${response.getStatusLine().getStatusCode()} - ${response}.")
                println ('Response:\n' + response.entity?.content?.getText("UTF-8"))
                System.exit(1)
            }

            def jsonString = EntityUtils.toString(response.getEntity())
            def slurper = new JsonSlurper()
            def testSuiteRun = slurper.parseText(jsonString)
            currentTestSuiteResultStatus = getTestSuiteResult(testSuiteRun.id)
            println ("\nTestSuite :\"${testSuiteName}\" -- result : ${currentTestSuiteResultStatus}")

            if(!currentTestSuiteResultStatus && allTestSuitesResultStatus) {
                allTestSuitesResultStatus = false
            }

        }
        return allTestSuitesResultStatus
    }
}
