/**
 * (c) Copyright IBM Corporation 2015, 2017. All Rights Reserved.
 * (c) Copyright HCL Technologies Ltd. 2018. All Rights Reserved.
 * This is licensed under the following license.
 * The Eclipse Public 1.0 License (http://www.eclipse.org/legal/epl-v10.html)
 * U.S. Government Users Restricted Rights:  Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
 */
package com.urbancode.air.plugin.cf.helper

import com.urbancode.air.CommandHelper
import com.urbancode.air.ExitCodeException

import java.util.regex.Matcher
import java.util.regex.Pattern
import org.apache.commons.io.FileUtils
import groovy.json.JsonSlurper
import org.apache.commons.io.IOUtils

class CFHelper {
    CommandHelper helper
    final def workDir = new File('.').canonicalFile
    boolean isAuthenticated
    def cfFile
    def props
    def api
    def username
    def password
    def selfSigned
    def organization
    def space
    def cfHome
    def cfVersion
    def interpreter
    def envVars

    final def isWindows = System.getProperty('os.name').contains("Windows")

    CFHelper(def props) {
        this.props = props
        helper = new CommandHelper(workDir)
        cfFile = props['commandPath']?.trim() ?: "cf"
        api = props['api']?.trim()
        username = props['user']?.trim()
        password = props['password']
        selfSigned = props['selfSigned'] ? props['selfSigned'].toBoolean() : ""
        organization = props['org']?.trim()
        space = props['space']?.trim()
        cfHome = props['cfHome']?.trim()
        interpreter = props['interpreter']
        envVars = gatherEnvironmentVariables(props['envVars']?:"")
        envVars.each {
            helper.addEnvironmentVariable(it.key, it.value)
        }

        /* Colorized output treated as ASCII control characters */
        helper.addEnvironmentVariable("CF_COLOR", "false")
    }

    void bindService() {
        def application = props['application']
        def service = props['service']

        setupEnvironment(api, organization, space)

        // Execute bind-service
        def commandArgs = [cfFile, "bind-service", application, service]

        runHelperCommand("[Action] Executing CF bind-service", commandArgs)
    }

    void createDomain() {
        def domain = props['domain']

        setupEnvironment(api, organization, space)

        // Execute create-domain
        def commandArgs = [cfFile, "create-domain", organization, domain]

        runHelperCommand("[Action] Executing CF create-domain", commandArgs)
    }

    void createOrUpdateUserProvidedService() {
        def service         = props['service'].trim()
        def credentials     = props['credentials'].trim()
        def logDrainURL     = props['logDrainURL'].trim()
        def routeServiceURL = props['routeServiceURL'].trim()

        setupEnvironment(api, organization, space)
        def services
        getCfVersion()
        services = getServices()

        def command = ""
        if (!services.contains(service)) {
            println "[Ok] Service not found. Creating the user-provided service..."
            command = "create-user-provided-service"
        }
        else {
            println "[Ok] Service found. Updating the user-provided service..."
            command = "update-user-provided-service"
        }

        // Execute create-user-provided-service or update-user-provided service
        println "[Ok] Credentials : ${credentials}"
        def commandArgs = [cfFile, command, service, "-p", credentials]

        if (logDrainURL) {
            commandArgs << "-l"
            commandArgs << logDrainURL
        }

        if (routeServiceURL) {
            commandArgs << "-r"
            commandArgs << routeServiceURL
        }

        runHelperCommand("[Action] Executing CF ${command}", commandArgs)
    }

    // run command and return service output (Get's element 0 on each line of the output)
    def getServiceOutput(def cmdArgs) {
        def output = []
        def serviceRowStartFlag = false
        def setOutput = {
            it.out.close() // close stdin
            try {
                // Look at each line, get the first word which is the service's name
                it.in.eachLine { line ->
                    println line
                    def lineContent = line.split("\\s+")
                    //Picking services after we cross service columns
                    if(serviceRowStartFlag==false) {
                        lineContent.each {
                            if(it.toString()=="name" || it.toString()=="service" || it.toString()=="plan" || it.toString()=="bound apps" || it.toString()=="last operation") {
                                serviceRowStartFlag = true
                            }
                        }
                    }else {
                        output << lineContent[0]
                    }
                }
            }
            catch (IOException ex) {
                println "[Error] I/O Error found when retrieving the Service list. Please review the output log."
                ex.printStackTrace()
                System.exit(1)
            }
            catch (Exception ex) {
                println "[Error] Unknown found when retrieving the Service list. Please review the output log."
                ex.printStackTrace()
                System.exit(1)
            }
            finally {
                it.waitFor()
                println "Service Output: ${output}"
            }
        }

        println "----------------------------------------------"
        helper.runCommand("[Action] Running command: ${cmdArgs.join(' ')}", cmdArgs, setOutput)
        println "----------------------------------------------"
        println "[Ok] Service Names Found: ${output}"
        return output
    }


