/*
* Licensed Materials - Property of IBM* and/or HCL**
* UrbanCode Deploy
* (c) Copyright IBM Corporation 2002, 2017. All Rights Reserved.
* (c) Copyright HCL Technologies Ltd. 2018. 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
*/
import com.urbancode.air.AirPluginTool;
import com.urbancode.air.CommandHelper;

final def workDir = new File('.').canonicalFile

def apTool = new AirPluginTool(args[0], args[1]);

final def props = apTool.getStepProperties();

final def errorRegexs = ["(?i)Error at line",
                                "(?i)invalid username/password; logon denied",
                                "(?i)the account is locked",
                                "ERROR:"];

// Gather Properties
def mode = props['mode'];
println "Mode : " + mode;
def oracleHome = props['oracleHome']
println "Oracle Home: ${oracleHome?:''}"
def sqlPlusExecutable = props['sqlPlusExecutable'];
println "Executable : " + sqlPlusExecutable;
def cntrlFileIncludes = props['controlFileIncludes'].split('\n');
def cntrlFileExcludes = props['controlFileExcludes'].split('\n');
def connectionIdUserToPasswordList = props['connectionIdUserToPassword'].split('\n');

// Iterate through the additionalEnvVars property and confirm validity
def unparsedEnvVars = props['additionalEnvVars'];
unparsedEnvVars = unparsedEnvVars.replace('\n', ",").split(',')
def envVars = unparsedEnvVars*.trim()
envVars.removeAll([""])
println "Environment Variables: "
for (var in envVars) {
    println "\t" + var
    def pair = var.split('=')
    if (pair.length > 2 || !pair[0].trim() || !var.contains('=')) {
        println "[Error] Invalid environment variable is given."
        println "[Possible Solution] Environment variable pair does not contain an '=' chracter to separate the variable and value."
        println "[Possible Solution] Environment variable pair contains more than one '=' character."
        println "[Possible Solution] The environment variable is empty or null."
        println "[Possible Solution] Delimit each variable and value with an '=' and separate each pair with a new line or comma."
        System.exit(1)
    }
}
// Iterate and parse the connectionId, username, and password strings
// Confirm both User name and password are given
// or
// Confirm if User name or password is not given, ORACLE_HOME is
def connectionIdToUserToPasswordMap = [:];
connectionIdUserToPasswordList.each { it ->
    // Example:
    // it            - username@connectionId=Password
    // firstParts[0] - username
    // firstParts[1] - connectionId=Password
    def firstParts = it.split("@",2);
    if (firstParts.length != 2) {
        System.out.println("[Error] Invalid User x Connection to password entry : " + it);
        System.out.println("[Possible Solution] Expected format is username@connectionId=Password or @connectionId.");
        System.exit(1);
    }
    else {
        def username = firstParts[0].trim();
        def connectionId
        def password
        // Example:
        // firstParts[1]  - connectionId=Password
        // secondParts[0] - connectionId
        // secondParts[1] - Password
        secondParts = firstParts[1].split("=");
        connectionId = secondParts[0];
        // Check that the secondPart has length of 1 or 2
        if (secondParts.length > 2) {
            System.out.println("[Error] Invalid User x Connection to password entry : " + it);
            System.out.println("[Possible Solution] Expected format is username@connectionId=Password or @connectionId.");
            System.exit(1);
        }
        else {
            // Length is 2. Assume, username and password given
            println secondParts
            if (secondParts.length == 2) {
                println "Length is 2"
                if (username) {
                    password = secondParts[1];
                }
                else {
                    System.out.println("[Error] The username was not specified: '${it}'.");
                    System.out.println("[Possible Solution] Enter a user name or password in the expected format 'username@connectionId=Password'.");
                    System.out.println("[Possible Solution] Enter a valid Oracle Home property and use the format '@connectionId'.");
                    System.exit(1);
                }
            }
            // Length is 1. Assume Oracle Wallet.
            else {
                println "Length is 1"
                if (username) {
                    System.out.println("[Error] The password was not specified: '${it}'.");
                    System.out.println("[Possible Solution] Enter a user name or password in the expected format 'username@connectionId=Password'.");
                    System.out.println("[Possible Solution] Enter a valid Oracle Home property and use the format '@connectionId'.");
                    System.exit(1);
                }
                System.out.println("[Warning] Username and Password not found. Defaulting to Oracle Wallet authorization for ${connectionId}.")
                password = "";
                // Oracle Wallet authorization but ORACLE_HOME was not specified
                if (!oracleHome) {
                    System.out.println("[Error] The Oracle Home property must be set if user name and password properties are empty: '${it}'.");
                    System.out.println("[Possible Solution] Enter a user name or password in the expected format 'username@connectionId=Password'.");
                    System.out.println("[Possible Solution] Enter a valid Oracle Home property and use the format '@connectionId'.");
                    System.exit(1);
                }
            }
        }
        def curUserPassMap = connectionIdToUserToPasswordMap[connectionId];
        if (curUserPassMap == null) {
            curUserPassMap = [:];
        }
        curUserPassMap[username] = password;
        connectionIdToUserToPasswordMap[connectionId]=curUserPassMap;
    }
}

