package de.hsadmin.cliClientConnector;

import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base64;

import de.hsadmin.core.model.AbstractEntity;
import de.hsadmin.core.model.GenericModuleImpl;
import de.hsadmin.core.model.ModuleInterface;
import de.hsadmin.core.model.TicketAuthentication;
import de.hsadmin.core.model.Transaction;
import de.hsadmin.core.model.onetier.TicketValidator;

/**
 * actually this is the core of the CLI-Client. The other CLI-Client is just a
 * rather simple HTTP Client that calls this Servlet.
 * 
 * @author Christof Donat
 * 
 */
public class CLIClientConnectorServlet extends HttpServlet {
	
	private static final long serialVersionUID = 7150004719303750077L;
	public static final String version = "CLI Servlet 2.0.0 (2011/May/21 09:00 MEST)";
	
	private Map<String, Class<?>> componentmap;
	private Map<String, String> componentDescriptions;
	private ArgumentParser parser;

	/**
	 * Servlet initialization
	 */
	public void init(ServletConfig cfg) {
		// init ticket validator
		String validateURL = cfg.getInitParameter("proxyValidateUrl");
		String serviceURL = cfg.getInitParameter("proxyServiceUrl");
		TicketValidator.getInstance().initialize(validateURL, serviceURL);
		// find components
		String cstring = cfg.getInitParameter("Components");
		String[] components = cstring.split(",");

		componentmap = new HashMap<String, Class<?>>();
		componentDescriptions = new HashMap<String, String>();
		for (int i = 0; i < components.length; i++) {
			// get everything for this component and create an entry.
			try {
				// component class
				String classname = cfg.getInitParameter("ComponentClass_"
						+ components[i]);
				if (classname == null)
					throw new NullPointerException(
							"no class name found for Component "
									+ components[i]);
				Class<?> cls = Class.forName(classname);
				componentmap.put(components[i], cls);
				// description
				String descr = cfg.getInitParameter("ComponentDescription_"
						+ components[i]);
				if (descr != null)
					componentDescriptions.put(components[i], descr);
				else
					componentDescriptions.put(components[i], "");
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
		}
		// TODO: get username, password from http session
		parser = new ArgumentParser(this);
	}

	/**
	 * set values to a given entity Object
	 * 
	 * @param o
	 *            the entity Object
	 * @param set
	 *            Hashtable with names and values that sould be changed in o
	 */
	private void setValues(Object o, Map<String, String> set, ModuleInterface module) {
		Iterator<String> keys = set.keySet().iterator();
		while (keys.hasNext()) {
			String key = keys.next();
			String[] ns = key.split("[.]", 2);
			Object realO = o;
			for (int i = 0; i < ns.length - 1; i++) {
				Method[] m = realO.getClass().getMethods();
				boolean oFound = false;
				for (int j = 0; j < m.length; j++) {
					if (m[j].getName().toLowerCase().equals(
							"get" + ns[i].toLowerCase())) {
						try {
							realO = m[j].invoke(realO, (Object[]) null);
							oFound = (realO != null);
							break;
						} catch (Exception e) {
							new TechnicalException(e);
						}
					}
				}
				if (!oFound)
					break;
			}
			Method[] m = realO.getClass().getMethods();
			String value = set.get(key);
			for (int j = 0; j < m.length; j++) {
				if (!m[j].getName().toLowerCase().equals(
						"set" + ns[ns.length - 1].toLowerCase()))
					continue;
				String type = m[j].getParameterTypes()[0].getCanonicalName();
				try {
					if (type.equals("java.lang.String"))
						m[j].invoke(realO, value);
					else if (type.equals("java.lang.Integer") || type.equals("int"))
						m[j].invoke(realO, Integer.parseInt(value));
					else if (type.equals("java.lang.Long") || type.equals("long"))
						m[j].invoke(realO, Long.parseLong(value));
					else if (type.equals("java.lang.Boolean") || type.equals("boolean"))
						m[j].invoke(realO, Boolean.valueOf(value));
					else if (type.equals("java.util.Date")) {
						DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, Locale.GERMANY);
						m[j].invoke(realO, df.parse(value));
					} else {
						Method m2 = module.getClass().getMethod(
								"findByString", Class.class, String.class);
						Object entity = 
							m2.invoke(module, m[j].getParameterTypes()[0], value);
						if (entity != null)
							m[j].invoke(realO, entity);
						else
							throw new BusinessException(
									"not object found for '" + value + "'");
					}
				} catch (Exception e) {
					throw new TechnicalException(e); // TODO: this needs to be
														// more specific for
														// some cases
				}
			}
		}
	}

