#!/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 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();

def totalStart = System.currentTimeMillis()
integration.runIntegration ()
def totalEnd = System.currentTimeMillis()

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;
    
    
    //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']
        this.mappingStatuses = props['mappingStatuses']
        this.mappingRelease = props['mappingRelease']
        this.mappingApplication = props['mappingApplication']
        this.mappingSeverities = props['mappingSeverities']
        this.mappingTypes = props['mappingTypes']
        this.initiatives = props['initiatives']
        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 () {
        //We load the integration provider and the rtcHelper library
        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 initiativeWorkItems = [] as List
            def workItemToInitiativeObject = [:]
            def workItemToChangeObject = [:]
            def changeToInitiative = [:]
            
            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
            }
            
            log("--------------------------------------------------------------")
            //We retrieve all changes for that provider from UCRelease
            def allChanges = new Change().getAllForIntegrationProvider(integrationProviderId)
            log ("Existing Changes for that Integration: "+allChanges.size())
            
            //We retrieve all initiatives for that provider from UCRelease
            def allInitiatives = new Initiative().getAllForIntegrationProvider (integrationProviderId)
            log ("Existing Initiatives for that Integration: "+allInitiatives.size())
            
            allChanges.each{
                item -> if (item.externalUrl != null) urlsExistingItem.add(item.externalUrl)
            }
            
            allInitiatives.each{
                item -> 
                    if (item.externalUrl != null) urlsExistingItem.add(item.externalUrl)
                    initiativeWorkItems.add(item.externalUrl);
                    workItemToInitiativeObject[item.externalUrl] =  item;
             }
            long endData = System.currentTimeMillis()
            log ("Retrieve data from Release in "+((endData-startData)/1000)+"s")
            //Initiatives
            //We create a map that contains info WorkItem / Parents
            def workItemToParentMap = [:]//rtcHelper.getWorkItemToParentMap(rtcProjectArea,query);
            log("--------------------------------------------------------------")
            println "LAST EXECUTIONDATE=>"+lastExecutionDate
            long start = System.currentTimeMillis()
            //List of changes retrieved from RTC by executing a specific query
            def importedChangeList = rtcHelper.getWorkAllItemsFromQuery(rtcProjectArea,query,urlsExistingItem,lastExecutionDate);
            long end = System.currentTimeMillis()
            log (importedChangeList.size()+" WorkItems imported in "+((end-start)/1000)+"s")
            log ("Number of WorkItems to import from RTC: "+importedChangeList.size())
            
            //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;
                    
                    // build out hash of WIids that are Initiatives
                    initiativeWorkItems.add(item.target);
                }
            }
            
            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, null)
                        bulkCreateChangeList.add(createdOrUpdatedChange)
                        countCreatedItem++
                    }
                    else {
                        createdOrUpdatedChange = setChangeData (item, existingChange, workItemToParentMap, initiativeWorkItems)
                        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)
                        }
                    }
                    
                    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
                            }
                        }
                    }
                }
            }
            
            log("--------------------------------------------------------------")
            //Some changes will be created
            if (bulkCreateChangeList.size() > 0) {
                def changeToCreateArray = bulkCreateChangeList as Change[]
                log ("Number of Changes created: "+bulkCreateChangeList.size())
                new Change().post(changeToCreateArray)
            }
            
            //Some changes will be updated
            if (bulkUpdateChangeList.size() > 0) {
                log ("Number of Changes updated: "+bulkUpdateChangeList.size())
                def changeToUpdateArray = bulkUpdateChangeList as Change[]
                new Change().put(changeToUpdateArray)
            }
             
            log ("Number of Changes turned to Initiatives: "+changeToInitiative.size())
            saveChangeInitAssociations(changeToInitiative)
            
            if (countWarning > 0) {
                log (countWarning+" items are already managed by an other Integration Provider")
            }
            
            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()
        }
    }
    
    //--------------------------------------------------------------
    def Change setChangeData (rtcChange, existingChange, workItemToParentMap, initiativeWorkItems) {
        def ucrChange = null;
        
        if (existingChange != null) {
             ucrChange = existingChange
        }
        else {
            ucrChange = new Change()
        }

        ucrChange.name(truncateTitle(rtcChange.summary))
        ucrChange.externalUrl(rtcChange.target)
        ucrChange.externalId(rtcChange.id)
        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 == "") {
                 println (rtcChange.summary+" is not a child of "+init.getName()+" anymore.")
                 ucrChange.initiative(null)
             }
        }
        //------------------------MAPPING RELEASE <=> TIMELINE-------------------------------------
        //We map the UCR Releases to RTC timelines
        //We update the Release mapped to the "Planned for" value
        def timeLineId = rtcChange.iterationId
        // Unassigned is null for RTC
        if(timeLineId == null) timeLineId = "Unassigned";
   
        def releaseId = getMappingRelease(timeLineId);
        if (releaseId != null) {
            def release = new Release().id(releaseId)
            if (release != null) {
                ucrChange.release(release);
            }
        }
        else {
            ucrChange.release(null);
        }

        //------------------------MAPPING APPLICATION <=> WI CATEGORY------------------------------
        //We map UCR Applications to RTC WorkItem categories
        def categoryId = rtcChange.categoryId;
        if(categoryId == null) categoryId = "Unassigned";
        def applicationId = getMappingApplication(categoryId);
        if (applicationId != null) {
            def application = new Application().id(applicationId)
            if (application != null) {
                ucrChange.application(application);
            }
        }
        else {
            ucrChange.application(null);
        }
        
        //------------------------MAPPING SEVERITIES-----------------------------------------------
        def rtcSeverity = rtcChange.severity
        def ucrSeverity = getMappingSeverities (rtcSeverity)
        
        if (ucrSeverity) {
            def ucrSeveritiesEnum = Change.Severity.values()
            //ucrChange.severity(ucrSeveritiesEnum.valueOf(ucrSeverity.toString()))
            
            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;
    }
    

    //----------------------------------------------------------------------------------------------
    def saveChangeInitAssociations(parentOfChanges) {
        parentOfChanges.each() { 
        item, value -> 
        item.initiative(value).save() }
    }
    
    //--------------------------------------------------------------
    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().getAllForIntegrationProvider (integrationProviderId)
        // Get all changes that were previously integrated.
        
        def changes = new Change().getAllForIntegrationProvider(integrationProviderId)

        // 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;
    }
    
    //--------------------------------------------------------------
    //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;
    }
    
    //--------------------------------------------------------------
    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)
    }
}
