package lu.tudor.santec.gecamed.agenda.ejb.session.beans;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;
import java.util.regex.Pattern;

import javax.ejb.EJB;
import javax.ejb.Stateful;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.jms.JMSException;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import lu.tudor.santec.gecamed.agenda.ejb.entity.beans.AgendaCalendar;
import lu.tudor.santec.gecamed.agenda.ejb.entity.beans.Appointment;
import lu.tudor.santec.gecamed.agenda.ejb.entity.beans.AppointmentType;
import lu.tudor.santec.gecamed.agenda.ejb.session.interfaces.AppointmentManager;
import lu.tudor.santec.gecamed.agenda.ejb.session.interfaces.DoctenaSyncInterface;
import lu.tudor.santec.gecamed.agenda.utils.AgendaAdminSettingsConstants;
import lu.tudor.santec.gecamed.agenda.utils.DoctenaFTPManager;
import lu.tudor.santec.gecamed.agenda.utils.IcalImportExporter;
import lu.tudor.santec.gecamed.core.ejb.session.beans.GECAMedSessionBean;
import lu.tudor.santec.gecamed.usermanagement.ejb.session.interfaces.LoginInterface;
import net.fortuna.ical4j.model.Calendar;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

/**
 * @author hermenj
 *
 * @version
 * <br>$Log: DoctenaSync.java,v $
 */
@Stateful
@TransactionManagement (TransactionManagementType.BEAN)
public class DoctenaSyncBean extends GECAMedSessionBean implements DoctenaSyncInterface {


	@PersistenceContext(unitName = "gecam")
	EntityManager em;

	@EJB
	LoginInterface login;
	
	@EJB
	AppointmentManager appManager;
	
	/**
	 * static logger for this class
	 */
	private static Logger logger = Logger.getLogger(DoctenaSyncBean.class.getName());
	
	public static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	public static final DateFormat TIMESTAMP = new SimpleDateFormat("yyyyMMddHHmmss");
	
	public static final String FILE_ENDING = ".ics";
	public static final String DOCTENA_FILENAME_PATTERN = Pattern.compile ("doctena_appointment_.*",Pattern.CASE_INSENSITIVE).pattern();
	public static final String BUSYTIME_FILENAME = 	"gecamed_appointment_";
	public static final String BUSYTIMES_FILENAME = "gecamed_appointments_";
	

	private DoctenaFTPManager ftp;
	private IcalImportExporter icalExporter;

	private String server;
	private Integer port;

	private String inbox;
	private String donebox;
	private String errorbox;

	private String username;
	private String password;

	private AgendaCalendar agenda;

	private AppointmentType doctenaType;

	private StringBuffer logText;

	private Integer syncMonths;
	
	private int pushedAppointments = 0;
	private int fetchedAppointments = 0;
	private int importedAppointments = 0;