	private String hasGetter(Class<?> eType, String name) {
		String rval = null;
		String[] ns = name.split("[.]", 2);
		String n1 = ns[0];
		Method meth = null;

		for (Method m : eType.getMethods()) {
			String n = m.getName();
			if (n.startsWith("get")) {
				String fn = m.getName().substring(3).toLowerCase();
				if (fn != "class" && fn.equals(n1)) {
					meth = m;
					rval = fn;
					break;
				}
			}
		}
		if (meth != null) {
			Class<?> returnType = meth.getReturnType();
			if (rval != null && ns.length > 1 && meth != null)
				return hasGetter(returnType, ns[1]);
			if (returnType.getCanonicalName().startsWith("de.hsadmin.mods")) {
				try {
					if (returnType.getMethod("getName") != null) {
						return rval + ".name";
					}
				} catch (Exception e) {
					// no method found
				}
			}
		}
		return rval;
	}

	/**
	 * builds a query from a where clause and some objectIDs.
	 * 
	 * @param eType
	 *            The class of the Entity Object for which the string should be
	 *            built. We need this to call the static method
	 *            "createQueryFromStringKey"
	 * @param where
	 *            Hashtable with where parameters. Only objects which match all
	 *            where parameters are found
	 * @param oids
	 *            Only objects with one of these object IDs are found
	 * 
	 * @return queryString a query string that can be used to select the
	 *         required Objects
	 */
	private String buildQuery(Class<?> eType, Map<String, String> where,
			ArrayList<String> oids) {
		String rval = "";

		boolean first = true;
		Iterator<String> wkeys = where.keySet().iterator();
		while (wkeys.hasNext()) {
			String k = (String) wkeys.next();
			String kname = hasGetter(eType, k);
			if (kname != null) {
				rval += (first ? "" : " and ") 
					+ "(obj." + AbstractEntity.escapeString(kname) + " = '" + AbstractEntity.escapeString(where.get(k)) + "')";
				first = false;
			}
		}

		String rv = "";
		if (oids != null)
			try {
				Method m;
				m = eType.getMethod("createQueryFromStringKey", String.class);

				first = true;
				for (String s : oids) {
					rv += (first ? "" : " or ") + "("
							+ (String) m.invoke(eType, s) + ")";
					first = false;
				}
				if (rv != "" && rval != "")
					rval = rval + " and (" + rv + ")";
				else if (rv != "")
					rval = rv;
			} catch (Exception e) {
				throw new TechnicalException(e);
			}

		return (rval == "") ? null : rval;
	}

	public class FunctionNotKnownException extends Exception {
		private static final long serialVersionUID = -6330015688609717838L;
	}

	public class UnknownModuleException extends Exception {
		private static final long serialVersionUID = 696641072107896601L;
	}

	private Object callAdd(Class<?> cls, Map<String, String> set, ModuleInterface module) {
		Transaction transaction = module.getTransaction();
		transaction.beginTransaction();
		try {
			Method m = module.getClass().getMethod("add", AbstractEntity.class);
			Object o = cls.newInstance();
			setValues(o, set, module);
			m.invoke(module, o);
			transaction.commitTransaction();
			return null;
		} catch (Exception e) {
			transaction.rollbackTransaction();
			// TODO: this needs to be more specific, but how?
			throw new TechnicalException(e);
		}
	}

	@SuppressWarnings("unchecked")
	private List<Object> callSearch(Class<?> cls, Map<String, String> where,
			ArrayList<String> oids, ModuleInterface module) {
		try {
			Method m = module.getClass().getMethod("search", Class.class,
					String.class, String.class);
			return (List<Object>) m.invoke(module, cls, 
					buildQuery(cls, where, oids), null);
		} catch (Exception e) {
			throw new TechnicalException(e); // TODO: this needs to be more
												// specific, but how?
		}
	}

	// / checks wheather all 'oids' are in 'list'
	private void checkOids(List<AbstractEntity> list, List<String> oids) {
		List<String> oidsNotFound = new ArrayList<String>();
		for (String id : oids) {
			boolean found = false;
			for (AbstractEntity e : list) {
				String foundKey = e.createStringKey();
				if (foundKey.equals(id)) {
					found = true;
					break;
				}
			}
			if (!found)
				oidsNotFound.add(id);
		}
		if (oidsNotFound.size() > 0) {
			throw new OidsNotFoundException(oids);
		}
	}

