package com.eactive.eai.message.manager;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.StringUtils;

import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.lifecycle.Lifecycle;
import com.eactive.eai.common.lifecycle.LifecycleException;
import com.eactive.eai.common.lifecycle.LifecycleListener;
import com.eactive.eai.common.lifecycle.LifecycleSupport;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.message.EncodingVar;
import com.eactive.eai.message.StandardMessage;
import com.eactive.eai.message.StandardMessageUtil;
import com.eactive.eai.message.filter.MessageFilter;
import com.eactive.eai.message.mapper.MessageMapper;
import com.eactive.eai.message.parser.StandardReader;
import com.eactive.eai.message.service.InterfaceMapper;
import com.eactive.eai.message.service.StandardMessageCoordinator;

public class StandardMessageManager implements Lifecycle {
	static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
	
	private static StandardMessageManager instance;
	private boolean started;
    private LifecycleSupport lifecycle = new LifecycleSupport(this);
    
/**
# standard-message-config.properties 
# layout : StandardMessage layout definition
layout.file.type=CSV
layout.file.path=./resources/standard-layout.csv
# mapper : StandardMessage's fields getter/setter interface
mapper.class=com.eactive.eai.message.service.DefaultInterfaceMapper
mapper.definition=./resources/standard-message-mapping-config.properties
# reader : parsing input data to StandardMessage
reader.JSON=com.eactive.eai.message.parser.JsonReader
reader.XML=com.eactive.eai.message.parser.XmlReader
reader.FLAT=com.eactive.eai.message.parser.FlatReader
*/
	public static String STANDARD_MESSAGE_CONFIG = "standard.message.config";
	private String DEFAULT_CONFIG_PATH = "./standard-message-config.properties";
	private String DEFAULT_CONFIG_FILE = "standard-message-config.properties";
	
	private String LAYOUT_FILE_TYPE = "layout.file.type";
	private String LAYOUT_FILE_PATH = "layout.file.path";
	
	private String LAYOUT_FILTER = "layout.filter.FLAT";
	
	private String MAPPER_CLASS = "mapper.class";
	private String MAPPER_DEFINITION = "mapper.definition";
	private String READER_PREFIX = "reader.";
	
	private String ENCODE_FLAT = "encode.flat";
	private String ENCODE_XML = "encode.xml";
	private String ENCODE_JSON = "encode.json";
	
	private String COORDINATOR_CLASS = "message.coordinator.class";
	private String DEFAULT_COORDINATOR_CLASS = "com.eactive.eai.message.service.DefaultStandardMessageCoordinator";
	private String REQUEST_PROCESSOR_CLASS = "requestProcessor.class";
	
	private StandardMessage standardMessage = null;
	private InterfaceMapper mapper;
	private StandardMessageCoordinator messageCoordinator;
	
	private ConcurrentHashMap<String, StandardReader> readerMap = null;
	
	private Map<String, String> versionMap = new HashMap<>();;
	private String VERSION_LAYOUT_NAME = "version.layout.name";
	private String VERSION_LAYOUT_FILTER = "version.layout.filter.FLAT";
	private String VERSION_MAPPER = "version.mapper";
	private String VERSION_MAPPING_DEFINATION = "version.mapper.definition";
	private MessageMapper versionMapper;
	private StandardMessage versionMessage = null;
	private String versionLayoutName = null;
	
	public Map<String, String> getVersionMap() {
		return versionMap;
	}

	public void setVersionMap(Map<String, String> versionMap) {
		this.versionMap = versionMap;
	}

	public String getVersionLayoutName() {
		return versionLayoutName;
	}

	public void setVersionLayoutName(String versionLayoutName) {
		this.versionLayoutName = versionLayoutName;
	}
	
	public synchronized StandardMessage getVersionMessage() {
		if(versionMessage == null) return null;
		try {
			return (StandardMessage) versionMessage.clone();
		} catch (CloneNotSupportedException e) {
			return SerializationUtils.clone(standardMessage);
		}
	}
	
	private StandardMessageManager() {
		readerMap = new ConcurrentHashMap<>();
	}

	public static StandardMessageManager getInstance() {
		return LazyHolder.INSTANCE;
	}
	private static class LazyHolder {
		private static final StandardMessageManager INSTANCE = new StandardMessageManager();  
	}
	
