package ca.carleton.gcrc.couch.config;

import java.io.File;
import java.io.FileInputStream;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

import org.apache.log4j.Logger;

import ca.carleton.gcrc.couch.app.Document;
import ca.carleton.gcrc.couch.app.DocumentUpdateProcess;
import ca.carleton.gcrc.couch.app.impl.DocumentFile;
import ca.carleton.gcrc.couch.client.CouchClient;
import ca.carleton.gcrc.couch.client.CouchDb;
import ca.carleton.gcrc.couch.client.CouchDesignDocument;
import ca.carleton.gcrc.couch.client.CouchFactory;
import ca.carleton.gcrc.couch.config.impl.UpdateListener;
import ca.carleton.gcrc.couch.config.listener.ConfigListener;
import ca.carleton.gcrc.couch.config.listener.ConfigListenerCollection;
import ca.carleton.gcrc.couch.config.listener.ConfigWorker;
import ca.carleton.gcrc.couch.config.listener.CouchConfig;
import ca.carleton.gcrc.couch.config.listener.CouchConfigFactory;
import ca.carleton.gcrc.couch.export.ExportConfiguration;
import ca.carleton.gcrc.couch.fsentry.FSEntry;
import ca.carleton.gcrc.couch.fsentry.FSEntryFile;
import ca.carleton.gcrc.couch.onUpload.UploadListener;
import ca.carleton.gcrc.couch.onUpload.UploadWorker;
import ca.carleton.gcrc.couch.onUpload.UploadWorkerSettings;
import ca.carleton.gcrc.couch.onUpload.geojson.GeoJsonFileConverter;
import ca.carleton.gcrc.couch.onUpload.gpx.GpxFileConverter;
import ca.carleton.gcrc.couch.onUpload.mail.MailNotification;
import ca.carleton.gcrc.couch.onUpload.mail.MailNotificationImpl;
import ca.carleton.gcrc.couch.onUpload.mail.MailNotificationNull;
import ca.carleton.gcrc.couch.onUpload.multimedia.MultimediaFileConverter;
import ca.carleton.gcrc.nunaliit2.couch.replication.ReplicationWorker;
import ca.carleton.gcrc.olkit.multimedia.utils.MultimediaConfiguration;
import ca.carleton.gcrc.upload.OnUploadedListenerSingleton;
import ca.carleton.gcrc.upload.UploadServlet;
import ca.carleton.gcrc.upload.UploadUtils;

@SuppressWarnings("serial")
public class ConfigServlet extends HttpServlet {

	final static public String ATLAS_DESIGN_SERVER = "server";
	final static public String USER_DESIGN_AUTH = "_auth";

	final protected Logger logger = Logger.getLogger(this.getClass());
	
	private File configurationDirectory = null;
	private File fallbackConfigurationDirectory = null;
	private File rootDirectory = null;
	private File webInfDirectory = null;
	private String atlasName = null;
	private String serverName = null;
	private CouchClient couchClient = null;
	private CouchDb couchDb = null;
	private CouchDesignDocument couchDd = null;
	private CouchDb configDb = null;
	private CouchDesignDocument configDesign = null;
	private String couchReplicationUserName = null;
	private String couchReplicationPassword = null;
	private UploadWorker uploadWorker = null;
	private ReplicationWorker replicationWorker = null;
	private ConfigWorker configWorker = null;
	private MailNotification mailNotification = null;
	
	public ConfigServlet() {
		
	}

	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		
		logger.info("Initializing Couch Configuration");
		
		ServletContext servletContext = config.getServletContext();

		// Figure out configuration directories
		try {
			computeConfigurationDirectories(servletContext);
		} catch(ServletException e) {
			logger.error("Error while computing configuration directories",e);
			throw e;
		}
		
		// Instantiate CouchDb client
		try {
			initCouchDbClient(servletContext);
		} catch(ServletException e) {
			logger.error("Error while initializing couch client",e);
			throw e;
		}
		
		// Upload design documents for atlas server
		try {
			initAtlasServerDesignDocument(servletContext);
		} catch(ServletException e) {
			logger.error("Error while initializing design document for atlas server",e);
			throw e;
		}
		
