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.config.ZCThreadLocal;
import com.catalyst.integ.ZCIntegRequest;
import com.catalyst.integ.ZCIntegResponse;

public class JavaintegInvoker {

	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(String t) throws Exception {

		HashMap<String, Object> map = new HashMap<String, Object>();
		JSONObject jObject = new JSONObject(t);
		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 responseString, String invokerDir) throws Exception {
		String responseFilePath = Paths.get(invokerDir, "../user_res_body").toString();

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

	private static void throwAndExit(Exception err, int exitCode, String invokerDir) {
		try {
			String sStackTrace = getStackTraceAsString(err); // stack trace as a string
			if (exitCode > 0) {
				writeResponse(new JSONObject() {
					{
						put("Error", sStackTrace);
					}
				}.toString(), 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 JavaintegInvoker().new LogHandler();
			rootLogger.addHandler(handler);

			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();
			
			Path configJsonPath = Paths.get(fnExePath, "catalyst-config.json");

			JSONObject configJson = null;

			File configFile = new File(configJsonPath.toString()); // read catalyst-config.json
			if (configFile.exists()) {
				StringBuilder sb = new StringBuilder();
				try (BufferedReader br = Files.newBufferedReader(configJsonPath)) {

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

				} catch (IOException e) {
					throwAndExit(e, 500, invokerDir);
				}

				try {
					configJson = new JSONObject(sb.toString());
				} catch (JSONException ex) {
					throwAndExit(ex, 500, invokerDir);
				}
			}
			ZCThreadLocal.putValue("CONFIG_JSON", configJson);

			HashMap<String, Object> projectData = jsonToMap(args[3]);
			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, JavaintegInvoker.class.getClassLoader());
			Class<?> cls = Class.forName(fnExeName, true, child);

			setZCThreadLocalProject(projectData);
			setZCThreadLocalAuth(authData);
			
			JSONObject requestBody = new JSONObject(args[2]);
			requestBody.put("timestamp", System.currentTimeMillis());
			ZCIntegRequest integRequest = new ZCIntegRequest();
			integRequest.setRequestBody(requestBody);

			Method runner = cls.getMethod("runner", ZCIntegRequest.class);
			ZCIntegResponse integResponse = 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);
					}
				}, 900000L); // 15 mins
			}

			try {
				integResponse = (ZCIntegResponse) runner.invoke(cls.getDeclaredConstructor().newInstance(), integRequest);
			} catch (Exception e) {
				throwAndExit(e, 532, invokerDir);
			}
			
			if(integResponse == null) {
				System.exit(0);
			}
			JSONObject response = new JSONObject();
			response.put("Status", integResponse.getStatus());
			response.put("ContentType", integResponse.getContentType());
			response.put("ResponseBody", integResponse.getResponseBody());

			writeResponse(response.toString(), invokerDir);
			System.exit(0);
		} catch (Exception e) {
			throwAndExit(e, 0, invokerDir);
		}
	}
}
