#!/usr/bin/env groovy
/*
 * Licensed Materials - Property of IBM Corp.
 * IBM UrbanCode Release
 * (c) Copyright IBM Corporation 2014, 2017. All Rights Reserved.
 *
 * U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
 * GSA ADP Schedule Contract with IBM Corp.
 */
import com.urbancode.air.*

import java.net.URI
import java.net.URL
import java.nio.charset.Charset
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Calendar

import org.apache.commons.lang.StringUtils
import org.apache.log4j.Logger
import org.apache.log4j.Level

import com.urbancode.commons.util.query.QueryFilter.FilterType
import static com.urbancode.release.rest.framework.Clients.*
import com.urbancode.release.rest.framework.Clients
import com.urbancode.release.rest.framework.QueryParams.FilterClass
import com.urbancode.release.rest.models.Application
import com.urbancode.release.rest.models.Change
import com.urbancode.release.rest.models.Change.Severity
import com.urbancode.release.rest.models.Change.Status
import com.urbancode.release.rest.models.ChangeType
import com.urbancode.release.rest.models.Initiative
import com.urbancode.release.rest.models.internal.IntegrationProvider
import com.urbancode.release.rest.models.internal.PluginIntegrationProvider
import com.urbancode.release.rest.models.Release

import com.urbancode.air.plugin.jira.JiraApplication
import com.urbancode.air.plugin.jira.JiraComponent
import com.urbancode.air.plugin.jira.JiraChange
import com.urbancode.air.plugin.jira.JiraFactory
import com.urbancode.air.plugin.jira.JiraRelease

final def workDir = new File('.').canonicalFile
def apTool = new AirPluginTool(this.args[0], this.args[1])
def props = apTool.getStepProperties()

//We launch the integration here

def integration = new JiraIntegration (props)
integration.releaseAuthentication()
integration.runIntegration ()

public class JiraIntegration {

    private static final Logger log = Logger.getLogger(JiraIntegration.class)

    JiraFactory factory

    Map<String, Map<String, Change>> changeMap
    HashMap<String,String> customFields

    def integrationProviderId
    def provider
    def releaseToken
    def serverUrl
    def jiraUrl
    def jiraUser
    def jiraPassword
    def proxyHost
    def proxyPort
    def proxyUser
    def proxyPass
    def autoMapApplications
    def mappingApplications
    def mappingRelease
    def mappingStatuses
    def mappingTypes
    def mappingSeverities
    def mappingReleaseCopies
    def selectedApplication
    def selectedRelease
    def selectedInitiative
    def createInitiatives
    def fullIntegration
    def slurper
    def epicsAsInitiatives
    def jiraTypesToExclude

    // ----- Custom Issue as Initiatives Fields -----

    def useCustomIssueType
    def customIssueType
    def customIssueLink
    def customIssueLinkDepth


    def allUcrChangeTypes

    def changesByExternalId // Map of UCD ids to UCR Changes
    def allReleasesNameId
    def allApplicationsNameId
    def allInitiativesNameId = []

    def addChangeList = []
    def updateChangeList = []
    def epicsAsInitiativesList = []
    def deleteChangeList = []
    def externalIdField = "FormattedID"

    def jiraReleaseToReleaseTemplate = [:]

    //counts used for logging
    int newChangeCount = 0
    int updatedChangeCount = 0
    int deletedChangeCount = 0

    final def defaultDate = "1900-01-01T12:00"

