/*
* Licensed Materials - Property of IBM Corp.
* IBM UrbanCode Build
* IBM UrbanCode Deploy
* IBM UrbanCode Release
* IBM AnthillPro
* (c) Copyright IBM Corporation 2002, 2013. All Rights Reserved.
*
* U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
* GSA ADP Schedule Contract with IBM Corp.
*/

/*
 * JIRA REST API Documentation: https://docs.atlassian.com/jira/REST/latest/
 */

package com.urbancode.air.plugin.automation

import com.urbancode.commons.httpcomponentsutil.CloseableHttpClientBuilder
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovy.util.XmlSlurper
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpDelete
import org.apache.http.client.utils.URIBuilder
import org.apache.http.entity.StringEntity
import org.apache.http.HttpEntity
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.util.EntityUtils

/**
 * Helper class for the JIRA Plugin. Makes many basic REST calls that assist in creating and
 * modifying JIRA issues. In addition, several functions are made to help format the correct
 * URI for the REST calls.
 */
public class JIRAHelper {
    /**
    * Empty constructor
    */
    public JIRAHelper() {
    }

    /**
    * Returns the CloseableHttpClient object that will connect and authenticate with the JIRA Server
    * Unless switching accounts, this object can be used for all REST calls. In addition, it is
    * set with Preemptive Authentication to send the authenticating message early to reduce overhead.
    *
    * @param username String representation of the username used to login into JIRA
    * @param password String representation of the password used to login into JIRA
    * @return         Object contains the authentication information to connect with JIRA
    * @see            CloseableHttpClient
    */
    public CloseableHttpClient createClient(String username, String password) {
        CloseableHttpClientBuilder clientBuilder = new CloseableHttpClientBuilder()
        clientBuilder.setUsername(username)
        clientBuilder.setPassword(password)
        clientBuilder.setPreemptiveAuthentication(true)

        CloseableHttpClient client = clientBuilder.buildClient()
        return client
    }

    /**
     * Constructs and returns the formatted address for the specified REST call. JIRA REST calls
     * constructed in this function will have the following structure:
     * <serverUrl>/rest/api/latest/<resource>/<target>/<command>
     *
     * @param serverUrl The full URL of the JIRA server
     * @param resource  The data entity that will be defined by the target
     * @param target    The key, id, or type of the resource
     * @param command   The transition that will take place on the target
     * @return          The fully constructed address for the REST call
     */
    public String createAPIAddress(String serverUrl, String resource, String target, String command) {
        return createAPIAddress(serverUrl, resource, target + "/" + command)
    }

    /**
     * Constructs and returns the formatted address for the specified REST call. JIRA REST calls
     * constructed in this function will have the following structure:
     * <serverUrl>/rest/api/latest/<resource>/<target>
     *
     * @param serverUrl The full URL of the JIRA server
     * @param resource  The data entity that will be defined by the target
     * @param target    The key, id, or type of the resource
     * @return          The fully constructed address for the REST call
     */
    public String createAPIAddress(String serverUrl, String resource, String target) {
        return createAPIAddress(serverUrl, resource + "/" + target)
    }

    /**
     * Constructs and returns the formatted address for the specified REST call. JIRA REST calls
     * constructed in this function will have the following structure:
     * <serverUrl>/rest/api/latest/<resource>
     *
     * @param serverUrl The full URL of the JIRA server
     * @param resource  The data entity that will be defined by the target
     * @return          The fully constructed address for the REST call
     */
    public String createAPIAddress(String serverUrl, String resource) {
        String fullAddress = serverUrl + "/rest/api/latest/" + resource
        return checkHTTP(fullAddress)
    }

    /**
     * Confirm that the URL begins with either http or https. If it does not, then https:// will
     * be appended to the front of the string. This method is used in conjunction with the
     * createAPIAddress() functions to construct a proper REST call.
     *
     * @param url The string representation of a URL
     * @return    String that beings with http, otherwise https:// will be appended to the front
     */
    public String checkHTTP(String url) {
        if (!url.startsWith("http")) {
            url = "https://" + url
        }
        return url
    }

    /**
     * Calls a REST Get request and returns the CloseableHttpResponse
     *
     * @param client    The CloseableHttpClient that contains the necessary authentication information.
     * @param fullUrl   Expects the full String URL for the given REST call
     * @return          The CloseableHttpResponse for the GET REST Call
     */
     public CloseableHttpResponse doHttpGet(CloseableHttpClient client, String fullUrl){
         URI uri = new URIBuilder(fullUrl).setParameter("Accept", "application/json").build()
         HttpGet getRequest = new HttpGet(uri)

         // Execute the REST call
         CloseableHttpResponse response = client.execute(getRequest)
         return response
     }

