import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeoutException;
import java.util.logging.Handler;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import org.json.JSONException;
import org.json.JSONObject;

import com.catalyst.Context;
import com.catalyst.config.ZCThreadLocal;
import com.catalyst.cron.CRON_STATUS;
import com.catalyst.cron.CronRequest;
import com.catalyst.cron.impl.DefaultCronRequest;
import com.catalyst.impl.DefaultContext;

public class JavacronInvoker {

	private static final Integer MESSAGE_LENGTH = 1500;

	public class LogHandler extends Handler {

		@Override
		public void publish(LogRecord record) {
			try {
				String exceptionMessage = "";
				if (record.getThrown() != null) {
					exceptionMessage = getStackTraceAsString(record.getThrown());
				}
				String message = new SimpleFormatter().format(record) + " " + exceptionMessage;
				if (message.length() > MESSAGE_LENGTH) {
					message = message.substring(0, MESSAGE_LENGTH);
				}

				System.out.println("[" + record.getLevel().getName() + "] : " + message);

			} catch (Exception e) {
				System.out.println(getStackTraceAsString(e));
			}

		}

		@Override
		public void flush() {
			// TODO Auto-generated method stub

		}

		@Override
		public void close() throws SecurityException {
			// TODO Auto-generated method stub

		}

	}

	private static String getStackTraceAsString(Throwable throwable) {
		StringWriter stringWriter = new StringWriter();
		throwable.printStackTrace(new PrintWriter(stringWriter));
		return stringWriter.toString();
	}

	private static HashMap<String, Object> jsonToMap(Object ob) throws Exception {
		if(ob instanceof String) {
			return jsonToMap((String) ob);
		}
		
		if (ob instanceof JSONObject) {
			return jsonToMap((JSONObject) ob);
		} 
		throw new Exception("Unexpected json input");
	}
	private static HashMap<String, Object> jsonToMap(String t) throws Exception {
		return jsonToMap(new JSONObject(t));
	}
	private static HashMap<String, Object> jsonToMap(JSONObject jObject) throws Exception {

		HashMap<String, Object> map = new HashMap<String, Object>();
		Iterator<?> keys = jObject.keys();

		while (keys.hasNext()) {
			String key = (String) keys.next();
			Object value = jObject.get(key);
			map.put(key, value);

		}
		return map;
	}

	private static void writeResponse(String response, Integer status, String invokerDir) throws Exception {
		// write response data
		String responseFilePath = Paths.get(invokerDir, "../user_res_body").toString();

		BufferedWriter responseWriter = new BufferedWriter(new FileWriter(responseFilePath));
		responseWriter.write(response);
		responseWriter.close();

		// write response meta
		String metaFilePath = Paths.get(invokerDir, "../user_meta.json").toString();

		BufferedWriter metaWriter = new BufferedWriter(new FileWriter(metaFilePath));
		JSONObject metaJson = new JSONObject();
		metaJson.put("statusCode", status);
		metaWriter.write(new JSONObject().put("response", metaJson).toString());
		metaWriter.close();
	}

	private static void throwAndExit(Exception err, int exitCode, String invokerDir) {
		try {
			String sStackTrace = getStackTraceAsString(err); // stack trace as a string
			if (exitCode == 532) {
				writeResponse("CODE_EXCEPTION", exitCode, invokerDir);
			} else if (exitCode == 500) {
				writeResponse("INTERNAL_SERVER_ERROR", 500, invokerDir);
			} else if (exitCode == 408) {
				writeResponse("TIMEOUT", exitCode, invokerDir);
			}
			System.out.println(sStackTrace);
		} catch (Exception e) {
			System.out.println(e);
		}
		System.exit(exitCode);
	}

	private static void setZCThreadLocalProject(HashMap<String, Object> project) throws Exception {

		JSONObject catalystConfig = new JSONObject();
		catalystConfig.put("project_id", project.get("x-zc-projectid").toString());
		catalystConfig.put("project_key", project.get("x-zc-project-key").toString());
		catalystConfig.put("project_domain", project.get("x-zc-project-domain").toString());
		catalystConfig.put("environment", project.get("x-zc-environment").toString());
		
		ZCThreadLocal.putValue("CATALYST_CONFIG", catalystConfig.toString());
	}

	private static void setZCThreadLocalAuth(HashMap<String, Object> auth) throws Exception {
		JSONObject catalystAuth = new JSONObject();
		
		String adminAuthHeaderType = auth.get("x-zc-admin-cred-type").toString();
		String adminAuthToken = auth.get("x-zc-admin-cred-token").toString();
		
		JSONObject adminAuth = new JSONObject();
		adminAuth.put((adminAuthHeaderType.equals("token")) ? "access_token" : "ticket", adminAuthToken); // No I18N
		catalystAuth.put("admin_cred", adminAuth);

		JSONObject clientAuth = new JSONObject();
		if(auth.containsKey("x-zc-user-cred-type") && auth.containsKey("x-zc-user-cred-token")) {
			String userAuthHeaderType = auth.get("x-zc-user-cred-type").toString();
			String userAuthToken = auth.get("x-zc-user-cred-token").toString();

			clientAuth.put((userAuthHeaderType.equals("token")) ? "access_token" : "ticket", userAuthToken); // No I18N
		}
		if(auth.containsKey("x-zc-user-type")) { // No I18N
			String userType = auth.get("x-zc-user-type").toString(); // No I18N
			clientAuth.put("user_type", userType); // No I18N
		}
		if (auth.containsKey("x-zc-cookie")) {
			String cookie = auth.get("x-zc-cookie").toString();
			clientAuth.put("cookie", cookie);
		}
		catalystAuth.put("client_cred", clientAuth);
		ZCThreadLocal.putValue("CATALYST_AUTH", catalystAuth.toString()); // No I18N

		// For backward compatibility, to be remove in future.
		if(auth.containsKey("x-zc-cookie")) {
			String cookie = auth.get("x-zc-cookie").toString();
			ZCThreadLocal.putValue("client_cookie", cookie); // No I18N
		}
		// to be removed in future
	}