    // Main Constructor for Jira Plugin
    JiraIntegration (props) {
        
        //log.setLevel(Level.DEBUG)

        this.integrationProviderId  = props['releaseIntegrationProvider']
        this.releaseToken           = props['releaseToken']
        this.serverUrl              = props['releaseServerUrl']
        this.jiraUrl                = props['jiraUrl']
        this.jiraUser               = props['jiraUser']
        this.jiraPassword           = props['jiraPassword']
        this.proxyHost              = props['proxyHost']
        this.proxyPort              = props['proxyPort']
        this.proxyUser              = props['proxyUser']
        this.proxyPass              = props['proxyPass']
        this.fullIntegration        = Boolean.parseBoolean(props['fullIntegration'])
        this.autoMapApplications    = Boolean.parseBoolean(props['autoMapApplications'])
        this.mappingApplications    = props['mappingApplications']
        this.mappingRelease         = props['mappingRelease']
        this.mappingStatuses        = props['mappingStatuses']
        this.mappingTypes           = props['mappingTypes']
        this.mappingSeverities      = props['mappingSeverities']
        this.selectedApplication    = props['selectedApplication']
        this.selectedRelease        = props['selectedRelease']
        this.selectedInitiative     = props['selectedInitiative']
        this.createInitiatives      = Boolean.parseBoolean(props['createInitiatives'])
        this.slurper                = new groovy.json.JsonSlurper()
        this.epicsAsInitiatives     = Boolean.parseBoolean(props['useEpicAsInitiative'])
        this.mappingReleaseCopies   = slurper.parseText(props['mappingReleaseCopies'])
        this.jiraTypesToExclude     = props['jiraTypesToExclude']

        this.useCustomIssueType     = Boolean.parseBoolean(props['useCustomIssueType'])
        if(useCustomIssueType) {
            this.customIssueType        = props['customIssueType']
            this.customIssueLink        = props['customIssueLink']
            this.customIssueLinkDepth   = props['customIssueLinkDepth']
        }

        println "Application Mapping Field -> " + this.selectedApplication;
        println "Release Mapping Field -> " + this.selectedRelease; 

        mappingReleaseCopies.each{ item -> 

            if (item.releaseTemplates != "NONE_VALUE") {
               this.jiraReleaseToReleaseTemplate[item.jiraProjectsForReleases] = item.releaseTemplates
            }
        }

        if(jiraUrl.charAt(jiraUrl.length() -1) != '/') {
            this.jiraUrl = this.jiraUrl + '/'
        }
    }

    //----------------------------------------------------------------------------------------------
    private static enum ChangeUpdate {
        ADDED, UPDATED, DELETED, NONE;
    }

    //--------------------------------------------------------------
    //Authentication with Release
    def releaseAuthentication () {
        Clients.loginWithToken(serverUrl, releaseToken)
    }

    //--------------------------------------------------------------
    def init () {
        //We retrieve the full provider object
        this.provider = new PluginIntegrationProvider().id(integrationProviderId).get()

        //We need to make sure that all changes or initiatives imported from that integration won't be editable
        this.provider.disabledFields("change", "type", "name", "status", "release", "application", "description", "severity", "initiative")
        this.provider.disabledFields("initiative", "description", "name")
        this.provider.save()
    }

