/*
* Licensed Materials - Property of IBM Corp.
* IBM UrbanCode Deploy
* (c) Copyright IBM Corporation 2011, 2017. 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.docker

import com.urbancode.air.CommandHelper
import com.urbancode.air.AirPluginTool

import org.apache.tools.ant.types.Commandline



public class DockerHelper {

    CommandHelper ch
    AirPluginTool apTool
    String swarmManagerAddr

    public DockerHelper(AirPluginTool apTool) {
        this.ch = new CommandHelper(new File("."))
        this.apTool = apTool
    }

    public static String escapeContainerName(String unescapedName) {
        // replace all of the un representable characters with an underscore
        String escapedName = (unescapedName =~ /[^a-zA-Z0-9_.-]/).replaceAll("_");
        // the first character is even more restrictive
        return (escapedName =~ /(^[^a-zA-Z0-9])/).replaceFirst('a$1')
    }

    public static String getImageTag(String dockerRegistryName, String image, String tag) {
        String result = ""
        if (dockerRegistryName) {
            result += dockerRegistryName + '/'
        }
        result += image
        if (tag) {
            result += ':' + tag
        }
        return result
    }

    // Login to IBM Container Services by running 'bx login' and 'bx cr login' processes
    public def icsLogin(Properties props) {
        def username = props['dockerRegistryUserName']
        def password = props['dockerRegistryUserPassword']
        def icsApi = props['icsApi']
        def icsSpace = props['icsSpace']
        def icsOrg = props['icsOrg']
        def icsApiKey = props['icsApiKey']
        def command = ["bx", "login"]

        if (icsApi) {
            command << "-a"
            command << icsApi
        }
        if (icsSpace) {
            command << "-s"
            command << icsSpace
        }
        if (icsOrg) {
            command << "-o"
            command << icsOrg
        }

        // Need to add support for '-c AccountID' at some point.
        // Would need changes to ImageRegistry resource role, Promote Image step,
        // and Docker source plugin.

        // Are we using API Key or user/password?
        if (icsApiKey) {
            command << "--apikey"
            command << icsApiKey
        }
        else {
            command << "-u"
            command << username
            command << "-p"
            command << password
        }

        CommandHelper bxLoginCH = new CommandHelper(new File("."))
        def raw
        bxLoginCH.runCommand("Logging into Bluemix", command, { proc ->
            proc.out.close() // close stdin
            proc.consumeProcessErrorStream(System.out) // forward stderr
            raw = proc.text.trim();
        })

        // Login to Bluemix Container Regsitry now that we're logged into Bluemix
        command = ["bx", "cr", "login"]
        CommandHelper bxCRLoginCH = new CommandHelper(new File("."))

        bxCRLoginCH.runCommand("Logging into Bluemix Container Registry", command, { proc ->
            proc.out.close() // close stdin
            proc.consumeProcessErrorStream(System.out) // forward stderr
            raw = proc.text.trim();
        })

    }

    // Get the manager or worker swarm token
    public String getSwarmToken(Properties props) {
        CommandHelper swarmTokenCH = new CommandHelper(new File("."))
        def globalOptions = props['globalOptions']?.trim() //These options, such as -H, go before the command
        String envVars = props['envVars']
        def commandOptions = props['commandOptions']
        def tokenType = props['swarmTokenType']
        def command = ["docker"]
        boolean quiet = false

        if (globalOptions) {
            command.addAll(splitArguments(globalOptions))
        }
        command.addAll(["swarm", "join-token"])
        if (commandOptions) {
            command.addAll(splitArguments(commandOptions))
            // Check if user specified the quiet option
            if (commandOptions =~ /--quiet/ || commandOptions =~ /-q/) {
                quiet = true
            }
        }
        command << tokenType
        if (envVars) {
            LinkedHashMap parsedEnvVars = parseNLDelimitedKeyValuePairs(envVars)
            setEnvVars(parsedEnvVars, swarmTokenCH)
        }

        def joinToken
        def rc, cmdOutput
        def sout = new ByteArrayOutputStream(), serr = new ByteArrayOutputStream()
        swarmTokenCH.ignoreExitValue(true)
        rc = swarmTokenCH.runCommand("Getting swarm join-token for ${tokenType}", command, { proc ->
            proc.waitForProcessOutput(sout, serr)
        })
        cmdOutput = sout.toString("UTF-8")
        if (quiet) {
            joinToken = cmdOutput
        }
        else {
            def tokenStart = cmdOutput.indexOf("--token")
            def tokenStr = cmdOutput.substring(tokenStart)
            def outputList = tokenStr.tokenize()
            joinToken = outputList[1]
            swarmManagerAddr = outputList[2]
        }
        println cmdOutput /* Write to stdout so we can see all cmd output */
        /* Display stderr if any present */
        if (serr.size() > 0) {
            println serr.toString("UTF-8")
        }
        /* Throw exception if the join-token cmd failed now that we have displayed any error messages */
        if (rc) {
            throw new Exception("Command failed with exit code: " + rc)
        }
        return joinToken
    }

    /* Return the host:port for the swarm manager node. Caller will need to have invoked getSwarmToken() previously. */
    public String getSwarmManagerAddr() {
        return swarmManagerAddr
    }

    // Create a new service
    public String serviceCreate(Properties props) {
        // Usage: docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]
        CommandHelper serviceCreateCH = new CommandHelper(new File("."))
        def globalOptions = props['globalOptions']?.trim() //These options, such as -H, go before the command
        String envVars = props['envVars']
        def commandOptions = props['commandOptions']
        def image = props['image']
        def imageCommand = props['imageCommand']
        def command = ["docker"]

        if (globalOptions) {
            command.addAll(splitArguments(globalOptions))
        }
        command.addAll(["service", "create"])
        if (commandOptions) {
            command.addAll(splitArguments(commandOptions))
        }
        command << image
        if (imageCommand) {
            command.addAll(splitArguments(imageCommand))
        }
        if (envVars) {
            LinkedHashMap parsedEnvVars = parseNLDelimitedKeyValuePairs(envVars)
            setEnvVars(parsedEnvVars, swarmTokenCH)
        }

        def serviceID
        def rc
        def sout = new ByteArrayOutputStream(), serr = new ByteArrayOutputStream()
        serviceCreateCH.ignoreExitValue(true)
        rc = serviceCreateCH.runCommand("Creating service for image ${image}", command, { proc ->
            proc.waitForProcessOutput(sout, serr)
        })
        serviceID = sout.toString("UTF-8")
        println serviceID
        /* Display stderr if any present */
        if (serr.size() > 0) {
            println serr.toString("UTF-8")
        }
        /* Throw exception if the service create cmd failed now that we have displayed any error messages */
        if (rc) {
            throw new Exception("Command failed with exit code: " + rc)
        }
        return serviceID
    }

    // Create a new secret
    public String secretCreate(Properties props) {
        // Usage: docker secret create [OPTIONS] SECRET file
        CommandHelper secretCreateCH = new CommandHelper(new File("."))
        def globalOptions = props['globalOptions']?.trim() //These options, such as -H, go before the command
        String envVars = props['envVars']
        def commandOptions = props['commandOptions']
        def secretName = props['secretName']
        def secretFile = props['secretFile']
        def command = ["docker"]

        if (globalOptions) {
            command.addAll(splitArguments(globalOptions))
        }
        command.addAll(["secret", "create"])
        if (commandOptions) {
            command.addAll(splitArguments(commandOptions))
        }
        command.addAll([secretName, secretFile])
        if (envVars) {
            LinkedHashMap parsedEnvVars = parseNLDelimitedKeyValuePairs(envVars)
            setEnvVars(parsedEnvVars, swarmTokenCH)
        }

        def secretID
        def rc
        def sout = new ByteArrayOutputStream(), serr = new ByteArrayOutputStream()
        secretCreateCH.ignoreExitValue(true)
        rc = secretCreateCH.runCommand("Creating secret ${secretName} from file ${secretFile}", command, { proc ->
            proc.waitForProcessOutput(sout, serr)
        })
        secretID = sout.toString("UTF-8")
        println secretID
        /* Display stderr if any present */
        if (serr.size() > 0) {
            println serr.toString("UTF-8")
        }
        /* Throw exception if the secret create cmd failed now that we have displayed any error messages */
        if (rc) {
            throw new Exception("Command failed with exit code: " + rc)
        }
        return secretID
    }

    // Create a new configuration file
    public String configCreate(Properties props) {
        // Usage: docker config create [OPTIONS] CONFIG file
        CommandHelper configCreateCH = new CommandHelper(new File("."))
        def globalOptions = props['globalOptions']?.trim() //These options, such as -H, go before the command
        String envVars = props['envVars']
        def commandOptions = props['commandOptions']
        def configName = props['configName']
        def configFile = props['configFile']
        def command = ["docker"]

        if (globalOptions) {
            command.addAll(splitArguments(globalOptions))
        }
        command.addAll(["config", "create"])
        if (commandOptions) {
            command.addAll(splitArguments(commandOptions))
        }
        command.addAll([configName, configFile])
        if (envVars) {
            LinkedHashMap parsedEnvVars = parseNLDelimitedKeyValuePairs(envVars)
            setEnvVars(parsedEnvVars, swarmTokenCH)
        }

        def configID
        def rc
        def sout = new ByteArrayOutputStream(), serr = new ByteArrayOutputStream()
        configCreateCH.ignoreExitValue(true)
        rc = configCreateCH.runCommand("Creating configuration file ${configName} from file ${configFile}", command, { proc ->
            proc.waitForProcessOutput(sout, serr)
        })
        configID = sout.toString("UTF-8")
        println configID
        /* Display stderr if any present */
        if (serr.size() > 0) {
            println serr.toString("UTF-8")
        }
        /* Throw exception if the config create cmd failed now that we have displayed any error messages */
        if (rc) {
            throw new Exception("Command failed with exit code: " + rc)
        }
        return configID
    }

    public def runDockerCommand (String dockerClientCommand, Properties props) {
        def globalOptions = props['globalOptions']?.trim() //These options, such as -H, go before the command
        String envVars = props['envVars']
        def commandOptions = props['commandOptions']
        def registry = props['registry']

        def container = props['container']
        def containerCommand = props['containerCommand']

        def image = props['image']
        def tag = props['tag']

        def email = props['dockerRegistryUserEmail']
        def username = props['dockerRegistryUserName']
        def password = props['dockerRegistryUserPassword']

        def stackName = props['stackName']
        def composeFile = props['composeFile']
        def bundleFile = props['bundleFile']

        def joinToken = props['joinToken']
        def managerAddress = props['managerAddress']

        def serviceNames = props['serviceNames']
        def secretNames = props['secretNames']
        def configNames = props['configNames']

        def args = ['docker']
        Map<String, Object> commandArgs = new HashMap<String, Object>()

        if (globalOptions) {
            args.addAll(splitArguments(globalOptions))
        }

        // Use splitArguments to handle when dockerClientCommand is "swarm init", "stack deploy", etc
        args.addAll(splitArguments(dockerClientCommand))

        if (commandOptions) {
            args.addAll(splitArguments(commandOptions))
        }

        if (envVars) {
            LinkedHashMap parsedEnvVars = parseNLDelimitedKeyValuePairs(envVars)
            setEnvVars(parsedEnvVars)
        }

        switch (dockerClientCommand) {
            //Usage: docker config rm CONFIG [CONFIG...]
            case "config rm":
                args.addAll(splitArguments(configNames))
                break

            //Usage: docker secret rm SECRET [SECRET...]
            case "secret rm":
                args.addAll(splitArguments(secretNames))
                break

            //Usage: docker service update [OPTIONS] SERVICE
            //Usage: docker service rm SERVICE [SERVICE...]
            //Usage: docker service scale SERVICE=REPLICAS [SERVICE=REPLICAS...]
            case ["service rm", "service scale", "service update"]:
                args.addAll(splitArguments(serviceNames))
                break

            //Usage: docker swarm join [OPTIONS] host:port
            case "swarm join":
                args.addAll(["--token", joinToken.trim()])
                args << managerAddress.trim()
                break

            //Usage: docker swarm init [OPTIONS]
            //Usage: docker swarm leave [OPTIONS]
            case ["swarm init", "swarm leave"]:
                // Nothing else to add to cmd
                break

            //Usage: docker stack deploy [OPTIONS] STACK
            //Usage: docker stack rm STACK
            case ["stack deploy", "stack rm"]:
                if (composeFile) {
                    args.addAll(["--compose-file", composeFile.trim()])
                }
                else if (bundleFile) {
                    args.addAll(["--bundle-file", bundleFile.trim()])
                }
                args << stackName
                break

            //Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
            case "run":
                if (container) {
                    args.addAll(["--name", container])
                }

                args << image
                if (containerCommand) {
                    args.addAll(splitArguments(containerCommand))
                }
                break

            //Usage: docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]
            case "tag":
                args << image
                args << tag
                break

            //Usage: docker pull [OPTIONS] NAME[:TAG|@DIGEST]
            //Usage: docker push [OPTIONS] NAME[:TAG]
            case ["push", "pull"]:
                args << image
                break

            //Usage: docker login [OPTIONS] [SERVER]
            case "login":
                args.addAll(["-u", username, "-p", password])
                if (email) {
                    args << "-e"
                    args << email
                }
                if (registry) {
                    args << registry
                }
                // Redundant unless logged in via AWS
                commandArgs.put("printCommand", args*.replace(password, "****"))
                break

            //Usage: docker logout [SERVER]
            case "logout":
                if (registry) {
                    args << registry
                }
                break

            //Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]
            //Usage: docker start [OPTIONS] CONTAINER [CONTAINER...]
            //Usage: docker rm [OPTIONS] CONTAINER [CONTAINER...]
            case ["stop", "start", "rm"]:
                args.addAll(splitArguments(container))
                break

        }

        commandArgs.put("message", "Executing docker command...")
        commandArgs.put("command", args)
        ch.runCommand(commandArgs)
        println "Command executed with a successful exit code"
    }

    private def splitArguments (def rawString) {
        return Commandline.translateCommandline(rawString.trim())
    }

    /**
     * Adds onto CommandHelper's environment variables
     *
     * @param   envVars     Accepts any LinkedHashMap
     */
    private void setEnvVars(LinkedHashMap envVars) {
        setEnvVars(envVars, ch)
    }

    private void setEnvVars(LinkedHashMap envVars, CommandHelper ch) {
        for (envVar in envVars) {
            String key = envVar.key.toString()
            String value = envVar.value.toString()
            if (key) {
                try {
                    ch.addEnvironmentVariable(key, value)
                } catch(UnsupportedOperationException e) {
                    println("Environment variable, name:${key} value:${value},"
                    + ' not loaded. System may not allow modifications to'
                    + ' environment variables or may forbid certain variable'
                    + ' names or values.')
                    System.exit(1)
                } catch(IllegalArgumentException e) {
                    println("Environment variable, name:${key} value:${value},"
                    + ' not loaded. System may not allow modifications to'
                    + ' environment variables or may forbid certain variable'
                    + ' names or values.')
                    System.exit(1)
                } catch(SecurityException e) {
                    println("Environment variables not loaded. A security"
                    + ' manager exists and its checkPermission method does not'
                    + ' allow access to the process environment.')
                    System.exit(1)
                }
            }
            else {
                println("Environment variable, name:${key} value:${value}, not loaded."
                + ' Must specify a name. For example: name=value or name:')
                System.exit(1)
            }
        }
    }

    /**
     * [WSP] *CHAR [WSP] '=' [WSP] *CHAR [WSP] parsed into a LinkedHashMap
     *
     * @param   textArea     Accepts any [WSP] *CHAR [WSP] '=' [WSP] *CHAR [WSP]
     * @return               A linked hashmap of all the parsed value key. Values may be null.
     */
    private LinkedHashMap parseNLDelimitedKeyValuePairs (
        String nlSerperatedValueEqualKey) {
        def result = [:]
        nlSerperatedValueEqualKey.eachLine { text ->
            def mappy = text.split('=', 2)
            if (mappy[0].trim() && mappy.size() == 2) {
                result[mappy[0].trim()] = mappy[1].trim()
            } else {
                println 'Please use \'=\' for name value seperation. For example: HOME=/user/'
                system.exit(1)
            }
        }
        return result
    }
}