    // return a list of all services. api, org, and space must be initialized elsewhere
    def getServices() {
        def commandArgs = [cfFile, "services"]
        def services = getServiceOutput(commandArgs)

        return services
    }

    void createSubdomain() throws Exception{
        def subdomain = props['subdomain']
        def domain = props['domain']

        setupEnvironment(api, organization, space)

        // Execute create-domain
        def commandArgs = [cfFile, "create-domain", organization, subdomain + "." + domain]

        runHelperCommand("[Action] Executing CF create-domain", commandArgs)
    }

    void createRoute() {
        def domain = props['domain']
        def hostname = props['hostname']

        setupEnvironment(api, organization, space)

        // Execute create-route
        def commandArgs = [cfFile, "create-route", space, domain]

        if (hostname) {
            commandArgs << "-n"
            commandArgs << hostname
        }

        runHelperCommand("[Action] Executing CF create-route", commandArgs)
    }

    void createService() {
        def serviceName = props['service']
        def serviceType = props['serviceType']
        def plan = props['plan']

        setupEnvironment(api, organization, space)

        // Execute create-service
        def commandArgs = [cfFile, "create-service", serviceType, plan, serviceName]

        runHelperCommand("[Action] Executing CF create-service", commandArgs)
    }

    void deleteApp() {
        def app = props['application']
        def deleteRoutes = props['delete_Routes']

        setupEnvironment(api, organization, space)

        // Execute delete-app
        def commandArgs = [cfFile, "delete", app, "-f"]

        if (deleteRoutes == "true") {
            commandArgs << "-r"
        }

        runHelperCommand("[Action] Executing CF delete-app", commandArgs)
    }

    void deleteDomain() {
        def domain = props['domain']

        setupEnvironment(api, organization, space)

        // Execute delete-domain
        def commandArgs = [cfFile, "delete-domain", domain, "-f"]

        runHelperCommand("[Action] Executing CF delete-domain", commandArgs)
    }

    void deleteRoute() {
        def domain = props['domain']
        def hostname = props['hostname']

        setupEnvironment(api, organization, space)

        // Execute delete-route
        def commandArgs = [cfFile, "delete-route", domain]

        if (hostname) {
            commandArgs << "-n"
            commandArgs << hostname
        }

        commandArgs << "-f"

        runHelperCommand("[Action] Executing CF delete-route", commandArgs)
    }

    void deleteService() {
        def name = props['name']

        setupEnvironment(api, organization, space)

        // Execute delete-service
        def commandArgs = ["cf", "delete-service", name]

        commandArgs << "-f"

        runHelperCommand("[Action] Executing CF delete-service", commandArgs)
    }

    void deleteSubdomain() {
        def subdomain = props['subdomain']
        def domain = props['domain']

        setupEnvironment(api, organization, space)

        // Execute delete-domain
        def commandArgs = ["cf", "delete-domain", subdomain + "." + domain]

        commandArgs << "-f"

        runHelperCommand("[Action] Executing CF delete-domain", commandArgs)
    }

    void executeCFScript() {
        def script = props['script']
        def args = props['args']

        setupEnvironment(api, organization, space)

        def scriptName = ""
        def commandArgs = []
        if (isWindows) {
            scriptName = "cfScript.bat"
            commandArgs = ["cmd", "/C", scriptName]
        }
        else {
            scriptName = "cfScript.sh"
            commandArgs = ["sh", "-c", "bash " + scriptName]
        }
        File cfScript = new File(scriptName)
        cfScript.deleteOnExit()
        cfScript << script

        for (arg in args.split("\\s+")) {
            commandArgs << arg
        }

        if (!isWindows) {
            def chmod = ['chmod', '+x', scriptName]
            runHelperCommand('[Action] Making the runtime script executable...', chmod)
        }
        runHelperCommand("[Action] Executing CF commands", commandArgs)
    }