    //--------------------------------------------------------------
    def runIntegration () {

        //We load the integration provider and the httpClient
        init()
        factory = new JiraFactory(jiraUrl, jiraUser, jiraPassword, proxyHost, proxyPort, proxyUser, proxyPass)

        // If fullIntegration is true, use the defaultDate, else find the last run date
        def retrievalDate = getRetrievalDate(fullIntegration)

        // Retrieve all Applications Name and Id from UCR
        def appEntity = new Application()
        appEntity.format("name")
        allApplicationsNameId = appEntity.getAll()

        // Retrieve all Releases Name and Id from UCR
        def relEntity = new Release()
        relEntity.format("name")
        allReleasesNameId = relEntity.getAll()

        //Here we need to gather all Change Types created in Release in order to do the Mapping of Types
        allUcrChangeTypes= new ChangeType().getAll()

        syncAllProjectsAsReleases()

        log.debug("--------------------------------------------------------------")
        //We retrieve all changes for that provider from UCRelease
        //def allChanges = new Change().getAll()    // GATHER ALL CHANGES
        Change[] allChanges = change().filter("integrationProvider.id", FilterClass.UUID, FilterType.EQUALS, integrationProviderId).when().getAll()
        log.debug("Existing Changes for that Integration: " + allChanges.size())

        // Generate a Double HashMap where <AppID,<ExtChangeID,Change>>
        // (Allows multi-changes with multi-apps)
        changeMap = new HashMap<String, Map<String,Change>>()
        for (Change change : allChanges) {
            String appId = change.getApplication()?.id
            // If not found, set to the default/empty UCR Application ID value.
            if(!appId){
                appId = "NONE_VALUE"
            }

            if (!changeMap.containsKey(appId)) {
                changeMap.put(appId, new HashMap<String,Change>())
            }
            changeMap.get(appId).put(change.getExternalId(), change);
        }

        //We retrieve all initiatives for that provider from UCRelease
        allInitiativesNameId = new Initiative().getAll()
        log.debug("Existing Initiatives: " + allInitiativesNameId.size())

        // Get the Server Info (server time) before work item pull
        def serverInfo = factory.getServerInfo()

        customFields = factory.createCustomFieldArray(selectedApplication, selectedRelease, selectedInitiative, customIssueType, customIssueLink, customIssueLinkDepth)

        //Add/update changes
        // Assign Applications based on Auto Mapping Field
        List<JiraChange> jiraChanges = new ArrayList<JiraChange>();
        if (customFields.containsKey("Application")) {
            log.debug("Retreiving all issues, assigning Application based on custom field.")
            jiraChanges = factory.getChangesForApplication(retrievalDate, null, customFields, provider, this.jiraTypesToExclude)
            // Iterate through all Jira Issues
            for (JiraChange change : jiraChanges) {
                if (change.getProjectNames().size() > 0) {
                    // Iterate through all UCR Applications
                    allApplicationsNameId.each { ucrApp ->
                        // If Issue Project name(s) match App Name, create
                        if (change.getProjectNames().containsValue(ucrApp.getName())) {
                            gatherChange(ucrApp, [change])
                        } else {
                            def staleChange = changeMap.get(ucrApp.getId())?.get(change.getKey())
                            if(staleChange) {
                                println "The change " + change.getKey() + " no longer has the application " + ucrApp.getName() + " so the UCR change (" + staleChange.getId() + ") will be deleted"
                                new Change().delete([staleChange])
                            }
                        }
                    }
                } else {
                    gatherChange(null, [change])
                }
            }
        // Import the manually mapped Application and Projects
        } else {
            log.debug("Retrieving all manually mapped Applications and Projects/Components.")
            // Iterate through all applications and create HashMap, <Proj/CompIDs, Changes>
            // based on project and component ids (key), list of changes (value)
            Map<String, List<JiraChange>> buckets = new HashMap<String, ArrayList<JiraChange>>()
            for (JiraApplication jiraApplication : factory.getAllApplications()) {
                log.debug("Getting all Changes from JIRA APP for " + jiraApplication.getName() + " : " + jiraApplication.getId())
                // Gather all changes of given application id

                jiraChanges = factory.getChangesForApplication(retrievalDate, jiraApplication.getId(), customFields, provider, this.jiraTypesToExclude)

                // Create buckets if they do not exist
                if (!buckets.containsKey(jiraApplication.getId())) {
                    buckets.put(jiraApplication.getId(), new ArrayList<JiraChange>())
                }
                for (JiraComponent component : jiraApplication.getComponents()) {
                    if (!buckets.containsKey(component.getId())) {
                        buckets.put(component.getId(), new ArrayList<JiraChange>())
                    }
                }

                // Iterate through all changes and place in appropriate bucket
                for (JiraChange change : jiraChanges) {

                    // Add change to component bucket
                    for (JiraComponent component : change.getComponents()) {
                        buckets.get(component.getId()).add(change)
                    }

                    // Add change to application bucket
                    buckets.get(jiraApplication.getId()).add(change)
                }
            }
            // Create changes
            buckets.each { bucket ->
                def ucrApp = gatherChange(getMappingApplication(bucket.key), bucket.value)
            }
        }

        change().post(addChangeList)
        change().put(updateChangeList)
        change().delete(deleteChangeList)

        linkChangesToInitiatives(jiraChanges)

        // Set the ServerTime as the last execution date for next run
        log.debug("Last execution date set to: " + serverInfo.serverTime)
        provider.property("lastExecutionDate", serverInfo.serverTime).save()

        log.debug("--------------------------------------------------------------")
        addLogs(newChangeCount, updatedChangeCount, deletedChangeCount)

    }

    //----------------------------------------------------------------------------------------------
    private void addLogs(int newChangeCount, int updatedChangeCount, int deletedChangeCount) {
        println ("Number of Changes created: " + newChangeCount)
        println ("Number of Changes updated: " + updatedChangeCount)
        println ("Number of Changes removed: " + deletedChangeCount)
    }

