package com.urbancode.air.plugin.automation;

import com.urbancode.air.*
import org.apache.commons.httpclient.*
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.protocol.*
import org.jsoup.Jsoup

import com.urbancode.commons.util.IO
import com.urbancode.commons.util.https.OpenSSLProtocolSocketFactory

import java.util.regex.Pattern

public class PublishDefectReport extends AutomationBase {
    
    //**************************************************************************
    // CLASS
    //**************************************************************************
    
    //**************************************************************************
    // INSTANCE
    //**************************************************************************
    
    String defectPattern
    String taskPattern
    String usPattern
    
    def workspaceRef
    def defectIdSet = [] as Set
    def defectIdToChangeSetListMap = [:]
    def taskIdSet = [] as Set
    def taskIdToChangeSetListMap = [:]
    def usIdSet = [] as Set
    def usIdToChangeSetListMap = [:]
    
    def defectArtifactPattern
    def taskArtifactPattern
    def usArtifactPattern
    
    String issueUrlBase
    
    public void execute() {
        setupHttpClient()
        
        if (defectPattern) {
            defectArtifactPattern = java.util.regex.Pattern.compile(defectPattern)
        }
        
        if (taskPattern) {
            taskArtifactPattern = java.util.regex.Pattern.compile(taskPattern)
        }
        
        if (usPattern) {
            usArtifactPattern = java.util.regex.Pattern.compile(usPattern)
        }
        
        println "Getting Workspace"
        workspaceRef = configureWorkspace()
        println "Workspace Ref = ${workspaceRef}"
        
        try {
            String changesXml = getChangesets()
            
            parseChangesets(changesXml)
            
            String issuesXml = createXml()
            
            if (issuesXml) {
                sendPost(issuesXml)
            }
        }
        catch (Exception e) {
            e.printStackTrace()
            System.exit(1)
        }
    }
    
    private String getChangesets() {
        def authToken = System.getenv("AUTH_TOKEN")
        int buildLifeId = Integer.parseInt(System.getenv("BUILD_LIFE_ID"))
        
        String webUrl = System.getenv("WEB_URL")
        webUrl += webUrl.endsWith("/") ? "" : "/"
        String changesRequestUrl = webUrl + "rest/buildlife/${buildLifeId}/sourcechanges"
        
        println "\nGetting source changes from server"
        ProtocolSocketFactory socketFactory = new OpenSSLProtocolSocketFactory()
        Protocol https = new Protocol("https", socketFactory, 443)
        Protocol.registerProtocol("https", https)
        
        GetMethod getMethod = new GetMethod(changesRequestUrl)
        if (authToken) {
            getMethod.setRequestHeader("Authorization-Token", authToken)
        }
        
        String changesXml
        
        try {
            def responseCode = client.executeMethod(getMethod)
            InputStream responseStream = getMethod.getResponseBodyAsStream()
            
            changesXml = IO.readText(responseStream)
            if (!isGoodResponseCode(responseCode)) {
                throw new Exception("Failed to get build life source changes from the server: $changesXml")
            }
        }
        finally {
            getMethod.releaseConnection()
        }
        
        return changesXml
    }
    