	public void start() throws LifecycleException {
        if (started)
            throw new LifecycleException("RECEAICMM201");
        
        lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
        
        try {
           init();
        } catch(Exception e) {
            throw new LifecycleException(ExceptionUtil.getErrorCode(e,"RECEAICMM202"));
        }
        
        started = true;
        lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
    }
	
	public void stop() throws LifecycleException {
        if (!started)
            throw new LifecycleException("RECEAICMM203");
		
        lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
        readerMap.clear();
//        readerMap = null;
        started = false;
        lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
    }
	
	public void addLifecycleListener(LifecycleListener listener)
    {
        lifecycle.addLifecycleListener(listener);
    }
	
	public LifecycleListener[] findLifecycleListeners()
    {
        return lifecycle.findLifecycleListeners();
    }
	
	public void removeLifecycleListener(LifecycleListener listener)
    {
        lifecycle.removeLifecycleListener(listener);
    }
	
	public boolean isStarted() {
        return this.started;   
    }
	
	public synchronized StandardMessage getStandardMessage() {
		try {
			return (StandardMessage) standardMessage.clone();
		} catch (CloneNotSupportedException e) {
			return SerializationUtils.clone(standardMessage);
		}
	}
	
	public MessageMapper getVersionMapper() {
		return versionMapper;
	}

	public void setVersionMapper(MessageMapper versionMapper) {
		this.versionMapper = versionMapper;
	}

	public InterfaceMapper getMapper() {
		return mapper;
	}

	public void setMapper(InterfaceMapper mapper) {
		this.mapper = mapper;
	}
	
	public StandardMessageCoordinator getMessageCoordinator() {
		return messageCoordinator;
	}

	public void setMessageCoordinator(StandardMessageCoordinator messageCoordinator) {
		this.messageCoordinator = messageCoordinator;
	}

	public synchronized void reload() throws Exception {
    	if(logger.isWarn()) {
    		logger.warn("StandardMessageManager] reload all started ...");
    	}
    	init();
    	if(logger.isWarn()) {
    		logger.warn("StandardMessageManager] reload all finished ...");
    	}
    }
	