    void executeBashScript() {
        def script = props['script']
        def args = props['args']

        setupEnvironment(api, organization, space)

        // Execute script
        def commandArgs = []
        commandArgs = ["sh", "-c"]
        commandArgs << "bash " + script

        for (arg in args.split("\\s+")) {
            commandArgs << arg
        }

        runHelperCommand("[Action] Executing CF script", commandArgs)
    }

    void mapRoute() {
        def domain = props['domain']
        def hostname = props['hostname']
        def app = props['application']

        setupEnvironment(api, organization, space)

        // Execute map-route
        def commandArgs = [cfFile, "map-route", app, domain]

        if (hostname) {
            commandArgs << "-n"
            commandArgs << hostname
        }

        runHelperCommand("[Action] Executing CF map-route", commandArgs)
    }

    void pushApplication() {
        def appName = props['appName']
        def file = props['path']
        def manifest = props['manifest']
        def domain = props['domain']
        def subdomain = props['subdomain']
        def instances = props['instances']
        def memory = props['memory']
        def disk = props['disk']
        def buildpack = props['buildpack']
        def stack = props['stack']
        def timeout = props['timeout']
        def nostart = props['nostart']
        def noroute = props['noroute']
        def nomanifest = props['nomanifest']
        def nohostname = props['nohostname']
        def randomroute = props['randomroute']

        setupEnvironment(api, organization, space)

        // Push the application
        def commandArgs = [cfFile, "push"]

        if (appName) {
            commandArgs << appName
        }


        if (buildpack) {
            commandArgs << "-b"
            commandArgs << buildpack
        }

        if (manifest) {
            File manifestPath = new File(workDir, manifest)

            commandArgs << "-f"
            if (manifestPath.exists() && manifestPath.isFile()) {
                commandArgs << manifestPath.absolutePath
            }
            else {
                commandArgs << manifest
            }
        }

        if (instances) {
            commandArgs << "-i"
            commandArgs << instances
        }

        if (memory) {
            commandArgs << "-m"
            commandArgs << memory
        }

        if (disk) {
            commandArgs << "-k"
            commandArgs << disk
        }

        if (file) {
            File filePath = new File(workDir, file)

            commandArgs << "-p"
            if (filePath.exists() && filePath.isFile()) {
                commandArgs << filePath.absolutePath
            }
            else {
                commandArgs << file
            }
        }

        if (domain) {
            commandArgs << "-d"
            commandArgs << domain
        }

        if (subdomain) {
            commandArgs << "-n"
            commandArgs << subdomain
        }

        if (stack) {
            commandArgs << "-s"
            commandArgs << stack
        }

        if (timeout) {
            commandArgs << "-t"
            commandArgs << timeout
        }

        if (nostart == "true") {
            commandArgs << "--no-start"
        }

        if (noroute == "true") {
            commandArgs << "--no-route"
        }

        if (nomanifest == "true") {
            commandArgs << "--no-manifest"
        }

        if (nohostname == "true") {
            commandArgs << "--no-hostname"
        }

        if (randomroute == "true") {
            commandArgs << "--random-route"
        }

        runHelperCommand("[Action] Deploying CloudFoundry application", commandArgs)
    }

    void restartApp() {
        def app = props['application']

        setupEnvironment(api, organization, space)

        // Execute restart application
        def commandArgs = [cfFile, "restart", app]
        runHelperCommand("[Action] Executing CF restart APP", commandArgs)
    }

    void startApp() {
        def app = props['application']

        setupEnvironment(api, organization, space)

        // Execute start application
        def commandArgs = [cfFile, "start", app]
        runHelperCommand("[Action] Executing CF start APP", commandArgs)
    }

    void stopApp() {
        def app = props['application']

        setupEnvironment(api, organization, space)

        // Execute stop application
        def commandArgs = [cfFile, "stop", app]

        runHelperCommand("[Action] Executing CF stop APP", commandArgs)
    }

    void unbindService() {
        def service = props['service']
        def app = props['application']

        setupEnvironment(api, organization, space)

        // Execute unbind-service
        def commandArgs = [cfFile, "unbind-service", app, service]

        runHelperCommand("[Action] Executing CF unbind-service", commandArgs)
    }

