/*
 * Licensed Materials - Property of IBM Corp. and/or HCL**
 * IBM UrbanCode Build
 * (c) Copyright IBM Corporation 2015, 2018. All Rights Reserved.
 * (c) Copyright HCL Corporation 2019 All Rights Reserved.
 *
 * U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
 * GSA ADP Schedule Contract with IBM Corp.
 *
 * * Trademark of International Business Machines
 * ** Trademark of HCL Technologies Limited
 */
package com.urbancode.air.plugin.scm

import org.apache.http.client.methods.CloseableHttpResponse

import java.text.SimpleDateFormat

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.ChangeSet
import com.urbancode.air.plugin.scm.changelog.ChangeSetFileEntry
import com.urbancode.air.plugin.scm.changelog.ChangeSetFilter
import com.urbancode.air.plugin.scm.changelog.ChangeSetXMLHelper
import com.urbancode.commons.httpcomponentsutil.HttpClientBuilder
import com.urbancode.commons.util.IO

public class SCMChangelog extends SCMStep {

    //**************************************************************************
    // CLASS
    //**************************************************************************
    final static protected SimpleDateFormat CHANGE_DATE_FORMAT = new SimpleDateFormat("MM-dd-yyyy;hh:mm:ss")
    final static protected SimpleDateFormat HARVEST_DATE_FORMAT = new SimpleDateFormat("MM/dd/yyyy")
    final static protected String REVISION_START_DELIMITER = "--------------"

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

    final ChangeSetFilter changeSetFilter = new ChangeSetFilter()
    Date startDate
    Date endDate
    String changesUrl
    String[] includeFiles

    void executeChangelog() {
        CHANGE_DATE_FORMAT.timeZone = TimeZone.getTimeZone("UTC")

        String logOutput = runLogCommand()
        Collection<ChangeSet> changeSets = parseChangesets(logOutput)

        if (changeSets) {
            ChangeSetXMLHelper xmlHelper = new ChangeSetXMLHelper()
            xmlHelper.repoType = REPO_TYPE
            String xml = xmlHelper.toXML(changeSets)
            println "Sending ${changeSets.size()} changes"
            sendPostRequest(xml)
        }
        else {
            println "No changes detected"
        }
    }