println connectionIdToUserToPasswordMap;
println "Control File Includes : " + cntrlFileIncludes.join(' ');
println "Control File Excludes : " + cntrlFileExcludes.join(' ');

// Gathering Control Files files to import
def ant = new AntBuilder();

def cntrlFiles = ant.fileset(dir:".") {
    for (include in cntrlFileIncludes) {
        ant.include(name:include);
    }
    for (exclude in cntrlFileExcludes) {
        ant.exclude(name:exclude);
    }
}

println("Control Files Found : ");
cntrlFiles.each { cntrlFile ->
    println cntrlFile;
}

// Parse through control files for valid format
def parseLine = { line, lineNumber ->
    def parts = line.split("\t");
    if (parts.length != 4) {
        System.out.println("[Error] Illegal Control File. Line ${lineNumber} does not match expected: scriptName\\tconnectionId\\tuser\\tABORT_FLAG");
        System.out.println(line);
        System.exit(1);
    }
    def atts = [:];
    atts['script'] = parts[0];
    atts['connId'] = parts[1];
    atts['user']   = parts[2];
    // Set as empty string if user id == "wallet"
    if (atts['user'] == "wallet") {
        atts['user'] = ""
    }
    atts['abort']  = Boolean.valueOf(parts[3]);
    return atts;
}

// Retrieve password from connectionIdToUserToPasswordMap
def getPassword = { connId, user ->
    def password;
    def userToPassword = connectionIdToUserToPasswordMap[connId];
    if (userToPassword == null) {
        System.out.println("[Error] No passwords configured for connId ${connId}");
        System.exit(1);
    }
    else {
        password = userToPassword[user];
        if (password == null && user != null) {
            System.out.println("[Error] No passwords configured for connId ${connId} and user ${user.trim()?:'Oracle Wallet'}");
            System.exit(1);
        }
    }
    return password;
}

// Create and run the sqlplus command
def runSQLPlusScript = { user, password, connectionID, existingFileName, workingDirectory ->
    def result = 0;
    def ch = new CommandHelper(workingDirectory);
    for (var in envVars) {
        def pair = var.split('=')
        ch.addEnvironmentVariable(pair[0], pair[1]);
    }
    if (oracleHome) {
        ch.addEnvironmentVariable("ORACLE_HOME", oracleHome);
    }
    ch.ignoreExitValue(true);
    System.out.println("-----------------------------------------------------------------");
    def cmdArgs = [sqlPlusExecutable, "${user}/${password}@${connectionID}", '@'+existingFileName];
    def errorFound = false;
    println cmdArgs.join(" ")
    def exitValue = ch.runCommand("[Action] Running SQLPlus Command...", cmdArgs) { proc ->
        proc.withWriter { writer ->
            writer.write("exit\n");
        }
        BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.in));
        BufferedReader errInput = new BufferedReader(new InputStreamReader(proc.err));
        def th1 = Thread.start {
            def line
            while ((line = stdInput.readLine()) != null) {
                println line
                errorRegexs.each { regex ->
                    if (line =~ regex) {
                        errorFound = true;
                    }
                }
            }
        }
        def th2 = Thread.start {
            def line
            while ((line = errInput.readLine()) != null) {
                println line
                errorRegexs.each { regex ->
                    if (line =~ regex) {
                        errorFound = true;
                    }
                }
            }
        }
        th1.join();
        th2.join();
    }

    if (exitValue != 0) {
        result = exitValue;
    }
    else if (errorFound) {
        result = 1;
    }

    return result;
}

