/*
* 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.servicenow

import java.net.URL

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 com.urbancode.air.AirPluginTool
import com.urbancode.commons.httpcomponentsutil.HttpClientBuilder
import com.urbancode.commons.httpcomponentsutil.PreemptiveAuthHttpClient

public class HelperRestClientJsonV1 {
    def airPluginTool
    def auth
    def fullURL
    def idRaw
    def password
    def props = []
    def proxyHost
    def proxyPass
    def proxyPort
    def proxyUser
    def query
    def rawFields
    def rawTableLabel
    def serverUrl
    def username
    DefaultHttpClient client
    HttpClientBuilder clientBuilder
    String changeRequestId

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

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

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

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

        idRaw = props['id']
        password = props['password']
        proxyHost = props['proxyHost'].trim()
        query = props['query']
        rawFields = props['fields']
        rawTableLabel = props['table']
        username = props['username']

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

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

        if (proxyHost) {
            proxyPass = props['proxyPass']
            proxyPort = props['proxyPort'].trim()
            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()
    }

    // 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"
            def result = getRecords(tableName + '/' + 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())
        }
    }

    // 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"
            def parsedJson = getRecords(tableName + '/' + id.trim())

            for (fieldName in fieldNames) {
                def fieldValue = parsedJson.result."${fieldName.key.toString().toLowerCase()}"
                if (fieldValue) {
                    if (!fieldName.value.toString().equalsIgnoreCase(fieldValue)) {
                        println "[Error] ${fieldName.key.toString()} does not match ${fieldName.value.toString()}; ${fieldName.key.toString()} is ${fieldValue}.\n"
                        System.exit(1)
                    }
                    println "[Ok] ${fieldName.key.toString()} equals ${fieldName.value.toString()} for Record.\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)

        for (id in ids) {
            println "[Action] Updating record ${id}.\n"
            def method = new HttpPut(serverUrl + '/api/now/v1/table/' + tableName + '/' + id.trim())
            method.addHeader("Accept", "application/json")
            method.addHeader("Content-Type", "application/json")

            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."
                System.exit(1)
            }

            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 V1 call with an encoded query (number=) and returns specified field values
    String[] getChangeFields() {
        def fieldNames = getFieldNames(rawFields)

        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)
        }

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


        String[] result = [parsedJson.result[0].state, parsedJson.result[0].phase]
        return result
    }

    // 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
    }

    //// 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(identified by sys_id)
    def setTaskStatus(String taskId, String status) {
        HttpPatch patch = new HttpPatch(serverUrl + '/api/now/v1/table/change_task/' + taskId)
        patch.addHeader("Accept", "application/json")
        patch.addHeader("Content-Type", "application/json")
        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."
            System.exit(1)
        }

        def newStatus = getTaskStatus(taskId)
        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")

        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."
            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 = new HttpPost(serverUrl + '/api/now/v1/table/' + tableName)
        method.addHeader("Accept", "application/json")
        method.addHeader("Content-Type", "application/json")

        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."
            System.exit(1)
        }
        else {
            println "[Ok] Table '${tableName}' updated successfully with status ${statusCode}."
            println "[Ok] Record: '${parsedJson.result.sys_id}' as '${parsedJson.result.number}' created."
        }
        airPluginTool.setOutputProperty('recordSystemID', parsedJson.result.sys_id)
        airPluginTool.setOutputProperty('recordSystemNumber', parsedJson.result.number)
    }

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

        for (id in ids) {
            deleteRecord(tableName + '/' + 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")

        def resp = client.execute(delete)
        def statusCode = resp.getStatusLine().getStatusCode()
        if (statusCode != 204) {
            println "[Error] Request failed with status ${statusCode}. Exiting Failure."
            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")
        def resp = client.execute(get)

        def statusCode = resp.getStatusLine().getStatusCode()
        if (statusCode < 200 || statusCode >= 300) {
            println "[Error] Request failed with status ${statusCode}. Exiting Failure."
            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')
    }
}