    void unmapRoute() {
        def app = props['application']
        def domain = props['domain']
        def hostname = props['hostname']

        setupEnvironment(api, organization, space)

        // Execute unmap-route
        def commandArgs = [cfFile, "unmap-route", app]

        if (domain) {
            commandArgs << domain
        }

        if (hostname) {
            commandArgs << "-n"
            commandArgs << hostname
        }

        runHelperCommand("[Action] Executing CF unmap-route", commandArgs)
    }

    // set the api, organization, and space targets for the cf executable
    void setupEnvironment(def api, def organization, def space) {
        // Setup path
        def curPath = System.getenv("PATH")

        def pluginHome = new File(System.getenv("PLUGIN_HOME"))

        println "[Action] Setup of path using plugin home: " + pluginHome
        def binDir = new File(pluginHome, "bin")
        def newPath = curPath+":"+binDir.absolutePath
        helper.addEnvironmentVariable("PATH", newPath)

        // change location of config.json file
        def cfHomeDir
        def tempHome = false

        if (cfHome) {
            cfHomeDir = new File(cfHome)
        }
        else {
            cfHomeDir = new File(workDir, "cf-" + new Random().nextInt())
            cfHomeDir.mkdir()
            tempHome = true
        }

        if (!cfHomeDir.exists() && !cfHomeDir.isDirectory()) {
            throw new ExitCodeException("[Error] The CF_HOME directory '${cfHomeDir.toString()}' does not exist.")
        }

        println ("Setting CF_HOME to '${cfHomeDir}'")
        helper.addEnvironmentVariable("CF_HOME", cfHomeDir.absolutePath)

        // Set cf api
        def commandArgs = [cfFile, "api", api]

        if (selfSigned) {
            commandArgs << "--skip-ssl-validation"
        }

        runHelperCommand("[Action] Setting cf target api", commandArgs)

        // Authenticate with username and password
        if (!isAuthenticated && (username && password)) {
            if (organization && space) {
                commandArgs = [cfFile, "auth", username, password]

                runHelperCommand("[Action] Authenticating with CloudFoundry", commandArgs, false)
            }

            else {
                commandArgs = [cfFile, "login", "-u", username, "-p", password]

                runHelperCommand("[Action] Logging into CloudFoundry", commandArgs, false)
            }
            isAuthenticated = true
        }
        else {
            println "[Ok] Skipping Cloud Foundry authentication because the Username and/or Password properties are empty or authentication already occurred."
        }

        if (organization) {
            // Set target org
            commandArgs = [cfFile, "target", "-o", organization]
            runHelperCommand("[Action] Setting CloudFoundry target organization", commandArgs)
        }

        if (space) {
            // Ensure space exists. create-space does nothing if space exists
            commandArgs = [cfFile, "create-space", space]

            runHelperCommand("[Action] Creating CloudFoundry space", commandArgs)

            // Set target space
            commandArgs = [cfFile, "target", "-s", space]

            runHelperCommand("[Action] Setting CloudFoundry target space", commandArgs)
        }

        // Set dir to be recursively deleted after all inner files are created
        if (tempHome) {
            FileUtils.forceDeleteOnExit(cfHomeDir)
        }
    }

    /* methods used only during auto-discovery.. logout occurs after discovery is finished */

    // return a list of all organizations in the specified api endpoint
    def getOrgs() {
        setupEnvironment(api, null, null)
        def commandArgs = [cfFile, "orgs"]
        def unparsedOrgs = getOutput(commandArgs)
        def orgs = parseOrgsAndSpaces(unparsedOrgs)

        return orgs
    }

    // return a list of all spaces in the specified organization
    def getSpaces(def org) {
        setupEnvironment(api, org, null)
        def commandArgs = [cfFile, "spaces"]
        def unparsedSpaces = getOutput(commandArgs)
        def spaces = parseOrgsAndSpaces(unparsedSpaces)

        return spaces
    }