    //----------------------------------------------------------------------------------------------
    private void linkChangesToInitiatives(List<JiraChange> jiraChanges) {
        if(!this.epicsAsInitiatives) {
            return
        }

        Map<String, List<String>> initiativeToChanges = new HashMap<String, List<String>>()

        // Build out map
        for (JiraChange jiraChange : jiraChanges) {
            List<String> parentKeys = jiraChange.getParentLinks()
            List<String> childKeys = jiraChange.getChildLinks()

            for (String parentKey : parentKeys) {
                if (!initiativeToChanges.containsKey(parentKey)) {
                    ArrayList<String> children = new ArrayList<String>();
                    children.add(jiraChange.getKey())
                    initiativeToChanges.put(parentKeys, children)
                } else {
                    if (!initiativeToChanges.get(parentKey).contains(jiraChange.getKey())) {
                        initiativeToChanges.get(parentKey).add(jiraChange.getKey())
                    }
                }
            }

            if (childKeys.size() > 0) {
                initiativeToChanges.put(jiraChange.getKey(), childKeys)
            }
        }

        for (String parentKey : initiativeToChanges.keySet()) {

            Initiative[] ucrInitiatives = (new Initiative()).filter("externalId", FilterClass.STRING, FilterType.EQUALS, parentKey).when().getAll()
            Initiative ucrInitiative = ucrInitiatives.length > 0 ? ucrInitiatives[0] : null

            if (ucrInitiative) {
                Change[] ucrChanges = (new Change()).filter("externalId", FilterClass.STRING, FilterType.IN, initiativeToChanges.get(parentKey).toArray()).when().getAll()
                for (Change ucrChange : ucrChanges) {
                    ucrChange.setInitiative(ucrInitiative)
                    ucrChange.save()
                }
            }
        }
    }

    //----------------------------------------------------------------------------------------------
    /**
     * @returns whether the change was added, updated, or neither
     */
    private void gatherChange(Application application, List<JiraChange> jiraChanges) {
        for (JiraChange change : jiraChanges) {

            log.debug("Updating change " + change.getName())

            if(this.epicsAsInitiatives && change.typeName == "Epic") {
                buildInitative(change)
            } else if (this.useCustomIssueType && change.typeName == customIssueType) {
                buildInitative(change)
            } else {
                ChangeUpdate changeUpdate = buildChange(change, application, changeMap);
                if (changeUpdate == ChangeUpdate.ADDED) {
                    newChangeCount++;
                } else if (changeUpdate == ChangeUpdate.UPDATED) {
                    updatedChangeCount++;
                } else if (changeUpdate == ChangeUpdate.DELETED) {
                    deletedChangeCount++;
                }
            }
        }
    }