	public static void main(String[] args) {
		String invokerDir = args[0];
		try {
			LogManager manager = LogManager.getLogManager();
			manager.reset();
			Logger rootLogger = manager.getLogger("");
			LogHandler handler = new JavacronInvoker().new LogHandler();
			rootLogger.addHandler(handler);

			Path requestFilePath = Paths.get(invokerDir, "../user_req_body");

			HashMap<String, Object> userBody = new HashMap<String, Object>();

			File f = new File(requestFilePath.toString());
			if (f.exists()) {
				StringBuilder sb = new StringBuilder();
				try (BufferedReader br = Files.newBufferedReader(requestFilePath)) {

					String line;
					while ((line = br.readLine()) != null) {
						sb.append(line).append("\n");
					}

				} catch (IOException e) {
					sb = new StringBuilder().append("{}");
				}

				try {
					userBody = jsonToMap(sb.toString());
				} catch (JSONException err) {
					userBody = new HashMap<String, Object>();
				}
			}

			HashMap<String, Object> target = jsonToMap(args[1]);
			String fnExeName = (String) target.get("index");
			String fnName = (String) target.get("name");
			String fnExePath = Paths.get(invokerDir, "../../", "functions", fnName).normalize().toString();

			HashMap<String, Object> queryData = jsonToMap(args[2]);
			String projectData = args[3];
			HashMap<String, Object> projectDataMap = jsonToMap(projectData);
			HashMap<String, Object> authData = jsonToMap(args[4]);

			File[] jarFiles = new File(fnExePath).listFiles(new FilenameFilter() {
				@Override
				public boolean accept(File dir, String name) {
					if(name.endsWith(".jar")) {
						return true;
					}
					return false;
				}
			});
			int jarCount = jarFiles.length;
			URL[] URLs = new URL[jarCount + 1];
			URLs[0] = new File(Paths.get(fnExePath).toString()).toURI().toURL();
			for (int i = 1; i <= jarCount; i++) {
				URLs[i] = jarFiles[i - 1].toURI().toURL();
			}
			URLClassLoader child = new URLClassLoader(URLs, JavacronInvoker.class.getClassLoader());
			Class<?> cls = Class.forName(fnExeName, true, child);

			setZCThreadLocalProject(projectDataMap);
			setZCThreadLocalAuth(authData);

			DefaultCronRequest defaultCron = new DefaultCronRequest();

			HashMap<String, Object> userData = new HashMap<String, Object>();

			userBody.forEach((key, value) -> {
				userData.put(key, value);
			});
			queryData.forEach((key, value) -> {
				userData.put(key, value);
			});

			defaultCron.setCronDetails(userData.get("cron_details") != null ? (JSONObject) userData.get("cron_details") : null);
			defaultCron.setRemainingExecutionCount(userData.get("remaining_count") != null ? (Integer) userData.get("remaining_count") : -1);
			defaultCron.setCronParam(jsonToMap(userData.get("data") != null ? userData.get("data") : new JSONObject()));
			defaultCron.setProjectDetails(userData.get("project_details") != null ? (JSONObject) userData.get("project_details") : new JSONObject(projectData));
			CronRequest cronRequest = defaultCron;

			DefaultContext defaultContext = new DefaultContext(cls.getName(), 900000L);
			Context context = defaultContext;

			Method runner = cls.getMethod("handleCronExecute", CronRequest.class, Context.class);
			CRON_STATUS cronStatus = null;

			if(System.getenv("DEBUG") != null && System.getenv("DEBUG").equals("false")) {
				Timer executionTimer = new Timer(true);
				executionTimer.schedule(new TimerTask() {
					@Override
					public void run() {
						throwAndExit(new TimeoutException("function execution timeout"), 408, invokerDir);
					}
				}, defaultContext.getMaxExecutionTimeMs());
			}

			try {	
				cronStatus = (CRON_STATUS) runner.invoke(cls.getDeclaredConstructor().newInstance(), cronRequest, context);
			} catch (Exception e) {
				throwAndExit(e, 532, invokerDir);
			}

			if(cronStatus == null) {
				writeResponse("UNINTENTIONAL_TERMINATION", 531, invokerDir);
			} else {
				int status = cronStatus.getStatus();
				writeResponse(cronStatus.name(), status == 500 ? 530 : status, invokerDir);
			}

			System.exit(0);
		} catch (Exception e) {
			throwAndExit(e, 500, invokerDir);
		}
	}
}
