/*
* Licensed Materials - Property of IBM* and/or HCL**
* UrbanCode Deploy
* (c) Copyright IBM Corporation 2013, 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.servicenow

import java.net.URL
import java.text.DateFormat
import java.text.SimpleDateFormat

import groovy.json.JsonBuilder
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.slurpersupport.GPathResult

import org.apache.http.client.methods.HttpDelete
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPatch
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.HttpPut
import org.apache.http.entity.StringEntity
import org.apache.http.Header
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.util.EntityUtils
import org.apache.http.NameValuePair
import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.util.EntityUtils


import com.urbancode.air.AirPluginTool
import com.urbancode.air.XTrustProvider
import com.urbancode.commons.httpcomponentsutil.HttpClientBuilder
import com.urbancode.commons.httpcomponentsutil.PreemptiveAuthHttpClient

public class HelperRestClientJsonV1 {
    def airPluginTool
    def idRaw
    def props = []
    def query
    def rawFields
    def rawTableLabel
    def serverUrl
    def headerParam
    def sysparm_input_display_value
    DefaultHttpClient client
    String changeRequestId

    public HelperRestClientJsonV1(def airPluginToolIn) {
        airPluginTool = airPluginToolIn
        props = airPluginTool.getStepProperties()

        if (props['changeRequestId']) {
            changeRequestId = props['changeRequestId'].trim()
        }

        def fullURL

        try {
            fullURL = new URL(props['serverUrl'].trim())
        } catch (MalformedURLException e) {
            println e.getMessage()
            System.exit(1)
        }

        serverUrl = 'https://'+ fullURL.getAuthority()

        idRaw = props['id']
        query = props['query']
        rawFields = props['fields']
        rawTableLabel = props['table']

        def password = props['password']
        def clientID = props['clientID']
        def clientSecret = props['clientSecret']
        def proxyHost = props['proxyHost'].trim()
        def trustAllCerts  = Boolean.valueOf(props['trustAllCerts'])
        def username = props['username']
        sysparm_input_display_value  = Boolean.valueOf(props['sysparm_input_display_value'])

        println "[Info] Using serverUrl: '${serverUrl}'"
        println "[Info] Using username: '${username}'"

        HttpClientBuilder clientBuilder = new HttpClientBuilder()
        clientBuilder.setPassword(password)
        clientBuilder.setPreemptiveAuthentication(false)
        clientBuilder.setUsername(username)

        if (trustAllCerts) {
            XTrustProvider.install()
            clientBuilder.setTrustAllCerts(true)
        }

        if (proxyHost) {
            def proxyPass = props['proxyPass']
            def proxyPort = props['proxyPort'].trim()
            def proxyUser = props['proxyUser'].trim()

            println "[Info] Using Proxy Host ${proxyHost}"
            println "[Info] Using Proxy Port ${proxyPort}"

            clientBuilder.setProxyHost(proxyHost)
            clientBuilder.setProxyPassword(proxyPass)
            clientBuilder.setProxyPort(Integer.valueOf(proxyPort))
            clientBuilder.setProxyUsername(proxyUser)
            if(proxyUser) {
              println "[Info] Using Proxy User ${proxyUser}"
            }
        }

        client = clientBuilder.buildClient()

        if(clientID && clientSecret) {
            println "Authenticating with Oauth2"
            HttpPost getTokenMethod = new HttpPost(serverUrl + '/oauth_token.do');
            getTokenMethod.addHeader("Accept", "application/json");
            getTokenMethod.addHeader("Content-Type", "application/x-www-form-urlencoded");

            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
            nameValuePairs.add(new BasicNameValuePair("grant_type", "password"));
            nameValuePairs.add(new BasicNameValuePair("client_id", clientID));
            nameValuePairs.add(new BasicNameValuePair("client_secret", clientSecret));
            nameValuePairs.add(new BasicNameValuePair("username", username));
            nameValuePairs.add(new BasicNameValuePair("password", password));

        
            getTokenMethod.setEntity(new UrlEncodedFormEntity(nameValuePairs, "UTF-8"));

            def response = client.execute(getTokenMethod);
            def entity = EntityUtils.toString(response.getEntity());

            def slurper = new JsonSlurper();
            def parsedJson = slurper.parseText(entity)

            headerParam = "Bearer "+ parsedJson.access_token

        }

        if(password && !clientID && !clientSecret){
            println "Authenticating with Basic Auth"
            headerParam = "Bearer "
        }

    }

    // Unused. Issues a GET API V1 call with an encoded query (number=) and returns newline equals list
    // of fields.
    public def getTableRecords() {
        def tableName = rawTableLabel.trim()

        def ids = getIDs()

        for (id in ids) {
            println "[Action] Retrieving record ${id}.\n"
            String sys_id = getSysIdForTable(id, tableName)
            def result = getRecords(tableName + '/' + sys_id.trim())
            println "[Ok] Record, ${id}, recieved.\n"

            def parsedJsonTableMap = result.result
            def sb = StringBuilder.newInstance()
            parsedJsonTableMap.each {
               if (it.value.getClass() == String) {
                   sb <<"${it.key}=${it.value}"
               } else {
                   sb <<"${it.key}=${it.value.value}"
               }
               sb << '\n'
            }

            airPluginTool.setOutputProperty(id, sb.toString())

            String idUrl = serverUrl+ "/nav_to.do?uri=" + tableName + ".do?sys_id=" + sys_id
            airPluginTool.setOutputProperty(id + "_URL", idUrl)
        }
    }

    // Issues a GET API V1 call with an encoded query (number=) and checks fields against user input
    public def checkRecordsFields() {
        def tableName = rawTableLabel.trim()

        def fieldNames = getFieldNames(rawFields)

        def ids = getIDs()

        for (id in ids) {
            println "[Action] Checking record ${id}.\n"
            String sys_id = getSysIdForTable(id, tableName)

            String idUrl = serverUrl+ "/nav_to.do?uri=" + tableName + ".do?sys_id=" + sys_id
            airPluginTool.setOutputProperty(id + "_URL", idUrl)

            def parsedJson = getRecords(tableName + '/' + sys_id.trim())

            for (fieldName in fieldNames) {
                def fieldValue = parsedJson.result."${fieldName.key.toString().toLowerCase()}"
                if (fieldValue) {
                    if(fieldName.value.toString().equalsIgnoreCase(fieldValue)) {
                        println "[Ok] ${fieldName.key.toString()} equals ${fieldName.value.toString()} for Record.\n"
                    } else {
                        println "[Ok] ${fieldName.key.toString()} does not match ${fieldName.value.toString()}; ${fieldName.key.toString()} is ${fieldValue}.\n"    
                    }
                }
                else {
                    println "[Warning] Field ${fieldName.key.toString()} was not found and is being ignored. Please enter proper fields."
                }
            }

            println "[Ok] Record, ${id}, checked.\n"
        }
    }

    // Issues a GET API V1 call with an encoded query (number=) and returns approval value
    public def updateRecordsFields() {
        def fieldNames = getFieldNames(rawFields)

        def jsonfieldNames = JsonOutput.toJson(fieldNames)
        def tableName = rawTableLabel.trim()

        def ids = getIDs()
        StringEntity postEntity = new StringEntity(jsonfieldNames)
        def method

        for (id in ids) {
            println "[Action] Updating record ${id}.\n"
            String sys_id = getSysIdForTable(id, tableName)
            if(sysparm_input_display_value) {
                method = new HttpPut(serverUrl + '/api/now/v1/table/' + tableName + '/' + sys_id.trim() + '?sysparm_input_display_value=true')
            } else {
                method = new HttpPut(serverUrl + '/api/now/v1/table/' + tableName + '/' + sys_id.trim())
            }
            method.addHeader("Accept", "application/json")
            method.addHeader("Content-Type", "application/json")
            if(headerParam != 'Bearer ') {
                method.addHeader("Authorization", headerParam )
            }

            method.setEntity(postEntity)

            def resp = client.execute(method)
            def statusCode = resp.getStatusLine().getStatusCode()
            if (statusCode < 200 || statusCode >= 300) {
                println "[Error] Update to record ${id} failed with status ${statusCode}. Exiting Failure."
                println ('Response:\n' + resp.entity?.content?.getText("UTF-8"))
                System.exit(1)
            }

            String idUrl = serverUrl+ "/nav_to.do?uri=" + tableName + ".do?sys_id=" + sys_id
            airPluginTool.setOutputProperty(id + "_URL", idUrl)

            println "[Ok] Record, ${id}, updated.\n"
        }
    }

    // Issues a GET API V1 call with an encoded query (number=) and returns approval value
    public def query() {
        def tableName = rawTableLabel.trim()
        def parsedJson = getRecords(tableName + '?sysparm_query=' + query)
        return parsedJson
    }

    // Issues a GET API V1 call with an encoded query (number=) and returns approval value
    String getApproval() {
        def parsedJson = getRecords('change_request?sysparm_query=number=' + changeRequestId)
        if (parsedJson.result.size() == 0) {
            println "[Error] No Change Request found with ID ${changeRequestId}. Exiting failure."
            System.exit(1)
        }
        return parsedJson.result[0].approval
    }

    // Issues a GET API V1 call with an encoded query (number=) and returns 'state' value
    Integer getStatus() {
        def parsedJson = getRecords('change_request?sysparm_query=number=' + changeRequestId)
        if (parsedJson.result.size() == 0) {
            println "[Error] No Change Request found with ID ${changeRequestId}. Exiting failure."
            System.exit(1)
        }
        return Integer.valueOf(parsedJson.result[0].state)
    }

    // Issues a GET API call and returns a size 2 array of the deployment window
    def getDeploymentWindow(String startDateField, String endDateField) {
        def parsedJson = getRecords('change_request?sysparm_query=number=' + changeRequestId)
        if (parsedJson.result.size() == 0) {
            println "[Error] No Change Request found with ID ${changeRequestId}. Exiting failure."
            System.exit(1)
        }
        def failure = false
        def pattern = "yyyy-MM-dd HH:mm:ss"

        //Read the time from ServiceNow as UTC. JVM will convert java date to system timezone for comparing
        DateFormat dateformat = new SimpleDateFormat(pattern)
        dateformat.setTimeZone(TimeZone.getTimeZone("UTC"))

        //check that specified fields exist
        if (parsedJson.result[0].start_date == null) {
            println("[Error] No date field exists in ${changeRequestId} with name '${startDateField}'")
            failure = true
        }
        if (parsedJson.result[0].end_date == null) {
            println("[Error] No date field exists in ${changeRequestId} with name '${endDateField}'")
            failure = true
        }

        if (failure){
            println("[Solution] Please update the field names to valid date fields.")
            System.exit(1)
        }

        def startDate = dateformat.parse(parsedJson.result[0].start_date)
        def endDate = dateformat.parse(parsedJson.result[0].end_date)

        def window = [startDate, endDate]

        return window
    }

    // Issues a GET API V1 call with an encoded query (number=) and returns 'sys_id' value
    String getChangeRequestSysId() {
        def parsedJson = getRecords('change_request?sysparm_query=number=' + changeRequestId)
        if (parsedJson.result.size() == 0) {
            println "[Error] No Change Request found with ID ${changeRequestId}. Exiting failure."
            System.exit(1)
        }
        return parsedJson.result[0].sys_id
    }

    String getSysIdForTable(String requestId, String tableName) {
        def parsedJson = getRecords(tableName + '?sysparm_query=number=' + requestId)
        if (parsedJson.result.size() == 0) {
            println "[Error] No ${tableName} found with ID ${requestId}. Exiting failure."
            System.exit(1)
        }
        return parsedJson.result[0].sys_id
    }

    //// Issues a GET API V1 call with an encoded query (number=) and returns JSON body
    def getTaskList(String changeRequestSysId) {
        def parsedJson = getRecords('change_task?sysparm_query=change_request=' + changeRequestSysId)
        return parsedJson
    }

    // Issues a PATCH API V1 call to edit the state value of a change task
    def setTaskStatus(String taskId, String status) {
        String sysId = getSysIdForTable(taskId, "change_task")
        HttpPatch patch = new HttpPatch(serverUrl + '/api/now/v1/table/change_task/' + sysId)
        patch.addHeader("Accept", "application/json")
        patch.addHeader("Content-Type", "application/json")
        if(headerParam != 'Bearer ') {
            patch.addHeader("Authorization", headerParam)
        }
        String updateInfo = "{'state':'" + status + "'}"
        StringEntity postEntity = new StringEntity(updateInfo)
        patch.setEntity(postEntity)
        println "[Action] Setting task ${taskId} with status ${status}"
        println updateInfo

        def resp = client.execute(patch)
        def statusCode = resp.getStatusLine().getStatusCode()
        if (statusCode < 200 || statusCode >= 300) {
            println "[Error] Request failed with status ${statusCode}. Exiting Failure."
            println ('Response:\n' + resp.entity?.content?.getText("UTF-8"))
            System.exit(1)
        }

        def newStatus = getTaskStatus(sysId)
        if (newStatus != Integer.valueOf(status)) {
            println "[Error] Status was not updated. Exiting Failure"
            System.exit(1)
        }
    }

    // Issues a GET API V1 call to a Change Task determined by encoded query (sys_id=)
    // and returns its State value
    Integer getTaskStatus(String taskId) {
        def parsedJson = getRecords('change_task?sysparm_query=sys_id=' + taskId)

        if (parsedJson.result.size() == 0) {
            println "[Error] No Change Request found with ID ${changeRequestId}. Exiting failure."
            System.exit(1)
        }

        return Integer.valueOf(parsedJson.result[0].state)
    }

    // Sets the status of a Change Request
    def setStatus(String status) {
        HttpEntityEnclosingRequestBase method = null
        def sysId = getChangeRequestSysId()

        method = new HttpPut(serverUrl + '/api/now/v1/table/change_request/' + sysId)
        method.addHeader("Accept", "application/json")
        method.addHeader("Content-Type", "application/json")
        if(headerParam != 'Bearer ') {
            method.addHeader("Authorization", headerParam)
        }

        String updateInfo = "{'state':'" + status + "'}"
        StringEntity postEntity = new StringEntity(updateInfo)
        method.setEntity(postEntity)

        def resp = client.execute(method)

        def statusCode = resp.getStatusLine().getStatusCode()
        if (statusCode < 200 || statusCode >= 300) {
            println "[Error] Request failed with status ${statusCode}. Exiting Failure."
            println ('Response:\n' + resp.entity?.content?.getText("UTF-8"))
            System.exit(1)
        }

        def newStatus = getStatus()
        if (newStatus != Integer.valueOf(status)) {
            println "[Error] Status was not updated. Exiting Failure"
            println EntityUtils.toString(resp.getEntity())
            System.exit(1)
        }
    }

    // Insert a table row macthing TableName using the POST API V1 with a JSON
    // Reqeust Body
    def insertRow() {
        def fieldNames = getFieldNames(rawFields)

        def jsonfieldNames = JsonOutput.toJson(fieldNames)
        def tableName = rawTableLabel.trim()
        StringEntity postEntity = new StringEntity(jsonfieldNames)
        def method
        if(sysparm_input_display_value) {
            method = new HttpPost(serverUrl + '/api/now/v1/table/' + tableName + '?sysparm_input_display_value=true')
        } else {
            method = new HttpPost(serverUrl + '/api/now/v1/table/' + tableName)
        }
        method.addHeader("Accept", "application/json")
        method.addHeader("Content-Type", "application/json")
        if(headerParam != 'Bearer ') {
            method.addHeader("Authorization", headerParam)
        }

        method.setEntity(postEntity)

        def resp = client.execute(method)
        def parsedJson
        def entity = EntityUtils.toString(resp.getEntity())

        def slurper = new JsonSlurper()
        try {
            parsedJson = slurper.parseText(entity)
        }
        catch (groovy.json.JsonException e) {
            println ""
            println "Failed to parse response body. Printing useful debugging information and exiting."
            println ""
            println "Response status code: ${statusCode}."
            println ""
            println "Header:"
            Header[] headers = resp.getAllHeaders()
            for (Header header : headers) {
                System.out.println(header.getName() + ":"+ header.getValue())
                }
            println ""
            println "Body:"
            println entity
            println ""
            println "Stacktrace:"
            e.printStackTrace()
            System.exit(1)
        }
        def statusCode = resp.getStatusLine().getStatusCode()
        if (statusCode < 200 || statusCode >= 300) {
            println "[Error] Update to table ${tableName} failed with status ${statusCode}. Exiting Failure."
            println ('Response:\n' + resp.entity?.content?.getText("UTF-8"))
            System.exit(1)
        }
        else {
            println "[Ok] Table '${tableName}' updated successfully with status ${statusCode}."
            if(parsedJson.result.number != null) {
                println "[Ok] Record: '${parsedJson.result.sys_id}' as '${parsedJson.result.number}' created."
            } else {
                println "[Ok] Record: '${parsedJson.result.sys_id}' is  created."
            }
        }

        String idUrl = serverUrl+ "/nav_to.do?uri=" + tableName + ".do?sys_id=" + parsedJson.result.sys_id

        airPluginTool.setOutputProperty('recordSystemID', parsedJson.result.sys_id)
        if(parsedJson.result.number != null) { 
            airPluginTool.setOutputProperty('recordSystemNumber', parsedJson.result.number)
        }
        airPluginTool.setOutputProperty("recordSystemURL", idUrl)
    }

    // Deletes a table row matching TableName and sys_id
    def deleteRow() {
        def tableName = rawTableLabel.trim()
        def ids = getIDs()

        for (id in ids) {
            String sys_id = getSysIdForTable(id, tableName)
            deleteRecord(tableName + '/' + sys_id.trim())
        }
    }

    // Deletes all Table rows macthing a TableName and an encoded query
    def deleteMultipleRows() {
        def condition = props['condition']
        def tableName = props['table']

        while (condition.startsWith('=')) {
            condition = condition.substring(1, condition.length())
        }

        println "[Action] Deleting row from table ${tableName}"
        println "[Action] Deleting rows with condition ${condition}"

        def queryJson = getRecords(tableName + '?sysparm_query=' + condition)
        def querySize = queryJson.result.size()

        for (int i = 0; i < querySize; i++) {
            deleteRecord(tableName + "/" + queryJson.result.sys_id[i])
            println "Deleted " + tableName + " record with sys_id=" + queryJson.result.sys_id[i]
        }
    }

//////////////////////////////// HELPER METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
    // Excutes ServiceNows V1 REST API DELETE call. input form: {TableName}/{sys_id}
    def deleteRecord(String tableAndSys_id) {
        HttpDelete delete = new HttpDelete(serverUrl + "/api/now/v1/table/" + tableAndSys_id)
        delete.addHeader("Accept", "application/json")
        delete.addHeader("Content-Type", "application/json")
        if(headerParam != 'Bearer ') {
            delete.addHeader("Authorization", headerParam)
        }

        def resp = client.execute(delete)
        def statusCode = resp.getStatusLine().getStatusCode()
        if (statusCode != 204) {
            println "[Error] Request failed with status ${statusCode}. Exiting Failure."
            println ('Response:\n' + resp.entity?.content?.getText("UTF-8"))
            System.exit(1)
        }
    }

    // Accepts any string compatible with ServiceNow's API GET Retreive Record appended to serverUrl + '/api/now/v1/table/'
    def getRecords(String query) {
        def parsedJson

        def encodedQuery = encodeQuery(query)

        HttpGet get = new HttpGet(serverUrl + '/api/now/v1/table/' + encodedQuery)
        get.addHeader("Accept", "application/json")
        if(headerParam != 'Bearer '){
            get.addHeader("Authorization", headerParam)
        }
        def resp = client.execute(get)

        def statusCode = resp.getStatusLine().getStatusCode()
        if (statusCode < 200 || statusCode >= 300) {
            println "[Error] Request failed with status ${statusCode}. Exiting Failure."
            println ('Response:\n' + resp.entity?.content?.getText("UTF-8"))
            System.exit(1)
        }
        def entity = EntityUtils.toString(resp.getEntity())

        def slurper = new JsonSlurper()
        try {
            parsedJson = slurper.parseText(entity)
        }
        catch (groovy.json.JsonException e) {
            println ""
            println "Failed to parse response body. Printing useful debugging information and exiting."
            println ""
            println "Response status code: ${statusCode}."
            println ""
            println "Header:"
            Header[] headers = resp.getAllHeaders()
            for (Header header : headers) {
                System.out.println(header.getName() + ":"+ header.getValue())
            }
            println ""
            println "Body:"
            println entity
            println ""
            println "Stacktrace:"
            e.printStackTrace()
            System.exit(1)
        }
        return parsedJson
    }

    // Custom URL encoder for Service Now API
    def encodeQuery(String query) {
        query = query.replaceAll("\\<", "%3C")
        query = query.replaceAll("\\>", "%3E")
        query = query.replaceAll("\\{", "%7B")
        query = query.replaceAll("}", "%7D")
        query = query.replaceAll("\\^","%5E")
        query = query.replaceAll("\\ ","%20")
        return query
    }

    /**
     * [WSP] *CHAR [WSP] '=' [WSP] *CHAR [WSP] parsed into a LinkedHashMap
     *
     * @param   textArea     Accepts any [WSP] *CHAR [WSP] '=' [WSP] *CHAR [WSP]
     * @return               A linked hashmap of all the parsed value key. Values may be null.
     */
    private LinkedHashMap getFieldNames (
        String nlSeparatedValueEqualKey) {
        def result = [:]
        nlSeparatedValueEqualKey.eachLine { text ->
            String[] fieldValue = text.split('=', 2)*.trim()
            if (fieldValue[0] && fieldValue.size() == 2) {
                result[fieldValue[0]] = fieldValue[1]
            }
            else {
                println "[Error] Failed on line: " + text
                println '[Possible Solution] Use \'=\' for name value seperation. For example: approval=approved'
                System.exit(1)
            }
        }
        return result
    }

    private def getIDs(){
        return idRaw.trim().split('\n')
    }
}
