package com.urbancode.air.plugin.scm

import com.urbancode.air.*
import com.urbancode.air.plugin.scm.changelog.*

import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Pattern
import java.text.SimpleDateFormat

import org.apache.commons.httpclient.HttpClient
import org.apache.commons.httpclient.UsernamePasswordCredentials
import org.apache.commons.httpclient.auth.AuthScope
import org.apache.commons.httpclient.methods.StringRequestEntity
import org.apache.commons.httpclient.methods.PostMethod
import org.apache.commons.httpclient.params.HttpClientParams
import org.apache.commons.httpclient.protocol.Protocol
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory

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


public class SCMChangelog extends SCMStep {

    //**************************************************************************
    // CLASS
    //**************************************************************************

    //**************************************************************************
    // INSTANCE
    //**************************************************************************

    // changelog boundaries
    Date startDate = null
    Date endDate = null
    String startRevision = null
    String endRevision = null
    String changesUrl = null
    String depotName = null
    boolean traverseParentStreams = false

    // filters for changeset
    final ChangeSetFilter changeSetFilter = new ChangeSetFilter()
    List<ChangeSet> changeSets

    /**
     *
     * @return xml document of changes
     */
    public def execute() {
        boolean error = false
        
        this.login()
        
        try {
            // Get Change Log
            def logOutput = runLogCommand()
    
            // Construct xml message and send to server
            if (changeSets) {
                ChangeSetXMLHelper xmlHelper = new ChangeSetXMLHelper()
                xmlHelper.repoType = REPO_TYPE
                String xml = xmlHelper.toXML(changeSets)
                sendPostRequest(xml)
            }
            else {
                println "No changes detected"
            }
        }
        catch (ExitCodeException e) {
            println e.getMessage()
            error = true
        }
        finally {
            this.logout()
            
            if (error) {
                System.exit(1)
            }
        }
    }

    /**
     * Parse an input of accurev xml hist command into a list of AHP ChangeSets
     * @param logOutput output from a `accurev hist -fx` command
     */
    public void parseLogIntoChangeSets(String logOutput) {

        if (!changeSets) {
            changeSets = []
        }
        
        groovy.util.slurpersupport.GPathResult log = new XmlSlurper().parseText(logOutput);

        log.transaction.each{ trans ->
            ChangeSet changeSet = new ChangeSet();
            changeSet.id = trans.@id.text();
            changeSet.user = trans.@user.text();
            changeSet.date = new Date((trans.@time.text() as Long) * 1000);
            changeSet.message = trans.comment.text();

            def changeSetType = trans.@type.text()
            def workItems = new LinkedHashSet();

            trans.version.each{ ver ->
                def path = ver.@path.text() ;

                // Accurev seems to prefix all of their file paths with
                // /./ so I am removing it here for consistency with the rest
                // of our file paths.
                path = path.replace('^/\\./', '');

                ChangeSetFileEntry entry = new ChangeSetFileEntry();
                entry.revision = changeSet.id;
                entry.type = changeSetType;
                entry.path = path;
                changeSet.fileSet << entry;

                // find workItem meta-data elements
                ver.issueNum.each{ workItems << it.text() }
            }

            if (workItems) {
                changeSet.properties['workItems'] = workItems.join(',');
            }
            if (depotName) {
                changeSet.properties['depotName'] = depotName
            }

            boolean allowAuthor = changeSet.hasAllowedAuthor(changeSetFilter);
            boolean allowAnyPaths = changeSet.hasAllowedPath(changeSetFilter);
            if (allowAuthor && allowAnyPaths) {
                changeSets << changeSet
            }
            else {
                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
            }
        }
    }

    protected String calculateRange() {

        // Should range use to startBuildlifeId["workspace.revision.$srcName"] ||
        //       blProps["workspace.revision.$srcName"] if present ?

        final def range;
        if (startRevision && endRevision) {
            range = "$endRevision-$startRevision"
        }
        else {
            range = scmHelper.formatDateRange(startDate, endDate)
        }
        return range;
    }

    protected String runLogCommand(def stream) {
        final def range = calculateRange();
        def logCommand = [this.scmCommand, "hist"]
        if (host) {
            logCommand << '-H' << host
        }
        
        logCommand << '-a' << '-s' << stream << '-t' << range << '-fx'
        
        def logOutput = null
        cmdHelper.runCommand("Getting Changelog for stream " + stream, logCommand) { Process proc ->
            proc.out.close()
            proc.consumeProcessErrorStream(System.out)
            logOutput = proc.text
            println logOutput
            if(!(logOutput?.trim())) {
                println("No Changes Found");
            }
        }
        return logOutput
    }
    
    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 {
           ProtocolSocketFactory socketFactory = new OpenSSLProtocolSocketFactory()
           Protocol https = new Protocol("https", socketFactory, 443)
           Protocol.registerProtocol("https", https)
    
           PostMethod postMethod = new PostMethod(url)
           if (authToken) {
               postMethod.setRequestHeader("Authorization-Token", authToken)
               postMethod.setRequestHeader("Content-Type", "application/xml")
           }
           
           println "Sending ${changeSets.size()} changes"
           postMethod.setRequestEntity(new StringRequestEntity(xml));
    
           HttpClient client = new HttpClient()
    
           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("Failed to upload source changes. StatusCode: ${responseCode}")
           }
       }
    }
    
    private boolean isGoodResponseCode(int responseCode) {
        return responseCode >= 200 && responseCode < 300;
    }
    
    private def findParentStream(def stream) {
        def output
        def showCommand = [scmCommand, "show", "-fx", "-s", stream, "streams"]
        
        cmdHelper.runCommand("Getting stream information for stream " + stream, showCommand) {
            Process proc ->
            proc.out.close()
            proc.consumeProcessErrorStream(System.out)
            output = proc.text
        }
        
        def parentStreamName = output.find(~/basis=\"(.*)\"/){match, parent -> return parent.trim()}

        if (parentStreamName) {
            println "Stream $stream has backing stream $parentStreamName\n"
        }
        else {
            println "Stream $stream did not have a backing stream\n"
        }
        
        return parentStreamName
    }
    
    private void runLogCommand() {
        def stream = streamName
        def logOutput
        
        while (stream) {
            logOutput = runLogCommand(stream)
            
            // the result change set data
            parseLogIntoChangeSets(logOutput);
            
            stream = traverseParentStreams ? findParentStream(stream) : null
        }
    }
}