	@SuppressWarnings("unchecked")
	private List<Object> callUpdate(Class<?> cls, Map<String, String> where,
			ArrayList<String> oids, Map<String, String> set,
			ModuleInterface module) {
		// better safe than sorry - alsd hso same behavior as UNIX rm
		if (where.size() == 0 && oids.size() == 0)
			throw new BusinessException(
					"better safe than sorry - 'update' needs a -w/--where-query or object id");
		Transaction tx = module.getTransaction();
		tx.beginTransaction();
		try {
			Method m = module.getClass().getMethod("search", Class.class,
					String.class, String.class);
			List<AbstractEntity> list = (List<AbstractEntity>) m.invoke(module, cls,
					buildQuery(cls, where, oids), null);
			checkOids(list, oids);
			Method m2 = module.getClass().getMethod("update", AbstractEntity.class);
			for (int i = 0; i < list.size(); i++) {
				AbstractEntity entity = list.get(i);
				tx.detach(entity);
				setValues(entity, set, module);
				m2.invoke(module, entity);
			}
			tx.commitTransaction();
		} catch (Exception e) {
			tx.rollbackTransaction();
			// TODO: this needs to be more specific, but how?
			throw new TechnicalException(e); 
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	private void callDelete(Class<?> cls, Map<String, String> where,
			ArrayList<String> oids, ModuleInterface module) {
		// better safe than sorry - also same behavior as UNIX rm
		if (where.size() == 0 && oids.size() == 0)
			throw new BusinessException(
					"better safe than sorry - 'update' needs a -w/--where-query or object id");
		Transaction tx = module.getTransaction();
		tx.beginTransaction();
		try {
			Method m = 
				module.getClass().getMethod("search", Class.class, String.class, String.class);
			List<AbstractEntity> list = 
				(List<AbstractEntity>) m.invoke(module, cls, buildQuery(cls, where, oids), null);
			checkOids(list, oids);
			Method m2 = module.getClass().getMethod("delete", AbstractEntity.class);
			for (int i = 0; i < list.size(); i++) {
				Object o = list.get(i);
				m2.invoke(module, o);
			}
			tx.commitTransaction();
		} catch (Exception e) {
			tx.rollbackTransaction();
			// TODO: this needs to be more specific, but how?
			throw new TechnicalException(e); 
		}
	}

	/**
	 * Call one of the EntitySessions methods
	 * 
	 * @param moduleName
	 *            Defines the Entity class that will be given to the
	 *            EntitySession
	 * @param function
	 *            Defines the method that will be called. The function can be
	 *            "add", "search", "update" and "delete".
	 * @param where
	 *            Reduces the set of Entity objects which the function operates
	 *            on to those matched by the where parameters. Only for
	 *            "search", "update" and "delete". Will be ignored otherwise.
	 * @param set
	 *            Set these values on all Objects - only for "add" and "update".
	 *            Will be ignored otherwise.
	 * @param oids
	 *            Works on Objects with these IDs. Only for "search", "update"
	 *            and "delete". Will be ignored for "add".
	 * @return foundObjects
	 * @throws FunctionNotKnownException
	 */
	public List<Object> callModule(String moduleName, String function,
			Map<String, String> where, Map<String, String> set,
			ArrayList<String> oids, ModuleInterface module)
			throws FunctionNotKnownException, UnknownModuleException {
		List<Object> rval = new ArrayList<Object>();

		// handle calls to the virtual module "modules"
		if (moduleName.equals("modules")) {
			// only search, no manipulation is possible
			if (function.equals("search")) {
				Iterator<?> m = componentDescriptions.keySet().iterator();
				while (m.hasNext()) {
					String mn = (String) m.next();
					rval.add(new ModuleModel(mn, componentDescriptions.get(mn)));
				}
			} else if (function.equals("version")) {
				rval.add(new VersionModel(version));
			} else {
				throw new FunctionNotKnownException();
			}
			return rval;
		}

		// find the component for the named module
		Class<?> cls = componentmap.get(moduleName);
		if (cls == null)
			throw (new UnknownModuleException());

		// call the appropriate methods
		if (function.equals("add"))
			rval.add(callAdd(cls, set, module));
		else if (function.equals("search")) {
			List<Object> r = callSearch(cls, where, oids, module);
			if (r != null)
				return r;
		} else if (function.equals("update")) {
			List<Object> r = callUpdate(cls, where, oids, set, module);
			if (r != null)
				return callUpdate(cls, where, oids, set, module);
		} else if (function.equals("delete")) {
			callDelete(cls, where, oids, module);
		} else
			throw (new FunctionNotKnownException());
		return rval;
	}

	/**
	 * handle put method
	 */
	public void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
		try {
			ModuleInterface module = null;
			Transaction tx = null;
			// check login
			String auth = req.getHeader("authorization");
			if (auth == null) {
				// no login information at all - get it
				resp.setHeader("WWW-Authenticate",
						"Basic realm=\"CLIClientConnector\"");
				resp.getOutputStream().println("login Failed.");
				resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
				return;
			} else {
				// parse login information
				String[] a = auth.split(" ", 2);
				String ticket = a[1];
				byte[] decoded = Base64.decodeBase64(ticket.trim().getBytes());
				String s = new String(decoded);
				a = s.split(":", 2);
				// try to log in
				String login = a[0];
				ticket = a[1];
				try {
					if (TicketAuthentication.getInstance().login(login, ticket)) {
						// login successful
						tx = new Transaction(login);
						module = new GenericModuleImpl(tx);

						// read arguments
						BufferedReader read = req.getReader();

						String tmpbuf;
						ArrayList<String> arguments = new ArrayList<String>();
						while ((tmpbuf = read.readLine()) != null) {
							arguments.add(tmpbuf);
						}

						// actually handle the request and write result to output
						String output = parser.parse(arguments, module);
						resp.getWriter().write(output);
					} else {
						resp.addHeader("X-hsadmin-error", "authentication failed");
					}
				} catch (Exception e) {
					resp.addHeader("X-hsadmin-error", "exception: " + e.getMessage());
				} finally {
					if (tx != null) {
						tx.close();
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
