#!/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.rtc.OslcHelper;
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 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 serverUrl;
    def oslcUrl;
    def oslcKey;
    def oslcSecret;
    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.oslcUrl = props['oslcUrl']
        this.oslcKey = props['oslcKey']
        this.oslcSecret = props['oslcSecret']
        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 OslcHelper(oslcUrl, oslcKey, oslcSecret, 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
        init()
        //We create or update each of them
        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 = [:]
        
        //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().getAll()
        log ("Existing Changes for that Integration: "+allChanges.size())
        
        //We retrieve all initiatives for that provider from UCRelease
        def allInitiatives = new Initiative().getAll()
        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;
         }
        
        //Initiatives
        //We create a map that contains info WorkItem / Parents
        def workItemToParentMap = rtcHelper.getWorkItemToParentMap(query);
        log("--------------------------------------------------------------")
        //List of changes retrieved from RTC by executing a specific query
        def importedChangeList = rtcHelper.executeUpdateForQuery(query, lastExecutionDate, urlsExistingItem);
        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.about.toString()] =  initiative;
                
                // build out hash of WIids that are Initiatives
                initiativeWorkItems.add(item.about.toString());
            }
        }
        
        importedChangeList.each{
            item -> 
            def existingChange = doesChangeAlreadyExists (item.identifier.toString(), 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.identifier.toString())
                    }
                }
                
                workItemToChangeObject[item.about.toString()] = createdOrUpdatedChange;
                
                // climb the heritage map

                def closestInitWorkItemId = rtcHelper.findClosestInitiativeParent(item, workItemToParentMap, initiativeWorkItems);
               
                // set entry in map
                if(closestInitWorkItemId != "" && closestInitWorkItemId != null) {
                    def parent = workItemToInitiativeObject[closestInitWorkItemId];
                    changeToInitiative[createdOrUpdatedChange] = parent;
                    println ("Item: "+item.title+" is a child of "+parent.getName()+".")
                }
            }
        }
        
        log("--------------------------------------------------------------")
        //Some changes will be created
        if (bulkCreateChangeList.size() > 0) {
            def changeToCreateArray = bulkCreateChangeList as Change[]
            log ("Number of Changes that will be created: "+bulkCreateChangeList.size())
            new Change().post(changeToCreateArray)
        }
        
        //Some changes will be updated
        if (bulkUpdateChangeList.size() > 0) {
            log ("Number of Changes that will be updated: "+bulkUpdateChangeList.size())
            def changeToUpdateArray = bulkUpdateChangeList as Change[]
            new Change().put(changeToUpdateArray)
        }
         
        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")
        }
        
        def currentTime = new Date()
		println ("Last execution set to: "+currentTime)
        provider.property("lastExecution", currentTime.time.toString()).save()
    }
    
    //--------------------------------------------------------------
    def Change setChangeData (rtcChange, existingChange, workItemToParentMap, initiativeWorkItems) {
        def ucrChange = null;
        
        if (existingChange != null) {
             ucrChange = existingChange
        }
        else {
            ucrChange = new Change()
        }

        ucrChange.name(cleanupChangeName(rtcChange.title+""))
        ucrChange.externalUrl(rtcChange.about.toString())
        ucrChange.externalId(rtcChange.identifier.toString())
        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 = rtcHelper.findClosestInitiativeParent(rtcChange, workItemToParentMap, initiativeWorkItems);
             if (closestInitWorkItemId == "") {
                 println (rtcChange.title+" is not a child of "+init.getName()+" anymore.")
                 ucrChange.initiative(null)
             }
        }
        //------------------------MAPPING RELEASE <=> TIMELINE-------------------------------------
        //We map the UCR Releases to RTC timelines
        def uriTimeline = rtcHelper.getTimeLine(rtcChange)
        // We update the Release mapped to the "Planned for" value
        def timeLineId = rtcHelper.getUUIDFromURI(uriTimeline);
        // 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 uriCategory = rtcHelper.getCategory(rtcChange)
        def categoryId = rtcHelper.getUUIDFromURI(uriCategory);
        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 = rtcHelper.getSeverity(rtcChange)
        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 = rtcHelper.getTypeFromWorkItem(rtcChange)
        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 = rtcHelper.getTypeFromWorkItem(rtcChange)
         //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);
            //We need to save the initiative to make sure
            //modifications on it would be reflected in UCR
            match.save()
        }
        // 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.about.toString()) {
                result = initiative;
                break;
            }
        }

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

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

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

        // 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.identifier);
            initiative.externalUrl(workItem.about.toString());
            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
                def timelineShortName = timelineId.substring(timelineId.lastIndexOf("/")+1, timelineId.length())
                if (timeline == timelineShortName) {
                    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
                
                def severityLongName = severity.toString()
                def shortName = severityLongName.substring(severityLongName.lastIndexOf("/")+1, severityLongName.length())
                
                if (shortName == rtcSeverity) {
                    mappedSeverity = ucrSeverity
                }
           }
        }
        else {
            log ("No mapping for severities set")
        }
        return mappedSeverity;
    }
        
    //----------------------------------------------------------------------------------------------
    def getMappingType(oslcType) {

        def mappedType = null;
        if (mappingTypes != null) {
            def fullMapping = slurper.parseText(mappingTypes)
            fullMapping.each { 
                def rtcType = it.rtcTypes 
                def ucrType = it.ucrTypes
                
                if (rtcType == oslcType) {
                    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;
    }
    
    //--------------------------------------------------------------
    //Cleanup HTML tags from the summary
    def removeHtmlTags(summary){
        return summary.replaceAll("\\<[^>]*>","");
    }
    
    //--------------------------------------------------------------
    //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)
    }
}
