package org.inria.bmajwatcher.server;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;

import org.apache.log4j.Logger;
import org.inria.biomaj.session.bank.BiomajSQLQuerier;
import org.inria.biomaj.sql.SQLConnection;
import org.inria.biomaj.sql.SQLConnectionFactory;
import org.inria.bmajwatcher.client.services.SchedulingService;
import org.inria.bmajwatcher.server.utils.CronParser;
import org.jdom.Element;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * Service that handles scheduling.
 * The banks to be updated at a given time are specified in
 * the job description.
 * 
 * @author rsabas
 *
 */
public class SchedulingServiceImpl extends RemoteServiceServlet implements SchedulingService {

	private static final long serialVersionUID = 1L;
	
	private JobSerializer serializer;
	private static Logger log = Logger.getLogger(SchedulingServiceImpl.class);

	private Scheduler sched;
	private Map<String, CronTrigger> waitingTriggers = new HashMap<String, CronTrigger>();
	private static final String DEFAULT_GROUP = "default";
	
	private Map<String, Map<Date, Map<String, List<String>>>> sessionLogs = new HashMap<String, Map<Date, Map<String,List<String>>>>();
	private static final int CACHE_DURATION = 30000; // How long cache is kept in millis
	private static Map<String, Long> lastCallToSessionLogs = new HashMap<String, Long>(); // key is "<year>-<month>", value is last call date in millis
	
	private Map<String, Date> firstDayOfMonth = new HashMap<String, Date>();
	
	private static long MILLIS_IN_MONTH = 1000L * 3600L * 24L * 31L;
	
	
	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		
		System.setProperty("BIOMAJ_ROOT", getServletContext().getInitParameter("BIOMAJ_ROOT")); // Used in biomaj.jar
		
		serializer = JobSerializer.getInstance(getServletContext().getInitParameter("JOBS_LOCATION"));
		
		loadJobs(serializer.getJobs());
		/*
		createTrigger("test", "0/7 * * * * ?");
		addBankToTrigger("hoho", "test");*/
		
