/*
* Licensed Materials - Property of IBM* and/or HCL**
* UrbanCode Deploy
* UrbanCode Build
* UrbanCode Release
* AnthillPro
* (c) Copyright IBM Corporation 2011, 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 java.util.Map;

import com.urbancode.air.AirPluginTool

import static java.lang.Integer.valueOf

import com.ibm.urbancode.zos.jes.JESJob;
import com.ibm.urbancode.zos.jes.JobUtil;
import com.ibm.jzos.PdsDirectory;
import com.ibm.jzos.ZFile;
import com.ibm.jzos.ZUtil;
import com.ibm.jzos.CatalogSearch;
import com.ibm.jzos.ZFileException;
import com.ibm.jzos.RecordReader;
import com.ibm.jzos.ZFileConstants;
import com.ibm.urbancode.zos.jes.passticket.PassTicketUtil;
import com.ibm.urbancode.zos.dataset.util.DataSetReader;

try{
	def apTool = new AirPluginTool(this.args[0], this.args[1])
	final def workDir = new File('.').canonicalFile
	def props = apTool.getStepProperties();

	final def mvsFilename = props['mvsFilename']?.trim();
	final def ussFilename = props['ussFilename']?.trim();
	final def jclString = props['jclString']?.trim();
	final def jobCard = props['jobCard']?.trim();
	final def explicitTokens = props['explicitTokens']?.trim();
	final def explicitTokensForeachJob = props['explicitTokensForeachJob']?.trim();
	final def waitForJob = Boolean.valueOf(props['waitForJob']?.trim());
	final def stopOnFail = Boolean.valueOf(props['stopOnFail']?.trim());
	final def timeout = props['timeout']?.trim();
	final def showOutput = props['showOutput']?.trim();
	final def cutOff = props['cutOff']?.trim();
	final def maxRC = props['maxRC']?.trim();

	final def hostname = props['hostname']?.trim();
	final def userid = props['userid']?.trim();
	final def password = props['password']?.trim();
	final def port = props['port']?.trim();
	final def usePassticket = Boolean.valueOf(props['usePassticket']?.trim());
	final def irrracfLibraryPath 	= props['irrracfLibraryPath']?.trim();
	final def APPL_ID="FEKAPPL";
	
	if(!hostname || hostname.length() < 1){
		throw new IllegalArgumentException("Host Name can not be empty.");
	}
	if(!userid || userid.length() < 1){
		throw new IllegalArgumentException("User ID can not be empty.");
	}
	userid = userid.toUpperCase();
	
	if(!password || password.length() < 1){
		if(usePassticket){
			println "Password is empty. Using PassTicket authentication."
			password = PassTicketUtil.generatePassTicket(userid,APPL_ID,irrracfLibraryPath);
		}else{
			throw new IllegalArgumentException("Password can not be empty.");
		}
	}
	if(!port || port.length() < 1){
		throw new IllegalArgumentException("Port can not be empty.");
	}
	
	if(!explicitTokensForeachJob){
		explicitTokensForeachJob = "";
	}
	if(!explicitTokens){
		explicitTokens = "";
	}
	
	def tempList = ("\n" + explicitTokensForeachJob).split("\n//");
	def explicitTokensForeachJobList = [];
	tempList.each{
		def line = it.trim();
		if(line && (line.length()>0) && (line!="//") ){
			explicitTokensForeachJobList.add(line);
		}
	}
	if(explicitTokensForeachJobList.size() == 0) {
		//if explicitTokensForeachJobList is empty, we submit the job once
		explicitTokensForeachJobList.add("");
	}
		
	def jobcount = 0;
	def failcount = 0;
	explicitTokensForeachJobList.each { 
		def mergedRule = explicitTokens + "\n" + it;
		mergedRule = mergedRule.trim();
		def jobUtil = new JobUtil(hostname,port,userid,password)
		if(waitForJob){
			try{
				int timeoutValue = valueOf(timeout as String);
				jobUtil.setTimeout(timeoutValue);
			}catch(NumberFormatException e){
				throw new IllegalArgumentException("Time Out isn't a number.");
			}
			try{
				int cutOffValue = valueOf(cutOff as String);
				jobUtil.setCutoff(cutOffValue);
			}catch(NumberFormatException e){
				throw new IllegalArgumentException("Cut Off isn't a number.");
			}
			try{
				int maxRCValue = valueOf(maxRC as String);
				jobUtil.setMaxReturnCode(maxRCValue);
			}catch(NumberFormatException e){
				throw new IllegalArgumentException("Max Return Code isn't a number.");
			}
			jobUtil.setShowOutput(showOutput);
		}

		Map jcls = [:];
		if(mvsFilename && mvsFilename.length() > 0){
			mvsFilename.eachLine {  
				DataSetReader dsr = new DataSetReader(it);
				jcls.putAll(dsr.getDataSets());
			}
		}else if(ussFilename && ussFilename.length() > 0){
			jcls.put(ussFilename, readUssFile(ussFilename));
		}else if(jclString && jclString.length() > 0){
			jcls.put("Step JCL",jclString);
		}else{
			throw new IllegalArgumentException("At least one JCL input (JCL Dataset, JCL File or JCL) must be specified. ");
		}
		
		if(jcls.size()>1 && explicitTokensForeachJobList.size() > 1){
			println "Replace Token sets for Each Job must be used with a single JCL."
			System.exit(1);
		}else if(jcls.size()>1){
			println "=================================== Total ${jcls.size()} jobs to submit ============================================================"
		}else if (explicitTokensForeachJobList.size() >1){
			println "=================================== Total ${explicitTokensForeachJobList.size()} jobs to submit ============================================================"
		}

		jcls.each{k,v ->
			jobcount ++;
			if(explicitTokensForeachJobList.size() > 1 || jcls.size() > 1){
				println "=================================== Submitting job${jobcount} : ${k} =================================================================="
			}else{
				println "=================================== Submitting job : ${k} =================================================================="
			}
			
			def success = submitOneJob(jobUtil, k, v, jobCard, mergedRule, waitForJob, apTool, jobcount);
			if(!success && stopOnFail){
				println "Step failed because Stop On Fail is turned on and job${jobcount} : ${k} failed."
				System.exit(1);
			}
			if(!success){
				failcount ++ ;
			}
		}	

	} // each job
	
	println "${jobcount} job(s) completed. ";
	apTool.setOutputProperty("jobCount", "${jobcount}");
	apTool.storeOutputProperties();
	
	if(failcount > 0){
		println "${failcount} job(s) failed. ";
		System.exit(1);
	}

	if(jobcount == 0){
		System.exit(1);
	}else{
		System.exit(0);
	}
}catch (Exception e) {
    println "Error Running Job: ${e.message}";
	e.printStackTrace();
    System.exit(1);
}


def submitOneJob(JobUtil jobUtil, String jclname, String jcl, jobCard, String mergedRule, waitForJob, apTool, jobcount){
	jobUtil.setJclString(jcl);
	
	if(mergedRule && mergedRule.length() >0){
		// replace tokens
		jobUtil.setJclString(replaceJCL(jobUtil.getJclString(),mergedRule));
	}

	def updatedContent = checkJobCard(jobUtil.getJclString(), jobCard);//check job card before sumit
	if(updatedContent != jobUtil.getJclString()){
		//default job card added
		jobUtil.setJclString(updatedContent)
	}
	def jobid = jobUtil.submitJob();
	
	apTool.setOutputProperty("JobId" + jobcount, jobid);
	apTool.setOutputProperty("JobJCL" + jobcount, jclname);
	//when multiple job is run, this is the last job's id
	apTool.setOutputProperty("JobId", jobid);
	apTool.setOutputProperty("JobJCL", jclname);
	
	//wait before check JCL Error
	Thread.sleep(500);
	if(jobUtil.isJclError(jobid)){
		def job = jobUtil.getJob(jobid, true);
		apTool.setOutputProperty("JobReturnCode", job.getReturnCode()==null?"":job.getReturnCode());
		apTool.setOutputProperty("JobReturnInfo", job.getReturnInfo()==null?"":job.getReturnInfo());
		apTool.setOutputProperty("JobReturnStatus", job.getReturnStatus()==null?"":job.getReturnStatus());
		
		jobUtil.disconnect();
		apTool.storeOutputProperties();
		return false;
	}
	
	if(waitForJob){
		def success = jobUtil.waitForJob(jobid);
		jobUtil.printLog(jobid);

		def job = jobUtil.getJob(jobid, true);
		apTool.setOutputProperty("JobReturnCode" + jobcount, job.getReturnCode()==null?"":job.getReturnCode());
		apTool.setOutputProperty("JobReturnInfo" + jobcount, job.getReturnInfo()==null?"":job.getReturnInfo());
		apTool.setOutputProperty("JobReturnStatus" + jobcount, job.getReturnStatus()==null?"":job.getReturnStatus());

		apTool.setOutputProperty("JobReturnCode", job.getReturnCode()==null?"":job.getReturnCode());
		apTool.setOutputProperty("JobReturnInfo", job.getReturnInfo()==null?"":job.getReturnInfo());
		apTool.setOutputProperty("JobReturnStatus", job.getReturnStatus()==null?"":job.getReturnStatus());

		jobUtil.disconnect();
		apTool.storeOutputProperties();
		return success;
	}
	apTool.storeOutputProperties();
	return true;
}


def readMvsFile(String name) {
	RecordReader reader = null;
	StringBuffer buffer = new StringBuffer();
	try{
		def dsName = ZFile.getSlashSlashQuotedDSN(name,true);
		if(!ZFile.exists(dsName)) {
			println "${name} does not exist.";
			System.exit(1);
		}
		
		reader= RecordReader.newReader(dsName, ZFileConstants.FLAG_DISP_SHR);
		byte[] recordBuf = new byte[reader.getLrecl()];
		int bytesRead;
		while ((bytesRead = reader.read(recordBuf)) > 0) {
			buffer.append(new String(recordBuf)).append("\n");
		}
	} catch(ZFileException zfileEx){
		println "Error reading : ${name}";
		if(92 == zfileEx.getErrno() && 152240184 == zfileEx.getErrorCode()){//Check if it is access issue
			println "${ZUtil.getCurrentUser()} can not read ${name}";
		}
		println zfileEx.message;
		
		if (reader != null) {
			try {
				reader.close();
			} catch (ZFileException zfe) {
				zfe.printStackTrace();
			} finally {
				System.exit(1);
			}
		}
		
		System.exit(1);
	} finally {
		if (reader != null) {
			try {
				reader.close();
			} catch (ZFileException zfe) {
				zfe.printStackTrace();  
			}
		}
	} 
	return buffer.toString();
}

def readUssFile(String name) {
	return new File(name).text;
}

def createStringAfterFillBlank(oriByteArr, replacedByteArr, TmpByteArrLength, remainLength, blank){
	byte [] currentBytesAfterTrancate = new byte[TmpByteArrLength];
	Arrays.fill(currentBytesAfterTrancate, blank);
	int withoutLineMetaInfoLength = replacedByteArr.length - remainLength;
	System.arraycopy(replacedByteArr, 0, currentBytesAfterTrancate, 0, withoutLineMetaInfoLength);//Copy 'real' content into temp array
	if(remainLength>0){
		System.arraycopy(oriByteArr, oriByteArr.length-remainLength, currentBytesAfterTrancate, TmpByteArrLength-remainLength, remainLength);//Copy row information content back to temp array, and also the switch line indicate
	}
	
	return new String(currentBytesAfterTrancate, ZUtil.getDefaultPlatformEncoding());
}

def createStringAfterTruncateBlank(oriByteArr, replacedByteArr, TmpByteArrLength, remainLength, blank, lineNumber, line){
	byte [] currentBytesAfterTrancate = new byte[TmpByteArrLength];
	Arrays.fill(currentBytesAfterTrancate, blank);
	
	int withoutLineMetaInfoLength = replacedByteArr.length - remainLength;
	int tmpStartOfmetaInfoLength = withoutLineMetaInfoLength;
	while(withoutLineMetaInfoLength-- >0 && replacedByteArr[withoutLineMetaInfoLength] == blank){};//Truncate whitespace
	if(withoutLineMetaInfoLength+1>71){//content bigger than 71 will cause issue.
		println "Error: line $lineNumber excceeds 71 bytes.";
		println " $lineNumber: $line";
		return(true);
	}else{
		System.arraycopy(replacedByteArr, 0, currentBytesAfterTrancate, 0, withoutLineMetaInfoLength+1);//Copy 'real' content into temp array
		if(remainLength>0){
			System.arraycopy(oriByteArr, oriByteArr.length-remainLength, currentBytesAfterTrancate, TmpByteArrLength-remainLength, remainLength);//Copy row information content back to temp array
		}
		
		return(new String(currentBytesAfterTrancate, ZUtil.getDefaultPlatformEncoding()));
	}
}

//Check the jcl for job statement, try to add the default job card if no job statement is found
//Return the original jcl or the jcl after adding default job statement
//If failed to come up with a valid JCL, exit the process. 
def checkJobCard(jcl, jobCard) {
	if(jobCard!=null){
		//remove any empty lines from job card
		jobCard = jobCard.trim()
	}
	
	if(jcl != null && jcl.length()>0){
		//Check job statement in the JCL
		def jclLines= jcl.split("\n")
		def firstLine = jclLines[0];
		if(!(firstLine =~ /\bJOB\b/) || !(firstLine.startsWith("//"))){
			println "No JOB statement found in the JCL."
			if(jobCard != null && jobCard.length() > 0){
				//check job statement in job card
				println "Adding default JOB statement"
				def jobCardLines = jobCard.split("\n")
				def firstLineJobCard = jobCardLines[0];
				if(!(firstLineJobCard =~ /\bJOB\b/) || !(firstLineJobCard.startsWith("//"))){
					println "Error: JOB statement must be provided in the first line."
					System.exit(1);
				}else{
					return jobCard + "\n" + jcl
				}
			}else{
				println "No default JOB statement found"
				println "Error: JOB statement must be provided in the first line.";
				System.exit(1);
			}
		}else{
			return jcl
		}
	}else{
		println "Error:JCL content is empty";
		System.exit(1);
	}
}
def replaceJCL(String jcl, String rules){
	//Write rules into properties file
	def Properties properties = new Properties();
    rules.eachLine {
        if (it && it.indexOf('->') > 0) {
            def index = it.indexOf('->')
            def propName = it.substring(0, index).trim()
            def propValue = index < (it.length() - 2) ? it.substring(index + 2) : ""
            properties.setProperty(propName, propValue)
//            println 'added: ' + propName + ':' + propValue
        }
        else if (it) {
            println "Found invalid explicit token $it - missing -> separator"
			System.exit(1);
        }
    }
	def propFile = new File(".").createTempFile("property", "properties");
	propFile.withOutputStream { outStream ->
	    properties.store(outStream, 'Auto generated property file')
	}

	// Write data set content into a temp file
	File f= new File(".").createTempFile("mvs", "tmpmvsfile");
	f.deleteOnExit();
	PrintWriter pw = new PrintWriter(f);
	pw.println(jcl);
	pw.close();

	def originalLines = jcl.split("\n");
	byte[] oriRecBuf = null;
	//Run ant replace
	def ant = new AntBuilder()
	String encode = ZUtil.getDefaultPlatformEncoding();
	ant.replace(
		file:f.canonicalPath,
		summary: 'true',
		defaultexcludes: 'no',
		replacefilterfile: propFile.canonicalPath,
		encoding: encode)
	String targetJcl =f.text;
	f.delete();
	propFile.delete();
	
	println "========================================================================================================================"
	println "JCL after replacing token."
	//check for exceeding 80
	def lines=targetJcl.split("\n")
	def i = 1
	byte[] recBuf = null;
	
	boolean contentExceedMaxNum = false;
	def linesAfterTrancateArr = [];
	def currentLineNumberMetaInfo = [];
	def whiteSpaceStr = new String(" ", ZUtil.getDefaultPlatformEncoding());
	byte[] whitespaceBytes= whiteSpaceStr.getBytes(ZUtil.getDefaultPlatformEncoding());
	byte byte4WhiteSpace = whitespaceBytes[0];
	
	for(line in lines){
		if(line.startsWith('//*')){//ignore comment statement
			linesAfterTrancateArr.add(line);
			i++;
			continue;
		}
		
		recBuf = line.getBytes(ZUtil.getDefaultPlatformEncoding());
		def columNum = recBuf.length;
		oriRecBuf = originalLines[i-1].getBytes(ZUtil.getDefaultPlatformEncoding());
		def oriColumNum = oriRecBuf.length;
		
		if(oriColumNum == 80) {//Means contains line number meta information
			if(columNum<80){
				linesAfterTrancateArr.add(createStringAfterFillBlank(oriRecBuf, recBuf, 80, 9, byte4WhiteSpace));
			}else if(columNum==80){
				linesAfterTrancateArr.add(line);
			}else if(columNum > 80){
				def afterTruncate = createStringAfterTruncateBlank(oriRecBuf, recBuf, 80, 9, byte4WhiteSpace, i, line);
				if(true == afterTruncate){
					contentExceedMaxNum = true;
				}else{
					linesAfterTrancateArr.add(afterTruncate);
				}
			}
		} else {//Not contains line number meta information
			if(oriColumNum == 72 && oriRecBuf[71] != byte4WhiteSpace){//contains continue line information
				if(columNum<72){
					linesAfterTrancateArr.add(createStringAfterFillBlank(oriRecBuf, recBuf, 72, 1, byte4WhiteSpace));
				}else if(columNum==72){
					linesAfterTrancateArr.add(line);
				}else if(columNum>72){
					def afterTruncate = createStringAfterTruncateBlank(oriRecBuf, recBuf, 72, 1, byte4WhiteSpace, i, line);
					if(true == afterTruncate){
						contentExceedMaxNum = true;
					}else{
						linesAfterTrancateArr.add(afterTruncate);
					}
				}
			}else{
				if(columNum<=71){
					linesAfterTrancateArr.add(line);
				}else if(columNum>71){
					def afterTruncate = createStringAfterTruncateBlank(oriRecBuf, recBuf, 72, 0, byte4WhiteSpace, i, line);
					if(true == afterTruncate){
						contentExceedMaxNum = true;
					}else{
						linesAfterTrancateArr.add(afterTruncate);
					}
				}
			}
		}
		
		i++
	}
	
	if(contentExceedMaxNum){
		println "Job not submitted because the content exceed 71 bytes after replacing tokens.";
		System.exit(1);
	}else{
		targetJcl = linesAfterTrancateArr.join("\n");
	}
	
	println "========================================================================================================================"
	println (targetJcl);
	
	
	return targetJcl;
}

System.exit(0);
