#!/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 org.apache.commons.lang.StringUtils
import org.apache.log4j.Logger

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.JiraFactory
import com.urbancode.air.plugin.jira.JiraApplication
import com.urbancode.air.plugin.jira.JiraChange
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, Change> changeMap
    HashMap<String,String> customFields

    def integrationProviderId
    def provider
    def releaseToken
    def serverUrl
    def jiraUrl
    def jiraUser
    def jiraPassword
    def autoMapApplications
    def mappingApplications
    def mappingRelease
    def mappingStatuses
    def mappingTypes
    def mappingSeverities
    def selectedApplication
    def selectedRelease
    def selectedInitiative
    def createInitiatives
    def fullIntegration
    def slurper

    def allUcrChangeTypes

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

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

    //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) {
        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.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()

        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)

        // 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()

        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())

        changeMap = new HashMap<String, Change>(allChanges.size())
        for (Change change : allChanges) {
            changeMap.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)

        //Add/update changes
        // Assign Applications based on Custom 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)
            for (JiraChange change : jiraChanges) {
                Application mappedApplication = new Application().id("NONE_VALUE")
                allApplicationsNameId.each { ucrApp ->
                    if (ucrApp.getName().equals(change.getProjectName())) {
                        mappedApplication = ucrApp
                        // break
                    }
                }
                gatherChange(mappedApplication, [change])
            }
        //Only import when Jira application name matches UCR application name
        } else if (autoMapApplications) {
            log.debug("Retreiving all auto mapped Applications and Projects with identical names.")
            for (JiraApplication jiraApplication : factory.getAllApplications()) {
                Application mappedApplication = new Application().id("NONE_VALUE")
                allApplicationsNameId.each { tempApp ->
                    if (tempApp.getName().equals(jiraApplication.getName())) {
                        mappedApplication = tempApp
                    }
                }
                log.debug("Getting all Changes from JIRA APP for " + jiraApplication.getName() + " : " + jiraApplication.getId())
                jiraChanges = factory.getChangesForApplication(retrievalDate, jiraApplication.getId(), customFields, provider)
                gatherChange(mappedApplication, jiraChanges)
            }
        // Import the manually mapped Application and Projects
        } else {
            log.debug("Retreiving all manually mapped Applications and Projects.")
            for (JiraApplication jiraApplication : factory.getAllApplications()) {
                Application mappedApplication = getMappingApplication(jiraApplication.getId())
                // ONLY GET CHANGES THAT CAME AFTER THE LAST RECIEVED WORK ITEM
                log.debug("Getting all Changes from JIRA APP for " + jiraApplication.getName() + " : " + jiraApplication.getId())
                jiraChanges = factory.getChangesForApplication(retrievalDate, jiraApplication.getId(), customFields, provider)
                gatherChange(mappedApplication, jiraChanges)
            }
        }

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

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

    //----------------------------------------------------------------------------------------------
    /**
     * @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())
            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;
        if (changeMap.containsKey(jiraChange.getKey())) {

            if (createChangeIfReleaseFound(changeMap.get(jiraChange.getKey()), jiraChange, application)) {
                log.debug("Updating '${jiraChange.getKey()}' in application: '${application.getName()}'")
                updateChangeList << changeMap.get(jiraChange.getKey())
                result = ChangeUpdate.UPDATED;
            }
            else {
                log.debug("Deleting '${jiraChange.getKey()}' in application: '${application.getName()}'")
                deleteChangeList << changeMap.get(jiraChange.getKey())
                result = ChangeUpdate.DELETED;
            }

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

        }
        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 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 the last alphabetically found matching Release.
    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
                }
            }
            // Create Initiative if it does not exist
            if (createInitiatives && !mappedInitiative) {
                mappedInitiative = new Initiative()
                mappedInitiative.setExternalId(initiativeId)
                mappedInitiative.setName(initiativeName)
                mappedInitiative.integrationProvider(new PluginIntegrationProvider().id(integrationProviderId))
                mappedInitiative.save()

                // Retreive new full list of Initiatives
                allInitiativesNameId = new Initiative().getAll()
            }
        }
        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"
    }

}
