/*
* 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 org.apache.http.HttpEntity
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.DefaultHttpClient

import com.urbancode.air.plugin.scm.changelog.*
import com.urbancode.commons.httpcomponentsutil.HttpClientBuilder
import org.apache.http.util.EntityUtils

import java.nio.charset.StandardCharsets


public class SCMChangelog extends SCMStep {

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

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

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

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

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

        // Get Change Log
        def logOutput = runLogCommand()

        // the result change set data
        changeSets = parseLogIntoChangeSets(logOutput);

        // Construct XML message and upload
        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 changelog command into a list of Air ChangeSets
     * @param logOutput output from a {@link #runLogCommand()} method
     * @return List of ahp changesets
     */
    public List<ChangeSet> parseLogIntoChangeSets(String logOutput) {

        List<ChangeSet> changeSets = []

        // parse output
        def entrySepPattern = '\0|'+UNIT_SEPARATOR;
        for (String entry : logOutput.split("(?:$entrySepPattern)\0")) {
            if (!entry) {
                // skip empty string - usually start or end of log output
                continue;
            }
            String[] chunks = entry.split(entrySepPattern);

            ChangeSet changeSet = new ChangeSet();
            changeSet.id = chunks[0];
            changeSet.user = chunks[1];
            changeSet.date = new Date(chunks[2].toLong() * 1000);
            changeSet.message = chunks[3]?.trim();

            // read file paths
            for (int i = 4; i < chunks.length;) {
                def changeSetType = chunks[i++].trim(); // first type entry has a newline char
                def filePath = chunks[i++];

                ChangeSetFileEntry fileEntry = new ChangeSetFileEntry();
                fileEntry.revision = changeSet.id;
                fileEntry.type = changeSetType;
                fileEntry.path = filePath;
                changeSet.fileSet << fileEntry;
            }
            changeSets << changeSet;
        }

        // filter results
        changeSets.retainAll{ ChangeSet changeSet ->
            boolean allowAuthor = changeSet.hasAllowedAuthor(changeSetFilter);
            boolean allowAnyPaths = changeSet.hasAllowedPath(changeSetFilter);
            if (allowAuthor && allowAnyPaths) {
                // retain changeset
                return true;
            }
            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
                // exclude changeset
                return false;
            }
        }

        return changeSets;
    }

    protected String runLogCommand() {
        // we want  a trailing separator from the change sets
        // use unit-separator %x1F in format string, null (%x00) breaks output on msysgit
        // %B is preferred as it doesn't reformat the commit messages, but %s%n%b will have to suffice
        def format = ['%H', '%an', '%at', '%s%n%b', ''].join(UNIT_SEPARATOR_FMT)

        def logCommand = [scmCommand, '--no-pager', 'log', '-z', '--no-renames', '--name-status', "--format=format:$format"]
        String startPoint = startRevision ?: lookupRevision(startDate);
        String endPoint = endRevision ?: lookupRevision(endDate);

        // QP should use Dates, CL uses revisions
        if (startPoint || endPoint) {
            def range = "${startPoint ?: ''}..${endPoint ?: 'HEAD'}"
            logCommand << range
        }

        logCommand << "$branch";
        logCommand << '--'

        def logOutput = getCommandText(logCommand);
        return logOutput.replaceFirst('\0$', ''); // trim the dangling null
    }

    protected String formatChangelogDate(Date date) {
        def df = this.getDateArgumentFormatter()
        return date ? df.format(date) : null;
    }
    
    private void sendPostRequest(String xml) {
        // construct the URL with property replacements
        String url = changesUrl
        def authToken = System.getenv("AUTH_TOKEN")
        
        // Debug/testing stub
        if (url.startsWith("file://")) {
            File xmlOut = new File(directory, "xmlOut.xml")
            xmlOut << xml
        }
        else {
            HttpClientBuilder clientBuilder = new HttpClientBuilder();
            clientBuilder.setTrustAllCerts(true);
            DefaultHttpClient client = clientBuilder.buildClient();

            HttpPost postMethod = new HttpPost(url);
            if (authToken) {
                postMethod.setHeader("Authorization-Token", authToken)
                postMethod.setHeader("Content-Type", "application/xml")
            }

            println "Sending ${changeSets.size()} changes"
            postMethod.setEntity(new StringEntity(xml))

            CloseableHttpResponse httpResponse = client.execute(postMethod)
            int responseCode = httpResponse.getStatusLine().getStatusCode()

            HttpEntity entity = postMethod.getEntity()
            String entityString = EntityUtils.toString(entity, StandardCharsets.UTF_8)
            if (isGoodResponseCode(responseCode)) {
                System.out.println(entityString)
                println ""
            }
            else {
                System.err.println(entityString)
                throw new RuntimeException("Failed to upload source changes. StatusCode: ${responseCode}")
            }
        }
    }
    
    private boolean isGoodResponseCode(int responseCode) {
        return responseCode >= 200 && responseCode < 300;
    }
}
