/*
* 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.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
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.JiraRelease;
import com.urbancode.air.plugin.jira.JiraStatus;
import com.urbancode.air.plugin.jira.JiraType;
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 HttpClient 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) {
        this.jiraBaseUrl = jiraBaseUrl;
        this.userName = userName;
        this.password = password;

        HttpClientBuilder builder = new HttpClientBuilder();
        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 {
        List<JiraApplication> result = new ArrayList<JiraApplication>();
        JSONArray json = new JSONArray(makeRequest("project"));
        for (int i = 0; i < json.length(); i++) {
            result.add(new JiraApplication(json.getJSONObject(i)));
        }
        return result;
    }

    /**
     * gets all versions from jira and returns them as a list of JiraReleases
     */
    //----------------------------------------------------------------------------------------------
    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), project.getName()));
            }
        }
        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) 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 + "'"
        }
        //a list of fields to be returned (we only request the properties we need for the integration)
        url += "&fields=key,description,summary,issuetype,status,fixVersions,priority";
        // 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);
        //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") == "fixVersions") {
                result.add(new JiraField(field));
            }
        }
        return result;
    }

    //----------------------------------------------------------------------------------------------
    private HashMap<String,String> createCustomFieldArray(String applicationField, String releaseField, String initiativeField) {
        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]
        }
        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 {
        HttpGet request = new HttpGet(formatUrl() + "rest/api/2/" + url);
        request.addHeader("accept", "application/json");
        request.addHeader("authorization", "Basic " + getAuth());
        HttpResponse response = client.execute(request);
        return EntityUtils.toString(response.getEntity());
    }

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

    /**
     * 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");
        request.addHeader("authorization", "Basic " + getAuth());

        HttpResponse response = client.execute(request);
        if (response.getStatusLine().getStatusCode() == 0) {
            throw new TranslatableException("Unable to connect to JIRA.");
        }
        if (response.getStatusLine().getStatusCode() == 401) {
            throw new TranslatableException("Unable to log in to JIRA: Username / password");
        }
        if (response.getStatusLine().getStatusCode() == 403) {
            throw new TranslatableException("Unable to log in to JIRA: Captcha challenge");
        }
        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;
    }
}