// Determine if the given connection has been confirmed before
def connectionAlreadyTested = { connId, user, testedConns ->
    def result = false;
    def userList = testedConns[connId];
    if (userList != null && userList.contains(user)) {
        result = true;
    }
    return result;
}

// After confirmed, the connection is added to the list of trusted connections
def addTestedConnection = { connId, user, testedConns ->
    def userList = testedConns['connId'];
    if (userList == null) {
        userList = [];
    }
    if (!userList.contains(user)) {
        userList.add(user);
    }
    testedConns[connId] = userList;
}

// Confirm is connection is valid
def testConnection = { connId, user, password, testedConns ->
   if (!connectionAlreadyTested(connId, user, testedConns)) {
       System.out.println("[Action] Testing connection to ${connId} with user ${user.trim()?:'Oracle Wallet'}");
       def tempSQLFile = File.createTempFile("select",".sql");
       tempSQLFile.delete();
       tempSQLFile = new File(workDir, tempSQLFile.name);
       tempSQLFile << "select 1\n";
       def exit = runSQLPlusScript(user, password, connId, tempSQLFile.name, workDir);
       addTestedConnection(connId, user, testedConns);
       tempSQLFile.delete();
       if (exit != 0) {
           System.out.println("[Error] Failure connecting to ${connId} as ${user}!");
           System.exit(exit);
       }
   }
}

// Verify the control file is written and formatted correctly and with valid credentials
def verifyControlFile = { controlFile, testedConns ->
    System.out.println();
    System.out.println();
    System.out.println();
    System.out.println("************************************************************************************************");
    System.out.println("[Action] Verifing control file " + controlFile.absolutePath);
    def curWorkDir = controlFile.parentFile;
    def lineNumber = 1;
    controlFile.eachLine { line ->
        def atts   = parseLine(line, lineNumber);
        def script = atts['script'];
        def connId = atts['connId'];
        def user   = atts['user'];
        File scriptFile = new File(curWorkDir, script);
        if (!scriptFile.isFile()) {
            System.out.println("[Error] Script " + script + " does not exist in expected location : " + scriptFile.absolutePath);
            System.out.println("As declared by control file " + controlFile.absolutePath + " on line " + lineNumber);
            System.exit(1);
        }
        else {
            def password = getPassword(connId, user);
            testConnection(connId, user, password, testedConns);
        }
        lineNumber ++;
    }
}

// Execute the given control file
def executeCntrFile = { controlFile ->
    System.out.println();
    System.out.println();
    System.out.println();
    System.out.println("************************************************************************************************");
    System.out.println("[Action] Executing Control File " + controlFile.absolutePath);
    def curWorkDir = controlFile.parentFile;
    def lineNumber = 1;
    controlFile.eachLine { line ->
        def atts   = parseLine(line, lineNumber);
        def script = atts['script'];
        def connId = atts['connId'];
        def user   = atts['user'];
        def abort  = atts['abort'];
        def password = getPassword(connId, user);
        File scriptFile = new File(curWorkDir, script);
        def exit = runSQLPlusScript(user, password, connId, scriptFile.name, scriptFile.parentFile);
        if (exit != 0) {
            System.out.println("[Error] Failure executing script " + script + " defined in control file " + controlFile.name + " at line:");
            System.out.println(lineNumber + " " + line);
            System.out.println("Error found in the control file at line number: " + lineNumber + ".")
            if (abort) {
                System.exit(exit);
            }
            else {
                System.out.println("[Warning] Abort set to false, ignoring and continuing...");
            }
        }
        lineNumber ++;
    }
}

// Iterate through all control files with their valid connections
def testedConnections = [:];
cntrlFiles.each { cntrFile ->
    verifyControlFile(cntrFile.file, testedConnections);
}

// Execute the control files if the mode is 'deploy'
if (mode == "deploy") {
    cntrlFiles.each { cntrFile ->
        executeCntrFile(cntrFile.file);
    }
}


System.exit(0);