		try {
			sched.start();
		} catch (SchedulerException e) {
			log.error("Scheduling error", e);
		}
		
	}
	
	
	@Override
	public void destroy() {
		try {
			sched.shutdown(true);
		} catch (SchedulerException e) {
			e.printStackTrace();
		}
		super.destroy();
	}

	
	private void loadJobs(Element root) {
		
		SchedulerFactory sf = new StdSchedulerFactory();
		try {
			sched = sf.getScheduler();
		} catch (SchedulerException e) {
			log.error("Scheduler loading error", e);
		}
		
		List<?> users = root.getChildren(JobSerializer.USER);
		for (Object user : users) {
			Element usr = (Element) user;
			List<?> jobs = usr.getChildren(JobSerializer.JOB);
			for (Object job : jobs) {
				Element current = (Element) job;
				List<?> banks = current.getChildren();
				CronTrigger trigger = null;
				try {
					trigger = new CronTrigger(current.getAttributeValue(JobSerializer.JOB_NAME),
							DEFAULT_GROUP, current.getAttributeValue(JobSerializer.JOB_CRON));
					trigger.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING);
					if (banks.size() == 0) {	
						waitingTriggers.put(current.getAttributeValue(JobSerializer.JOB_NAME), trigger);
					} else {
						JobDetail jobDetail = new JobDetail(current.getAttributeValue(JobSerializer.JOB_NAME),
								DEFAULT_GROUP, UpdateJob.class);
						String description = "";
						for (Object bank : banks) {
							Element elt = (Element) bank;
							description += " " + elt.getAttributeValue(JobSerializer.BANK_NAME);
						}
						jobDetail.setDescription(description.trim());
						
						try {
							sched.scheduleJob(jobDetail, trigger);
						} catch (SchedulerException e) {
							log.error("Scheduling error", e);
						}
					}
				} catch (ParseException ex) {
					log.error("Invalid cron syntax", ex);
				}
			}
		}
	}


	@Override
	public void addBanksToTrigger(String[] banks, String triggerName) {
		for (String bankName : banks)
			log.debug("Add bank '" + bankName + "' to trigger '" + triggerName + "'");
		
		serializer.addBanksToJob(banks, triggerName);
		try {
			CronTrigger trig;
			JobDetail jobDetail;
			String description = "";
			
			if ((trig = waitingTriggers.get(triggerName)) == null) { // Existing trigger in the scheduler
				jobDetail = (JobDetail) sched.getJobDetail(triggerName, DEFAULT_GROUP).clone();
				description = jobDetail.getDescription();
				trig = (CronTrigger) sched.getTrigger(triggerName, DEFAULT_GROUP).clone();
				sched.deleteJob(triggerName, DEFAULT_GROUP);
			} else {
				waitingTriggers.remove(triggerName);
				jobDetail = new JobDetail(triggerName, DEFAULT_GROUP, UpdateJob.class);
			}
			
			for (String bankName : banks) {
				description += " " + bankName;
			}
			jobDetail.setDescription(description.trim());
			sched.scheduleJob(jobDetail, trig);
			
		} catch (SchedulerException e) {
			log.error("Scheduling error", e);
		}
	}

	@Override
	public int createTrigger(String user, String label, String cron) {
		log.debug("Create trigger : " + label + "," + cron + " for user '" + user  + "'");
		
		/*
		 * A trigger can only be registered to the scheduler with
		 * a job. As we don't yet have jobs to attach the trigger
		 * to, we temporarily store the trigger in a map until
		 * addBankToTrigger is called.
		 */
		
		if (!serializer.jobExists(label)) {
		
			try {
				CronTrigger ct = new CronTrigger(label, DEFAULT_GROUP, cron);
				ct.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING);
				waitingTriggers.put(label, ct);
				
				serializer.createJob(user, label, cron);
				// OK
				return 1;
			} catch (ParseException ex) {
				log.error("Cron syntax error", ex);
				// Syntax error
				return -1;
			}
		}
		
		// Job exists
		return 0;
	}

	@Override
	public void deleteTrigger(String label) {
		log.debug("Delete trigger : " + label);
		
		serializer.deleteJob(label);
		if (waitingTriggers.containsKey(label))
			waitingTriggers.remove(label);
		else {
			try {
				sched.deleteJob(label, DEFAULT_GROUP);
			} catch (SchedulerException e) {
				log.error("Scheduling error", e);
			}
		}
	}

	@Override
	public void removeBanksFromTrigger(String[] bankNames, String triggerName) {
		if (bankNames.length == 0)
			return;
		
		for (String bankName : bankNames)
			log.debug("Remove bank '" + bankName + "' from trigger : " + triggerName);
		
		serializer.removeBanksFromJob(bankNames, triggerName);
		
		try {
			
			JobDetail detail = (JobDetail) sched.getJobDetail(triggerName, DEFAULT_GROUP).clone();
			Trigger trig = (Trigger) sched.getTrigger(triggerName, DEFAULT_GROUP).clone();
			
			
			String[] banks = detail.getDescription().split(" ");
			if (banks.length > 1) { // Remove the bank in the description
				String newDesc = "";
				for (String bank : banks) {
					if (!arrayContains(bankNames, bank))
						newDesc += " " + bank;
				}
				detail.setDescription(newDesc.trim());
			} else { // Only one bank, unschedule
				detail.setDescription("");
			}
			sched.deleteJob(triggerName, DEFAULT_GROUP);
			sched.scheduleJob(detail, trig);
			
		} catch (SchedulerException e) {
			log.error("Scheduling error", e);
		}
	}
	
	private boolean arrayContains(String[] array, String toFind) {
		for (String s : array) {
			if (s.equals(toFind))
				return true;
		}
		return false;
	}

	@Override
	public boolean renameTrigger(String oldName, String newName) {
		log.debug("Rename trigger : " + oldName + " -> " + newName);
		
		
		if (serializer.renameJob(oldName, newName)) {
			
			
			if (waitingTriggers.containsKey(oldName)) {
				CronTrigger trg = waitingTriggers.get(oldName);
				trg.setName(newName);
				waitingTriggers.put(newName, trg);
				waitingTriggers.remove(oldName);
			} else {
				try {
					String expr = ((CronTrigger) sched.getTrigger(oldName, DEFAULT_GROUP)).getCronExpression();
					CronTrigger trig = new CronTrigger(newName, DEFAULT_GROUP, expr);
					trig.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING);
					JobDetail detail = (JobDetail) sched.getJobDetail(oldName, DEFAULT_GROUP).clone();
					detail.setName(newName);
					sched.unscheduleJob(oldName, DEFAULT_GROUP);
					sched.scheduleJob(detail, trig);
				} catch (SchedulerException e) {
					log.error("Scheduling error", e);
				} catch (ParseException e) {
					log.error("Parsing error", e);
				}
			}
			
			return true;
		}
		
		return false;
	}

	@Override
	public boolean setCron(String label, String cron) {
		log.debug("Set cron : " + label + "," + cron);
		
		if (waitingTriggers.containsKey(label)) {
			
			try {
				CronTrigger ct = new CronTrigger(label, DEFAULT_GROUP, cron);
				ct.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING);
				waitingTriggers.put(label, ct);
				return true;
			} catch (ParseException e) {
				log.error("Cron syntax error", e);
			}
			
		} else {
			try {
				String jobName = sched.getTrigger(label, DEFAULT_GROUP).getJobName();
				CronTrigger trig = new CronTrigger(label, DEFAULT_GROUP, jobName, DEFAULT_GROUP, cron);
				trig.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING);
				sched.rescheduleJob(label, DEFAULT_GROUP, trig);	
				serializer.setCron(label, cron);
				return true;
			} catch (SchedulerException e) {
				log.error("Scheduling error", e);
			} catch (ParseException e) {
				log.error("Cron syntax error", e);
			}
		}
		
		return false;
	}

	@Override
	public Map<String, List<String>> getJobs(String user) {
		return serializer.getJobsAsMap(user);
	}
	
	@Override
	public Map<Date, String> getJobsDates(String user, Date current, int range) {
		
		boolean admin = UserManagerServiceImpl.adminLogin().equals(user);
		
		Map<String, List<String>> jobs = serializer.getJobsAsMap(user);
		Set<String> keys = jobs.keySet();
		Map<Date, String> res = new HashMap<Date, String>();
		for (String key : keys) {
			String banks = "";
			for (String bank : jobs.get(key))
				banks += bank + ",";
			String[] split = key.split(":");
			List<Date> dates = CronParser.getDates(split[1], current, range);
			for (Date date : dates) {
				if (date.compareTo(new Date()) > 0) {
					if (banks.length() > 0) {
						res.put(date, split[0] + "_:_" + banks.substring(0, banks.length() - 1));
					} else {
						res.put(date, split[0] + "_:_");
					}
				}
			}
		}
		
		
		
		getMonthLogs(current, user, admin);
		// Look for previous sessions logs
		if (range == 0) { // Month view
			Calendar cal = Calendar.getInstance();
			cal.setTime(current);
			cal.set(Calendar.DAY_OF_MONTH, 1);
			cal.set(Calendar.HOUR_OF_DAY, 0);
			cal.set(Calendar.MINUTE, 0);
			cal.set(Calendar.SECOND, 0);
			
			Date today = new Date();
			while (cal.getTime().compareTo(today) <= 0 && cal.get(Calendar.DAY_OF_MONTH) < cal.getActualMaximum(Calendar.DAY_OF_MONTH)) {
				String logs = getLogsForDate(cal.getTime(), user, admin);
				if (!logs.trim().isEmpty())
					res.put(cal.getTime(), "Session logs_:_" + logs);
				cal.add(Calendar.DAY_OF_MONTH, 1);
			}
		} else {
			Calendar cal = Calendar.getInstance();
			cal.setTime(current);
			cal.set(Calendar.HOUR_OF_DAY, 0);
			cal.set(Calendar.MINUTE, 0);
			cal.set(Calendar.SECOND, 0);
			
			Date today = new Date();
			int i = 0;
			while (cal.getTime().compareTo(today) <= 0 && i++ < range) {
				
				String logs = getLogsForDate(cal.getTime(), user, admin);
				if (!logs.trim().isEmpty())
					res.put(cal.getTime(), "Session logs_:_" + logs);
				cal.add(Calendar.DAY_OF_MONTH, 1);
			}
		}
		
		return res;
	}
	
	
	private String getLogsForDate(Date date, String user, boolean admin) {
		
//		refreshUserLogs(user, date, admin);
		
//		getMonthLogs(date, user, admin);
		
		// Refresh the logs of the day
//		updateTodayLogs(date, user, admin);
		
		StringBuilder sb = new StringBuilder();
		if (sessionLogs.get(user).containsKey(date)) {
			sb.append("<ul>");
			for (String bank : sessionLogs.get(user).get(date).keySet()) {
				sb.append("<li>" + bank + "</li>");
				sb.append("<ul>");
				List<String> logs = sessionLogs.get(user).get(date).get(bank);
				for (String log : logs) {
					String[] split = log.split("_:_");
					if (split.length > 1 && !split[1].trim().isEmpty()) {
						String substr1 = split[1].substring(0, split[1].lastIndexOf('/'));
						String substr2 = substr1.substring(0, substr1.lastIndexOf('/'));
						int index = substr2.lastIndexOf('/') + 1;
						
						String relativePath = split[1].substring(index);
						String status = "";
						if (split[2] != null && split[2].equals("OK")) {
							status = "&nbsp;[<b><span style=\"color:green\">" + split[2] + "</span></b>]";
						} else if (split[2] != null && split[2].equals("KO")) {
							status = "&nbsp;[<b><span style=\"color:red\">" + split[2] + "</span></b>]";
						}
						sb.append("<li>" + split[0] + " : <a href=\"logs/" + relativePath + "\">" + split[1] + "</a>" + status + "</li>");
					} else {
						sb.append("<li>" + split[0] + " : <b>[DELETED]</b> : " + split[1] + "</li>");
					}
				}
				sb.append("</ul>");
			}
			sb.append("</ul>");
		}
		
		return sb.toString();
	}
	
	
	/**
	 * Returns logs for the whole month containing the given date.
	 * 
	 * @param date
	 * @param login logs for that login group
	 * @param is login actually admin
	 */
	private void getMonthLogs(Date date, String login, boolean admin) {
		
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		calendar.set(Calendar.DAY_OF_MONTH, 1);
		calendar.set(Calendar.HOUR_OF_DAY, 0);
		calendar.set(Calendar.MINUTE, 0);
		calendar.set(Calendar.SECOND, 0);
		calendar.set(Calendar.MILLISECOND, 0);
		
		// Was cache was updated recently ?
		String key = calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH);
		if (lastCallToSessionLogs.get(key) != null && (System.currentTimeMillis() - lastCallToSessionLogs.get(key)) < CACHE_DURATION) {
			return;
		}
		
		lastCallToSessionLogs.clear(); // Clear as we only log content for one month
		
		sessionLogs.put(login, new HashMap<Date, Map<String,List<String>>>());
		
		firstDayOfMonth.put(login, calendar.getTime());
		
		SQLConnection connection = SQLConnectionFactory.getConnection();
		Statement stat = connection.getStatement();
	