    //----------------------------------------------------------------------------------------------
    /**
     * @returns whether the change was added, updated, deleted, or neither
     */
    private ChangeUpdate buildChange(JiraChange jiraChange, Application application, Map<String, Change> changeMap) {
        ChangeUpdate result = ChangeUpdate.NONE;
        // Grab Issue if it already exists in UCR (Based on AppId and External Id), else null

        def appId = "NONE_VALUE"
        def appName = "No app"
        def hasApp = false
        if(application) {
            appId = application.id
            appName = application.getName()
            hasApp = true
        }

        Change ucrChange = changeMap.get(appId)?.get(jiraChange.getKey())

        if(!ucrChange) {
            if(hasApp) {
                // Handle change that previously didn't have an app
                Change ucrChangeNoApp = changeMap.get("NONE_VALUE")?.get(jiraChange.getKey())

                if(ucrChangeNoApp) {
                    ucrChangeNoApp.setApplication(new Application().id(appId))
                    ucrChange = ucrChangeNoApp
                    changeMap.get("NONE_VALUE")?.remove(jiraChange.getKey())
                }
            } else {
                // Handle changes that no longer are associated to an app
                def changesToDelete = []
                changeMap.keySet().each { appIdIter ->
                    Change ucrChangeToRemove = changeMap.get(appIdIter)?.get(jiraChange.getKey())
                    if (ucrChangeToRemove) {
                        changesToDelete << ucrChangeToRemove
                    }

                    if(changesToDelete.size() > 0) {
                        changeMap.get(appIdIter)?.remove(jiraChange.getKey())
                        println "Deleting: " + changesToDelete
                        new Change().delete((Change[])changesToDelete)
                    }
                };
            }
        }

        // Found in UCR, update or delete
        if (ucrChange) {
            if (createChangeIfReleaseFound(ucrChange, jiraChange, application)) {
                log.debug("Updating '${jiraChange.getKey()}' in application: '${appName}'")
                updateChangeList << ucrChange
                result = ChangeUpdate.UPDATED;
            }
            else {
                log.debug("Deleting '${jiraChange.getKey()}' in application: '${appName}'")
                deleteChangeList << ucrChange
                result = ChangeUpdate.DELETED;
            }

        }
        // Not found in UCR, create
        else {
            ucrChange = new Change();
            if (createChangeIfReleaseFound(ucrChange, jiraChange, application)) {
                ucrChange.setExternalId(jiraChange.getKey());
                ucrChange.setIntegrationProvider(new PluginIntegrationProvider().id(integrationProviderId))
                log.debug("Adding '${ucrChange.getName()}' in application: '${appName}'")
                addChangeList << ucrChange
                result = ChangeUpdate.ADDED;
            }
            else {
                // No release mapped, not creating Change

                def releaseTemplateId = jiraReleaseToReleaseTemplate[jiraChange.getActualProjectId()]

                def jiraReleases = jiraChange.getReleases()

                def createdNewRelease = false;

                if (releaseTemplateId != null) {
                    jiraReleases.each { jiraRelease ->

                        // Create Release only if the release date is in the future
                        if(jiraRelease.releaseDate != null && jiraRelease.releaseDate > System.currentTimeMillis()) {
                            def externalReleaseId = generateReleaseExternalId(jiraRelease.id, jiraReleaseToReleaseTemplate[jiraRelease.id])
                            Release newRelease = cloneRelease(releaseTemplateId, jiraChange.getActualProjectId(), jiraRelease.getName(), jiraRelease.getReleaseDate(), externalReleaseId)
                            updateReleaseApps(newRelease.id, jiraChange.getActualProjectId())
                            createdNewRelease = true
                        }

                    }
                }

                // If we just created a release, we need to create the change
                if(createdNewRelease) {
                    if (createChangeIfReleaseFound(ucrChange, jiraChange, application)) {
                        ucrChange.setExternalId(jiraChange.getKey());
                        ucrChange.setIntegrationProvider(new PluginIntegrationProvider().id(integrationProviderId))
                        log.debug("Adding '${ucrChange.getName()}' in application: '${appName}'")
                        addChangeList << ucrChange
                        result = ChangeUpdate.ADDED;
                    }
                }
            }

        }
        return result;
    }

    private Release cloneRelease (String ucrTemplateId, String jiraProjectId, String newReleaseName, Long releaseDate, String externalReleaseId) {
        def newRelease = new Release().id(ucrTemplateId).cloneRelease(newReleaseName);

        newRelease = newRelease.targetDate(releaseDate + (1L + 12L * 60L * 60L * 1000L) - 1L).setUpdateMilestones(true).setExternalId(externalReleaseId).save();

        return newRelease
    }

    private void updateReleaseApps (String releaseId, String jiraProjectId) {
        List<JiraComponent> components = factory.getComponentsForProject(jiraProjectId);

        updateReleaseApps (releaseId, components)
    }

    private void updateReleaseApps (String releaseId, List<JiraComponent> components) {

        List<Application> apps = new ArrayList();

        components.each { component ->
        
            def ucrApp = getMappingApplication(component.getId())

            if (ucrApp.id == "NONE_VALUE") {
                ucrApp = null;

                allApplicationsNameId.each { ucrApplication ->
                    // If Issue Project name(s) match App Name, create
                    if (component.getName() == ucrApplication.getName()) {
                        ucrApp = ucrApplication;
                    }
                }
            }

            if (ucrApp) {
                apps.add(new Application().id(ucrApp.id))
            }
        }

        if (apps.size() > 0) {
            new Release().id(releaseId).addApplications(apps)
        }

    }

    private void updateTargetDate(String releaseId, String name, Long releaseDate, String externalReleaseId) {
        new Release().id(releaseId).name(name).targetDate(releaseDate + (1L + 12L * 60L * 60L * 1000L) - 1L).setUpdateMilestones(true).setExternalId(externalReleaseId).save();
    }

