/*
* 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 java.text.SimpleDateFormat

import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity

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

public class SCMChangelog extends SCMStep {

    //**************************************************************************
    // CLASS
    //**************************************************************************
    
    //**************************************************************************
    // INSTANCE
    //**************************************************************************

    // Changelog boundaries
    Date startDate
    Date endDate
    String startRevision
    String endRevision

    // SVN path information
    String fromBranch
    String fromTag
    String svnBranches
    String svnTags
    String svnTrunk
    String svnRoot
    String changesUrl
    String repoOffset = ""
    boolean isSvnRepo = true
    protected String checkoutPath = "/"

    // Create dates to format output later
    protected SimpleDateFormat SVN_DATE
    protected SimpleDateFormat SVN_DATE_OUT_FORMAT
    protected SimpleDateFormat AIR_DATE_FORMAT

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

    
    /**
     *
     * @return xml document of changes
     */
    public def execute() {
        SVN_DATE = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S Z")
        SVN_DATE.timeZone = TimeZone.getTimeZone("GMT")

        SVN_DATE_OUT_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S'Z'")
        SVN_DATE_OUT_FORMAT.timeZone = TimeZone.getTimeZone("GMT")

        AIR_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S Z")
        AIR_DATE_FORMAT.timeZone = TimeZone.getTimeZone("GMT")
        
        // Validate data before proceeding
        println "Validating revisions"
        validate()
        
        initCheckoutPath()
        
        parseTrueOffset()

        // Get Change Log
        def logOutput = runLogCommand()
        
        // The result change set data
        changeSets = parseLogIntoChangeSets(logOutput);

        // Construct Air XML message and upload/save
        if (changeSets) {
            ChangeSetXMLHelper xmlHelper = new ChangeSetXMLHelper()
            xmlHelper.repoType = REPO_TYPE;
            String xml = xmlHelper.toXML(changeSets);
            sendPostRequest(xml)
        }
        else {
            println "No changes detected"
            return null
        }
    }
    
    /**
    * Parse an input of svn xml log command into a list of ChangeSets
    * @param logOutput output from a `svn log` command
    * @return list of changesets
    */
   public List<ChangeSet> parseLogIntoChangeSets(String logOutput) {

       List<ChangeSet> changeSets = []
       
       groovy.util.slurpersupport.GPathResult log = new XmlSlurper().parseText(logOutput);

       log.logentry.each{ svnchangeset ->
           ChangeSet changeSet = new ChangeSet();
           String dateText = svnchangeset.date[0].text().replaceFirst('[0-9]{3}Z$', 'Z')
           Date svnDate = SVN_DATE_OUT_FORMAT.parse(dateText)

           changeSet.id = svnchangeset.@revision.text();
           changeSet.user = svnchangeset.author[0].text();
           changeSet.message = svnchangeset.msg[0].text();
           changeSet.date = svnDate

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

           svnchangeset.paths[0].path.each() { path ->
               def operation = path.@action.text()
               def filename = path.text()

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

               hasAllowedPaths = hasAllowedPaths || changeSet.hasAllowedPath(changeSetFilter)
           }
           
           boolean allowedDate = true
           if (startDate && endDate) {
               allowedDate = svnDate.after(startDate) && svnDate.before(endDate)
           }

           if (hasAllowedAuthor && hasAllowedPaths && allowedDate) {
               changeSets << changeSet
           }
           else {
               def message = new StringBuilder("Changeset ${changeSet.id} skipped because ")
               if (!hasAllowedAuthor) {
                   message << "it has excluded author ${changeSet.user}"
               }
               if (!hasAllowedPaths) {
                   if (!hasAllowedAuthor) {
                       message << " and "
                   }
                   message << "it contains only excluded file paths (${changeSet.fileSet.collect{it.path}})"
               }
               if (!allowedDate) {
                   if (!hasAllowedAuthor || !hasAllowedPaths) {
                       message << " and "
                   }
                   message << "it has an invalid date for the given date range"
               }
               println message
           }
       }

       return changeSets;
   }

    private String runLogCommand() {;
        def logCommand = [this.scmCommand, 'log', '--non-interactive', '--no-auth-cache', '--verbose', '--xml']

        if (username) {
            logCommand << "--username" << this.username
            
            if (password) {
                logCommand << "--password" << this.password
            }
        }

        // Order range as  end:start so that limiting will give the one closest to end-date
        if (startRevision || endRevision) {
            logCommand << '--revision'
            def revArg = new StringBuilder()

            if (startRevision) {
                revArg += startRevision
            }
            if (endRevision) {
                if (revArg.length()) {
                    revArg += ':' 
                }
                
                revArg += endRevision
            }
           
            logCommand << revArg
        }

        logCommand << (svnRoot + this.checkoutPath)

        def logOutput = null
        cmdHelper.runCommand('Getting Changelog', logCommand) { Process proc ->
            proc.consumeProcessErrorStream(System.out)
            proc.out.close()
            logOutput = proc.text
            println logOutput
           
            if(!(logOutput?.trim())) {
                println("No Changes Found");
            }
        }
        return logOutput
    }
   
   protected void validate() {
       if (!svnRoot) {
           throw new Exception("An svn Repository Url must be specified")
       }
       
       if (startRevision && startDate) {
           throw new Exception('You cannot specify both a start revision and a start date at the same time')
       }
       else if (!startRevision && startDate != null) {
           startRevision = '{'+SVN_DATE.format(startDate)+'}'
       }
       
       if (endRevision && endDate) {
           throw new Exception('You cannot specify both an end revision and an end date at the same time')
       }
       else if (!endRevision && endDate != null) {
           endRevision = '{'+SVN_DATE.format(endDate)+'}'
       }
   }
   
   protected void initCheckoutPath() {
       cleanPaths()
       
       this.checkoutPath += svnTrunk
       //normalize checkoutPath file separators and trim tail '/' if present
       this.checkoutPath = this.checkoutPath.replaceAll('/+', '/').replaceAll('/$', '')
       changeSetFilter.checkoutPath = this.checkoutPath
   }
   
   public void cleanPaths() {
       if (svnRoot) {
           svnRoot = svnRoot.endsWith("/") ? svnRoot.substring(0, svnRoot.length() - 1) : svnRoot
       }
       
       if (svnTrunk) {
           svnTrunk = svnTrunk.startsWith("/") ? svnTrunk.substring(1) : svnTrunk
           svnTrunk = svnTrunk.endsWith("/") ? svnTrunk.substring(0, svnTrunk.length() - 1) : svnTrunk
       }
   }
   
   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")
           }
           
           println "Sending ${changeSets.size()} 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 void parseTrueOffset() {
       def command = [this.scmCommand, 'info', '--non-interactive', '--no-auth-cache']
       
       if (username) {
           command << "--username" << this.username
           
           if (password) {
               command << "--password" << this.password
           }
       }
       command << (svnRoot + this.checkoutPath)
       
       def output = null
       cmdHelper.runCommand('Getting Repository Info', command) { Process proc ->
           proc.consumeProcessErrorStream(System.out)
           proc.out.close()
           output = proc.text
           //println output
       }
       def fullUrl = output.find(~'(?m)^URL: (.*)$') { match, url -> return url }
       def root = output.find(~'(?m)^Repository Root: (.*)$') {match, repoOffset -> return repoOffset }
       
       repoOffset = fullUrl.substring(root.length())
       changeSetFilter.repoOffset = this.repoOffset
       //println "Repository Root: ${repoOffset}"
   }
}