//		Date today = new Date();
		long end = calendar.getTimeInMillis() + MILLIS_IN_MONTH;
		/*
		if ((firstDayOfMonth.getTime() + millisInMonth) > today.getTime()) {
			calendar.clear();
			calendar.setTime(today);
			calendar.set(Calendar.HOUR_OF_DAY, 23);
			calendar.set(Calendar.MINUTE, 59);
			calendar.set(Calendar.SECOND, 59);
			calendar.set(Calendar.MILLISECOND, 0);
			calendar.add(Calendar.DAY_OF_MONTH, -1);
			end = calendar.getTimeInMillis();
		}*/
		
		String query = "";
		if (admin) {
			query = "SELECT startTime,logfile,status FROM session WHERE startTime >= '" + new Timestamp(calendar.getTimeInMillis()) + "' AND startTime < '" +
					new Timestamp(end) + "'";
		} else {
			query = "SELECT startTime,logfile,status FROM session WHERE startTime >= '" + new Timestamp(calendar.getTimeInMillis()) + "' AND startTime < '" +
					new Timestamp(end) + "' AND ref_idupdateBank IN " +
						"(SELECT idupdateBank FROM updateBank WHERE ref_idconfiguration IN " +
						"(SELECT idconfiguration FROM configuration WHERE ref_idbank IN " +
						"(SELECT idbank FROM bank WHERE ref_iduser IN " +
						"(SELECT ref_iduser FROM bw_user_has_group WHERE ref_idgroup IN " +
						"(SELECT ref_idgroup FROM bw_user_has_group WHERE ref_iduser=" +
						"(SELECT iduser FROM bw_user WHERE login='" + login + "'))))))";
		}
		
		ResultSet rs = connection.executeQuery(query, stat);
		populateSessionLogs(rs, login);
		SQLConnectionFactory.closeConnection(stat);
		
		lastCallToSessionLogs.put(key, System.currentTimeMillis());
	}
	
	private void populateSessionLogs(ResultSet rs, String user) {
		
		Calendar calendar = Calendar.getInstance();
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
		
		try {
			while (rs.next()) {
				Date d = rs.getDate(BiomajSQLQuerier.SESSION_START);
				calendar.clear();
				calendar.setTime(d);
				calendar.set(Calendar.HOUR_OF_DAY, 0);
				calendar.set(Calendar.MINUTE, 0);
				calendar.set(Calendar.SECOND, 0);
				calendar.set(Calendar.MILLISECOND, 0);
				
				Date time = new Date(rs.getTimestamp(1).getTime());
				
				String log = rs.getString(BiomajSQLQuerier.LOG_FILE); // .../bankName/sessionid/logfile
				if (!log.trim().isEmpty()) {
					String status = rs.getBoolean(BiomajSQLQuerier.SESSION_STATUS) ==  true ? "OK" : "KO";
					String substr1 = log.substring(0, log.lastIndexOf('/'));
					String substr2 = substr1.substring(0, substr1.lastIndexOf('/'));
					int index = substr2.lastIndexOf('/') + 1;
					String bankName = log.substring(index, substr2.length());
	//				int index = log.substring(0, log.lastIndexOf('/'));
					
					if (sessionLogs.get(user).containsKey(calendar.getTime())) {
						if (sessionLogs.get(user).get(calendar.getTime()).containsKey(bankName)) {
							sessionLogs.get(user).get(calendar.getTime()).get(bankName).add(sdf.format(time) + "_:_" + log + "_:_" + status);
						} else {
							List<String> list = new ArrayList<String>();
							list.add(sdf.format(time) + "_:_" + log + "_:_" + status);
							sessionLogs.get(user).get(calendar.getTime()).put(bankName, list);
						}
					} else {
						Map<String, List<String>> bankLogs = new HashMap<String, List<String>>();
						List<String> list = new ArrayList<String>();
						list.add(sdf.format(time) + "_:_" + log + "_:_" + status);
						bankLogs.put(bankName, list);
						sessionLogs.get(user).put(calendar.getTime(), bankLogs);
					}
				}
			}
		} catch (SQLException e) {
			log.error(e);
		}
	}


	@Override
	public boolean isStarted() {
		try {
			return !sched.isInStandbyMode();
		} catch (SchedulerException e) {
			e.printStackTrace();
			return false;
		}
	}


	@Override
	public boolean stop() {
		log.debug("Putting scheduler on stand-by");
		try {
			if (!sched.isInStandbyMode()) {
				sched.standby();
			}
			return true;
		} catch (SchedulerException e) {
			e.printStackTrace();
			log.error(e);
			return false;
		}
	}


	@Override
	public boolean start() {
		log.debug("Resuming scheduler");
		try {
			if (sched.isInStandbyMode()) {
				sched.start();
			}
			return true;
		} catch (SchedulerException e) {
			e.printStackTrace();
			log.error(e);
			return false;
		}
	}
	
}