	@SuppressWarnings("rawtypes")
	private void initReaderFactory(Properties config) {
		if (readerMap == null) {
	        readerMap = new ConcurrentHashMap<>();
	    }
		
		Class cl = null;
		StandardReader reader = null;
		
		Set<Object> keys = config.keySet();
		
		for(Object key: keys) {
			String keyStr = (String)key;
			if(keyStr.startsWith(READER_PREFIX)) {
				String name = keyStr.substring(READER_PREFIX.length());
				String readerClassName = config.getProperty(keyStr);
								
				try {
					cl = Class.forName(readerClassName);
					reader = (StandardReader)cl.newInstance();
				} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
					if(logger.isWarn()) logger.warn(String.format("Invalid class path : %s %s", keyStr, readerClassName), e);
					continue;
				}
				readerMap.put(name, reader);
				if(logger.isWarn()) logger.warn(String.format("# Add reader : %s %s", keyStr, readerClassName));
			}
		}
	}
	
	public StandardReader getReader(String messageType) {
		return readerMap.get(messageType);
	}
		
	@SuppressWarnings("rawtypes")
	public void init() throws Exception {
		try {
			Properties config = loadConfigFile();
			logger.debug(config.toString());
			
			String encodeFlat = config.getProperty(ENCODE_FLAT, "euc-kr");
			String encodeXml = config.getProperty(ENCODE_XML, "utf-8");
			String encodeJson = config.getProperty(ENCODE_JSON, encodeXml);
			String requestProcessorClass = config.getProperty(REQUEST_PROCESSOR_CLASS);
			if(logger.isDebug()) {
				logger.debug( "{} : {}", ENCODE_FLAT, encodeFlat );
				logger.debug( "{} : {}", ENCODE_XML, encodeXml );
				logger.debug( "{} : {}", ENCODE_JSON, encodeJson );
				logger.debug( "{} : {}", REQUEST_PROCESSOR_CLASS, requestProcessorClass );
			}
			EncodingVar.flatEncoding = encodeFlat;
			EncodingVar.xmlEncoding = encodeXml;
			EncodingVar.jsonEncoding = encodeJson;
			StandardMessageUtil.requestProcessorClass = requestProcessorClass;
			
			String layoutFileType = config.getProperty(LAYOUT_FILE_TYPE, "CSV");
			String path = config.getProperty(LAYOUT_FILE_PATH);
			if(logger.isDebug()) {
//				logger.debug( String.format("%s : %s", LAYOUT_FILE_TYPE, layoutFileType) );
//				logger.debug( String.format("%s : %s", LAYOUT_FILE_PATH, path) );
				logger.debug( "{} : {}", LAYOUT_FILE_TYPE, layoutFileType );
				logger.debug( "{} : {}", LAYOUT_FILE_PATH, path );
			}

			
			if("CSV".contentEquals(layoutFileType.toUpperCase())) {
				standardMessage = StandardMessageUtil.generateMessageFromCsvFile(path);
			} else if(StringUtils.startsWithIgnoreCase(layoutFileType, "DB")) {
				String layout = StringUtils.substring(layoutFileType, 3);
				standardMessage = StandardMessageUtil.generateMessageFromDb(StringUtils.trimToNull(layout));
			} else {
				standardMessage = StandardMessageUtil.generateMessageFromCsvFile(path);
			}
			
			if(standardMessage != null) {
				standardMessage.setReadPosition(0);
			}
			String flatFilterClass = config.getProperty(LAYOUT_FILTER);
			logger.debug("{} : {}", LAYOUT_FILTER, flatFilterClass);
			
			String versionFlatFilterClass = config.getProperty(VERSION_LAYOUT_FILTER);
			logger.debug("{} : {}", VERSION_LAYOUT_FILTER, versionFlatFilterClass);
			
			
			if(StringUtils.isNotBlank(flatFilterClass)) {
				try {
					Class cl = Class.forName(flatFilterClass);
					MessageFilter flatFilter = (MessageFilter)cl.newInstance();				
					standardMessage.setFlatFilter(flatFilter);	
				} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
					if(logger.isWarn()) logger.warn(String.format("configure %s : %s", LAYOUT_FILTER, flatFilterClass), e);
				}
			}
			
			String coordinatorClass = config.getProperty(COORDINATOR_CLASS, DEFAULT_COORDINATOR_CLASS);
			logger.debug("{} : {}", COORDINATOR_CLASS, coordinatorClass);
			try {
				Class cl = Class.forName(coordinatorClass);
				this.messageCoordinator = (StandardMessageCoordinator) cl.newInstance();
			} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
				if (logger.isWarn())
					logger.warn(String.format("configure %s : %s", COORDINATOR_CLASS,
							config.getProperty(COORDINATOR_CLASS)), e);
			}
			
			String mapperClass = config.getProperty(MAPPER_CLASS);
			String mapperFilePath = config.getProperty(MAPPER_DEFINITION);
			if(logger.isDebug()) logger.debug( String.format("%s : %s", MAPPER_CLASS, mapperClass) );
			if(logger.isDebug()) logger.debug( String.format("%s : %s", MAPPER_DEFINITION, mapperFilePath) );
			
			if(mapperClass == null) {
				throw new Exception("StandardMessageManager | config error, not found config : " + MAPPER_CLASS);
			}
			if(mapperFilePath == null) {
				throw new Exception("StandardMessageManager | config error, not found config : " + MAPPER_DEFINITION);
			}
			
			initReaderFactory(config);
			
			Class cl = null;
			try {
				cl = Class.forName(mapperClass);
				mapper = (InterfaceMapper)cl.newInstance();
				Properties mapperMapPropties = loadConfigFileProperties(mapperFilePath);
				HashMap<String, String> map = propertyToMap(mapperMapPropties);				
				mapper.initPathMap(map);
			} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
				e.printStackTrace();
				return;
			}
			if(logger.isDebug()) logger.debug("mapperClass : {}", mapper.getClass().getCanonicalName());
			
			standardMessage.setBizDataPath(mapper.getPath(InterfaceMapper.BIZ_DATA));
			
			String versionMapperClass = config.getProperty(VERSION_MAPPER);
			if(StringUtils.isEmpty(versionMapperClass)) {
				versionMapperClass = "com.eactive.eai.message.mapper.DefaultMessageMapper";
			}
			if(logger.isDebug()) logger.debug("version.mapper : {}", versionMapperClass);
			try {
				Class clazz = Class.forName(versionMapperClass);
				versionMapper = (MessageMapper)clazz.newInstance();				
			} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
				e.printStackTrace();
				return;
			}
			if(logger.isDebug()) logger.debug("version.mapper class : {}", versionMapper.getClass().getCanonicalName());
			
			String versionMapFilePath = config.getProperty(VERSION_MAPPING_DEFINATION);
			if(logger.isDebug()) logger.debug( String.format("%s : %s", VERSION_MAPPING_DEFINATION, versionMapFilePath) );
			if(versionMapFilePath != null) {
				Properties versionMapPropties = loadConfigFileProperties(versionMapFilePath);
				versionMap = propertyToMap(versionMapPropties);
			}
			if(logger.isDebug()) logger.debug("versionMap size ="+ versionMap.size() );
			
			versionLayoutName = config.getProperty(VERSION_LAYOUT_NAME);
			if(logger.isDebug()) logger.debug( String.format("%s : %s", VERSION_LAYOUT_NAME, versionLayoutName) );
			if(versionLayoutName != null) {
				versionMessage = StandardMessageUtil.generateMessageFromDb(versionLayoutName);
				versionMessage.setReadPosition(0);
				
				if(StringUtils.isNotBlank(versionFlatFilterClass)) {
					try {
						Class vcl = Class.forName(versionFlatFilterClass);
						MessageFilter versionFlatFilter = (MessageFilter)vcl.newInstance();				
						versionMessage.setFlatFilter(versionFlatFilter);	
					} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
						if(logger.isWarn()) logger.warn(String.format("configure %s : %s", VERSION_LAYOUT_FILTER, versionFlatFilterClass), e);
					}
				}
			}
		}
		catch(Exception ex) {
			ex.printStackTrace();
			logger.error("init failed", ex);
		}
	}
	
	private HashMap<String, String> propertyToMap(Properties prop) {
	    HashMap<String, String> retMap = new HashMap<>();
	    for (Map.Entry<Object, Object> entry : prop.entrySet()) {
	        retMap.put(String.valueOf(entry.getKey()).trim(), String.valueOf(entry.getValue()).trim());
	    }
	    return retMap;
	}
	
	private Properties loadConfigFile() throws Exception {
		String configFile = getConfigFile();
		return loadConfigFileProperties(configFile);
	}
	
	private Properties loadConfigFileProperties(String configFile) throws Exception {
		try {
			return loadFileProperties(configFile);
		}
		catch(Exception e) {
//			logger.warn("StandardMessageManager | loadFileProperties failed. - " + configFile, e);
			logger.warn("StandardMessageManager | loadFileProperties failed. - " + configFile +", " +e.getMessage());
		}
		// try classpath file
		return loadClassPathProperties(configFile);
	}
	
	private Properties loadFileProperties(String filePath) throws Exception {
		logger.debug("configFile : ", filePath);
		Properties p = new Properties();
		try(InputStream in = new FileInputStream(filePath)) {
			p.load(in);
			return p;
		} catch(Exception e) {
			throw new Exception("StandardMessageManager | Cannot load config file. - " + filePath, e);
		}
	}
	
	private Properties loadClassPathProperties(String filePath) throws Exception {
		Properties p = new Properties();
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		logger.warn("StandardMessageManager | ClassLoader : load. - "+ filePath);
		try( InputStream in = classLoader.getResourceAsStream(filePath) ) {
			p.load(in);
			return p;
		} catch(Exception e) {
			if(logger.isWarn()) {
//				logger.warn("StandardMessageManager | ClassLoader : Cannot load config file. - " + filePath, e);
				logger.warn("StandardMessageManager | ClassLoader : Cannot load config file. - " + filePath +", " +e.getMessage());
			}
		}

		logger.warn("StandardMessageManager | ClassLoader : load default - "+ DEFAULT_CONFIG_FILE);
		try( InputStream in = classLoader.getResourceAsStream(DEFAULT_CONFIG_FILE) ) {
			p.load(in);
			logger.warn("StandardMessageManager | ClassLoader : load default success - "+ DEFAULT_CONFIG_FILE);
			return p;
		} catch(Exception e) {
			logger.error("StandardMessageManager | ClassLoader : Cannot load default config file. - " + DEFAULT_CONFIG_FILE, e);
			throw new Exception("StandardMessageManager | ClassLoader : Cannot load default config file. - " + DEFAULT_CONFIG_FILE, e);
		}
	}
	private String getConfigFile() {
		String configFilePath = System.getProperty(STANDARD_MESSAGE_CONFIG);
		if (StringUtils.isEmpty(configFilePath)) {
			return DEFAULT_CONFIG_PATH; 
		} else {
			return configFilePath;
		}
	}
}