		// Upload design documents for user auth
		try {
			initAuthDesignDocument(servletContext);
		} catch(ServletException e) {
			logger.error("Error while initializing design document for user auth",e);
			throw e;
		}
		
		// Upload documents to database
		try {
			initDatabaseDocuments(servletContext);
		} catch(ServletException e) {
			logger.error("Error while initializing design document for atlas server",e);
			throw e;
		}
		
		// Configure replication worker
		try {
			initReplicationWorker(servletContext);
		} catch(ServletException e) {
			logger.error("Error while initializing replication worker",e);
			throw e;
		}
		
		// Configure config listeners
		try {
			initCouchConfigListener(servletContext);
		} catch(ServletException e) {
			logger.error("Error while initializing couch config listener",e);
			throw e;
		}
		
		// Configure multimedia
		try {
			initMultimedia(servletContext);
		} catch(ServletException e) {
			logger.error("Error while initializing multimedia",e);
			throw e;
		}

		// Configure mail notification
		try {
			initMail(servletContext);
		} catch(ServletException e) {
			logger.error("Error while initializing mail notification",e);
			mailNotification = new MailNotificationNull();
		}
		
		// Configure upload
		try {
			initUpload(servletContext);
		} catch(ServletException e) {
			logger.error("Error while initializing upload",e);
			throw e;
		}
		
		// Configure export
		try {
			initExport(servletContext);
		} catch(ServletException e) {
			logger.error("Error while initializing export service",e);
			throw e;
		}
		
		// Upload logs on startup
		try {
			uploadLogs(servletContext);
		} catch(ServletException e) {
			logger.error("Error while uploading logs",e);
		}
		