    //----------------------------------------------------------------------------------------------
    /**
     * @returns whether the change was added, updated, deleted, or neither
     */
    private ChangeUpdate buildInitative(JiraChange jiraChange) {
        ChangeUpdate result = ChangeUpdate.NONE;

        String externalId = jiraChange.getKey()
        Initiative[] ucrInitiatives = (new Initiative()).filter("externalId", FilterClass.STRING, FilterType.EQUALS, externalId).when().getAll()
        Initiative ucrInitiative = ucrInitiatives.length > 0 ? ucrInitiatives[0] : null

        // Found in UCR, update or delete
        if (ucrInitiative) {
            createInitiative(ucrInitiative, jiraChange)
        }
        // Not found in UCR, create
        else {
            ucrInitiative = new Initiative();
            createInitiative(ucrInitiative, jiraChange)
            println "Created Initiative"
        }
        return result;
    }

    //----------------------------------------------------------------------------------------------
    /**
     * @returns Updates Change object if release is mapped. True if object updated and release found
     */
    private boolean createChangeIfReleaseFound(Change change, JiraChange jiraChange, Application application) {
        boolean result = false
        Release release = getMappingRelease(jiraChange.getReleases())
        if (release) {
            createChange(change, jiraChange, application, release)
            result = true
        }

        return result
    }

    //----------------------------------------------------------------------------------------------
    /**
     * @returns Updates Change object
     */
    private void createChange(Change change, JiraChange jiraChange, Application application, Release release) {
        def name = jiraChange.getName()?:""
        name = name.size() > 255 ? name.substring(0, 255) : name
        change.setName(name)

        def desc = jiraChange.getDescription()?:""
        desc = desc.size() > 4000 ? desc.substring(0, 4000) : desc
        change.setDescription(desc)

        change.setApplication(application)
        change.setRelease(release?:getMappingRelease(jiraChange.getReleases()))
        change.setType(getMappingType(jiraChange.getTypeId()))
        change.setStatus(getMappingStatus(jiraChange.getStatusId()))
        change.setSeverity(getMappingSeverity(jiraChange.getPriorityId()))
        change.setInitiative(getMappingInitiative(jiraChange))
        change.setExternalUrl(getIFrameHTML(jiraUrl, jiraChange.getKey()))
    }

    //----------------------------------------------------------------------------------------------
    /**
     * @returns Updates Change object
     */
    private void createInitiative(Initiative initiative, JiraChange jiraChange) {
        def name = jiraChange.getName()?:""

        def initiativeName = jiraChange.getInitiativeName();
        if(initiativeName) {
            name = initiativeName
        }

        name = name.size() > 255 ? name.substring(0, 255) : name
        initiative.setName(name)

        def desc = jiraChange.getDescription()?:""
        desc = desc.size() > 4000 ? desc.substring(0, 4000) : desc

        initiative.setDescription(desc)
        initiative.setExternalId(jiraChange.getKey())
        initiative.integrationProvider(new PluginIntegrationProvider().id(integrationProviderId))
        initiative.save()
    }

    //----------------------------------------------------------------------------------------------
    /**
     * @returns the application mapped to a jira project
     */
    private Application getMappingApplication(String jiraApplicationId) {
        Application mappedApplication = new Application().id("NONE_VALUE")
        if (mappingApplications != null) {
            def fullMapping = slurper.parseText(mappingApplications)
            fullMapping.each {
                def jiraProjectsId = it.jiraProjects
                def applicationsId = it.applications
                if (jiraApplicationId == jiraProjectsId) {
                    mappedApplication = new Application().id(applicationsId)
                }
            }
        } else {
            println ("No mapping for project set")
        }
        return mappedApplication;

    }

