#!/usr/bin/env groovy
/*
 * Licensed Materials - Property of IBM Corp.
 * IBM UrbanCode Release
 * (c) Copyright IBM Corporation 2014. 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.util.Date;
import java.util.SortedSet;
import com.urbancode.urelease.plugin.util.BulkClassLoader;
import com.urbancode.urelease.plugin.rtc.plain.api.RTCClientHelper;
import com.urbancode.release.rest.models.Change;
import com.urbancode.release.rest.models.Initiative;
import com.urbancode.release.rest.models.Release;
import com.urbancode.release.rest.models.Application;
import com.urbancode.release.rest.models.ChangeType;
import com.urbancode.release.rest.models.internal.PluginIntegrationProvider;
import com.urbancode.release.rest.framework.Clients;

final def workDir = new File('.').canonicalFile

def currentDirectory = new File(getClass().protectionDomain.codeSource.location.path).parent
//The RTC Client Plain Java API contains a very long list of jars that can not be added manually
//to the command defined in the plugin.xml.
//For that reason we need to load them dynamically before doing anything with the API
//org.apache.commons.logging_1.0.4 is conflicting with Groovy dependencies and need to be excluded
String[] exclude = ["org.apache.commons.logging_1.0.4"] as String[]
BulkClassLoader.addAllFiles(currentDirectory+"/lib/RTCClient",exclude)

def apTool = new AirPluginTool(this.args[0], this.args[1]);
def props = apTool.getStepProperties();

//We launch the integration here
def integration = new RTCIntegration (props)
integration.releaseAuthentication();

integration.runIntegration ()

public class RTCIntegration {

    def static final EMPTY_TITLE = "No Title";
    
    def integrationProviderId
    def provider;
    def ucrClient;
    def releaseToken;
    def rtcServerUrl;
    def userLogin;
    def userPassword;
    def rtcProjectArea;
    def serverUrl;
    def projectArea;
    def query;
    def mappingStatuses;
    def mappingRelease;
    def mappingTypes;
    def mappingApplication;
    def mappingSeverities;
    def initiatives;
    def slurper;
    def rtcHelper;
    def allUcrChangeTypes;
    def allReleases;
    def allApplications;
    def associateApplications;
    def applicationToReleaseMapping;
    
    def importIfUnassignedApplication;
    def importIfUnassignedRelease;
    
    //Main constructor
    RTCIntegration (props) {
        this.integrationProviderId = props['releaseIntegrationProvider']
        this.releaseToken = props['releaseToken'];
        this.serverUrl = props['releaseServerUrl'];
        this.rtcServerUrl = props['rtcServerUrl']
        this.userLogin = props['userLogin']
        this.userPassword = props['userPassword']
        this.projectArea = props['projectArea']
        this.query = props['query']
        
        //Mapping
        this.mappingStatuses = props['mappingStatuses']
        this.mappingRelease = props['releaseMapping']
        this.mappingApplication = props['applicationMapping']
        this.mappingSeverities = props['mappingSeverities']
        this.mappingTypes = props['mappingTypes']
        this.initiatives = props['initiatives']
        this.associateApplications = props['associateApplications']
        
        this.importIfUnassignedApplication = props['importIfUnassignedApplication']
        this.importIfUnassignedRelease = props['importIfUnassignedRelease']
        this.slurper = new groovy.json.JsonSlurper()
    }
    
    //--------------------------------------------------------------
    //Authentication with Release
    def releaseAuthentication () {
        Clients.loginWithToken(serverUrl, releaseToken);
    } 
    
    //--------------------------------------------------------------
    def init () {
        //Authentication to a specific project area
        this.rtcHelper = new RTCClientHelper()    
        this.rtcHelper.login(userLogin,userPassword,rtcServerUrl)
        this.rtcProjectArea = rtcHelper.getProjectAreaByName(projectArea)
        //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 () {
        long startInit = System.currentTimeMillis()
        init()
        
        try {
            long endInit = System.currentTimeMillis()
            log ("init in "+((endInit-startInit)/1000)+"s")
            def countCreatedItem = 0;
            def countWarning = 0;
            //We need to save or create in bulk
            def bulkCreateChangeList = [] as List
            def bulkUpdateChangeList = [] as List
            //We build a list of all existing uri
            def urlsExistingItem = [] as List
            def workItemToInitiativeObject = [:]
            def workItemToChangeObject = [:]
            def changeToInitiative = [:]
            //Map of all the applications that have to be added to a Release
            applicationToReleaseMapping = [:]
            
            log("--------------------------------------------------------------")
            long startData = System.currentTimeMillis()
            //We set the last execution date
            def lastRtcExecution = provider.getProperty("lastExecution");
            def lastExecutionDate = null;
            if (lastRtcExecution != null) {
                lastExecutionDate = new Date(lastRtcExecution.toLong());
                println "Last Execution Date: "+lastExecutionDate
            }

            allReleases = new Release().getAll()
            println "Existing Releases: "+allReleases.size()
            allApplications = new Application().getAll()
            println "Existing Applications: "+allApplications.size()
            
            //We retrieve all changes for that provider from UCRelease
            def allChanges = new Change().getAll()
            log ("Existing Changes: "+allChanges.size())
            
            //We retrieve all initiatives for that provider from UCRelease
            def allInitiatives = new Initiative().getAll()
            log ("Existing Initiatives: "+allInitiatives.size())
            
            allChanges.each{
                item -> if (item.externalUrl != null) urlsExistingItem.add(item.externalUrl)
            }
            
            allInitiatives.each{
                item -> 
                    if (item.externalUrl != null) urlsExistingItem.add(item.externalUrl)
                    //We build a map of workItemID:workItemObject
                    workItemToInitiativeObject[item.externalId] =  item;
             }
            long endData = System.currentTimeMillis()
            log ("Retrieve data from Release in "+((endData-startData)/1000)+"s")
            log("--------------------------------------------------------------")
            //Initiatives
            //We create a map that contains info WorkItem / Parents
            def workItemToParentMap = [:]//rtcHelper.getWorkItemToParentMap(rtcProjectArea,query);
    
            long start = System.currentTimeMillis()
            //List of changes retrieved from RTC by executing a specific query. 
            //We also loads the custom attributes used for mapping
            def importedChangeList = rtcHelper.getWorkAllItemsFromQueryWithCustomAttributes(rtcProjectArea,query,urlsExistingItem,lastExecutionDate);
            long end = System.currentTimeMillis()
            log ("Query fetching performance: "+importedChangeList.size()+" in "+((end-start)/1000)+"s")
            log("--------------------------------------------------------------")
            //Here we need to gather all Change Types created in Release in order to do the Mapping of Types
            allUcrChangeTypes= new ChangeType().getAll();
            
            //Check for initiatives. If some items are in fact initiatives lets create them
            importedChangeList.each {
                item -> 
                if (doesWorkItemMapToInitiative (item)) {
                    def initiative = createOrUpdateInitiative(item);
                    
                    // build out map of WIid to Initiative
                    workItemToInitiativeObject[item.id] =  initiative;
                }
            }
            
            importedChangeList.each{
                item -> 
                def existingChange = doesChangeAlreadyExists (item.id, allChanges)
                
                if (!doesWorkItemMapToInitiative (item)) {
                    //We keep a reference to the created or updated item
                    def createdOrUpdatedChange = null;
                    
                    if (existingChange == null) {
                        createdOrUpdatedChange = setChangeData (item, null, null)
                        //if the change is null it is more likely that it has been excluded from the list
                        //because of its mapping
                        if (createdOrUpdatedChange != null) {
                            bulkCreateChangeList.add(createdOrUpdatedChange)
                            countCreatedItem++
                        }
                    }
                    else {
                        createdOrUpdatedChange = setChangeData (item, existingChange, workItemToParentMap)
                        //If createdOrUpdatedChange is null it means that the Change does not match anymore Releases or Applications
                        //and has been removed
                        if (createdOrUpdatedChange != null) {
                            def warningFlag = false;
                            if (existingChange.integrationProvider.id != null && existingChange.integrationProvider.id != provider.id) {
                                countWarning++;
                                warningFlag = true;
                            }
                            else {
                                bulkUpdateChangeList.add(createdOrUpdatedChange)
                            }
                            
                            if (!warningFlag) {
                                def ucrExistingChange = new Change().externalId(item.id)
                            }
                        }
                    }
                    
                    //If the changes have not been excluded lets map it to an initiative
                    if (createdOrUpdatedChange != null) {
                        workItemToChangeObject[item.target] = createdOrUpdatedChange;
                        // climb the heritage map
                        if (this.initiatives != null) {
                            def closestInitWorkItemId = item.parentId;
                            // set entry in map
                            if(closestInitWorkItemId != "" && closestInitWorkItemId != null) {
                                def parent = workItemToInitiativeObject[closestInitWorkItemId];
                                //If the initiative is not found it means the workitem has an initiatives not part of the results
                                if (parent != null) {
                                    changeToInitiative[createdOrUpdatedChange] = parent;
                                }
                                else {
                                    println "No parent initiatives for "+closestInitWorkItemId
                                }
                            }
                        }
                    }
                }
            }
            
            //Some changes will be created
            if (bulkCreateChangeList.size() > 0) {
                def changeToCreateArray = bulkCreateChangeList as Change[]
                log ("Number of Changes that are created: "+bulkCreateChangeList.size())
                
                long startPost = System.currentTimeMillis()
                new Change().post(changeToCreateArray)
                long endPost = System.currentTimeMillis()
                log ("Created in Release in "+((endPost-startPost)/1000)+"s")
            }
            
            //Some changes will be updated
            if (bulkUpdateChangeList.size() > 0) {
                log ("Number of Changes that are updated: "+bulkUpdateChangeList.size())
                def changeToUpdateArray = bulkUpdateChangeList as Change[]
                long startPut = System.currentTimeMillis()
                new Change().put(changeToUpdateArray)
                long endPut = System.currentTimeMillis()
                log ("Updated in Release in "+((endPut-startPut)/1000)+"s")
            }
             
             
            if (changeToInitiative.size() > 0) { 
                log ("Number of Changes that will be turned to Initiatives: "+changeToInitiative.size())
                saveChangeInitAssociations(changeToInitiative)
            }
            
            if (countWarning > 0) {
                log (countWarning+" items are already managed by an other Integration Provider")
            }
            
            if(this.associateApplications == "true") {
                autoMappingApplicationsToRelease(applicationToReleaseMapping)
            }
            
            def currentTime = new Date()
            println ("Last execution set to: "+currentTime)
            provider.property("lastExecution", currentTime.time.toString()).save()
            
        }
        finally {
            //We need to logout the API Client or it might slow down other connection
            rtcHelper.logout()
        }
    }
    
    //--------------------------------------------------------------
    //this method set all the attribute to a Release change from its WorkItem 
    def Change setChangeData (rtcChange, existingChange, workItemToParentMap) {
        def ucrChange = null;
        
        if (existingChange != null) {
             ucrChange = existingChange
        }
        else {
            ucrChange = new Change()
        }

        ucrChange.name(cleanupChangeName(rtcChange.summary))
        ucrChange.externalUrl(rtcChange.target)
        ucrChange.externalId(rtcChange.id)
        //We set its integration provider
        ucrChange.setIntegrationProvider(new PluginIntegrationProvider().id(integrationProviderId))

        //Here we want to make sure that if the change has been removed from the initiative we reset the initiative field
        Initiative init = ucrChange.getInitiative();
        if (init != null) {
             def closestInitWorkItemId = rtcChange.parentId;
             if (closestInitWorkItemId == "") {
                 ucrChange.initiative(null)
             }
        }
        
        //----MAPPING RELEASES----
        def releaseId = mapConcepts(rtcChange,"Release")
        if (releaseId != null && releaseId != "Unassigned") {
            def release = new Release().id(releaseId)
            if (release != null) {
                ucrChange.release(release);
            }
        }
                
        //----MAPPING APPLICATIONS----
        def applicationId = mapConcepts(rtcChange,"Application")
        if (applicationId != null && applicationId != "Unassigned") {
            def application = new Application().id(applicationId)
            if (application != null) {
                ucrChange.application(application);
                
                if(this.associateApplications == "true") {
                    //If a Release was mapped to that change already
                    if (ucrChange.release != null) {
                        
                        SortedSet applicationList = new TreeSet();
                         
                        //We add the application to the list of applications to map to the release
                        //if needed
                        if (applicationToReleaseMapping[ucrChange.release.id] != null) {
                            applicationList = applicationToReleaseMapping[ucrChange.release.id]
                            applicationList.add(applicationId);
                        }
                        else {
                            applicationList.add(applicationId);
                        }
                        
                        applicationToReleaseMapping[ucrChange.release.id] = applicationList
                    }
                }
            }
        }
        
        //----CHECK FOR EXCLUSION----
        if (releaseId == null || applicationId == null) {
            //If it was an already imported change we need to remove it
            if (ucrChange.id != null) {
                ucrChange.delete()
            }
            return null;
        }
        

        if (releaseId == "Unassigned" && importIfUnassignedRelease == "false") {
            //If it was an already imported change we need to remove it
            if (ucrChange.id != null) {
                ucrChange.delete()
            }
            return null;
        }
 
        if (applicationId == "Unassigned" && importIfUnassignedApplication == "false") {
            //If it was an already imported change we need to remove it
            if (ucrChange.id != null) {
                ucrChange.delete()
            }
            return null;
        }
        
        //----MAPPING SEVERITIES----
        def rtcSeverity = rtcChange.severity
        def ucrSeverity = getMappingSeverities (rtcSeverity)
        
        if (ucrSeverity) {
            def ucrSeveritiesEnum = Change.Severity.values()
            
            for (s in ucrSeveritiesEnum) {
                if (s.toString() == ucrSeverity) {
                    ucrChange.severity(s)
                }
            }
        }
        
        //----MAPPING TYPES----
        def rtcType = rtcChange.type
        def ucrType = getMappingType(rtcType)
        if (ucrType != null) {
            ucrChange.type(ucrType)
        }
        
        //----MAPPING STATUSES----
        //We map the UCR statuses to RTC statuses
        def workItemStatus = rtcChange.status
        def ucrStatus = getMappingStatus(workItemStatus);
        if (ucrStatus != null) {
            ucrChange.status(ucrStatus)
        }
        
        return ucrChange;
    }
    
    //----------------------------------------------------------------------------------------------
    //This method map a Change to a Release concept using a custom field value defined and check 
    //if the value and the Release concept match
    //Only Release and Application are being mapped that way
    def mapConcepts(workItem,concept) {
        def objectMappedId;
        def mapAgainstField;
        def mapAgainstValue;
        def isUnassigned = false;
        
        if (concept == "Release") {
            if (mappingRelease != null) {
                mapAgainstField = mappingRelease
            }
        }
        
        if (concept == "Application") {
            if (mappingApplication != null) {
                mapAgainstField = mappingApplication
            }
        }
           
        //Filed Against and Planned For are not custom attributes and must be handled separately
        if (mapAgainstField != null) {
            if (mapAgainstField == "Filed Against") {
                 mapAgainstValue = workItem.filedAgainst
            } else if (mapAgainstField == "Planned For") {
                 mapAgainstValue = workItem.plannedFor
            } else if (mapAgainstField == "Found In") {
                 mapAgainstValue = workItem.foundIn
            } else if (workItem.customAttributes != null) {
                workItem.customAttributes.each{
                    item -> if(item.name == mapAgainstField) {
                        mapAgainstValue = item.value
                        if (item.unassigned == true) {
                            isUnassigned = true;
                        }
                    }
                }
            }
        }
        
        if (!isUnassigned) {
            if (mapAgainstValue != null) {
                if (concept == "Release") {
                    allReleases.each {
                    item ->
                        if (item.name == mapAgainstValue) {
                             objectMappedId = item.id
                        }
                    }
                }
                
                if (concept == "Application") {
                    allApplications.each {
                    item ->
                        if (item.name == mapAgainstValue) {
                             objectMappedId = item.id
                        }
                    }
                }
            }
        }
        else {
           //Mapped against unassigned !
            objectMappedId = "Unassigned";
        }
        
        return objectMappedId
    }

    //----------------------------------------------------------------------------------------------
    def saveChangeInitAssociations(parentOfChanges) {
        parentOfChanges.each() { 
        item, value -> 
            //We don't import items without release or application
            //if (item.release != null && item.application != null) {
                item.initiative(value).save()
            //}
        }
    }
    
    //----------------------------------------------------------------------------------------------
    def autoMappingApplicationsToRelease(applicationToReleaseMapping) {
        applicationToReleaseMapping.each() {
        item, value ->
            if (value != null) {
                def countAddedApps = 0;
                def release = new Release().id(item);
                //We need to get the release detail in order to load the existing applications
                release.format("detail");
                release = release.get()
                
                SortedSet applicationList = value;
                //For each applications we check if they are already part of the release
                applicationList.each{
                    appId ->
                    def application = new Application().id(appId)
                    def isApplicationAlreadyAdded = false;
                    if (release.applications != null) {
                        release.applications.each {
                        app ->
                            if (app.id == appId) {
                               isApplicationAlreadyAdded = true;
                            }
                        }
                    }
                    
                    if (!isApplicationAlreadyAdded) {
                        countAddedApps++;
                        //we add the application to the Release
                        release.addApplications(application)
                    }
                }
                
                println countAddedApps+" application(s) added to the Release "+release.name
            }
        }
    }
    
    //--------------------------------------------------------------
    def  doesWorkItemMapToInitiative (rtcChange) {
        def mappedToInitiative = false
        def workItemType = rtcChange.type
         //parses the initiatives
         if (this.initiatives != null) {
             def init = slurper.parseText(this.initiatives)
            
             init.each{
                 item ->  if (item == workItemType) mappedToInitiative = true
             }
         }
         return mappedToInitiative;
    }
    
    //--------------------------------------------------------------
    def Initiative createOrUpdateInitiative (workItem) {
        // Get all initiatives that were previously integrated.
        def initiatives = new Initiative().getAll()
        // Get all changes that were previously integrated.
        
        def changes = new Change().getAll()

        // Find out if we have a matching initiative that was created already from this change request
        Initiative match = matchInitiativeFromExternalUrl(workItem, initiatives);

        // If the initiative does exist
        if(match != null) {
            //update the initiative
            setInitiativeData(match, workItem);
        }
        // If the initiative does not exist
        else {
            // create the initiative
            def initiative = new Initiative();
            setInitiativeData(initiative, workItem)

            initiative.save()
            match = initiative;
        }

        //Find the change for that request and remove it
        def changeMatch = matchChangeFromExternalUrl(workItem, changes);
        // We have a change that was previously created from this workItem that is now an
        // Initiative. So we should KILL IT.
        if(changeMatch != null) {
            changeMatch.delete()
        }

        return match;
    }
    
    //----------------------------------------------------------------------------------------------
    def Initiative matchInitiativeFromExternalUrl(workItem,list) {
        Initiative result = null;

        for(Initiative initiative : list) {
            if(initiative.externalUrl == workItem.target) {
                result = initiative;
                break;
            }
        }

        return result;
    }
    
    //----------------------------------------------------------------------------------------------
    def Change matchChangeFromExternalUrl(workItem,list) {
        Change result = null;

        for(Change change : list) {
            if(change.externalUrl == workItem.target) {
                result = change;
                break;
            }
        }

        return result;
    }
    
    //----------------------------------------------------------------------------------------------
    def setInitiativeData(initiative, workItem) {
        //We update Name and Description
        initiative.name(workItem.summary);
        initiative.description(workItem.summary);

        // If there is no external id, we assume we have a new init and
        // have to set it via the data in workItem.
        
        if(initiative.externalId == null) {
            initiative.externalId(workItem.id);
            initiative.externalUrl(workItem.target);
            initiative.integrationProvider(new PluginIntegrationProvider().id(integrationProviderId));
        }
    }

    //--------------------------------------------------------------
    def getMappingRelease (timeline) {
        def mappedRelease = null;
        if (mappingRelease != null) {
            def fullMapping = slurper.parseText(mappingRelease)
            fullMapping.each { 
                def timelineId = it.timelines 
                def releaseId = it.releases
                if (timeline == timelineId) {
                    mappedRelease = releaseId
                }
           }
        }
        else {
            log ("No mapping for releases set")
        }
        return mappedRelease;
    }
    
    //--------------------------------------------------------------
    def getMappingApplication (category) {
        def mappedApplication = null;
        if (mappingApplication != null) {
            def fullMapping = slurper.parseText(mappingApplication)
            fullMapping.each { 
                def categoryId = it.categories 
                def applicationId = it.applications
                if (categoryId == category) {
                    mappedApplication = applicationId
                }
           }
        }
        else {
            log ("No mapping for applications set")
        }
        return mappedApplication;
    }
    
    //--------------------------------------------------------------
    def getMappingStatus (status) {
        def mappedStatus = null;
        if (mappingStatuses != null) {
            def fullMapping = slurper.parseText(mappingStatuses)
            fullMapping.each { 
                def ucrStatus = it.ucrStatuses 
                def rtcStatus = it.rtcStatuses
               if (rtcStatus == status) {
                   for (Change.Status statusType : Change.Status.values()) {
                       if(statusType.toString() == ucrStatus) {
                           mappedStatus = statusType
                       }
                   }
                }
           }
        }
        else {
            log ("No mapping for statuses set")
        }
        return mappedStatus;
    }
    
    //--------------------------------------------------------------
    def getMappingSeverities (severity) {
        def mappedSeverity = null;
        if (mappingSeverities != null) {
            def fullMapping = slurper.parseText(mappingSeverities)
            fullMapping.each { 
                def rtcSeverity = it.rtcSeverities 
                def ucrSeverity = it.ucrSeverities
                                
                if (severity == rtcSeverity) {
                    mappedSeverity = ucrSeverity
                }
           }
        }
        else {
            log ("No mapping for severities set")
        }
        return mappedSeverity;
    }
        
    //----------------------------------------------------------------------------------------------
    def getMappingType(type) {

        def mappedType = null;
        if (mappingTypes != null) {
            def fullMapping = slurper.parseText(mappingTypes)
            fullMapping.each { 
                def rtcType = it.rtcTypes 
                def ucrType = it.ucrTypes
                
                if (rtcType == type) {
                    mappedType = ucrType
                }
           }
        }
        else {
            log ("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 cleanupChangeName (summary) {
        summary = truncateTitle(summary);
        summary = removeHtmlTags(summary);
        return summary;
    }
    
    //--------------------------------------------------------------
    //Some title go over our limit 255 chars
    def truncateTitle(title){
        if (title != null) {
            if (title == "") {
                title = EMPTY_TITLE;
            }
            if (title.length()>255) {
                title = title.substring(0, 252)+"...";
            }
        }
        else {
            title = EMPTY_TITLE;
        }
        return title;
    }
    
    //--------------------------------------------------------------
    //Cleanup HTML tags from the summary
    def removeHtmlTags(summary){
        return summary.replaceAll("\\<[^>]*>","");
    }
    
    //--------------------------------------------------------------
    def Change doesChangeAlreadyExists (externalId, allChanges) {
        Change existingChange = null;
        
        def i = 0;
        for (i = 0; i <= allChanges.size() -1; i++) {
            if (allChanges[i].externalId == externalId) {
                existingChange = allChanges[i]
                break;
            }
        }
        return existingChange;
    }
    
    //--------------------------------------------------------------
    def log (logText) {
        println (logText)
    }
}