     /**
      * Calls a REST POST request and returns the CloseableHttpResponse
      *
      * @param client    The CloseableHttpClient that contains the necessary authentication information.
      * @param fullUrl   The complete URL address of the REST POST call.
      * @param postData  The data structure that contains the information for the post request.
      * @return          The CloseableHttpResponse for the POST REST Call.
      */
      public CloseableHttpResponse doHttpPost(CloseableHttpClient client, String fullUrl, def postData){
          // Form that Issue JSON for the REST call
          StringEntity issueEntity = new StringEntity(postData.toString(), "UTF-8")
          issueEntity.setContentType("application/json")

          // Create HttpPost Request and append Issue JSON
          URI uri = new URIBuilder(fullUrl).setParameter("Accept", "application/json").build()
          HttpPost postRequest = new HttpPost(uri)
          postRequest.setEntity(issueEntity)

          // Execute the REST Call
          CloseableHttpResponse response = client.execute(postRequest)
          return response
      }

      /**
       * On success, REST JIRA responses often return information about the given resource. They are
       * returned as a JSON entity. This function will retrieve the response entity and convert
       * the JSON in a data structure of lists and maps.
       *
       * @param response  The HTTP response message the JIRA server responded with
       * @return          The returned enitity in an interpretable data structure of lists and maps
       */
      public def httpResponse2Map(CloseableHttpResponse response){
          // Parse the returned JSON
          HttpEntity returnEntity = response.getEntity()
          String json = EntityUtils.toString(returnEntity)
          JsonSlurper slurper = new JsonSlurper()
          def map = slurper.parseText(json)
          return map
      }

      /**
       * Create an Issue with the given JSON mapping. Return a Boolean based on its successful
       * creation.
       *
       * @param client    The CloseableHttpClient that contains the necessary authentication information.
       * @param serverUrl The URL address of the JIRA server.
       * @param issueJSON The JSON data structure that contains the information for the Issue
       * @return          Boolean based on the creation success of the issue. Expect status = 201
       */
      public Boolean createIssue(CloseableHttpClient client, String serverUrl, JsonBuilder issueJSON){
          // Construct the HTTP addresses
          String fullAddress = createAPIAddress(serverUrl, "issue")
          CloseableHttpResponse response = doHttpPost(client, fullAddress, issueJSON)
          try {
              def statusLine = response.getStatusLine()
              def statusCode = statusLine.getStatusCode()

              // Confirm the REST call was a success, fail otherwise
              if (statusCode == 201) {
                def issueResponse = httpResponse2Map(response)
                println "Creation of new Issue ${issueResponse.key} was successful."
                return true
              }
              else if (statusCode == 400) {
                println "Create Issue Failure: Input is invalid"
                println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                return false
              }
              else {
                println "Create Issue Failure: Unexpected Status Code"
                println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                return false
              }
          }
          finally {
              response.close()
          }
      }

     /**
      * Creates and calls the REST call to perform a transition on an issue. The POST data has the
      * will be appended to the call and sent to the server. The response is then analyzed and
      * will respond accordingly.
      *
      * @param client    The CloseableHttpClient that contains the necessary authentication information.
      * @param serverUrl The URL address of the JIRA server.
      * @param issue     The Issue key or ID whose transition will be updated.
      * @param updateJSON The JSON data structure that contains the update information for the Issue
      * @return          Boolean based on the updates success of the issue. Expect status = 204
      */
     public Boolean updateIssue(CloseableHttpClient client, String serverUrl, String issueId, JsonBuilder updateJSON){
         // Create HttpPost request and append Issue JSON
         String fullAddress = createAPIAddress(serverUrl, "issue", issueId, "transitions")
         CloseableHttpResponse response = doHttpPost(client, fullAddress, updateJSON)
         try {
             def statusLine = response.getStatusLine()
             def statusCode = statusLine.getStatusCode()

             // Analyze returned JSON...
             if (statusCode == 204) {
                 println "Successfully performed transition on issue ${issueId}"
                 return true
             }
             else if (statusCode == 400) {
                 println "Skipping Issue ${issueId}: No transition was specified."
                 println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                 return false
             }
             else if (statusCode == 404) {
                 println "Skipping Issue ${issueId}: Issue does not exist or you do not " +
                     "have the correct permissions to view it."
                 println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                 return false
             }
             else {
                 println "Skipping Issue ${issueId}: Unexpected Status Code."
                 println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                 return false
             }
         }
         finally {
             response.close()
         }
     }

