/*
* Licensed Materials - Property of IBM Corp.
* IBM UrbanCode Build
* (c) Copyright IBM Corporation 2012, 2014. All Rights Reserved.
*
* U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
* GSA ADP Schedule Contract with IBM Corp.
*/
package com.urbancode.air.plugin.scm

import groovy.json.JsonSlurper
import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity

import com.urbancode.air.plugin.scm.changelog.*
import com.urbancode.commons.httpcomponentsutil.HttpClientBuilder
import com.urbancode.commons.util.IO

public class SCMChangelog extends SCMStep {
    
    def srcName
    def rtcSnapshot
    def rtcBaseline
    def startSnapshot
    def isStartBaseline
    def endSnapshot
    def isEndBaseline
    String startBuildLifeId
    String changesUrl
    
    final static def CHANGELOG_DATE_FORMAT = 'yyyy-MM-dd HH:mm:ss.S Z'
    
    final ChangeSetFilter changeSetFilter = new ChangeSetFilter()
    protected Map<String, Map<String, ChangeSet>> componentToChangeSetsMap = new LinkedHashMap<String, Map<String, ChangeSet>>()
    
    public def execute() {
        // If running tests, we don't want to look up build lives to get snapshot names
        if (changesUrl.startsWith("file://")) {
            startSnapshot = "prev-snapshot"
            endSnapshot = "build-snapshot"
        }
        else {
            setUpSnapshots()
        }
        
        if (startSnapshot && endSnapshot) {
            def componentSet
            if (!components && (isStartBaseline || isEndBaseline)) {
                componentSet = getWorkspaceInfo().componentList
            }
            else if (components) {
                componentSet = components.split(',')
            }
            def changelogs = getChangelog(startSnapshot, isStartBaseline ? 'baseline' : 'snapshot',
                    endSnapshot, isEndBaseline ? 'baseline' : 'snapshot', componentSet)
            parseChangelog(changelogs)
            setAddedDateForChangeSets(workspace)
            
            // Construct xml message and send to server
            if (!componentToChangeSetsMap.isEmpty()) {
                ChangeSetXMLHelper xmlHelper = new ChangeSetXMLHelper()
                xmlHelper.repoType = REPO_TYPE
                String xml = xmlHelper.toXML(componentToChangeSetsMap)
                sendPostRequest(xml)
            }
            else {
                println "No changes detected"
            }
        
        }
        else {
            println "Start Buildlife ID: ${startBuildLifeId}"
            println "Start Snapshot: ${startSnapshot} ${isStartBaseline?'is baseline':''}"
            println "End Snapshot: ${endSnapshot} ${isEndBaseline?'is baseline':''}"
            println "Cannot get a changelog without start and end snapshots!"
        }
    }
    
    private void setUpSnapshots() {
        def propertiesXml
        if (startBuildLifeId) {
            propertiesXml = getPropertiesXml(startBuildLifeId)
            parseProperties(propertiesXml)
            startSnapshot = rtcSnapshot
            isStartBaseline = rtcBaseline
        }
        
        String endBuildLifeId = System.getenv("BUILD_LIFE_ID")
        propertiesXml = getPropertiesXml(endBuildLifeId)
        parseProperties(propertiesXml)
        endSnapshot = rtcSnapshot
        isEndBaseline = rtcBaseline
    }
    
    private String getPropertiesXml(String buildLifeId) {
        def authToken = System.getenv("AUTH_TOKEN")
        
        // construct the URL with property replacements
        String baseUrl = System.getenv("WEB_URL")
        
        baseUrl += baseUrl.endsWith("/") ? "" : "/"
        String url = baseUrl + "rest/buildlife/${buildLifeId}/properties"
        
        println "Sending request to $url"

        HttpGet getMethod = new HttpGet(url)
        if (authToken) {
            getMethod.addHeader("Authorization-Token", authToken)
        }

        HttpClientBuilder builder = new HttpClientBuilder()
        builder.setTrustAllCerts(true)
        HttpClient client = builder.buildClient()

        HttpResponse response = client.execute(getMethod)
        def responseCode = response.statusLine.statusCode
        InputStream responseStream = response.entity.content
        
        String propertiesXml = IO.readText(responseStream)
        
        if (!isGoodResponseCode(responseCode)) {
            throw new Exception("Failed to get build life properties from the server: $propertiesXml")
        }
        
        getMethod.releaseConnection()
        
        return propertiesXml
    }
    
