package com.urbancode.air.plugin.scm

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

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

import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.text.SimpleDateFormat


public class SCMChangelog {

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

    static protected final String REPO_TYPE = 'mercurial'
    static protected final SCMHelper hgHelper = new SCMHelper();
    static protected final DateParser dateParser = new DateParser();

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

    def out = System.out;

    protected String hgCmd = "hg"
    protected File directory;
    protected String branch

    // changelog boundaries
    def startRevision;
    def endRevision;
    def startDate;
    def endDate;
    String changesUrl

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

    /**
     *
     * @return xml document of changes
     */
    public def execute() {

        def logOutput = runLogCommand()
        changeSets = parseLogIntoChangeSets(logOutput);
        
        if (changeSets) {
            ChangeSetXMLHelper xmlHelper = new ChangeSetXMLHelper()
            xmlHelper.repoType = REPO_TYPE;
            def xml = xmlHelper.toXML(changeSets);
            sendPostRequest(xml)
        }
        
    }
    
    public String runLogCommand() {
        final def cmdHelper = new CommandHelper(directory);
        
        // range should default to startBuildlifeId["workspace.revision.$srcName"] to blProps["workspace.revision.$srcName"]
        def revisionRange = "$startRevision:$endRevision"
        def dateRange = hgHelper.formatDateRange(startDate, endDate)

        def logCommand = [hgCmd, "log"]
        if (revisionRange != ':') {
            logCommand << "-r" << revisionRange
        }
        else if (dateRange) {
            logCommand << "-d" << dateRange
        }
        logCommand << "--template"
        logCommand << "id:{node}\\n" +
                      "author:{author|user}\\n" +
                      "date:{date|isodate}\\n" +
                      "description:{desc}\\n" +
                      "files:{files}\\n" +
                      "file-adds:{file_adds}\\n" +
                      "file-dels:{file_dels}\\n" +
                      "=============\\n";

        // Get HG Log
        def logOutput = null
        cmdHelper.runCommand('Getting Changelog', logCommand) { proc ->
            proc.consumeProcessErrorStream(System.out)
            proc.out.close()
            logOutput = proc.text
            println logOutput
            if(!(logOutput?.trim())) {
                println("No Changes Found");
            }
        }
        
        return logOutput
    }
    
    public List<ChangeSet> parseLogIntoChangeSets(String logOutput) {

        List<ChangeSet> changeSets = []

        // Parse HG Log into changesets
        logOutput.split('=============\n').each{ changesetEntry ->
            if(!changesetEntry || changesetEntry.trim().length() == 0) {
                //not a real commit probably the first empty part of the string
                return;
            }
            def changesetEntryLines = changesetEntry.readLines()

            def changeSet = [id:null, user:null, message:null, date:null, fileSet:[]]

            changeSet.id      = changesetEntry.find(~/(?m)^id:(.*)$/){match, id -> return id}
            changeSet.user    = changesetEntry.find(~/(?m)^author:(.*)$/){match, user -> return user}
            changeSet.message = changesetEntry.find(~/(?m)^description:(.*)$/){match, desc -> return desc}
            changeSet.date    = dateParser.parseDate(changesetEntry.find(~/(?m)^date:(.*)$/){match, date -> return date})

            def files   = changesetEntry.find(~/(?m)^files:(.*)$/){match, fileslist -> return fileslist }?.tokenize() ?: []
            def addfiles = changesetEntry.find(~/(?m)^file-adds:(.*)$/){match, fileadds -> return fileadds }?.tokenize() ?: []
            def rmfiles  = changesetEntry.find(~/(?m)^file-dels:(.*)$/){match, filedels -> return filedels }?.tokenize() ?: []
            def modFiles = files - addfiles - rmfiles;

            boolean hasAllowedAuthor = changeSetFilter.authorIsIncluded(changeSet.user)
            boolean hasAllowedPaths = false

            files.each{ filename ->
                def operation = ''
                if (addfiles.contains(filename)) {
                    operation = 'A'
                }
                else if (rmfiles.contains(filename)) {
                    operation = 'D'
                }
                else {
                    operation = 'M'
                }
                changeSet.fileSet << [type: operation, rev: changeSet.id, path: filename]
                hasAllowedPaths = hasAllowedPaths || changeSetFilter.pathIsIncluded(filename)
            }

            if (hasAllowedAuthor && hasAllowedPaths) {
                changeSets << changeSet
            }
            else {
                def message = new StringBuilder("Changeset ${changeSet.id} skipped because ")
                if (!hasAllowedAuthor) {
                    message << "it has excluded author ${changeSet.user}"
                }
                if (!hasAllowedAuthor && !hasAllowedPaths) {
                    message << " and "
                }
                if (!hasAllowedPaths) {
                    message << "it contains only excluded file paths (${changeSet.fileSet.collect{it.path}})"
                }
                println message
            }
        }
        
        return changeSets
    }
    
    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;
    }
}