    //--------------------------------------------------------------
    // Note: If a JIRA change has multiple releases, then it will always be mapped
    // with closest release date.
    // If creating new release, it will create the first one
    def getMappingRelease (def jiraReleases) {
        def mappedRelease = null;

        for (JiraRelease jiraRelease : jiraReleases) {
            // Logic if Custom Field Mapping
            if (customFields.containsKey("Release")) {
                allReleasesNameId.each { init ->
                    if (init.getId() == jiraRelease.getId() ||
                            init.getName() == jiraRelease.getName()) {
                        mappedRelease = selectRelease(mappedRelease, init.getId())
                    }
                }
            }
            // Logic if Manual Mapping
            // else if (mappingRelease != null) {
            //     def fullMapping = slurper.parseText(mappingRelease)
            //     fullMapping.each {
            //         def jiraReleaseId = it.jiraReleases
            //         def releaseId = it.releases
            //         if (jiraRelease.getId() == jiraReleaseId) {
            //             mappedRelease = selectRelease(mappedRelease, releaseId)
            //         }
            //     }
            //}
            else {
                println ("No mapping for releases set")
            }
        }

        return mappedRelease;
    }

    //--------------------------------------------------------------
    def selectRelease (Release mappedRelease, String releaseId) {
        Release temp = new Release().id(releaseId).get();
        //find the release with the closest target date
        //essentially picks a random release on a tie
        if (mappedRelease == null) {
            mappedRelease = temp;
        }
        else if (temp != null) {

            if (mappedRelease.getTargetDate() == null && temp.getTargetDate()) {
                mappedRelease = temp;
            }
            else if (temp.getTargetDate() != null && (mappedRelease.getTargetDate() > temp.getTargetDate())) {
                mappedRelease = temp;
            }
        }
        return mappedRelease
    }
    //--------------------------------------------------------------
    def getMappingStatus (String status) {
        def mappedStatus = Status.NONE;
        if (mappingStatuses) {
            def fullMapping = slurper.parseText(mappingStatuses)
            fullMapping.each {
                def ucrStatus = it.ucrStatuses
                def jiraStatus = it.jiraStatuses
                if (jiraStatus == status) {
                    for (Change.Status statusType : Change.Status.values()) {
                        if (statusType.toString() == ucrStatus) {
                            mappedStatus = statusType
                        }
                    }
                }
            }
        }
        else {
            println ("No mapping for statuses set")
        }
        return mappedStatus;
    }

    //----------------------------------------------------------------------------------------------
    def getMappingType(String type) {
        def mappedType = null
        if (mappingTypes) {
            def fullMapping = slurper.parseText(mappingTypes)
            fullMapping.each {
                def jiraType = it.jiraTypes
                def ucrType = it.ucrTypes
                if (jiraType == type) {
                    mappedType = ucrType
                }
            }
        }
        else {
            println ("No mapping for types set")
        }

        return getReleaseChangeTypeById(mappedType)
    }

    //--------------------------------------------------------------
    def getReleaseChangeTypeById (typeId) {
        def releaseChangeType = null
        allUcrChangeTypes.each {
            if (typeId == it.id) {
                releaseChangeType = it
            }
        }
        return releaseChangeType;
    }

    //----------------------------------------------------------------------------------------------
    def getMappingSeverity(String severity) {
        Severity mappedSeverity = null
        if (mappingSeverities) {
            def fullMapping = slurper.parseText(mappingSeverities)
            fullMapping.each {
                def jiraPriority = it.jiraPriorities
                def ucrSeverity = it.ucrSeverities
                if (jiraPriority == severity) {
                    // NONE status does not exist, so set to null
                    if (ucrSeverity.toString() == "NONE") {
                        mappedSeverity = null
                    } else {
                        mappedSeverity = ucrSeverity
                    }
                }
            }
        }
        else {
            println ("No mapping for severities set")
        }
        return mappedSeverity
    }
    //----------------------------------------------------------------------------------------------
    def getMappingInitiative(JiraChange change) {
        Initiative mappedInitiative = null
        def initiativeName = change.getInitiativeName()
        def initiativeId = change.getInitiativeId()
        if (initiativeName) {
            allInitiativesNameId.each { init ->
                if (init.getExternalId() == initiativeId ||
                    init.getName() == initiativeName) {
                    mappedInitiative = init
                }
            }
            List<JiraChange> epics = factory.getEpicDetail(initiativeId, customFields);

            if (createInitiatives) {
                epics.each { epic ->
                    buildInitative(epic)
                }
            }

            Initiative[] ucrInitiatives = (new Initiative()).filter("externalId", FilterClass.STRING, FilterType.EQUALS, initiativeId).when().getAll()

            Initiative ucrInitiative = ucrInitiatives.length > 0 ? ucrInitiatives[0] : null

            return ucrInitiative
        }
        return mappedInitiative
    }