    Collection<ChangeSet> parseChangesets(String logOutput) {
        // Map package name to parsed ChangeSet
        Map<String, ChangeSet> changeSetMap = [:] as Map<String, ChangeSet>

        // flag to determine if we are reading changes or the header
        boolean isReadingChanges = false

        String previousLine = null

        List<String> columnHeaders = new ArrayList<String>()

        logOutput.eachLine { String line ->
            if (isReadingChanges && line) {
                ChangeSet parsedChangeSet = parseChangeSet(line, columnHeaders)
                addOrAggregateChangeSet(changeSetMap, parsedChangeSet)
            }
            else if (line.startsWith(REVISION_START_DELIMITER)) {
                isReadingChanges = true
                if (previousLine) {
                    columnHeaders = readColumnHeaders(previousLine)
                    println "Parsed column headers: $columnHeaders"
                }
                else {
                    throw new IllegalStateException("Did not find column names!")
                }
            }
            else if (line.length() == 0) {
                isReadingChanges = false
            }
            previousLine = line
        }

        Collection<ChangeSet> changeSets = changeSetMap.values()

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

    String runLogCommand() {
        File logFile = File.createTempFile("hsv-", ".log", directory)
        logFile.deleteOnExit()
        List cmdArgs = commandHelperBase()
        String parsedStart = formatDate(startDate, false)
        // need to add a day to the end date in order to get any revisions for that day
        // because harvest does not accept time on the command line and only looks
        // until the begining of that day.
        String parsedEnd = formatDate(endDate, true)
        ["-iv", "av", "-id",  "sd", parsedStart, parsedEnd, "-o", logFile].each {
            cmdArgs << it
        }

        if (includeFiles.size() > 0) {
            cmdArgs << "-s"
            for (file in includeFiles) {
                cmdArgs << file
            }
        }

        cmdHelper.runCommand('Getting Changelog', cmdArgs)

        return logFile.getText('UTF-8')
    }

    /**
     * Either add the changeset as a new entry to changeSetMap, or aggregate it using an existing ChangeSet for the
     * same package.
     * @param changeSetMap
     * @param changeSet
     */
    void addOrAggregateChangeSet(Map changeSetMap, ChangeSet changeSet) {
        if (changeSetMap.containsKey(changeSet.id)) {
            ChangeSet existingChange = (ChangeSet) changeSetMap.get(changeSet.id)
            existingChange.id = changeSet.id
            existingChange.user = changeSet.user
            existingChange.date = changeSet.date

            // add the new ChangeSet's file entry to the existing ChangeSet's list of file entries
            ChangeSetFileEntry fileEntry = changeSet.fileSet.get(0)
            existingChange.fileSet << fileEntry
            changeSetMap.put(changeSet.id, existingChange)
        }
        else {
            changeSetMap.put(changeSet.id, changeSet)
        }
    }

    /**
     * Read the column headers from a given line. The expectation is that this line is guaranteed to have the column
     * headers. No specific checks are done in this function to check the validity.
     * @param line
     * @return
     */
    List<String> readColumnHeaders(String line) {
        List columnHeaders = [] as List

        // Parsing column headers expects a tab or a newline character as the delimiting character
        String[] array = line.split("[\\t\\n]")
        for (String element : array) {
            columnHeaders.add(element?.trim()?.toLowerCase())
        }

        return columnHeaders
    }

    /**
     * Parse a line of output into a ChangeSet object, based on the given column headers.
     * @param line The line of input to parse.
     * @param columnHeaders The column headers that has been read prior to this call and describes the data in each line.
     * @return
     */
    ChangeSet parseChangeSet(String line, List columnHeaders) {
        List data = [] as List

        int startIndex = 0
        int endIndex
        String columnData

        while (startIndex < line.size()) {
            endIndex = line.indexOf('\t', startIndex + 1)
            if (endIndex == -1) {
                columnData = line.substring(startIndex, line.size())
                if ("\t".equals(columnData)) {
                    data.add('')
                }
                else {
                    data.add(columnData)
                }
                break
            }
            else {
                columnData = line.substring(startIndex, endIndex)
                // special case for when a column is empty (Harvest uses a delimiter in place of absent data)
                if ("\t".equals(columnData)) {
                    data.add('')
                }
                else {
                    data.add(columnData)
                }
                startIndex = endIndex + 1
            }
        }

        return createChangeSet(columnHeaders, data)
    }

    /**
     * Create a ChangeSet object based on column headers and a line of data.
     * @param columnHeaders
     * @param data
     * @return
     */
    ChangeSet createChangeSet(List columnHeaders, List data) {
        ChangeSet changeSet = new ChangeSet()
        changeSet.id = data.get(columnHeaders.indexOf("package"))
        changeSet.user = data.get(columnHeaders.indexOf("modifier"))
        changeSet.date = CHANGE_DATE_FORMAT.parse((String) data.get(columnHeaders.indexOf("modified")))

        ChangeSetFileEntry fileEntry = new ChangeSetFileEntry()
        fileEntry.revision = data.get(columnHeaders.indexOf("version"))
        fileEntry.type = data.get(columnHeaders.indexOf("tag"))
        fileEntry.path = getCompletePath((String) data.get(columnHeaders.indexOf("path")), (String) data.get(columnHeaders.indexOf("name")))
        changeSet.fileSet.add(fileEntry)

        return changeSet
    }

    void sendPostRequest(String xml) {
        // construct the URL with property replacements
        String url = changesUrl
        String 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 {
            HttpClientBuilder clientBuilder = new HttpClientBuilder()
            clientBuilder.setTrustAllCerts(true)
            DefaultHttpClient client = clientBuilder.buildClient()

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

            postMethod.setEntity(new StringEntity(xml))

            CloseableHttpResponse response = client.execute(postMethod)
            int 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}")
            }
        }
    }

    boolean isGoodResponseCode(int responseCode) {
        return responseCode >= 200 && responseCode < 300
    }

    String formatDate(Date date, boolean addADay) {
        if (addADay) {
            Calendar c = Calendar.getInstance()
            c.setTime(date)
            c.add(Calendar.DAY_OF_YEAR, 1)
            date = c.getTime()
        }
        return HARVEST_DATE_FORMAT.format(date)
    }

    String getCompletePath(String viewPath, String fileName) {
        String result = ""
        if (viewPath != null && fileName != null) {
            String separator = "\\"
            if (viewPath.indexOf('/') != -1) {
                separator = "/"
            }
            result = viewPath + separator + fileName
        }

        return result
    }
}