/*
 * Copyright 2012 LinkedIn, Inc
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package azkaban.scheduler;

import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.log4j.Logger;

import org.joda.time.DateTimeZone;
import org.joda.time.ReadablePeriod;

import azkaban.utils.db.AbstractJdbcLoader;
import azkaban.utils.GZIPUtils;
import azkaban.utils.JSONUtils;
import azkaban.utils.Props;


public class JdbcScheduleLoader extends AbstractJdbcLoader implements ScheduleLoader {
	private static Logger logger = Logger.getLogger(JdbcScheduleLoader.class);

	private EncodingType defaultEncodingType = EncodingType.GZIP;
	
	private static final String scheduleTableName = "schedules";

	private static String SELECT_ALL_SCHEDULES =
			"SELECT schedule_id, project_id, project_name, flow_name, status, first_sched_time, timezone, period, last_modify_time, next_exec_time, submit_time, submit_user, enc_type, schedule_options FROM " + scheduleTableName;
	
	private static String INSERT_SCHEDULE = 
			"INSERT INTO " + scheduleTableName + " ( project_id, project_name, flow_name, status, first_sched_time, timezone, period, last_modify_time, next_exec_time, submit_time, submit_user, enc_type, schedule_options) values (?,?,?,?,?,?,?,?,?,?,?,?,?)";
	
	private static String REMOVE_SCHEDULE_BY_KEY = 
			"DELETE FROM " + scheduleTableName + " WHERE schedule_id=?";
	
	private static String UPDATE_SCHEDULE_BY_KEY = 
			"UPDATE " + scheduleTableName + " SET status=?, first_sched_time=?, timezone=?, period=?, last_modify_time=?, next_exec_time=?, submit_time=?, submit_user=?, enc_type=?, schedule_options=? WHERE schedule_id=?";
	
	private static String UPDATE_NEXT_EXEC_TIME = 
			"UPDATE " + scheduleTableName + " SET next_exec_time=? WHERE schedule_id=?";

	public EncodingType getDefaultEncodingType() {
		return defaultEncodingType;
	}

	public void setDefaultEncodingType(EncodingType defaultEncodingType) {
		this.defaultEncodingType = defaultEncodingType;
	}
	
	public JdbcScheduleLoader(Props props) {
		super(props);
	}

	@Override
	public List<Schedule> loadSchedules() throws ScheduleManagerException {
		logger.info("Loading all schedules from db.");
		Connection connection = getConnection();

		QueryRunner runner = new QueryRunner();
		ResultSetHandler<List<Schedule>> handler = new ScheduleResultHandler();
	
		List<Schedule> schedules;
		
		try {
			schedules = runner.query(connection, SELECT_ALL_SCHEDULES, handler);
		} catch (SQLException e) {
			logger.error(SELECT_ALL_SCHEDULES + " failed.");

			DbUtils.closeQuietly(connection);
			throw new ScheduleManagerException("Loading schedules from db failed. ", e);
		} finally {
			DbUtils.closeQuietly(connection);
		}
		
		logger.info("Now trying to update the schedules");
		
		// filter the schedules
		for(Schedule sched : schedules) {
			if(!sched.updateTime()) {
				logger.info("Schedule " + sched.getScheduleName() + " was scheduled before azkaban start, skipping it.");
				schedules.remove(sched);
				removeSchedule(sched);
			}
			else {
				logger.info("Recurring schedule, need to update next exec time");
				try {
					updateNextExecTime(sched);
				} catch (Exception e) {
					e.printStackTrace();
					throw new ScheduleManagerException("Update next execution time failed.", e);
				} 
				logger.info("Schedule " + sched.getScheduleName() + " loaded and updated.");
			}
		}
		
		
				
		logger.info("Loaded " + schedules.size() + " schedules.");
		
		return schedules;
	}

	@Override
	public void removeSchedule(Schedule s) throws ScheduleManagerException {		
		logger.info("Removing schedule " + s.getScheduleName() + " from db.");

		QueryRunner runner = createQueryRunner();
		try {
			int removes =  runner.update(REMOVE_SCHEDULE_BY_KEY, s.getScheduleId());
			if (removes == 0) {
				throw new ScheduleManagerException("No schedule has been removed.");
			}
		} catch (SQLException e) {
			logger.error(REMOVE_SCHEDULE_BY_KEY + " failed.");
			throw new ScheduleManagerException("Remove schedule " + s.getScheduleName() + " from db failed. ", e);
		}
	}
	
	
	public void insertSchedule(Schedule s) throws ScheduleManagerException {
		logger.info("Inserting schedule " + s.getScheduleName() + " into db.");
		insertSchedule(s, defaultEncodingType);
	}

	public void insertSchedule(Schedule s, EncodingType encType) throws ScheduleManagerException {
		
		String json = JSONUtils.toJSON(s.optionsToObject());
		byte[] data = null;
		try {
			byte[] stringData = json.getBytes("UTF-8");
			data = stringData;
	
			if (encType == EncodingType.GZIP) {
				data = GZIPUtils.gzipBytes(stringData);
			}
			logger.debug("NumChars: " + json.length() + " UTF-8:" + stringData.length + " Gzip:"+ data.length);
		}
		catch (IOException e) {
			throw new ScheduleManagerException("Error encoding the schedule options. " + s.getScheduleName());
		}
		
		QueryRunner runner = createQueryRunner();
		try {
			int inserts =  runner.update( 
					INSERT_SCHEDULE, 
					s.getProjectId(),
					s.getProjectName(),
					s.getFlowName(), 
					s.getStatus(), 
					s.getFirstSchedTime(), 
					s.getTimezone().getID(), 
					Schedule.createPeriodString(s.getPeriod()), 
					s.getLastModifyTime(), 
					s.getNextExecTime(), 
					s.getSubmitTime(), 
					s.getSubmitUser(),
					encType.getNumVal(),
					data);
			
			long id = runner.query(LastInsertID.LAST_INSERT_ID, new LastInsertID());

			if (id == -1l) {
				throw new ScheduleManagerException("Execution id is not properly created.");
			}
			logger.info("Schedule given " + s.getScheduleIdentityPair() + " given id " + id);
			s.setScheduleId((int)id);
			
			if (inserts == 0) {
				throw new ScheduleManagerException("No schedule has been inserted.");
			}
		} catch (SQLException e) {
			logger.error(INSERT_SCHEDULE + " failed.");
			throw new ScheduleManagerException("Insert schedule " + s.getScheduleName() + " into db failed. ", e);
		}
	}
	
	@Override
	public void updateNextExecTime(Schedule s) throws ScheduleManagerException 
	{
		logger.info("Update schedule " + s.getScheduleName() + " into db. ");
		Connection connection = getConnection();
		QueryRunner runner = new QueryRunner();
		try {
			
			runner.update(connection, UPDATE_NEXT_EXEC_TIME, s.getNextExecTime(), s.getScheduleId()); 
		} catch (SQLException e) {
			e.printStackTrace();
			logger.error(UPDATE_NEXT_EXEC_TIME + " failed.", e);
			throw new ScheduleManagerException("Update schedule " + s.getScheduleName() + " into db failed. ", e);
		} finally {
			DbUtils.closeQuietly(connection);
		}
	}
	
	@Override
	public void updateSchedule(Schedule s) throws ScheduleManagerException {
		logger.info("Updating schedule " + s.getScheduleName() + " into db.");
		updateSchedule(s, defaultEncodingType);
	}
		
	public void updateSchedule(Schedule s, EncodingType encType) throws ScheduleManagerException {

		String json = JSONUtils.toJSON(s.optionsToObject());
		byte[] data = null;
		try {
			byte[] stringData = json.getBytes("UTF-8");
			data = stringData;
	
			if (encType == EncodingType.GZIP) {
				data = GZIPUtils.gzipBytes(stringData);
			}
			logger.debug("NumChars: " + json.length() + " UTF-8:" + stringData.length + " Gzip:"+ data.length);
		}
		catch (IOException e) {
			throw new ScheduleManagerException("Error encoding the schedule options " + s.getScheduleName());
		}

		QueryRunner runner = createQueryRunner();
	
		try {
			int updates =  runner.update( 
					UPDATE_SCHEDULE_BY_KEY, 
					s.getStatus(), 
					s.getFirstSchedTime(), 
					s.getTimezone().getID(), 
					Schedule.createPeriodString(s.getPeriod()), 
					s.getLastModifyTime(), 
					s.getNextExecTime(), 
					s.getSubmitTime(), 
					s.getSubmitUser(), 	
					encType.getNumVal(),
					data,
					s.getScheduleId());
			if (updates == 0) {
				throw new ScheduleManagerException("No schedule has been updated.");
			}
		} catch (SQLException e) {
			logger.error(UPDATE_SCHEDULE_BY_KEY + " failed.");
			throw new ScheduleManagerException("Update schedule " + s.getScheduleName() + " into db failed. ", e);
		}
	}

	
	private static class LastInsertID implements ResultSetHandler<Long> {
		private static String LAST_INSERT_ID = "SELECT LAST_INSERT_ID()";
		
		@Override
		public Long handle(ResultSet rs) throws SQLException {
			if (!rs.next()) {
				return -1l;
			}

			long id = rs.getLong(1);
			return id;
		}
		
	}
	
	public class ScheduleResultHandler implements ResultSetHandler<List<Schedule>> {
		@Override
		public List<Schedule> handle(ResultSet rs) throws SQLException {
			if (!rs.next()) {
				return Collections.<Schedule>emptyList();
			}
			
			ArrayList<Schedule> schedules = new ArrayList<Schedule>();
			do {
				int scheduleId = rs.getInt(1);
				int projectId = rs.getInt(2);
				String projectName = rs.getString(3);
				String flowName = rs.getString(4);
				String status = rs.getString(5);
				long firstSchedTime = rs.getLong(6);
				DateTimeZone timezone = DateTimeZone.forID(rs.getString(7));
				ReadablePeriod period = Schedule.parsePeriodString(rs.getString(8));
				long lastModifyTime = rs.getLong(9);
				long nextExecTime = rs.getLong(10);
				long submitTime = rs.getLong(11);
				String submitUser = rs.getString(12);
				int encodingType = rs.getInt(13);
				byte[] data = rs.getBytes(14);
				
				Object optsObj = null;
				if (data != null) {
					EncodingType encType = EncodingType.fromInteger(encodingType);

					try {
						// Convoluted way to inflate strings. Should find common package or helper function.
						if (encType == EncodingType.GZIP) {
							// Decompress the sucker.
							String jsonString = GZIPUtils.unGzipString(data, "UTF-8");
							optsObj = JSONUtils.parseJSONFromString(jsonString);
						}
						else {
							String jsonString = new String(data, "UTF-8");
							optsObj = JSONUtils.parseJSONFromString(jsonString);
						}	
					} catch (IOException e) {
						throw new SQLException("Error reconstructing schedule options " + projectName + "." + flowName);
					}
				}
				
				Schedule s = new Schedule(scheduleId, projectId, projectName, flowName, status, firstSchedTime, timezone, period, lastModifyTime, nextExecTime, submitTime, submitUser);
				if (optsObj != null) {
					s.createAndSetScheduleOptions(optsObj);
				}
				
				schedules.add(s);
			} while (rs.next());
			
			return schedules;
		}
		
	}
	
	private Connection getConnection() throws ScheduleManagerException {
		Connection connection = null;
		try {
			connection = super.getDBConnection(false);
		} catch (Exception e) {
			DbUtils.closeQuietly(connection);
			throw new ScheduleManagerException("Error getting DB connection.", e);
		}
		
		return connection;
	}
}