    //----------------------------------------------------------------------------------------------
    def getRetrievalDate(boolean useDefault) {
        def result = ""

        //We retrieve the last execution date, if null or useDefault == true use defaultDate
        def lastExecutionDate = provider.getProperty("lastExecutionDate")
        if (useDefault || !lastExecutionDate) {
            lastExecutionDate = defaultDate
        }
        else {
            println "Last Execution Date: " + lastExecutionDate
        }

        // Try and parse the date, if fails use default date and get everything
        try {
            // Any format that begins with yyyy-MM-dd'T'HH:mm will work
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm")
            result = sdf.parse(lastExecutionDate).format("yyyy/MM/dd'+'HH:mm")
        } catch (Exception ex) {
            println "[Warning] Unable to parse date. Please update JIRA Server Time Format to (yyyy-MM-dd'T'HH:mm) to minimize duplicate lookup."
            result = defaultDate
        }
        log.debug("Retrieval Date: " + result)
        return result
    }

    //----------------------------------------------------------------------------------------------
    def getIFrameHTML(String serverUrl, String issueKey) {
        // IFrame Format for JIRA: {JIRA_SERVER_URL}/si/jira.issueviews:issue-html/{KEY}/{KEY}.html
        return "${serverUrl}si/jira.issueviews:issue-html/${issueKey}/${issueKey}.html"
    }

    private void syncAllProjectsAsReleases() {
        if (shouldUpdateReleases()) {
            jiraReleaseToReleaseTemplate.each { k, v -> 
                def allVersions = factory.getVersions(k)

                List<JiraComponent> components = null;
                allVersions.each { version -> 
                    if (isInFuture(version.getReleaseDate())) {
                        if (components == null) {
                            components = factory.getComponentsForProject(k);
                        }

                        def externalReleaseId = generateReleaseExternalId(k, version.getId())

                        Release newRelease = releaseWithExternalId(externalReleaseId);
                        // Release newRelease = releaseWithName(version.getName());

                        if(newRelease == null && version.getReleaseDate()) {
                            newRelease = cloneRelease(v, k, version.getName(), version.getReleaseDate(), externalReleaseId)
                        } else {
                            updateTargetDate(newRelease.id, version.getName(), version.getReleaseDate(), externalReleaseId);
                        }

                        updateReleaseApps(newRelease.id, components)

                        println "Syncing Release: " + version.getName()
                    }
                }
            }

            provider.property("lastReleaseExecutionDate", System.currentTimeMillis().toString()).save()
        }
    }

    private Boolean shouldUpdateReleases() {
        Long fifteenMin = 10L * 60L * 1000L

        def lastEx = provider.getProperty("lastReleaseExecutionDate");

        if(fullIntegration) {
            println "Running Full Integration: Syncing Releases"
            return true
        } else if(!lastEx || System.currentTimeMillis() > Long.parseLong(provider.getProperty("lastReleaseExecutionDate")) + fifteenMin) {
            println "Syncing Releases"
            return true
        } else {
            println "Skipping Releases Sync.  This is performed every ten minutes."
            return false
        }
    }

    private isInFuture(Long date) { 
        if (date == null) {
            return false;
        }

        if (date > System.currentTimeMillis()) {
            return true
        } else {
            return false
        }
    }

    private Release releaseWithName(String releaseName) {
        Release query = new Release()
        query.filter("name", FilterClass.STRING, FilterType.EQUALS, releaseName)

        Release[] releases = query.getAll()

        if(releases.length == 0) {
            return null
        } else {
            return releases[0]
        }
    }

    private Release releaseWithExternalId(String externalReleaseId) {
        Release query = new Release()
        query.filter("externalId", FilterClass.STRING, FilterType.EQUALS, externalReleaseId)

        Release[] releases = query.getAll()

        if(releases.length == 0) {
            return null
        } else {
            return releases[0]
        }
    }

    private generateReleaseExternalId(String projectId, String versionId) {
        return projectId + "-" + versionId
    }
}