    private void parseChangesets(String changesXml) {
        if (!changesXml.contains("change-set")) {
            println "No changes detected. Nothing to publish."
            System.exit(0)
        }
        
        println "Parsing Changelog"
        
        new XmlSlurper().parseText(changesXml)."change-set".each { changeSetElem ->
            def defectMatcher
            def taskMatcher
            def usMatcher
            
            def changeComment = changeSetElem.'comment'.text()
            if (defectArtifactPattern) {
                defectMatcher = defectArtifactPattern.matcher(changeComment);
            }
            
            if (taskArtifactPattern) {
                taskMatcher = taskArtifactPattern.matcher(changeComment);
            }
            
            if (usArtifactPattern) {
                usMatcher = usArtifactPattern.matcher(changeComment);
            }
            
            while (defectMatcher?.find()) {
                def defectId
                if (defectMatcher.groupCount() > 0 ) {
                    // they specified a '(...)' group within the pattern, use that as the bug-id
                    defectId = defectMatcher.group(1)
                }
                else {
                    // use the whole matching substring as the bug-id
                    defectId = defectMatcher.group()
                }
                
                println "Found defect identifier '$defectId' in change comment: $changeComment"
                defectIdSet.add(defectId)
                def changeSetList = defectIdToChangeSetListMap[defectId]
                if (!changeSetList) {
                    changeSetList = []
                    defectIdToChangeSetListMap[defectId] = changeSetList
                }
                def changeId = changeSetElem."change-id"?.text() ? changeSetElem."change-id".text() : changeSetElem."id".text()
                changeSetList.add(changeId)
            }
            while (taskMatcher?.find()) {
                def taskId
                if (taskMatcher.groupCount() > 0 ) {
                    // they specified a '(...)' group within the pattern, use that as the bug-id
                    taskId = taskMatcher.group(1)
                }
                else {
                    // use the whole matching substring as the bug-id
                    taskId = taskMatcher.group()
                }
                
                println "Found task identifier '$taskId' in change comment: $changeComment"
                taskIdSet.add(taskId)
                def changeSetList = taskIdToChangeSetListMap[taskId]
                if (!changeSetList) {
                    changeSetList = []
                    taskIdToChangeSetListMap[taskId] = changeSetList
                }
                def changeId = changeSetElem."change-id"?.text() ? changeSetElem."change-id".text() : changeSetElem."id".text()
                changeSetList.add(changeId)
            }
            while (usMatcher?.find()) {
                def usId
                if (usMatcher.groupCount() > 0 ) {
                    // they specified a '(...)' group within the pattern, use that as the bug-id
                    usId = usMatcher.group(1)
                }
                else {
                    // use the whole matching substring as the bug-id
                    usId = usMatcher.group()
                }
                
                println "Found User Story identifier '$usId' in change comment: $changeComment"
                usIdSet.add(usId)
                def changeSetList = usIdToChangeSetListMap[usId]
                if (!changeSetList) {
                    changeSetList = []
                    usIdToChangeSetListMap[usId] = changeSetList
                }
                def changeId = changeSetElem."change-id"?.text() ? changeSetElem."change-id".text() : changeSetElem."id".text()
                changeSetList.add(changeId)
            }
        }
    }
    
    private String createXml() {
        if (usIdSet.size() == 0 && taskIdSet.size() == 0 && defectIdSet.size() == 0) {
            println "No Rally artifact references found in build life changes."
        }
        else {
            println "Creating Issues.xml"
            def issuesXml = new java.io.StringWriter()
            def builder = new groovy.xml.MarkupBuilder(issuesXml)
            builder.issues() {
                for (def defectId in defectIdSet) {
                    println "Adding ${defectId} to Issues.xml"
                    def defectData = findItemByName('defect', defectId)
                    def defectResult = new XmlParser().parseText(defectData).Results.Object;
                    if (defectResult.size() > 0) {
                        println "Found Rally defect $defectId - ${defectResult.'@refObjectName'}"

                        def defectObjectId = defectResult.ObjectID.text()
                        def defectDetails = new XmlParser().parseText(getItemDetails('defect', defectObjectId))
                        
                        // create the issue element
                        defectIdToChangeSetListMap.get(defectId).each() { changeId ->
                            addIssueForUpload(builder, 'Defect', defectId, changeId, defectDetails)
                        }
                    }
                    else {
                        println "Unable to find Rally defect $defectId"
                    }
                }
                
                for (def taskId in taskIdSet) {
                    println "Adding ${taskId} to Issues.xml"
                    def taskData = findItemByName('task', taskId)
                    def taskResult = new XmlParser().parseText(taskData).Results.Object;
                    if (taskResult.size() > 0) {
                        println "Found Rally task $taskId - ${taskResult.'@refObjectName'}"
                        
                        def taskObjectId = taskResult.ObjectID.text()
                        def taskDetails = new XmlParser().parseText(getItemDetails('task', taskObjectId))

                        // create the issue elements
                        taskIdToChangeSetListMap.get(taskId).each() { changeId ->
                            addIssueForUpload(builder, 'Task', taskId, changeId, taskDetails)
                        }
                    }
                    else {
                        println "Unable to find Rally task $taskId"
                    }
                }
                
                for (def usId in usIdSet) {
                    println "Adding ${usId} to Issues.xml"
                    def usData = findItemByName('hierarchicalrequirement', usId)
                    def usResult = new XmlParser().parseText(usData).Results.Object;
                    if (usResult.size() > 0) {
                        println "Found Rally User Story $usId - ${usResult.'@refObjectName'}"
                        
                        def usObjectId = usResult.ObjectID.text()
                        def usDetails = new XmlParser().parseText(getItemDetails('hierarchicalrequirement', usObjectId))

                        // create the issue elements
                        usIdToChangeSetListMap.get(usId).each() { changeId ->
                            addIssueForUpload(builder, 'User Story', usId, changeId, usDetails)
                        }
                    }
                    else {
                        println "Unable to find Rally User Story $usId"
                    }
                }
            }
            
            return issuesXml.toString()
        }
    }
    