	public void intSyncBean(AgendaCalendar agenda) throws Exception {
		
		this.agenda = agenda;
				
		this.logText= new StringBuffer();
		
		this.ftp = new DoctenaFTPManager();

		this.server = 	(String) this.login.getAdminSettingValue(AgendaAdminSettingsConstants.NAME, AgendaAdminSettingsConstants.DOCTENA_FTP_SERVER, AgendaAdminSettingsConstants.DEFAULT_DOCTENA_FTP_SERVER);
		this.port 	= 	(Integer)this.login.getAdminSettingValue(AgendaAdminSettingsConstants.NAME, AgendaAdminSettingsConstants.DOCTENA_FTP_PORT, AgendaAdminSettingsConstants.DEFAULT_DOCTENA_FTP_PORT);
		this.inbox 	= 	(String) this.login.getAdminSettingValue(AgendaAdminSettingsConstants.NAME, AgendaAdminSettingsConstants.DOCTENA_FTP_INBOX, AgendaAdminSettingsConstants.DEFAULT_DOCTENA_FTP_INBOX);
		this.donebox = 	(String) this.login.getAdminSettingValue(AgendaAdminSettingsConstants.NAME, AgendaAdminSettingsConstants.DOCTENA_FTP_DONEBOX, AgendaAdminSettingsConstants.DEFAULT_DOCTENA_FTP_DONEBOX);
		this.errorbox = (String) this.login.getAdminSettingValue(AgendaAdminSettingsConstants.NAME, AgendaAdminSettingsConstants.DOCTENA_FTP_ERRORBOX, AgendaAdminSettingsConstants.DEFAULT_DOCTENA_FTP_ERRORBOX);
		this.syncMonths = (Integer) this.login.getAdminSettingValue(AgendaAdminSettingsConstants.NAME, AgendaAdminSettingsConstants.DOCTENA_SYNCMONTHS, AgendaAdminSettingsConstants.DEFAULT_DOCTENA_SYNCMONTHS);
		
		this.username = agenda.getDoctenaLogin();
		this.password = agenda.getDoctenaPasswordClearText();

		Collection<AgendaCalendar> cals 	= appManager.getCalendars();
		Collection<AppointmentType> types 	= appManager.getAppointmentTypes(null, false);
		
		this.icalExporter = new IcalImportExporter(cals, types);
		
		for (AppointmentType appointmentType : types) {
			if (agenda.getId().equals(appointmentType.getCalendarId()) && appointmentType.getName().toLowerCase().equals("doctena.lu")) {
				this.doctenaType = appointmentType;
				break;
			}
		}
		if (this.doctenaType == null) {
			this.doctenaType = new AppointmentType();
			this.doctenaType.setName("doctena.lu");
			this.doctenaType.setIcon("type_doctena.png");
			this.doctenaType.setAppointmentTypeClass(0);
			this.doctenaType.setDuration(30);
			this.doctenaType.setCalendarId(agenda.getId());
			this.doctenaType = appManager.saveAppointmentType(null, this.doctenaType);
		}
		
	}
	
	public boolean connectFTP() throws Exception{
		if (this.ftp != null && this.ftp.isConnected()) {
			this.ftp.closeConnection();
		}
		try {
			return this.ftp.openConnection(server, port, username, password);		
		} catch (Exception e) {
			throw new Exception("Unable to connect to Doctena FTP: " + this.server + ":" + this.port + " with login " + this.username+"\n" , e);
		}
	}
	