    private void parseProperties(String propertiesXml) {
        new XmlSlurper().parseText(propertiesXml)."property".each { property ->
            String propName = property.@name
            if (propName == "rtc.snapshot.${srcName}") {
                rtcSnapshot = property.text() ?: ""
            }
            else if (propName == "rtc.baseline.${srcName}") {
                rtcBaseline = true
            }
        }
    }

    public def getChangelog(start, startType, end, endType, componentList) {
        def changelogs = []
        if (start && end) {
            JsonSlurper slurper = new JsonSlurper()
            def getChangelogCommand
            if (componentList && componentList.size() > 0) {
                if (startType == 'snapshot') {
                    startType = 'baseline'
                }
                if (endType == 'snapshot') {
                    endType = 'baseline'
                }
                componentList.each { component ->
                    getChangelogCommand = [command, '--non-interactive', 'compare',
                                           startType, start,
                                           endType, end,
                                           '-j',
                                           '-I', 'wsf',
                                           '-f', 'i',
                                           '-p', 'd',
                                           '-D', CHANGELOG_DATE_FORMAT,
                                           '-c', component,
                                           '-u', username,
                                           '-P', password,
                                           '-r', serverUrl]
                    runCommand('Get Changelog', getChangelogCommand) {
                        println it
                        changelogs << slurper.parseText(it)
                    }
                }
            }
            else {
                getChangelogCommand = [command, '--non-interactive', 'compare',
                                       startType, start,
                                       endType, end,
                                       '-j',
                                       '-I', 'wsf',
                                       '-f', 'i',
                                       '-p', 'd',
                                       '-D', CHANGELOG_DATE_FORMAT,
                                       '-u', username,
                                       '-P', password,
                                       '-r', serverUrl]
                runCommand('Get Changelog', getChangelogCommand) {
                    println it
                    changelogs << slurper.parseText(it)
                }
            }
        }
        
        return changelogs
    }

    /**
    {
        "direction": [
            {
                "components": [
                    {
                        "added": false,
                        "changesets": [
                            {
                                "author": {
                                "mail": "crr@us.ibm.com",
                                "userId": "crr",
                                "userName": "Chris Rees",
                                "uuid": "_02Cm4FvBEeSRKvT89YyRrg"
                            },
                                "comment": "Removed file",
                                "creationDate": "2015-06-02 09:34:41.460 -0400",
                                "item-type": "changeset",
                                "url": "https://hub.jazz.net:443/ccm06/",
                                "uuid": "_GUuwsAksEeW2sNZCj8hnqA",
                                "versionables": [
                                    {
                                        "item-type": "versionable",
                                        "path": "/File",
                                        "url": "https://hub.jazz.net:443/ccm06/",
                                        "uuid": "_eAlXcAkrEeWb6O_z9gvtVw"
                                    }
                                ]
                            }
                        ],
                        "item-type": "component",
                        "name": "crr | rtc-testing Default Component",
                        "removed": false,
                        "url": "https://hub.jazz.net:443/ccm06/",
                        "uuid": "_p-bJYHmXEeSi-OIwg-tlIA"
                    }
                ],
                "incoming-changes": true,
                "outgoing-changes": false
            }
        ]
    }
    **/
    protected void parseChangelog(changelogs) {
        changelogs.each { changelog ->
            changelog.direction.each { direction ->
                direction.components.each { component ->
                    Map<String, ChangeSet> idToChangeSetMap = new LinkedHashMap<String, ChangeSet>()
                    def componentName = component.name
                    component.changesets.each { changeSet ->
                        def id = changeSet.uuid
                        def user = changeSet.author.userId
                        def comment = changeSet.comment
                        def date = changeSet.creationDate

                        ChangeSet newChangeSet = new ChangeSet(user, comment, date)
                        def workItems = []
                        changeSet.workitems.each { workItem ->
                            workItems << workItem.'workitem-number'
                        }
                        if (!workItems.isEmpty()) {
                            newChangeSet.properties['workItems'] = workItems.join(',')
                        }

                        changeSet.versionables.each { file ->
                            ChangeSetFileEntry entry = new ChangeSetFileEntry()
                            entry.path = escapeFilePaths(file.path)
                            newChangeSet.fileSet << entry
                        }

                        if (addChangeSet(newChangeSet)) {
                            idToChangeSetMap.put(id, newChangeSet)
                        }
                    }

                    if (!idToChangeSetMap.isEmpty()) {
                        componentToChangeSetsMap.put(componentName, idToChangeSetMap)
                    }
                }
            }
        }
    }