    private void sendPost(String issuesXml) {
        int buildLifeId = Integer.parseInt(System.getenv("BUILD_LIFE_ID"))
        def authToken = System.getenv("AUTH_TOKEN")
        String webUrl = System.getenv("WEB_URL")
        webUrl += webUrl.endsWith("/") ? "" : "/"
        
        String postUrl = webUrl + "rest/buildlife/${buildLifeId}/issues"
        
        println "Uploading Issues.xml"
        //println issuesXml
        
        ProtocolSocketFactory socketFactory = new OpenSSLProtocolSocketFactory()
        Protocol https = new Protocol("https", socketFactory, 443)
        Protocol.registerProtocol("https", https)
 
        PostMethod postMethod = new PostMethod(postUrl)
        try {
            if (authToken) {
                postMethod.setRequestHeader("Authorization-Token", authToken)
                postMethod.setRequestHeader("Content-Type", "application/xml")
            }
            
            postMethod.setRequestEntity(new StringRequestEntity(issuesXml));
    
            def responseCode = client.executeMethod(postMethod)
            InputStream responseStream = postMethod.getResponseBodyAsStream()
            if (isGoodResponseCode(responseCode)) {
                IO.copy(responseStream, System.out)
                println ""
            }
            else {
                IO.copy(responseStream, System.err)
                throw new RuntimeException("Rally results upload to server failed. StatusCode: ${responseCode}")
            }
        }
        finally {
            postMethod.releaseConnection()
        }
    }
    
    private boolean isGoodResponseCode(int responseCode) {
        return responseCode >= 200 && responseCode < 300;
    }
    
    /**
     * Generate a URL to a HTML page of the Defect, Task or User Story
     * @param result The XML content from Rally of the item
     * @param type 
     * @return
     */
    private String getUrlForItem(xmlObject) {
        issueUrlBase = issueUrlBase.endsWith("/") ? issueUrlBase : (issueUrlBase + "/")
        def workspaceUrl = xmlObject.'Workspace'.'@ref'[0]
        def workspaceId = workspaceUrl.find(~'/workspace/(.*)$') { match, id -> return id }
        def urlType = xmlObject.name().toLowerCase()
        if (urlType.equals('hierarchicalrequirement')) {
            urlType = 'userstory'
        }
        return issueUrlBase  + "#/" + workspaceId + "/detail/${urlType}/" + xmlObject.'ObjectID'.text()
    }
    
    /**
     * Get the details for a Defect, Task or User Story
     * @param type One of: defect, task or hierarchicalrequirement
     * @param name Rally name identifier like DE1, TA1, or US1
     * @return XML content returned from Rally as a String
     */
    private String findItemByName(type, name) {
        HttpMethod method = null
        try {
            method = new GetMethod(url + type + "?workspace=" + URLEncoder.encode(workspaceRef, "UTF-8") +
                "&query=" + URLEncoder.encode("(FormattedID = " + name+ ")" ,"UTF-8") +
                "&" + URLEncoder.encode("fetch", "UTF-8") + 
                "=" + URLEncoder.encode("true", "UTF-8"))
            def result = client.executeMethod(method)
            return IO.readText(method.getResponseBodyAsStream())
        }
        finally {
            if (method != null) {
                method.releaseConnection();
            }
        }
    }

    /**
     * Get the details for a Defect, Task or User Story
     * @param type One of: defect, task or hierarchicalrequirement
     * @param id Rally identifier for the item
     * @return XML content returned from Rally as a String
     */
    private String getItemDetails(type, id) {
        // https://community.rallydev.com/slm/webservice/1.34/{type}/{id}
        HttpMethod method = null
        try {
            method = new GetMethod(url + type + "/" + id)
            def result = client.executeMethod(method)
            return IO.readText(method.getResponseBodyAsStream())
        }
        finally {
            if(method != null) {
                method.releaseConnection();
            }
        }
    }
    
    private void addIssueForUpload(builder, issueType, id, changeId, xmlObject) {
        builder.'issue'(id: id, "issue-tracker": "Rally", "change-id": changeId) {
            builder.'name'(xmlObject.'@refObjectName')
            builder.'type'(issueType)
            builder.'description'(Jsoup.parse(xmlObject.'Description'.text()).text())
            def ownerName = xmlObject?.'Owner'[0]?.'@refObjectName'
            if (ownerName) {
                builder.'owner'(ownerName)
            }
            builder.'status'(xmlObject.'State' ? xmlObject.'State'.text() : xmlObject.'ScheduleState'.text())
            builder.'url'(getUrlForItem(xmlObject))
            // properties supported - owner moved to first-class field
//            builder.'properties'() {
//                builder.'property'() {
//                    builder.'name'('Owner')
//                    builder.'value'(ownerName)
//                }
//            }
        }
    }
}