/*
 * Licensed Materials - Property of IBM Corp.
 * IBM UrbanCode Build
 * IBM UrbanCode Deploy
 * IBM UrbanCode Release
 * IBM AnthillPro
 * (c) Copyright IBM Corporation 2002, 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.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.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

    /**
     *  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) {
        HttpClientBuilder builder = new HttpClientBuilder()
        builder.setTrustAllCerts(true)
        builder.setPreemptiveAuthentication(true)
        builder.setUsername(username)
        builder.setPassword(password)
        client = builder.buildClient()
    }

    /**
     *  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 = 'http://' + 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 {
            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
        }
    }
}