    /**
     * Uses the REST Post call to add a new comment to the Issue. A status code of 201 is
     * expected when the add comment is successful. The call will fail if an unknown issue is
     * given. Any String postData input is valid.
     *
     * @param client    The CloseableHttpClient that contains the necessary authentication information.
     * @param serverUrl The URL address of the JIRA server.
     * @param issue     The Issue key or ID where the new comment will be added.
     * @param postData  The String data structure that contains the comment
     * @return          True or false based on the success of adding the comment
     */
     public Boolean addIssueComment(CloseableHttpClient client, String serverUrl, String issueId, String postData){
         // Construct the issue specific address and postRequest
         String fullAddress = createAPIAddress(serverUrl, "issue", issueId, "comment")
         CloseableHttpResponse response = doHttpPost(client, fullAddress, postData)
         try {
             def statusLine = response.getStatusLine()
             def statusCode = statusLine.getStatusCode()

             // Analyze returned JSON...
             if (statusCode == 201) {
                println "Adding Comment to issue ${issueId} was successful."
                return true
             }
             else if (statusCode == 404) {
                println "Skipping Issue '${issueId}': Invalid input. (ex. ID or Key of issue)"
                println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                return false
             }
             else {
                println "Skipping Issue ${issueId}: Unexpected Status Code."
                println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                throw new IllegalStateException("Critical Failure: Unexpected Status Code")
             }
         }
         finally {
             response.close()
         }
     }

    /**
     * Create a REST call that retrieves all information about the given issue. The call returns
     * the issue information in JSON format that is then parsed into a data structure of lists and
     * maps.
     *
     * @param client    The CloseableHttpClient that contains the necessary authentication information.
     * @param serverUrl The URL address of the JIRA server.
     * @param issue     The Issue key or ID whose information will be returned.
     * @return          Mapping of the Issue's information
     */
    public def getIssue(CloseableHttpClient client, String serverUrl, String issueId) {
        // Construct the URI to call upon
        String fullAddress = createAPIAddress(serverUrl, "issue", issueId)
        CloseableHttpResponse response = doHttpGet(client, fullAddress)
        try {
            def statusLine = response.getStatusLine()
            def statusCode = statusLine.getStatusCode()

            // Analyze returned JSON...
            if (statusCode == 200) {
                println("Retrieved issue ${issueId}...")
                return httpResponse2Map(response)
            }
            else if (statusCode == 404) {
                println "Skipping Issue ${issueId}: Issue does not exist or you do not " +
                    "have the correct permissions to view it."
                println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                return null
            }
            else {
                println "Skipping Issue ${issueId}: Unexpected Status Code."
                println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                return null
            }
        }
        finally {
            response.close()
        }
    }

    /**
     * Return the list of possible transitions for the given issue
     *
     * @param client    The CloseableHttpClient that contains the necessary authentication information.
     * @param serverUrl The URL address of the JIRA server.
     * @param issue     The Issue key or ID whose information will be returned.
     * @return          Mapping of the Issue's transitions
     */
    public def getIssueTransitions(CloseableHttpClient client, String serverUrl, String issueId){
        // Construct the URI to call upon
        String fullAddress = createAPIAddress(serverUrl, "issue", issueId, "transitions")
        CloseableHttpResponse response = doHttpGet(client, fullAddress)
        try {
            def statusLine = response.getStatusLine()
            def statusCode = statusLine.getStatusCode()

            // Analyze returned JSON...
            if (statusCode == 200) {
                println("Retrieved transition for issue ${issueId}...")
                return httpResponse2Map(response)
            }
            else if (statusCode == 404) {
                println "Skipping Issue ${issueId}: Issue does not exist or you do not " +
                    "have the correct permissions to view it."
                println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                return null
            }
            else {
                println "Skipping Issue ${issueId}: Unexpected Status Code."
                println "Error - StatusCode: ${statusCode} - Message: ${response.entity?.content?.text}"
                return null
            }
        }
        finally {
            response.close()
        }
    }

    /**
     * Issues can be put in different states that indicate progress on the issue.
     * In addition to its name, each transition is given a unique ID. This function will analyze
     * the issue, find its current state, and determine if the given transitions is a possible
     * following state. If it is, it will return the transition ID.
     *
     * @param client    The CloseableHttpClient that contains the necessary authentication information.
     * @param serverUrl The URL address of the JIRA server.
     * @param issue     The Issue key or ID whose transtion state is being analyzed.
     * @param transition    The next transition state in question
     * @return          If transition is a possible state, its id will be returned else null
     * Returns the transitions String ID. Return of null == failure
     */
    public String getTransitionId(CloseableHttpClient client, String serverUrl,
            String issueId, String transitionName){

        // Retrieve a mapping of all possible transitions for given issue
        def transitionsMap = getIssueTransitions(client, serverUrl, issueId)

        // Analyze the JSON transition map. Determine if given transition is contained within the issue
        String id = null
        def record = []
        for (def tempTransition : transitionsMap.transitions) {
            record << tempTransition.name
            if (tempTransition.name == transitionName){
                id = tempTransition.id
                break
            }
        }
        if (!id) {
            println "Could not find given transition for issue ${issueId}."
            println "Possible transition states: " + record
        }
        return id
    }