	public boolean syncDoctena(boolean fullSync, boolean debug) {
		
		Level level = Level.DEBUG;
		
		// write out as Info 
		if (debug) {
			level = Level.INFO;
		}
		
		this.setTransactionTimeout(600); // 10 mins
		this.openTransaction();
		
		boolean ok = true;
		Date syncDate = new Date();
		try {
			if (! this.ftp.isConnected()) {
				this.ftp.openConnection(server, port, username, password);
			}			
		} catch (Exception e) {
			log(Level.WARN, "Unable to connect to Doctena FTP: " + this.server + ":" + this.port + " with login " + this.username+"\n", e);
			ok = false;
			return ok;
		}
		Date lastSync = this.agenda.getDoctenaLastSync();
		if (lastSync != null) {
			log(level, " last sync for Agenda: "+ this.agenda + " was " + df.format(lastSync));				
		} else {
			log(level, " last sync for Agenda: "+ this.agenda + " was NEVER!");
		}
		
		int importedApps = 0;
		// fetch ical appointment list from server
		try {
			log(level, "============ fetching doctena ============ ");
			Collection<String> appFiles = listAppointments();
			if (appFiles != null && appFiles.size() > 0) {
				log(level, appFiles.size() + " doctena appointments found for login: " + this.username + ", starting import");
				fetchedAppointments = appFiles.size();
				for (String appFile : appFiles) {
					// import Appointments 
					log(level, "importing File: " + appFile);
					try {
						importedApps += importAppointment(appFile);
						log(level, "File: " + appFile + "imported into Calendar");
						this.ftp.renameFile("/" + inbox + "/" + appFile, "/" + donebox + "/" + appFile);		
						log(level, "File: " + appFile + "moved to " + donebox);
					} catch (Exception e) {
						log(Level.WARN, "Error importing : " + appFile + " to calendar " + this.agenda, e);
						try {
							this.ftp.renameFile("/" + inbox + "/" + appFile, "/" + errorbox + "/" + appFile);	
							log(level, "File: " + appFile + "moved to " + errorbox);
						} catch (Exception e2) {
							log(Level.WARN, "Unable to move File: " + appFile + " to " + errorbox);
						}
					}
				}
				log(Level.INFO, "DOCTENA: Imported " + appFiles.size() + " appointments from doctena login: " + this.username);
			} else {
				log(level,"No doctena appointments found on Server for login: " + this.username);
			}
		} catch (Exception e) {
			ok = false;
			log(Level.WARN, "Error fetching Doctena Appointments for Account: " + this.username + " to calendar " + this.agenda, e);
		}
		
		// close the fetch transaction
		this.closeTransaction (true);
		// and open the push transaction
		this.openTransaction();
		
		// push busytimes to the server
		try {

			log(level, "============ pushing doctena for " + this.syncMonths + " months ============ ");
			if (fullSync) {
				log(Level.INFO, " Full Sync! for " + this.syncMonths + " months");
				lastSync = null;
			}
			
			GregorianCalendar c = new GregorianCalendar();
			c.setTime(new Date());
						
			// find and push busy times month by month, as all month at once might take to long and explode the server 
			for (int i = 0; i < this.syncMonths; i++) {
				Date from = c.getTime();
				c.add(GregorianCalendar.MONTH, 1);
				Date to = c.getTime();
				c.set(GregorianCalendar.HOUR_OF_DAY, 0);
				c.set(GregorianCalendar.MINUTE, 0);
				c.set(GregorianCalendar.SECOND, 0);
				c.set(GregorianCalendar.MILLISECOND, 0);
				
				log(level, "fetching Appointmens for "+ this.agenda + " from " + df.format(from) + " to " + df.format(to) 
						+ (lastSync!=null?" modified after " + df.format(lastSync):""));
				
				List<Appointment> apps = this.appManager.getAppointments(this.agenda.getId(), from, to, lastSync, true, debug);
				
				if (apps != null && apps.size() > 0) {
				
					// remove already existing doctena appointments from upload list
					Vector<Appointment> oldDoctenaAppointments = new Vector<Appointment>();
					for (Appointment appointment : apps) {
						if (doctenaType.getId().equals( appointment.getTypeId())) {
							oldDoctenaAppointments.add(appointment);
						}
					}
					apps.removeAll(oldDoctenaAppointments);
			
					log(level, "found "+apps.size()+ " new appointments in Calendar " + agenda); 

					this.pushBusyTimes(apps, lastSync==null);				
					log(Level.INFO, "DOCTENA: Pushed "+apps.size()+ " new appointments from Calendar " + agenda); 
					pushedAppointments = apps.size();
				}
			}
			
		} catch (Exception e) {
			ok = false;
			log(Level.WARN, "Error pushing Doctena BusyTimes from Calendar " + this.agenda + " for Account: " + this.username + " to Server", e);
		}
		
		agenda.setDoctenaLastSync(syncDate);
		agenda = appManager.saveCalendar(null, agenda);

		this.closeTransaction (true);
		
		importedAppointments = importedApps; 
		
		if (importedApps > 0 ) {
			try {
				appManager.sendCalendarUpdateMessage("doctenaImport", AppointmentManager.MSG_UPDATE_CALENDAR_EVENTS, agenda.getId(), null);
			} catch (JMSException e) {
				e.printStackTrace();
			}
		}
		
		return ok;
	}
	
	public String getLog() {
		return this.logText.toString();
	}
	
	public AgendaCalendar getAgenda() {
		return agenda;
	}
	
	protected void log(Level priority, String message) {
		log(priority, message, null);
	}
	
