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.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.basic.BasicIO;
import com.catalyst.basic.impl.DefaultBasicIO;
import com.catalyst.config.ZCThreadLocal;
import com.catalyst.impl.DefaultContext;

public class JavabioInvoker {

	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(JSONObject responseJson, 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));
		String responseStr = responseJson.toString();
		responseWriter.write(responseStr);
		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);
		metaJson.put("Content-Type", "application/json");
		metaJson.put("Content-Length", responseStr.length());
		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(new JSONObject() {
					{
						put("error", sStackTrace);
					}
				}, exitCode, invokerDir);
			} else if (exitCode == 500) {
				writeResponse(new JSONObject() {
					{
						put("error", sStackTrace);
					}
				}, 500, invokerDir);
			} else if (exitCode == 408) {
				writeResponse(new JSONObject() {
					{
						put("error", sStackTrace);
					}
				}, 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 JavabioInvoker().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]);
			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, JavabioInvoker.class.getClassLoader());
			Class<?> cls = Class.forName(fnExeName, true, child);

			setZCThreadLocalProject(projectData);
			setZCThreadLocalAuth(authData);

			DefaultBasicIO defaultBasic = new DefaultBasicIO();

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

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

			defaultBasic.setParameterMap(userData);
			BasicIO basicIO = defaultBasic;

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

			Method runner = cls.getMethod("runner", Context.class, BasicIO.class);
			try {
				runner.invoke(cls.getDeclaredConstructor().newInstance(), context, basicIO);
			} catch (Exception e) {
				throwAndExit(e, 532, invokerDir);
			}

			JSONObject responseJson = new JSONObject() {
				{
					put("output", defaultBasic.getBuilder().toString());
				}
			};

			writeResponse(responseJson, defaultBasic.getStatus() == null ? 200 : defaultBasic.getStatus(), invokerDir);
			System.exit(0);
		} catch (Exception e) {
			throwAndExit(e, 500, invokerDir);
		}
	}
}
