/*
* Licensed Materials - Property of IBM Corp.
* IBM UrbanCode Release
* (c) Copyright IBM Corporation 2011, 2017. 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.jira;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import groovy.json.JsonSlurper

import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.HttpResponseException;
import org.apache.http.util.EntityUtils;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

import com.urbancode.air.i18n.TranslatableException;
import com.urbancode.air.i18n.TranslateUtil;
import com.urbancode.air.plugin.jira.JiraApplication;
import com.urbancode.air.plugin.jira.JiraChange;
import com.urbancode.air.plugin.jira.JiraComponent;
import com.urbancode.air.plugin.jira.JiraRelease;
import com.urbancode.air.plugin.jira.JiraStatus;
import com.urbancode.air.plugin.jira.JiraType;
import com.urbancode.air.XTrustProvider;
import com.urbancode.commons.httpcomponentsutil.HttpClientBuilder;
import com.urbancode.commons.webext.util.JSONUtilities;
import com.urbancode.release.rest.models.internal.IntegrationProvider;
import com.urbancode.release.rest.models.internal.PluginIntegrationProvider;

import com.ibm.icu.text.IDNA;
import com.ibm.icu.text.StringPrepParseException;


public class JiraFactory {

    //**********************************************************************************************
    // CLASS
    //**********************************************************************************************

    private String jiraBaseUrl;
    private String userName;
    private String password;
    private DefaultHttpClient client;

    final static private APPLICATION = "Application"
    final static private INITIATIVE  = "Initiative"
    final static private RELEASE     = "Release"
    //**********************************************************************************************
    // INSTANCE
    //**********************************************************************************************


    //----------------------------------------------------------------------------------------------
    public JiraFactory(String jiraBaseUrl, String userName, String password,
            String proxyHost, String proxyPort, String proxyUser, String proxyPass) {
        this.jiraBaseUrl = jiraBaseUrl;
        this.userName = userName;
        this.password = password;

        HttpClientBuilder builder = new HttpClientBuilder ();
        builder.setUsername(userName);
        builder.setPassword(password);

        builder.setPreemptiveAuthentication(true);

        if (proxyHost) {
            builder.setProxyHost(proxyHost);
            builder.setProxyPort(Integer.valueOf(proxyPort));
            if (proxyUser) {
                builder.setProxyUsername(proxyUser);
                builder.setProxyPassword(proxyPass);
            }
        }

        XTrustProvider.install();
        builder.setTrustAllCerts(true);

        client = builder.buildClient();
    }

    /**
     * gets all projects from jira and returns them as a list of JiraApplications
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraApplication> getAllApplications() throws ClientProtocolException, JSONException, IOException {
        return getAllApplications(true, true);
    }
	
	public List<JiraApplication> getSelectedApplications(boolean findComponents, boolean findVersions, def jiraProjectIDs) throws ClientProtocolException, JSONException, IOException {

		List<JiraApplication> result = new ArrayList<JiraApplication>();
		
		for(int count = 0; count < jiraProjectIDs.size(); count++ ) {
			JSONObject projectJson = new JSONObject(makeRequest("project/"+jiraProjectIDs.getAt(count)));
			
			JSONObject appJson = new JSONObject();
			
			appJson.put("name", projectJson.getString("name"));
			appJson.put("id", projectJson.getString("id"));
			
			JiraApplication app = new JiraApplication(appJson);
			
			if (findComponents) {
				app.setComponents(getComponents(app));
			}
			if (findVersions){
				app.setVersions(getVersions(app));
			}
			result.add(app);
			
		}
		return result;
	}

    public List<JiraApplication> getAllApplications(boolean findComponents, boolean findVersions) throws ClientProtocolException, JSONException, IOException {
        List<JiraApplication> result = new ArrayList<JiraApplication>();
        JSONArray json = new JSONArray(makeRequest("project"));
        for (int i = 0; i < json.length(); i++) {
            JiraApplication app = new JiraApplication(json.getJSONObject(i));
			
            if (findComponents) {
                app.setComponents(getComponents(app));
            }
            if (findVersions){
                app.setVersions(getVersions(app));
            }
            result.add(app);
        }
        return result;
    }

    /**
     * gets all fixVersions from jira associated with given project and returns as a JiraRelease
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraRelease> getVersions(JiraApplication project) throws ClientProtocolException, JSONException, IOException {
        return getVersions(project.getId())
    }

    /**
     * gets all fixVersions from jira associated with given project and returns as a JiraRelease
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraRelease> getVersions(String projectId) throws ClientProtocolException, JSONException, IOException {
        List<JiraRelease> result = new ArrayList<JiraRelease>();
        //get versions for the project
        JSONArray jsonVersions = new JSONArray(makeRequest("project/" + projectId + "/versions"));
        for (int j = 0; j < jsonVersions.length(); j++) {
            //add the versions to the result list
            result.add(new JiraRelease(jsonVersions.getJSONObject(j)));
        }
        return result;
    }

    /**
     * gets all fixVersions from jira associated with given project and returns as a JiraRelease
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraComponent> getComponentsForProject(String projectId) throws ClientProtocolException, JSONException, IOException {
        List<JiraRelease> result = new ArrayList<JiraRelease>();
        //get versions for the project
        JSONArray jsonComponents = new JSONArray(makeRequest("project/" + projectId + "/components"));
        for (int j = 0; j < jsonComponents.length(); j++) {
            //add the versions to the result list
            result.add(new JiraComponent(jsonComponents.getJSONObject(j)));
        }
        return result;
    }

    /**
     * gets all components from jira associated with given project and returns as a JiraComponent
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraComponent> getComponents(JiraApplication project) throws ClientProtocolException, JSONException, IOException {
        List<JiraComponent> result = new ArrayList<JiraComponent>();
        //get components for the project
        JSONArray jsonComponents = new JSONArray(makeRequest("project/" + project.getId() + "/components"));
        for (int j = 0; j < jsonComponents.length(); j++) {
            //add the components to the result list
            result.add(new JiraComponent(jsonComponents.getJSONObject(j)));
        }
        return result;
    }

    /**
     * gets all versions from jira and returns them as a list of JiraReleases
     */
    // Maintained if manual mapping of Planned For and Releases and add back
    //----------------------------------------------------------------------------------------------
    public List<JiraRelease> getAllVersions() throws ClientProtocolException, JSONException, IOException {
        List<JiraRelease> result = new ArrayList<JiraRelease>();
        //JIRA versions are children of JIRA projects.  We get all of the projects,
        //then for each project, we get all of its versions.
        JSONArray jsonProjects = new JSONArray(makeRequest("project"));
        for (int i = 0; i < jsonProjects.length(); i++) {
            JiraApplication project = new JiraApplication(jsonProjects.getJSONObject(i));
            //get versions for the project
            JSONArray jsonVersions = new JSONArray(makeRequest("project/" + project.getId() + "/versions"));
            for (int j = 0; j < jsonVersions.length(); j++) {
                //add the versions to the result list
                result.add(new JiraRelease(jsonVersions.getJSONObject(j)));
            }
        }
        return result;
    }

    /**
     * gets all issues from jira for a given initiative.
     * this method can return duplicate issues
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraChange> getChangesForApplication(String date, String projectId, HashMap<String,String> customFields, PluginIntegrationProvider integrationProvider, String jiraTypesToExclude) throws JSONException, ClientProtocolException, IOException {
        // Get Issues with updated after X date and specific project ID
        String url = "search?jql=updated%3E'" + date + "'"
		if (projectId) {
			url += "+AND+project='" + projectId + "'"
		}
        def typesToExclude = jiraTypesToExclude.tokenize(",")
        typesToExclude.each { typeInput ->
            def type = typeInput.trim()
            url += "+AND+issuetype!='" + type + "'"
        }
		
		

        //a list of fields to be returned (we only request the properties we need for the integration)
        url += "&fields=key,title,description,summary,issuetype,status,fixVersions,priority,components,issuelinks,project,Epic%20Name";

        // Append Custom Fields to the getIssue request
        customFields.each { custom ->
            url += ",${custom.value}"
        }

        println "-->> " + url

        //the max results will still be limited by a setting in JIRA.
        url += "&maxResults=-1";
        JSONObject json = new JSONObject(makeRequest(url));

        int maxResults = json.getInt("maxResults");
        int total = json.getInt("total");
        List<JiraChange> result = new ArrayList<JiraChange>();
        addChanges(result, json.getJSONArray("issues"), customFields);
        //we make the request multiple times if there are more changes than JIRA's maxResults.
        //"maxResults - 10" will leave 10 duplicates each page so that changes are not skipped
        //if an issue is created/deleted in JIRA while the integration is running.

        for (int i = 1; i * maxResults < total; i++) {
            json = new JSONObject(makeRequest(url + "&startAt=" + (i * maxResults)));
            addChanges(result, json.getJSONArray("issues"), customFields);
        }

        // for (int i = maxResults - 10; i < total; i += maxResults - 10) {
        //     json = new JSONObject(makeRequest(url + "&startAt=" + i));
        //     addChanges(result, json.getJSONArray("issues"), customFields);
        // }
        return result;
    }

    /**
     * gets all issues from jira for a given initiative.
     * this method can return duplicate issues
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraChange> getEpicDetail(String epicId, HashMap<String,String> customFields) throws JSONException, ClientProtocolException, IOException {
        // Get Issues with updated after X date and specific project ID
        String url = "search?jql=key='" + epicId + "'"

        //a list of fields to be returned (we only request the properties we need for the integration)
        url += "&fields=key,title,description,summary,issuetype,status,fixVersions,priority,components,issuelinks,project,Epic%20Name";
        // Append Custom Fields to the getIssue request
        customFields.each { custom ->
            url += ",${custom.value}"
        }
        //the max results will still be limited by a setting in JIRA.
        url += "&maxResults=-1";
        JSONObject json = new JSONObject(makeRequest(url));

        int maxResults = json.getInt("maxResults");
        int total = json.getInt("total");
        List<JiraChange> result = new ArrayList<JiraChange>();
        addChanges(result, json.getJSONArray("issues"), customFields, true);
        //we make the request multiple times if there are more changes than JIRA's maxResults.
        //"maxResults - 10" will leave 10 duplicates each page so that changes are not skipped
        //if an issue is created/deleted in JIRA while the integration is running.
        for (int i = maxResults - 10; i < total; i += maxResults - 10) {
            json = new JSONObject(makeRequest(url + "&startAt=" + i));
            addChanges(result, json.getJSONArray("issues"), customFields);
        }

        return result;
    }

    /**
     * gets all possible issue statuses from jira
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraStatus> getAllStatuses() throws ClientProtocolException, JSONException, IOException {
        List<JiraStatus> result = new ArrayList<JiraStatus>();
        JSONArray json = new JSONArray(makeRequest("status"));
        for (int i = 0; i < json.length(); i++) {
            result.add(new JiraStatus(json.getJSONObject(i)));
        }
        return result;
    }

    /**
     * gets all possible issue types from jira
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraType> getAllTypes() throws ClientProtocolException, JSONException, IOException {
        List<JiraType> result = new ArrayList<JiraType>();
        JSONArray json = new JSONArray(makeRequest("issuetype"));
        for (int i = 0; i < json.length(); i++) {
            result.add(new JiraType(json.getJSONObject(i)));
        }
        return result;
    }

    /**
     * gets all possible issue priorities from jira
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraSeverity> getAllSeverities() throws ClientProtocolException, JSONException, IOException {
        List<JiraSeverity> result = new ArrayList<JiraSeverity>();
        JSONArray json = new JSONArray(makeRequest("priority"));
        for (int i = 0; i < json.length(); i++) {
            result.add(new JiraSeverity(json.getJSONObject(i)));
        }
        return result;
    }

    /**
     * gets all possible issue fields from jira
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraField> getAllFields() throws ClientProtocolException, JSONException, IOException {
        List<JiraField> result = new ArrayList<JiraField>();
        result.add(new JiraField("-1", "-- None --")) // Added to ignore the the Custom Field option
        JSONArray json = new JSONArray(makeRequest("field"));
        for (int i = 0; i < json.length(); i++) {
            result.add(new JiraField(json.getJSONObject(i)));
        }
        return result;
    }

    /**
     * gets only default, custom select, and custom text fields
     */
    //----------------------------------------------------------------------------------------------
    public List<JiraField> getAllowedCustomFields() throws ClientProtocolException, JSONException, IOException {
        List<JiraField> result = new ArrayList<JiraField>();
        result.add(new JiraField("-1", "-- None --")) // Added to ignore the the Custom Field option
        JSONArray json = new JSONArray(makeRequest("field"));
        for (int i = 0; i < json.length(); i++) {
            JSONObject field = json.getJSONObject(i);
            JSONObject schema = JSONUtilities.getJsonObjectFromJsonObject(field, "schema")
            String type = null
            if (schema) {
                type = JSONUtilities.getStringFromJsonObject(schema, "type");
            }

            // If custom field, ensure it's either a select, text field, or 'fixVersions' field
            // 'fixVersions' is the only array field accepted
            if (type != "array"
                || JSONUtilities.getStringFromJsonObject(field, "id") == "versions"
                || JSONUtilities.getStringFromJsonObject(field, "id") == "components"
                || JSONUtilities.getStringFromJsonObject(field, "id") == "fixVersions") {
                result.add(new JiraField(field));
            }
        }
        return result;
    }

    // public List<JiraField> getAllowedCustomFieldsFromProject() throws ClientProtocolException, JSONException, IOException {
    //     List<JiraField> result = new ArrayList<JiraField>();
    //     result.add(new JiraField("-1", "-- None --")) // Added to ignore the the Custom Field option
    //     JSONObject json = new JSONObject(makeRequest("issue/createmeta?projectKeys=PLUGINS&expand=projects.issuetypes.fields"));
    //     JSONArray projectsArray = json.getJSONArray("projects");
    //     for (int i = 0; i < json.length(); i++) {
    //         JSONObject field = projectsArray.getJSONObject(i);
    //         JSONObject schema = JSONUtilities.getJsonObjectFromJsonObject(field, "schema")
    //         String type = null
    //         if (schema) {
    //             type = JSONUtilities.getStringFromJsonObject(schema, "type");
    //         }

    //         // If custom field, ensure it's either a select, text field, or 'fixVersions' field
    //         // 'fixVersions' is the only array field accepted
    //         if (type != "array"
    //             || JSONUtilities.getStringFromJsonObject(field, "id") == "versions"
    //             || JSONUtilities.getStringFromJsonObject(field, "id") == "components"
    //             || JSONUtilities.getStringFromJsonObject(field, "id") == "fixVersions") {
    //             result.add(new JiraField(field));
    //         }
    //     }
    //     return result;
    // }

    /**
     * gets only default, custom select, and custom text fields
     */
    //----------------------------------------------------------------------------------------------
    public String getCustomFieldId(fieldName) throws ClientProtocolException, JSONException, IOException {
        List<JiraField> result = new ArrayList<JiraField>();
        JSONArray json = new JSONArray(makeRequest("field"));

        for (int i = 0; i < json.length(); i++) {
            JSONObject field = json.getJSONObject(i);

            if (JSONUtilities.getStringFromJsonObject(field, "name") == fieldName) {
                return JSONUtilities.getStringFromJsonObject(field, "id")
            }
        }
    }

    //----------------------------------------------------------------------------------------------
    private HashMap<String,String> createCustomFieldArray(String applicationField, String releaseField, String initiativeField, String customIssueType, String customIssueLinkType,  String customIssueLinkDepth) {
        def result = [:]
        // applicationField exists and does not == '-- None --' value of -1 (getAllFields())
        if (applicationField && applicationField != "-1") {
            result << [Application : applicationField]
        }
        // releaseField exists and does not == '-- None --' value of -1 (getAllFields())
        if (releaseField && releaseField != "-1") {
            result << [Release : releaseField]
        }
        // initiativeField exists and does not == '-- None --' value of -1 (getAllFields())
        if (initiativeField && initiativeField != "-1") {
            result << [Initiative : initiativeField]
        }
        // We assume the epic name is the "Epic Name" custom field so if this exists, we want
        // to add this to our custom fields list
        def epicNameFieldId = getCustomFieldId("Epic Name")
        if(epicNameFieldId) {
            result << [InitiativeName : epicNameFieldId]
        }

        // Fields for Custom Issue Types as Initiatives
        if (customIssueType && customIssueType != "-1") {
            result << [IssueType : customIssueType]
        }

        if (customIssueLinkType && customIssueLinkType != "-1") {
            result << [IssueLinkType : customIssueLinkType]
        }

        if (customIssueLinkDepth && customIssueLinkDepth != "-1") {
            result << [IssueLinkDepth : customIssueLinkDepth]
        }

        return result
    }

    //----------------------------------------------------------------------------------------------
    private String formatUrl() {
        //We need to make sure URLs with special characters will be converted
        jiraBaseUrl = globalize(jiraBaseUrl);

        if (jiraBaseUrl.endsWith("/")) {
            return jiraBaseUrl;
        }
        return jiraBaseUrl + "/";
    }

    //----------------------------------------------------------------------------------------------
    private String getAuth() {
        String credentials = userName + ":" + password;
        return new String(new Base64().encode(credentials.getBytes(Charset.forName("UTF-8"))));
    }

    //----------------------------------------------------------------------------------------------
    private String makeRequest(String url) throws ClientProtocolException, IOException {
        def escapedUrl = escapeSingleQuote(url)
        HttpGet request = new HttpGet(formatUrl() + "rest/api/2/" + escapedUrl);
        
        request.addHeader("accept", "application/json");
        HttpResponse response = client.execute(request);
		int statusCode = response.getStatusLine().getStatusCode();
		if(statusCode !=  200) {
			throw new HttpResponseException(statusCode, response.getStatusLine().getReasonPhrase());
		}
		
        return EntityUtils.toString(response.getEntity());
    }

    //----------------------------------------------------------------------------------------------
    private String escapeSingleQuote(String url) {
        return url.replaceAll("\'", "%27").replaceAll(" ", "%20")
    }

    //----------------------------------------------------------------------------------------------
    private void addChanges(List<JiraChange> changes, JSONArray json, HashMap<String, String> customFields) throws JSONException {
        addChanges(changes, json, customFields, false)
    }

    //----------------------------------------------------------------------------------------------
    private void addChanges(List<JiraChange> changes, JSONArray json, HashMap<String, String> customFields, boolean skipInitiative) throws JSONException {
        for (int i = 0; i < json.length(); i++) {
            changes.add(new JiraChange(json.getJSONObject(i), customFields, this, skipInitiative));
        }
    }

    /**
     * Retrieve's JIRA Server Info
     * Used to retrieve the current server time
     */
    //----------------------------------------------------------------------------------------------
    public def getServerInfo() throws Exception {
        def result = []
        result = new JsonSlurper().parseText(makeRequest("serverInfo"))
        return result;

    }
    /**
     * logs in to jira to test the connection.
     * Will throw an exception if the login fails
     */
    //----------------------------------------------------------------------------------------------
    public String testConnection() throws Exception {
        HttpGet request = new HttpGet(formatUrl() + "rest/auth/1/session");
        request.addHeader("accept", "application/json");

        HttpResponse response = client.execute(request);
        if (response.getStatusLine().getStatusCode() == 0) {
            println "[Info] Response: " + EntityUtils.toString(response.getEntity());
            throw new TranslatableException("Unable to connect to JIRA.");
        }
        if (response.getStatusLine().getStatusCode() == 401) {
            println "[Info] Response: " + EntityUtils.toString(response.getEntity());
            throw new TranslatableException("Unable to log in to JIRA: Username / password");
        }
        if (response.getStatusLine().getStatusCode() == 403) {
            println "[Info] Response: " + EntityUtils.toString(response.getEntity());
            throw new TranslatableException("Unable to log in to JIRA: Captcha challenge");
        }
        if (response.getStatusLine().getStatusCode() == 404) {
            println "[Info] Response: " + EntityUtils.toString(response.getEntity());
            throw new TranslatableException("Unable to log in to JIRA: Invalid URL.");
        }
        return EntityUtils.toString(response.getEntity());
    }


    public String globalize(String url) {
        try {
            URL urlToGlobalize = new URL(url);
            //Here we convert the host name
            String udUrlAscii = IDNA.convertIDNToASCII(urlToGlobalize.getAuthority(), IDNA.DEFAULT).toString();
            //And we rebuild the URL with the new converted host name
            url = urlToGlobalize.getProtocol()+"://"+udUrlAscii+urlToGlobalize.getPath();
        }
        catch (MalformedURLException ex) {
            throw new RuntimeException(i18n("Malformed URL %s for Integration Provider", url), ex);
        }
        catch (StringPrepParseException ex) {
            throw new RuntimeException(i18n("Can not convert the URL %s from IDN to ASCII", url), ex);
        }

        return url;
    }
}
