package com.urbancode.air.plugin.scm

import com.urbancode.air.*
import com.urbancode.air.plugin.scm.changelog.*
import java.util.regex.Matcher

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
    //**************************************************************************
      
    // Changeset Variables
    final def PERFORCE_CHANGELOG_OUT_DATE = new java.text.SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
    final def CHANGE_SUMMARY_PATTERN = ~'(?m)^Change \\d+ by (\\S+)@\\S+ on (\\d+/\\d+/\\d+ \\d+:\\d+:\\d+)'
    private final def GMT = TimeZone.getTimeZone("GMT")
    private final def AIR_DATE_FORMAT = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S Z");
    
    final ChangeSetFilter changeSetFilter = new ChangeSetFilter()
    List<ChangeSet> changeSets
    
    Double timezoneDifference
    Long startDate
    Long endDate
    String changesUrl
    
    public def execute() {
        this.login()
    
        AIR_DATE_FORMAT.timeZone = GMT
        
        def logOutput = runLogCommand()
        changeSets = parseChangesets(logOutput)
        
        if (changeSets) {
            ChangeSetXMLHelper xmlHelper = new ChangeSetXMLHelper()
            xmlHelper.repoType = REPO_TYPE;
            def xml = xmlHelper.toXML(changeSets);
            sendPostRequest(xml)
        }
        else {
            println "No changes detected"
            return null
        }
    }
    
    public List<ChangeSet> parseChangesets(String logOutput) {
        List<ChangeSet> changeSets = []
        
        def typeMap = ['add':'A', 'edit':'M', 'delete':'D']
        
        // Change X on yyyy/MM/dd by user@client 'comment'
        logOutput.eachMatch(/(?m)^Change (\d+)/){ changematch, changeId ->
            def ChangeSet changeSet = new ChangeSet()
            changeSet.id = changeId
        
            // Get details
            def summary = ''
            def descCommand = createP4BaseCommand()
            descCommand << 'describe' << '-s' << changeId
            ch.runCommand(null, descCommand) { proc ->
                proc.out.close()                    // close   the stdIn  of the process
                proc.consumeProcessErrorStream(System.out) // forward the stdErr of the process
                summary = proc.text                 // capture the stdout of the process
                println summary                     // echo captured stdOut
            }
        
            // Summary Format:
            //   Change X by user@client on yyyy/MM/dd HH:mm:ss
            //   <some number of lines w/ or w/o content>
            //   Affected files ...
            //   ... //depot/file#3 edit
            //   ... //depot/file#3 edit
            //   ... //depot/file#3 edit
            //   ... //depot/file#3 edit
            //
        
            // first line is "Change X by user@client on yyyy/MM/dd HH:mm:ss"
            summary.find(CHANGE_SUMMARY_PATTERN) { summarymatch, user, dateString ->
                def p4Date = PERFORCE_CHANGELOG_OUT_DATE.parse(dateString) // TODO deal with 'unknown' date
        
                // Pick out the changelog comment (anything between the first line and "Affected files")
                int commentStart = summary.indexOf(summarymatch)+summarymatch.length()
                int commentEnd = summary.indexOf("Affected files ...", commentStart)
                if (commentStart > -1 && commentEnd > -1 && commentEnd > commentStart) {
                    changeSet.message = summary.substring(commentStart, commentEnd).trim()
                }
        
                changeSet.user = user
                changeSet.date = p4Date
        
                // Eventually there is a line "Affected files ..."
                // followed by lines of "... //depot/file#3 edit"
                summary.eachMatch(/(?m)^\.\.\. (.+)#\d+ (\S+)/){affectedmatch, file, operation ->
                    ChangeSetFileEntry entry = new ChangeSetFileEntry()
                    entry.type = typeMap[operation] ?: 'M' // convert operation to A, M, D types
                    entry.path = file
                    changeSet.fileSet << entry
                }
        
                boolean hasAllowedAuthor = changeSet.hasAllowedAuthor(changeSetFilter);
                boolean hasAllowedFile = changeSet.hasAllowedPath(changeSetFilter);
                if (hasAllowedAuthor && hasAllowedFile) {
                    changeSets << changeSet
                }
                else {
                    def message = new StringBuilder("Changeset ${changeSet.id} skipped because ")
                    if (!hasAllowedAuthor) {
                        message << "it has excluded author ${changeSet.user}"
                    }
                    if (!hasAllowedAuthor && !hasAllowedFile) {
                        message << " and "
                    }
                    if (!hasAllowedFile) {
                        message << "it contains only excluded file paths (${changeSet.fileSet.collect{it.path}})"
                    }
                    println message
                }
            }
        }
        
        return changeSets
    }
    
    public String runLogCommand() {
        final def startDateString = formatChangelogDateParameter(startDate, timezoneDifference) ?: null
        final def endDateString   = formatChangelogDateParameter(endDate,   timezoneDifference) ?: 'now'
        
        def changes;
        
        def command = createP4BaseCommand()
        command << 'changes' << '-s' << 'submitted' << '-i'
        if (startDateString) {
            command << "//${client}/...@${startDateString},${endDateString}"
        }
        else if (endDateString) { // isn't this always true?
            command << "//${client}/...@${endDateString}"
        }
        else {
            command << "//${client}/..."
        }
        
        ch.runCommand('Getting Changes', command) { proc ->
          proc.out.close()                    // close   the stdIn  of the process
          proc.consumeProcessErrorStream(System.out) // forward the stdErr of the process
          changes = proc.text                 // capture the stdout of the process
          println changes                     // echo captured stdout
        }
        
        return changes
    }
    
    public String formatChangelogDateParameter(Long commandDate, Double timeDifference){
        if (!commandDate) {
            return null
        }
    
        def PERFORCE_CHANGELOG_DATE = new java.text.SimpleDateFormat("yyyy/MM/dd:HH:mm:ss")
    
        // adjust date for difference between ahp-agent and p4-server
        if (timeDifference) {
            def p4Cal = Calendar.instance
            p4Cal.timeInMillis = commandDate
            p4Cal.add(Calendar.HOUR_OF_DAY, (int)timeDifference)
            int minDiff = (int)(timeDifference * 60) % 60
            if (minDiff) {
                p4Cal.add(Calendar.MINUTE, minDiff)
            }
            commandDate = p4Cal.timeInMillis
        }
    
        return PERFORCE_CHANGELOG_DATE.format(new Date(commandDate))
    }
    
    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(workDir, "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;
    }
}