    /**
     * The status represents the stage at which the issue is located in its lifecycle such
     * as 'Open' or 'Resolved.' This function will retrieve the status of the given Issue.
     *
     * @param client    The CloseableHttpClient that contains the necessary authentication information.
     * @param serverUrl The URL address of the JIRA server.
     * @param issue     The Issue key or ID whose status is being retrieved.
     * @return          The status of the given Issue
     */
    public String getIssueStatus(CloseableHttpClient client, String serverUrl, String issueId) {
        // Retrieve the Issue mapping. getIssue() failure will return null
        def statusMap = getIssue(client, serverUrl, issueId)

        // Analyze the JSON status map. Return the status name
        if (statusMap) {
            return statusMap.fields.status.name
        }
        return null
    }

    /**
     * The status category represents the general stage at which the issue is located in its lifecycle.
     * Each status is grouped under a variety different status categories such as: 'To-Do,'
     * 'In Progress,' 'Done,' or 'Complete.' This function will retrieve the status cateogry of the
     * given Issue's status.
     *
     * @param client    The CloseableHttpClient that contains the necessary authentication information.
     * @param serverUrl The URL address of the JIRA server.
     * @param issue     The Issue key or ID whose status is being retrieved.
     * @return          The status category of the given Issue's status
     */
    public String getIssueStatusCategory(CloseableHttpClient client, String serverUrl, String issueId) {
        // Retrieve the Issue mapping. getIssue() failure will return null
        def statusMap = getIssue(client, serverUrl, issueId)

        // Analyze the JSON status map. Return the status category name
        if (statusMap) {
            return statusMap.fields.status.statusCategory.name
        }
        return null
    }

    /**
     * Checks the JIRA server to confirm issue does or does not exist. Returns true or false
     * based on the issues existance. Expects a status code of 200 returned from the REST call
     *
     * @param client    The CloseableHttpClient that contains the necessary authentication information
     * @param serverUrl The URL address of the JIRA server
     * @param issue     The Issue key or ID whose existance will be determined
     * @return          True indicates the Issue does exist. False indicates that the Issue does not.
     */
     public Boolean doesIssueExist(CloseableHttpClient client, String serverUrl, String issueId){
        if (getIssue(client, serverUrl, issueId)) {
            return true
        }
        return false
    }

    /**
     * For certain transtion states, a resolution state can be specified to better explain the state
     * of a given issue. This function will confirm that a given resolution does exist. By default,
     * the resolution state can only be used for 'Close Issue' and 'Resolve Issue' transitions
     *
     * @param client        The CloseableHttpClient that contains the necessary authentication information.
     * @param serverUrl     The URL address of the JIRA server.
     * @param resolution    The way in which the issue can be closed. States specified in JIRA
     * @return              Boolean based on the resolution's existance in JIRA server
     */
    public Boolean doesResolutionExist(CloseableHttpClient client, String serverUrl, String resolutionName){
        // Construct the URI, run GET REST, and retrieve the entity mapping
        String fullAddress = createAPIAddress(serverUrl, "resolution")
        CloseableHttpResponse response = doHttpGet(client, fullAddress)
        try {
            def resolutionsMap = httpResponse2Map(response)

            // Analyze the JSON transition map. Determine if map contains the resolution
            def resolutionArray = []
            for (def foundResolution : resolutionsMap) {
                resolutionArray << foundResolution.name
                if (foundResolution.name == resolutionName){
                    return true
                }
            }

            println "Could not find '${resolutionName}' resolution."
            println "Available resolutions: " + resolutionArray
        }
        finally {
            response.close()
        }
        return false
    }


    /**
     * The resolution explains how the Issue has been closed. This function will determine if the
     * given resolution is valid. True or false will be returned based on valid resolution
     *
     * @param client    The CloseableHttpClient that contains the necessary authentication information.
     * @param serverUrl The URL address of the JIRA server.
     * @param issue     The Issue key or ID whose status being checked for the resolution
     * @param resolutionName     The resolution's name in question
     * @return          True if resolution is valid, else false
     */
    public Boolean isResolutionValid(CloseableHttpClient client, String serverUrl, String issueId, String resolutionName){
        if (resolutionName) {
            String issueStatusCategory = getIssueStatusCategory(client, serverUrl, issueId)
            if(issueStatusCategory == 'New' || issueStatusCategory == "In Progress") {
                if(doesResolutionExist(client, serverUrl, resolutionName)) {
                    return true;
                }
                else {
                    println "${resolutionName} does not exist."
                }
            }
        }
        else {
            println "No resolution name was given."
        }
        return false
    }
}