    def getCfVersion() {
        def setVersion = {
            it.out.close() // close stdin

            try {
                //set cf Version
                cfVersion = IOUtils.toString(it.in, "UTF-8")
            }
            catch (IOException ex) {
                println "[Error] I/O Error found when retrieving the cf client version. Please review the output log."
                ex.printStackTrace()
                System.exit(1)
            }
            catch (Exception ex) {
                println "[Error] Unknown found when retrieving the cf client version. Please review the output log."
                ex.printStackTrace()
                System.exit(1)
            }
        }

        def commandArgs = [cfFile, "-v"]
        println "----------------------------------------------"
        helper.runCommand("[Action] Running command: ${commandArgs.join(' ')}", commandArgs, setVersion)
        println "----------------------------------------------"
        println "CloudFoundary client version : ${cfVersion}"
        String VERSION_PATTERN = "(6\\.[0-9]{1,2}\\.[0-9]{1,2})"
        Pattern pattern = Pattern.compile(VERSION_PATTERN)
        Matcher matcher = pattern.matcher(cfVersion)
        if (matcher.find()) {
            cfVersion =  matcher.group()
        } else{
            cfVersion = "0"
        }
        cfVersion = cfVersion.toString().split("\\.").join('').toInteger()
    }

    // return all applications in the specified org and space
    def getApplications(def org, def space) {
        setupEnvironment(api, org, space)
        def commandArgs = [cfFile, "apps"]
        def unparsedApps = getOutput(commandArgs)

        return parseApps(unparsedApps)
    }

    // parse and return the output of organizations or spaces into lists
    def parseOrgsAndSpaces(def output) {
        def objects = []
        def outputLines = output.tokenize('\n')

        // remove first line of irrelevant output
        outputLines.remove(0)

        for (def object : outputLines) {
            object = object.trim()
            if (object != "name" && object != "") {
                objects.add(object)
            }
        }

        return objects
    }

    // return apps output as a list of application names
    def parseApps(def output) {
        def apps = []
        def outputLines = output.tokenize('\n')

        // length of longest application name formats the output
        def appLength = outputLines[2].indexOf("requested state")

        // remove all lines of irrelevant output
        outputLines.removeRange(0, 3)

        for (def line : outputLines) {
            def app = line.substring(0, appLength)
            apps << app
        }

        return apps
    }

    // run command and return output
    def getOutput(def cmdArgs) {
        def output

        def setOutput = {
            it.out.close() // close stdin
            def out = new PrintStream(System.out, true)
            def outputStream = new StringBuilder()
            try {
                it.waitForProcessOutput(outputStream, out)
            }
            catch (IOException ex) {
                println "[Error] I/O Error found when retrieving the command output. Please review the output log."
                ex.printStackTrace()
                System.exit(1)
            }
            catch (Exception ex) {
                println "[Error] Unknown error found when retrieving the command output. Please review the output log."
                ex.printStackTrace()
                System.exit(1)
            }
            finally {
                out.flush();
            }

            output = outputStream.toString()
        }

        println "----------------------------------------------"
        helper.runCommand("[Action] Running command: ${cmdArgs.join(' ')}", cmdArgs, setOutput)
        println "----------------------------------------------"

        return output
    }

    // logout of the Cloud Foundry Controller
    void logout() {
        if (isAuthenticated) {
            def commandArgs = [cfFile, "logout"]
            runHelperCommand("[Action] Logout from CloudFoundry system", commandArgs)
            isAuthenticated = false
        }
        else {
            println "[Warning] The user is still authenticated with Cloud Foundry."
        }
    }

    def runHelperCommand(def message, def baseArgs) {
        runHelperCommand(message, baseArgs, true)
    }

    def runHelperCommand(def message, def baseArgs, Boolean usePrefix) {
        def commandArgs = []
        if (interpreter && usePrefix) {
            commandArgs = ["sh", "-c"]
            commandArgs << baseArgs.join(" ")
        }
        else {
            commandArgs = baseArgs
        }

        try {
            println "----------------------------------------------"
            helper.runCommand(message, commandArgs)
            println "----------------------------------------------"
        } catch(ExitCodeException e){
            def errorMessage = "[Error] An error occurred while running the following command: ${commandArgs}"
            throw new ExitCodeException(errorMessage, e)
        }
    }

    def gatherEnvironmentVariables(String input) {
        def result = []
        def list = input.split("\n|,")*.trim() - ""
        for (int i = 0; i < list.size(); i++) {
            def temp = list[i].split("=", 2)
            if (temp.size() != 2) {
                throw new Exception("[Error] Invalid environment variables given: '${temp.toString()}'\n" +
                "Separate each the name and value with an equal sign. Separate each pair with a new line or comma.")
            }
            String key   = temp[0]
            String value = temp[1]

            if (!isWindows && value.contains(" ")) {
                value = '"'+ value + '"'
            }
            result << ["key": key, "value": value]
        }

        return result
    }
}