    /**
    {
        "changes": [
            {
                "added-by": "Chris Rees",
                "author": "Chris Rees",
                "changes": [
                    {
                        "inaccessible-change": false,
                        "path": "/<unresolved>/File",
                        "state": {
                            "add": false,
                            "conflict": false,
                            "content_change": false,
                            "delete": true,
                            "move": false,
                            "potential_conflict": false,
                            "property_change": false
                        },
                        "state-id": null,
                        "uuid": "_eAlXcAkrEeWb6O_z9gvtVw"
                    }
            ],
                "comment": "Removed file",
                "date-added": "02-Jun-2015 01:55 PM",
                "modified": "02-Jun-2015 09:34 AM",
                "state": {
                    "active": false,
                    "complete": true,
                    "conflict": false,
                    "current": false,
                    "current_merge_target": false,
                    "has_source": false,
                    "is_linked": false,
                    "is_source": false,
                    "potential_conflict": false
                },
                "url": "https://hub.jazz.net:443/ccm06/",
                "uuid": "_GUuwsAksEeW2sNZCj8hnqA"
            }
        ]
    }
    **/
    protected void setAddedDateForChangeSets(String workspaceOrStream) {
        componentToChangeSetsMap.each { component, changeSets ->
            def historyCommand = [command, '--non-interactive', 'show', 'history', '-j', '-m', changeSets.size(), '-w', workspaceOrStream,
                                  "-c", component, '-u', username, '-P', password, '-r', serverUrl]

            JsonSlurper slurper = new JsonSlurper()
            def componentHistory
            runCommand("Getting history for component '${component}'", historyCommand) {
                println it
                componentHistory = slurper.parseText(it)
            }

            componentHistory.changes.each { change ->
                def changeId = change.uuid
                Map<String, ChangeSet> componentChangeSets = componentToChangeSetsMap.get(component)
                ChangeSet changeSetToUpdate = componentChangeSets.get(changeId)
                if (changeSetToUpdate && change.'date-added') {
                    changeSetToUpdate.date = change.'date-added'
                }
            }
        }
    }

    protected boolean addChangeSet(ChangeSet changeSet) {
        boolean allowAuthor = changeSet.hasAllowedAuthor(changeSetFilter);
        boolean allowAnyPaths = changeSet.hasAllowedPath(changeSetFilter);
        if (!allowAuthor || !allowAnyPaths) {
            def message = new StringBuilder("Changeset ${changeSet.id} skipped because ")
            if (!allowAuthor) {
                message << "it has excluded author ${changeSet.user}"
            }
            if (!allowAuthor && !allowAnyPaths) {
                message << " and "
            }
            if (!allowAnyPaths) {
                message << "it contains only excluded file paths (${changeSet.fileSet.collect{it.path}})"
            }
            println message
        }
        
        return allowAuthor && allowAnyPaths
    }
    
    private void sendPostRequest(String xml) {
        // construct the URL with property replacements
        String url = changesUrl
        def authToken = System.getenv("AUTH_TOKEN")
        
        println "Sending request to $url"

        // Debug/testing stub
       if (url.startsWith("file://")) {
           File xmlOut = new File(directory, "xmlOut.xml")
           xmlOut << xml
       }
       else {
           HttpPost postMethod = new HttpPost(url)
           if (authToken) {
               postMethod.addHeader("Authorization-Token", authToken)
               postMethod.addHeader("Content-Type", "application/xml")
           }

           int changeSets = 0
           componentToChangeSetsMap.each { component, changeSetMap ->
               changeSets += changeSetMap.size()
           }
           println "Sending ${changeSets} changes"
           postMethod.setEntity(new StringEntity(xml));
           
           HttpClientBuilder builder = new HttpClientBuilder()
           builder.setTrustAllCerts(true)
           HttpClient client = builder.buildClient()
    
           HttpResponse response = client.execute(postMethod)
           def responseCode = response.statusLine.statusCode
           InputStream responseStream = response.entity.content
           if (isGoodResponseCode(responseCode)) {
               IO.copy(responseStream, System.out)
               println ""
           }
           else {
               IO.copy(responseStream, System.err)
               throw new RuntimeException("Failed to upload source changes. StatusCode: ${responseCode}")
           }
       }
    }
    
    private boolean isGoodResponseCode(int responseCode) {
        return responseCode >= 200 && responseCode < 300;
    }

    protected String escapeFilePaths(String path) {
        return path.replace('\\/', '/')
    }
}