	protected void log(Level priority, String message, Exception e) {
		this.logText.append(message + "\n");
//		if (e != null) {
//			StringWriter errors = new StringWriter();
//			e.printStackTrace(new PrintWriter(errors));
//			this.logText.append(errors.toString()+"\n");
//		}
		logger.log(priority, message, e);
	}

	private void pushBusyTimes(Collection<Appointment> apps, boolean fullSync) throws Exception { 
		
		if (! this.ftp.isConnected()) {
			this.ftp.openConnection(server, port, username, password);
		}
		
		if (fullSync) {
			// one file with all Appointments
			Calendar calendar = this.icalExporter.exportBuystimes(apps);
			byte[] busyIcal = this.icalExporter.createICAL(calendar);	
			
			this.ftp.uploadFile(busyIcal, inbox + "/" + BUSYTIMES_FILENAME + getTimeStamp() + FILE_ENDING);
			
			log(Level.INFO, "pushed Doctena File with "+apps.size()+ " new appointments to server.");	
		} else {
			// one file per Appointment
			for (Appointment appointment : apps) {
				Collection<Appointment> oneApp = new ArrayList<Appointment>();
				oneApp.add(appointment);
				Calendar calendar = this.icalExporter.exportBuystimes(oneApp);
				byte[] busyIcal = this.icalExporter.createICAL(calendar);		

				this.ftp.uploadFile(busyIcal, inbox + "/" + BUSYTIME_FILENAME + appointment.getAppointmentUID() + FILE_ENDING);
			}
			log(Level.INFO, "pushed "+apps.size()+ " Doctena Files with new appointments to server.");
		}
	}
	
	
	private Collection<String> listAppointments() throws Exception {
		
		if (! this.ftp.isConnected()) {
			this.ftp.openConnection(server, port, username, password);
		}

		return this.ftp.getListOfAvailableAppointments(inbox, DOCTENA_FILENAME_PATTERN);		
	}
	
	private int importAppointment(String appFile) throws Exception {
		byte[] appICal = this.ftp.downloadAppointment(appFile);

		int importedApps = 0;
		
		Calendar calendar = this.icalExporter.readICS(appICal);
		Collection<Appointment> apps = this.icalExporter.importAppointments(calendar, agenda.getId(), doctenaType.getId());
		Appointment appointment = null;
		if (apps != null) {
			for (Appointment app : apps) {
				importedApps++;
				appointment = app;
				// check if we know this appointment
				HashMap<String, String> descVals = appointment.getDescriptionValues();
				if (descVals.get(Appointment.DESC_UID) != null) {
					Appointment oldApp = this.appManager.getAppointmentByCalIDandDesc(agenda.getId(), "%"+Appointment.DESC_UID+"="+descVals.get(Appointment.DESC_UID)+"%");
					if (oldApp != null) {
						// delete existing old appointment
						this.appManager.deleteAppointment("doctenaImport", oldApp);					
					}
				}
				
				if (descVals.get(Appointment.DESC_CANCEL) != null && ("TRUE".equals(descVals.get(Appointment.DESC_CANCEL).toUpperCase()))) {
					log(Level.INFO, "Canceled Appointment " + appointment);
					continue;
				} else {
					log(Level.INFO, "Updating Appointment " + appointment);
				}	
				
				// import Appointment(s)
				if (this.doctenaType != null) {
					appointment.setTypeId(this.doctenaType.getId());
				}
				appointment = this.appManager.saveAppointment(appointment);
				log(Level.INFO, "Saved Appointment " + appointment);
			}
		} else {
			logger.warn("no appointments found in ICS calendar data");
		}
		return importedApps;
	}

	public static final String getTimeStamp() {
		return TIMESTAMP.format(new Date());
	}

	public int getPushedAppointments() {
		return pushedAppointments;
	}

	public int getFetchedAppointments() {
		return fetchedAppointments;
	}

	public int getImportedAppointments() {
		return importedAppointments;
	}
}
