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
    //**************************************************************************
    static final def GMT = TimeZone.getTimeZone("GMT")
    static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MMM-yy.HH:mm:ss")
    static final SimpleDateFormat CC_DATE = new SimpleDateFormat("yyyyMMdd.HHmmss");
    static final SimpleDateFormat AIR_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S Z")
    static {
        DATE_FORMAT.timeZone = GMT
        AIR_DATE_FORMAT.timeZone = GMT
    }

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

    // changelog boundaries
    Date startDate = null
    Date endDate = null
    String startRevision = null
    String endRevision = null
    String changesUrl = null
    def changeSets = []
    def fileFilters
    def userExcludes
    def includeFiles
    def excludeFiles

    public def execute() {
        setupWorkDir()
        
        if (fileFilters) {
            fileFilters.readLines().collect{it.trim()}.findAll{it}.each{filter ->
                def pattern = convertToPattern(filter.substring(1))
                if (filter.startsWith('+')) {
                    includeFiles << pattern
                }
                else if (filter.startsWith('-')) {
                    excludeFiles << pattern
                }
                else {
                  // not a valid expression
                }
            }
        }

        try {
            // Get Change Log
            runLogCommand()

            // Construct xml message and send to server
            if (changeSets) {
                uploadChangeSets()
            }
            else {
                println "No changes detected"
            }
        }
        catch (ExitCodeException e) {
            println e.getMessage()
            System.exit(1)
        }
    }

    protected void parseLogIntoChangeSets(def out) {
        //first we should split on --------->\n<---------
        System.out.println("Output");
        System.out.println("--------------------------------------------------------------------")
        System.out.println(out.toString());
        System.out.println("--------------------------------------------------------------------")
        
        def revsTemp = out.toString().split("--urbancodesplit-->");
        def revs = [];
        //remove trailing newline
        for (int j = 0; j < revsTemp.length-1; j++) {
            revs << revsTemp[j];
        }
        revs.each { revString ->
            //next we should split on \n
            def rev = [:];
            rev['comment'] = ''
        
            if (revString.readLines().size() < 6) {
                //we have to have 6 lines here to get any useful information out of a revision
                //if not we should skip it
                System.out.println("Skipping because not enough fields are present");
            }
            else {
                revString.eachLine { line ->
                    def index = line.indexOf(':')
                    def type = index > 0 ? line.substring(0, index) : line
                    def data = index > 0 ? line.substring(index + 1) : line
                    if (data) {
                        switch (type) {
                            case "Activity": 
                                rev['activity'] = data
                                break
                            case "Action": 
                                rev['action'] = data
                                break
                            case "Date": 
                                rev['date'] = CC_DATE.parse(data)
                                break
                            case "User": 
                                rev['user'] = data
                                break
                            case "Object": 
                                rev['object'] = data
                                break
                            case "Comment":
                            default:
                                rev['comment'] += '\n' + data
                                break
                        }
                    }
                    
                    //only add to the rev list if we determine it is acceptable
                    if (isRevisionAcceptable(rev) && isUserAcceptable(rev) && isFileAcceptable(rev)) {
                        changeSets.add(rev);
                    }
                }
            }
        }
    }

    protected boolean isRevisionAcceptable(def changeSet) {
        boolean result = true
        String action = changeSet['action']?.trim()
        if (action == "mkbranch") {
            println "This is a mkbranch revision and will be ignored."
            result = false
        }
        else if (action == "checkout") {
            println "This is a checkout revision and will be ignored."
            result = false
        }
        return result
    }

    protected boolean isUserAcceptable(def changeSet) {
        boolean result = true
        String user = changeSet['user'];
        if (userExcludes) {
            result = !userExcludes.contains(user)
        }
        return result
    }

    protected boolean isFileAcceptable(def changeSet) {
        boolean result = true
        String fileName = changeSet['file']
        if (excludeFiles) {
            result = excludeFiles.find{fileName ==~ it} != null
        }
        return result;
    }

    protected String runLogCommand(def stream) {
        loadRules.eachLine { fileName ->
            while (fileName.startsWith("\\") || fileName.startsWith("/")) {
                fileName = fileName.substring(1)
            }
            
            File file = new File(directory, fileName)
            ByteArrayOutputStream out = new ByteArrayOutputStream();

            if (file.exists() && file.isDirectory()) {
                def command = [scmCommand, "lshistory", "-d", "-branch", streamName]
                if (startDate) {
                    command << "-since" << DATE_FORMAT.format(startDate) + "UTC+00:00"
                }
                command << "-fmt" << "Activity:%[activity]p\\nAction:%o\\nDate:%Nd\\nUser:%u\\nObject:%n\\nComment:%Nc\\n--urbancodesplit-->\\n" << fileName

                ch.runCommand("Getting History for directory.", command) { proc ->
                    proc.out.close();
                    proc.waitForProcessOutput(out, System.out);
                }

                parseLogIntoChangeSets(out)
            }

            if (file.exists()) {
                def command = [scmCommand, "lshistory", "-r", "-branch", streamName]
                if (startDate) {
                    command << "-since" << DATE_FORMAT.format(startDate) + "UTC+00:00"
                }
                command << "-fmt" << "Activity:%[activity]p\\nAction:%o\\nDate:%Nd\\nUser:%u\\nObject:%n\\nComment:%Nc\\n--urbancodesplit-->\\n" << fileName

                ch.runCommand("Getting History for files and subdirectories in directory.", command) { proc ->
                    proc.out.close();
                    proc.waitForProcessOutput(out, System.out);
                }

                parseLogIntoChangeSets(out)
            }

            if (!file.exists()) {
                println file.absolutePath + " was not found and we cannot get history for it"
            }
        }
    }

    private void uploadChangeSets() {
        def xmlBuilder = new groovy.xml.StreamingMarkupBuilder();
        xmlBuilder.encoding = "UTF-8"
        def xml = xmlBuilder.bind{
            mkp.xmlDeclaration()
            'change-log'{
                changeSets.each{ changeSet ->
                    'change-set'(){
                        'id'(stripInvalidXMLChars(changeSet['activity']))
                        'user'(stripInvalidXMLChars(changeSet['user']))
                        'date'(AIR_DATE_FORMAT.format(changeSet['date']))
                        if (changeSet['comment']) {
                            'comment'(stripInvalidXMLChars(changeSet['comment']))
                        }
                        'repository-type'(stripInvalidXMLChars(REPO_TYPE))
                        'repository-id'(System.getenv("REPOSITORY_ID"))
                        'source-config-id'(System.getenv("SOURCE_CONFIG_ID"))
                        'file-set'{
                            'file'(
                                'change-type':stripInvalidXMLChars(changeSet['action']),
                                'revision-number':stripInvalidXMLChars(changeSet['activity']),
                                stripInvalidXMLChars(changeSet['object']))
                        }
                    }
                }
            }
        }

        sendPostRequest(xml.toString())
    }

    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 def convertToPattern(def antWildMat) {
       // trim leading / character from pattern
       def pattern = antWildMat.startsWith('/') ? antWildMat : '/' + antWildMat

       // deal with special regex-characters that should be interpreted as literals
       '\\.+[]^${}|()'.toCharArray().each{ c ->
           pattern = pattern.replace(''+c, '\\'+c)
       }
       pattern = pattern.replace('?', '.') // ? is a single-char wildcard

       // deal with ant-style wildcards
       StringBuffer result = new StringBuffer()
       def m = (pattern =~ '\\*\\*/|\\*\\*|\\*')
       while (m) {
           def token = m.group()
           def replacement;
           if (token == '**/') {
               replacement = '.*(?<=/)'
           }
           else if (token == '**') {
               replacement = '.*'
           }
           else {
               replacement = '[^/]*'
           }
           m.appendReplacement(result, java.util.regex.Matcher.quoteReplacement(replacement))
       }
       m.appendTail(result)
       return ~(result.toString())
    }
    
    /**
     * Strip any invalid xml characters (such as backspace) from the value.
     * @param value the value to strip (can be null)
     * @return the sanitized value (null if and only if value was null)
     */
    protected String stripInvalidXMLChars(String value) {
 
        // legal xml chars: #x9 | #xA | #xD | #x20-#xD7FF | #xE000-#xFFFD | #x10000-#x10FFFF
        def legalChars = ['\\u0009', '\\u000A', '\\u000D', '\\u0020-\\uD7FF', '\\uE000-\\uFFFD', '\\u10000-\\u10FFFF']
        def illegalChars = "[^${legalChars.join()}]"
        return !value ? value : value.replaceAll(illegalChars, '?')
    }
}