		logger.info("Completed Couch Configuration");
	}

	private Properties loadProperties(String baseName, boolean loadDefault) throws ServletException {

		// Load up contribution properties file....
		Properties props = null;
		{
			File propFile = new File(configurationDirectory, baseName);
			if( false == propFile.exists() || false == propFile.isFile() ) {
				propFile = null;
			}
			if( null == propFile ) {
				propFile = new File(fallbackConfigurationDirectory, baseName);
				if( false == propFile.exists() || false == propFile.isFile() ) {
					propFile = null;
				}
			}
			if( loadDefault && null == propFile ) {
				propFile = new File(fallbackConfigurationDirectory, baseName+".default");
				if( false == propFile.exists() || false == propFile.isFile() ) {
					propFile = null;
				}
			}
			if( null == propFile ) {
				logger.error("Property file location can not be determined for: "+baseName);
			} else {
				logger.info("Reading properties from "+propFile.getAbsolutePath());
				FileInputStream fis = null;
				try {
					fis = new FileInputStream(propFile);
					props = new Properties();
					props.load(fis);
				} catch (Exception e) {
					logger.error("Unable to read properties from "+propFile.getAbsolutePath(),e);
					props = null;
				} finally {
					if( null != fis ) {
						try {
							fis.close();
						} catch (Exception e) {
							// Ignore
						}
					}
				}
			}
		}
		
		return props;
	}
	
	private void computeConfigurationDirectories(ServletContext servletContext) throws ServletException {
		
		if( null == servletContext ) {
			throw new ServletException("No servlet context provided");
		}
		
		// Parse environment variables
		{
			Map<String,String> envMap = System.getenv();
			for(String variable : envMap.keySet()) {
				String value = envMap.get(variable);
				
				//logger.info("env("+variable+"):"+value);
				
				if( "NUNALIIT_CONF_DIR".equalsIgnoreCase(variable) ) {
					File atlasConfigDir = new File(value);
					if( atlasConfigDir.exists() && atlasConfigDir.isDirectory() ) {
						// OK
						configurationDirectory = atlasConfigDir;
					} else {
						logger.error("Configuration directory associated with environment varible not found. Ignoring. "+atlasConfigDir.getAbsolutePath());
					}
				}
			}
		}
		
		// Find root directory
		{
			if( null != servletContext ) {
				String rootString = servletContext.getRealPath(".");
				rootDirectory = new File(rootString);
				if( false == rootDirectory.exists() ) {
					throw new ServletException("Can not find root directory");
				}
			}
		}
		
		// Find WEB-INF directory
		{
			if( null != servletContext ) {
				String webInfString = servletContext.getRealPath("./WEB-INF");
				webInfDirectory = new File(webInfString);
				if( false == webInfDirectory.exists() ) {
					throw new ServletException("Can not find WEB-INF directory");
				}
			}
		}
		
		// Look for ./WEB-INF/atlas.properties or ./WEB-INF/atlas.properties.default
		{
			File atlasPropsFile = new File(webInfDirectory, "atlas.properties");
			if( false == atlasPropsFile.exists() 
			 || false == atlasPropsFile.isFile() ) {
				atlasPropsFile = null;
			}
			if( null == atlasPropsFile ) {
				atlasPropsFile = new File(webInfDirectory, "atlas.properties.default");
				if( false == atlasPropsFile.exists() 
				 || false == atlasPropsFile.isFile() ) {
					atlasPropsFile = null;
				}
			}
			if( null != atlasPropsFile ) {
				// Read in properties
				Properties atlasProps = new Properties();
				logger.info("Reading atlas properties from "+atlasPropsFile.getAbsolutePath());
				FileInputStream fis = null;
				try {
					fis = new FileInputStream(atlasPropsFile);
					atlasProps.load(fis);
				} catch (Exception e) {
					logger.error("Unable to read atlas properties from "+atlasPropsFile.getAbsolutePath(),e);
				} finally {
					if( null != fis ) {
						try {
							fis.close();
						} catch (Exception e) {
							// Ignore
						}
					}
				}
				
				// Look for atlas.config.dir
				if( null == configurationDirectory 
				 && atlasProps.containsKey("atlas.config.dir") ) {
					String atlasConfigDirString = atlasProps.getProperty("atlas.config.dir");
					logger.info("Atlas config directory specified: "+atlasConfigDirString);
					File atlasConfigDir = new File(atlasConfigDirString);
					if( atlasConfigDir.exists() && atlasConfigDir.isDirectory() ) {
						// OK
						configurationDirectory = atlasConfigDir;
					} else {
						logger.error("Invalid configuration directory specified. Ignoring.");
					}
				}
				
				// Look for atlas.name
				if( atlasProps.containsKey("atlas.name") ) {
					atlasName = atlasProps.getProperty("atlas.name");
					logger.info("Atlas name specified: "+atlasName);
					if( null == configurationDirectory ) {
						File atlasConfigDir = new File("/etc/nunaliit2",atlasName);
						File atlasConfigLegacyDir = new File("/etc/nunaliit2/couchdb",atlasName);
						if( atlasConfigDir.exists() 
						 && atlasConfigDir.isDirectory() ) {
							// OK
							configurationDirectory = atlasConfigDir;
						} else if( atlasConfigLegacyDir.exists() 
								&& atlasConfigLegacyDir.isDirectory() ) {
							// OK
							configurationDirectory = atlasConfigLegacyDir;
						} else {
							logger.error("Configuration directory associated with name not found. Ignoring. "+atlasConfigDir.getAbsolutePath());
						}
					}
				}
			}
		}
		
		// Fall back on WEB-INF directory
		{
			fallbackConfigurationDirectory = webInfDirectory;
		}

		if( null == configurationDirectory ) {
			configurationDirectory = fallbackConfigurationDirectory;
		}
		
		if( null == configurationDirectory ) {
			throw new ServletException("Can not determine configuration directory");
		}
		if( null == fallbackConfigurationDirectory ) {
			throw new ServletException("Can not determine fallback configuration directory");
		}
		
		logger.info("Configuration directory: "+configurationDirectory.getAbsolutePath());
		logger.info("Configuration directory on fallback: "+fallbackConfigurationDirectory.getAbsolutePath());
	}

	private void initCouchDbClient(ServletContext servletContext) throws ServletException {
		
		// Load up configuration information
		Properties props = loadProperties("couch.properties", false);
		
		// Our user is the admin user
		if( props.containsKey("couchdb.admin.user") ) {
			props.setProperty("couchdb.user", props.getProperty("couchdb.admin.user"));
		}
		if( props.containsKey("couchdb.admin.password") ) {
			props.setProperty("couchdb.password", props.getProperty("couchdb.admin.password"));
		}
		
		// Read couch db replication information
		if( props.containsKey("couchdb.replication.user") ) {
			couchReplicationUserName = props.getProperty("couchdb.replication.user");
		}
		if( props.containsKey("couchdb.replication.password") ) {
			couchReplicationPassword = props.getProperty("couchdb.replication.password");
		}
		
		// Create Couch Server from properties
		CouchFactory factory = new CouchFactory();
		try {
			couchClient = factory.getClient(props);
			
		} catch(Exception e) {
			logger.error("Unable to get Couch Server",e);
			throw new ServletException("Unable to get Couch Server",e);
		}
		
		// Create database
		try {
			if( props.containsKey("couchdb.dbUrl") ) {
				couchDb = factory.getDb(couchClient, props.getProperty("couchdb.dbUrl"));
			} else if( props.containsKey("couchdb.dbName") ) {
				couchDb = couchClient.getDatabase(props.getProperty("couchdb.dbName"));
			} else {
				throw new Exception("dbUrl or dbName must be provided");
			}
		} catch(Exception e) {
			logger.error("Unable to build Couch Database",e);
			throw new ServletException("Unable to build Couch Database",e);
		}
		logger.info("CouchDb configured: "+couchDb.getUrl());
	}

	private void initAtlasServerDesignDocument(ServletContext servletContext) throws ServletException {
		// Find root directory for design document
		File ddDir = null;
		{
			ddDir = new File(webInfDirectory, "uploadDesignDoc");
			if( false == ddDir.exists() || false == ddDir.isDirectory() ) {
				ddDir = null;
			}
			if( null == ddDir ) {
				throw new ServletException("Unable to find design document source for upload");
			}
		}

		try {
			DocumentUpdateProcess updateProcess = new DocumentUpdateProcess(couchDb);
			updateProcess.setListener(UpdateListener._singleton);
			
			FSEntry fileEntry = new FSEntryFile(ddDir);
			Document doc = DocumentFile.createDocument(fileEntry);

			updateProcess.update(
					doc
					,DocumentUpdateProcess.Schedule.UPDATE_EVEN_IF_MODIFIED
					);
			
		} catch(Exception e) {
			throw new ServletException("Problem pushing design document: "+ATLAS_DESIGN_SERVER, e);
		}
		
		try {
			couchDd = couchDb.getDesignDocument(ATLAS_DESIGN_SERVER);
		} catch (Exception e) {
			throw new ServletException("Unable to get design document", e);
		}
	}

	private void initAuthDesignDocument(ServletContext servletContext) throws ServletException {
		// Find root directory for design document
		File ddDir = null;
		{
			ddDir = new File(webInfDirectory, "userDesignAuth");
			if( false == ddDir.exists() || false == ddDir.isDirectory() ) {
				ddDir = null;
			}
			if( null == ddDir ) {
				throw new ServletException("Unable to find design document source for user auth");
			}
		}
		
		try {
			CouchDb userDb = couchClient.getDatabase("_users");
			DocumentUpdateProcess updateProcess = new DocumentUpdateProcess(userDb);
			updateProcess.setListener(UpdateListener._singleton);
			
			FSEntry fileEntry = new FSEntryFile(ddDir);
			Document doc = DocumentFile.createDocument(fileEntry);

			updateProcess.update(
					doc
					,DocumentUpdateProcess.Schedule.UPDATE_EVEN_IF_MODIFIED
					);

		} catch(Exception e) {
			throw new ServletException("Problem pushing design document: "+USER_DESIGN_AUTH, e);
		}
	}

	private void initDatabaseDocuments(ServletContext servletContext) throws ServletException {

		// Create update process for database
		DocumentUpdateProcess updateProcess = null;
		try {
			updateProcess = new DocumentUpdateProcess(couchDb);
			updateProcess.setListener(UpdateListener._singleton);
		} catch (Exception e) {
			throw new ServletException("Unable to create update process", e);
		}

		// Find root directory for initializing documents
		{
			File documentsDir = null;
			{
				documentsDir = new File(webInfDirectory, "initializeDocs");
				if( false == documentsDir.exists() || false == documentsDir.isDirectory() ) {
					documentsDir = null;
				}
			}
			
			if( null == documentsDir ) {
				logger.error("Unable to find document directory for initializing");
			} else {
				String[] fileNames = documentsDir.list();
				for(String fileName : fileNames) {
					File file = new File(documentsDir, fileName);
		
					try {
						FSEntry fileEntry = new FSEntryFile(file);
						Document doc = DocumentFile.createDocument(fileEntry);

						updateProcess.update(doc);

					} catch(Exception e) {
						throw new ServletException("Problem pushing document: "+file.getAbsolutePath(), e);
					}
				}
			}
		}
		
		// Find root directory for updating documents
		{
			File documentsDir = null;
			{
				documentsDir = new File(webInfDirectory, "updateDocs");
				if( false == documentsDir.exists() || false == documentsDir.isDirectory() ) {
					documentsDir = null;
				}
			}
			
			if( null == documentsDir ) {
				logger.error("Unable to find document directory for updating");
			} else {
				String[] fileNames = documentsDir.list();
				for(String fileName : fileNames) {
					File file = new File(documentsDir, fileName);
		
					try {
						FSEntry fileEntry = new FSEntryFile(file);
						Document doc = DocumentFile.createDocument(fileEntry);

						updateProcess.update(
							doc
							,DocumentUpdateProcess.Schedule.UPDATE_EVEN_IF_MODIFIED
							);

					} catch(Exception e) {
						throw new ServletException("Problem pushing document: "+file.getAbsolutePath(), e);
					}
				}
			}
		}
	}

	private void initReplicationWorker(ServletContext servletContext) throws ServletException {

		if( null == couchClient ) {
			throw new ServletException("Replication worker requires a CouchDb client");
		}
		
		try {
			replicationWorker = new ReplicationWorker();
			replicationWorker.setCouchClient(couchClient);
			replicationWorker.start();
			
		} catch (Exception e) {
			throw new ServletException("Error starting replication worker",e);
		}
	}

	private void initCouchConfigListener(ServletContext servletContext) throws ServletException {
		
		// Load up configuration information
		Properties props = loadProperties("config.properties", true);
		
		// Interpret configuration
		serverName = props.getProperty("config.serverName");
		String dbName = props.getProperty("config.dbName");

		if( null == serverName ) {
			throw new ServletException("Can not determine server name for querying configuration information");
		}
		if( null == dbName ) {
			throw new ServletException("Can not determine database name for querying configuration information");
		}
		
		logger.info("Server Name: "+serverName);
		
		try {
			configDb = couchClient.getDatabase(dbName);
			configDesign = configDb.getDesignDocument("config");
			
			ConfigListenerCollection configListener = new ConfigListenerCollection();
			List<ConfigListener> collection = new Vector<ConfigListener>();
			collection.add( new ReplicationConfigListener(
					couchReplicationUserName
					,couchReplicationPassword
					,replicationWorker
					) );
			configListener.setCollection(collection);

			configWorker = new ConfigWorker();
			configWorker.setDesignDocument(configDesign);
			configWorker.setServerName(serverName);
			configWorker.setConfigListener(configListener);
			configWorker.start();
			
		} catch (Exception e) {
			throw new ServletException("Error starting config listener worker",e);
		}
		
		logger.info("CouchDb configured: "+couchDb.getUrl());
	}

	private void initMultimedia(ServletContext servletContext) throws ServletException {
		
		// Load up configuration information
		Properties props = loadProperties("multimedia.properties", true);
		
		MultimediaConfiguration.configureFromProperties(props);
	}

	private void initMail(ServletContext servletContext) throws ServletException {
		
		// Load up configuration information
		Properties props = loadProperties("mail.properties", true);
		
		// Create mail notification
		MailNotificationImpl mail = null;
		try {
			mail = new MailNotificationImpl();
			mail.setMailProperties(props);
			
		} catch(Exception e) {
			logger.error("Unable to configure mail notification",e);
		}

		mailNotification = mail;
	}

	private void initUpload(ServletContext servletContext) throws ServletException {
		
		Properties props = loadProperties("upload.properties", true);
		servletContext.setAttribute(UploadUtils.PROPERTIES_ATTRIBUTE, props);

		// Repository directory (this is where files are sent to)
		File repositoryDir = UploadUtils.getMediaDir(servletContext);
		
		UploadListener uploadListener = new UploadListener(couchDd,repositoryDir);
		servletContext.setAttribute(UploadServlet.OnUploadedListenerAttributeName, uploadListener);
		OnUploadedListenerSingleton.configure(uploadListener);
		
		try {
			UploadWorkerSettings settings = new UploadWorkerSettings(props);
			settings.setAtlasName(atlasName);
			
			uploadWorker = new UploadWorker(settings);
			uploadWorker.setDesignDocument(couchDd);
			uploadWorker.setMediaDir(repositoryDir);
			uploadWorker.setMailNotification(mailNotification);
			{
				MultimediaFileConverter mmPlugin = new MultimediaFileConverter(props);
				mmPlugin.setAtlasName(atlasName);
				uploadWorker.addConversionPlugin( mmPlugin );
			}
			uploadWorker.addConversionPlugin( new GpxFileConverter() );
			uploadWorker.addConversionPlugin( new GeoJsonFileConverter() );
			uploadWorker.start();
		} catch (Exception e) {
			logger.error("Error starting upload worker",e);
			throw new ServletException("Error starting upload worker",e);
		}
	}

	private void initExport(ServletContext servletContext) throws ServletException {
		
		try {
			ExportConfiguration config = new ExportConfiguration();
			config.setCouchDb(couchDb);
			CouchDesignDocument atlasDesign = couchDb.getDesignDocument("atlas");
			config.setAtlasDesignDocument(atlasDesign);
			servletContext.setAttribute(ExportConfiguration.CONFIGURATION_KEY, config);

		} catch(Exception e) {
			logger.error("Error configuring export service",e);
			throw new ServletException("Error configuring export service",e);
		}
	}

	private void uploadLogs(ServletContext servletContext) throws ServletException {
		// Load up configuration information
		Properties props = loadProperties("install.properties", true);
		
		if( false == props.containsKey("cron.working.dir") ) {
			logger.error("Property cron.working.dir not set in install.properties. Not uploading logs.");
			return;
		}
		
		// See if directory where CRON scripts are located can be found
		File workingDir = new File( props.getProperty("cron.working.dir") );
		File cronDir = new File(workingDir,"cron");
		if( false == cronDir.exists() || false == cronDir.isDirectory() ) {
			logger.error("Can not find cron directory at: "+cronDir.getAbsolutePath()+". Not uploading logs.");
			return;
		}
		
		// See if CRON logs are present
		File cronLogFile = new File(cronDir, "cron.log");
		if( false == cronLogFile.exists() || false == cronLogFile.isFile() ) {
			logger.error("Can not find cron log file at: "+cronLogFile.getAbsolutePath()+". Not uploading logs.");
			return;
		}
		
		CouchConfigFactory factory = new CouchConfigFactory();
		factory.setServerName(serverName);
		factory.setConfigDesign(configDesign);
		CouchConfig couchConfig = null;
		try {
			couchConfig = factory.retrieveConfigurationObject();
		} catch (Exception e) {
			logger.error("Can not load configuration object. Not uploading logs.",e);
			return;
		}

		try {
			couchConfig.uploadCronLogs(cronLogFile);
			logger.info("cron.log uploaded to configuration object");
		} catch (Exception e) {
			logger.error("Error while uploading cron logs. Not uploading logs.",e);
			return;
		}
	}
	
	public void destroy() {
		try {
			uploadWorker.stopTimeoutMillis(5*1000); // 5 seconds
		} catch (Exception e) {
			logger.error("Unable to shutdown upload worker", e);
		}

		try {
			configWorker.stopTimeoutMillis(5*1000); // 5 seconds
		} catch (Exception e) {
			logger.error("Unable to shutdown config listener worker", e);
		}

		try {
			replicationWorker.stopTimeoutMillis(5*1000); // 5 seconds
		} catch (Exception e) {
			logger.error("Unable to shutdown replication worker", e);
		}
	}
}
