package com.eactive.eai.common.session; 

import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;

import com.eactive.eai.authserver.service.BearerTokenInfo;
import com.eactive.eai.common.ehcache.CustomRMICacheReplicatorFactory;
import com.eactive.eai.common.logger.EAIDBLogControl;
import com.eactive.eai.common.message.EAIMessage;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.sessioninfo.SessionInfoVO;
import com.eactive.eai.common.sessioninfo.TerminalInfoDAO;
import com.eactive.eai.common.sessioninfo.TerminalInfoVO;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.util.HealthCheckManager;
import com.openbanking.eai.common.token.AccessTokenVO;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.bootstrap.BootstrapCacheLoader;
import net.sf.ehcache.bootstrap.BootstrapCacheLoaderFactory;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.config.Configuration;
import net.sf.ehcache.config.FactoryConfiguration;
import net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory;
import net.sf.ehcache.distribution.RMICacheReplicatorFactory;
import net.sf.ehcache.event.CacheEventListener;
import net.sf.ehcache.event.CacheEventListenerFactory;

//use Ehcache
public class SessionManagerForEhcache extends SessionManager {
	// evictMaster Instance - single socket 관련
	private static Cache evictMasterCache = null;
	// Socket Session cache 정보  
	private static Cache cacheSocket = null;
	// Topic 용 cache 정보  
	private static Cache cacheTopic = null;
	// 비동기 거래 복원용
	private static Cache cacheStore = null;
	
	// WebSocket Login cache 정보
	private static Cache cacheLogin = null; // key = loginId
	private static Cache cacheConvertUserId = null; // 단말ID 를 userID로 변경
	// HTTP Polling 에 대한 timeout 관리용
	private static Cache httpLogin = null; // key == uuid
	// Terminal용 cache 정보 : ATM, 무인공과금
	private static Cache cacheTerminal = null;

	// HTTP Login 후 WebSocket Connection 일정 시간동안 없을시 Expired되며 자동 로그아웃 발생
	private static Cache cacheWebSocketTimeout = null;

	private static CacheManager manager = null;

	// expireThread run interval secs, default 10 secs
	int expireInterval = 10;
	Thread expireThread = null;
	String localServerName = null;
	
	private int cacheStoreDurationSecs = 60;
	
	private final String logPrefix = this.getClass().getName();
	
	public void init(int localPort, int remoteObjectPort) {
		PropManager pManager = PropManager.getInstance();
		Properties prpty = pManager.getProperties(SessionManager.SESSION_MANAGER_GROUPNAME);
		String mltIp = "230.0.0.1"; // 224.0.1.0 ~ 239.255.255.255
		String gPort = "4443";
		int sessionTimeout = 60;
		int webSocketTimeout = 60;
		
		int topicTimeout = 60;
		int topicMaxSize = 10000;
		
		int cacheStoreMaxSize = 10000;
		
		EAIServerManager eaiServerManager = EAIServerManager.getInstance();
		localServerName = eaiServerManager.getLocalServerName();

		if (prpty != null) {
			mltIp = prpty.getProperty(SessionManager.SESSION_MANAGER_MULTI_GROUP_IP);
			gPort = prpty.getProperty(SessionManager.SESSION_MANAGER_MULTI_GROUP_PORT);
			String sTimeout = prpty.getProperty(SessionManager.SESSION_MANAGER_SESSION_TIMEOUT);
			if (sTimeout != null) {
				try {
					sessionTimeout = Integer.parseInt(sTimeout);
					if (sessionTimeout < 30)
						sessionTimeout = 30;
				} catch (NumberFormatException ex) {
					sessionTimeout = 60;
				}
			}

			String sExpireInterval = prpty.getProperty(SessionManager.SESSION_MANAGER_EXPIRE_INTERVAL);
			if (sExpireInterval != null) {
				try {
					expireInterval = Integer.parseInt(sExpireInterval);
					if (expireInterval < 10)
						expireInterval = 10;
				} catch (NumberFormatException ex) {
					expireInterval = 10;
				}
			}

			String wTimeout = prpty.getProperty(SessionManager.SESSION_MANAGER_WEBSOCKET_TIMEOUT);
			if (wTimeout != null) {
				try {
					webSocketTimeout = Integer.parseInt(wTimeout);
				} catch (NumberFormatException ex) {
					webSocketTimeout = 60;
				}
			}
			
			String topicTimeoutSec = prpty.getProperty(SessionManager.CACHE_TOPIC_DURATION);
			if (topicTimeoutSec != null) {
				try {
					topicTimeout = Integer.parseInt(topicTimeoutSec);
					if (topicTimeout < 0)
						topicTimeout = 60;
				} catch (NumberFormatException ex) {
					topicTimeout = 60;
				}
			}
			String topicMax = prpty.getProperty(SessionManager.TOPIC_MAXSZIE);
			if (topicMax != null) {
				try {
					topicMaxSize = Integer.parseInt(topicMax);
					if (topicMaxSize < 0)
						topicMaxSize = 10000;
				} catch (NumberFormatException ex) {
					topicMaxSize = 10000;
				}
			}
			
			String durationSecs = prpty.getProperty(SessionManager.CACHE_STORE_DURATION);
			if (durationSecs != null) {
				try {
					cacheStoreDurationSecs = Integer.parseInt(durationSecs);
					if (cacheStoreDurationSecs < 60)
						cacheStoreDurationSecs = 60;
				} catch (NumberFormatException ex) {
					cacheStoreDurationSecs = 60;
				}
			}
			String cacheStoreMax = prpty.getProperty(SessionManager.CAHCE_STORE_MAXSIZE);
			if (cacheStoreMax != null) {
				try {
					cacheStoreMaxSize = Integer.parseInt(cacheStoreMax);
					if (cacheStoreMaxSize < 0)
						cacheStoreMaxSize = 10000;
				} catch (NumberFormatException ex) {
					cacheStoreMaxSize = 10000;
				}
			}
		}

		System.setProperty("net.sf.ehcache.skipUpdateCheck", "true");
		String bootstrapAsynchronously = "true";

		Configuration cf = new Configuration();
		FactoryConfiguration ppFactory = new FactoryConfiguration();
		/**
		 * manual로 설정할경우 shutdown시 멈춤현상이 발생 auto로 변경됨
		 * ppFactory.setClass("com.eactive.eai.common.session.TimeoutRMICacheManagerPeerProviderFactory");
		 * ppFactory.setProperties("peerDiscovery=manual,rmiUrls="+rmiUrls);
		 */
		ppFactory.setClass("net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory");
		ppFactory.setProperties(
				"peerDiscovery=automatic,multicastGroupAddress=" + mltIp + ", multicastGroupPort=" + gPort);
		ppFactory.setPropertySeparator(",");

		cf.addCacheManagerPeerProviderFactory(ppFactory);

		logger.debug(logPrefix + " >>>>INIT<<< Multicast ip = " + mltIp + ", port = " + gPort);

		FactoryConfiguration plFactory = new FactoryConfiguration();
		plFactory.setClass("net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory");
		plFactory.setProperties("port=" + localPort + ", remoteObjectPort=" + remoteObjectPort);

		plFactory.setPropertySeparator(",");
		cf.addCacheManagerPeerListenerFactory(plFactory);

		CacheConfiguration defaultCacheConfiguration = new CacheConfiguration();
		defaultCacheConfiguration.setName("ChannelSessionCache");
		defaultCacheConfiguration.setMaxElementsInMemory(200000);
		cf.addDefaultCache(defaultCacheConfiguration);

		manager = new CacheManager(cf);

		initSocket(manager, sessionTimeout, 10000, bootstrapAsynchronously);
		initTopic(manager, topicTimeout, topicMaxSize, bootstrapAsynchronously);
		initCacheStore(manager, cacheStoreDurationSecs, cacheStoreMaxSize, bootstrapAsynchronously);

		initLogin(manager, sessionTimeout, 20000, bootstrapAsynchronously);
		initHttpLogin(manager, sessionTimeout, 20000, bootstrapAsynchronously);
		initEvict(manager, sessionTimeout, 10, bootstrapAsynchronously);
		initTerminal(manager, sessionTimeout, 20000, bootstrapAsynchronously);
		initConvertUserId(manager, sessionTimeout, 20000, bootstrapAsynchronously);
		initWebSocketTimeout(manager, webSocketTimeout, 20000, bootstrapAsynchronously);

		/**
		 * expire event가 전파가 안되는 문제 발견(2022-01-13) 
		 * 조치는 master에서만 하는게 아니라 전체 인스턴스에서
		 * expireThread 실행되도록변경
		 */
		expireThread = new Thread() {
			@Override
			public void run() {
				String logPrefix = "SessionManager.expireThread] ";
				while (true) {
					String master = null;
					try {
						Thread.sleep(expireInterval * 1000);
						if (evictMasterCache.getSize() > 0) {
							evictMasterCache.evictExpiredElements();
							Element element = evictMasterCache.get(MASTER_KEY);
							if (element == null) {
								evictMasterCache.put(new Element(MASTER_KEY, localServerName));
								if (logger.isInfo())
									logger.info(logPrefix + "EVICT-PUT master = " + localServerName);
							} else {
								master = (String) element.getObjectValue();
								if (localServerName.equals(master)) {
									evictMasterCache.put(new Element(MASTER_KEY, localServerName));
								}
							}
						} else {
							evictMasterCache.put(new Element(MASTER_KEY, localServerName));
							if (logger.isInfo())
								logger.info(logPrefix + "INIT-PUT master = " + localServerName);
						}

						int cacheSize = cacheLogin.getSize();
						if (cacheSize > 0) {
							if (logger.isInfo()) {
								logger.info(logPrefix + "cacheLogin.evictExpiredElements = " + cacheSize);
							}
							cacheLogin.evictExpiredElements();
						}
						cacheSize = httpLogin.getSize();
						if (cacheSize > 0) {
							if (logger.isInfo()) {
								logger.info(logPrefix + "httpLogin.evictExpiredElements = " + cacheSize);
							}
							httpLogin.evictExpiredElements();
						}
					} catch (InterruptedException ie) {
						logger.error(logPrefix + "expireThread error - stop", ie);
						break;
					} catch (Exception e) {
						logger.error(logPrefix + "expireThread error - skip", e);
					}
				}
			}
		};
		expireThread.setName("SessionManagerForEhcache-expireThread");
		expireThread.start();
	}

	private synchronized void initEvict(CacheManager manager, int sessionTimeout, int maxSize, String bootstrapAsynchronously) {
		int masterTimeout = (expireInterval + expireInterval / 2);
		if (logger.isDebug()) {
			logger.debug(logPrefix + " create evictMasterCache cache - expire=" + masterTimeout + ", maxSize=" + maxSize);
		}
		evictMasterCache = new Cache(ADAPTER_MASTER_CAHCE_NAME, 10, false, false, masterTimeout, masterTimeout);

		CacheEventListenerFactory celf = new RMICacheReplicatorFactory();
		Properties celfProp = new Properties();
		celfProp.put("replicateAsynchronously", "false");
		celfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener cel = celf.createCacheEventListener(celfProp);
		evictMasterCache.getCacheEventNotificationService().registerListener(cel);
		
		@SuppressWarnings("rawtypes")
		BootstrapCacheLoaderFactory bclf = new RMIBootstrapCacheLoaderFactory();
		Properties bclfProp = new Properties();
		bclfProp.put("bootstrapAsynchronously", bootstrapAsynchronously);
		bclfProp.put("maximumChunkSizeBytes", "5000000");
		BootstrapCacheLoader bcl = bclf.createBootstrapCacheLoader(bclfProp);
		evictMasterCache.setBootstrapCacheLoader(bcl);
		
		manager.addCache(evictMasterCache);
	}

	private synchronized void initSocket(CacheManager manager, int sessionTimeout, int maxSize, String bootstrapAsynchronously) {
		if (logger.isDebug()) {
			logger.debug(logPrefix + " create AdapterSession cache sessionTimeout="+sessionTimeout +", maxSize=" + maxSize);
		}
		cacheSocket = new Cache(ADAPTER_CAHCE_NAME, maxSize, false, false, 0, 0);

		CacheEventListenerFactory celf = new RMICacheReplicatorFactory();
		Properties celfProp = new Properties();
		celfProp.put("replicateAsynchronously", "false");
		// celfProp.put("asynchronousReplicationIntervalMillis", "100");
		CacheEventListener cel = celf.createCacheEventListener(celfProp);
		cacheSocket.getCacheEventNotificationService().registerListener(cel);
		
		@SuppressWarnings("rawtypes")
		BootstrapCacheLoaderFactory bclf = new RMIBootstrapCacheLoaderFactory();
		Properties bclfProp = new Properties();
		bclfProp.put("bootstrapAsynchronously", bootstrapAsynchronously);
		bclfProp.put("maximumChunkSizeBytes", "5000000");
		BootstrapCacheLoader bcl = bclf.createBootstrapCacheLoader(bclfProp);
		cacheSocket.setBootstrapCacheLoader(bcl);

		manager.addCache(cacheSocket);
	}

	private synchronized void initTopic(CacheManager manager, int sessionTimeout, int maxSize, String bootstrapAsynchronously) {
		if (logger.isDebug()) {
			logger.debug(logPrefix + " create topic cache sessionTimeout="+sessionTimeout +", maxSize=" + maxSize);
		}
		cacheTopic = new Cache(TOPIC_CAHCE_NAME, maxSize, false, false, sessionTimeout, sessionTimeout);

		CacheEventListenerFactory celf = new RMICacheReplicatorFactory();
		Properties celfProp = new Properties();
		celfProp.put("replicateAsynchronously", "false");
		celfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener cel = celf.createCacheEventListener(celfProp);
		cacheTopic.getCacheEventNotificationService().registerListener(cel);

		// add Custom EventListener
		CacheEventListenerFactory ccelf = new CustomRMICacheReplicatorFactory();
		Properties ccelfProp = new Properties();
		ccelfProp.put("replicateAsynchronously", "false");
		ccelfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener ccel = ccelf.createCacheEventListener(ccelfProp);
		cacheTopic.getCacheEventNotificationService().registerListener(ccel);
		
		@SuppressWarnings("rawtypes")
		BootstrapCacheLoaderFactory bclf = new RMIBootstrapCacheLoaderFactory();
		Properties bclfProp = new Properties();
		bclfProp.put("bootstrapAsynchronously", bootstrapAsynchronously);
		bclfProp.put("maximumChunkSizeBytes", "5000000");
		BootstrapCacheLoader bcl = bclf.createBootstrapCacheLoader(bclfProp);
		cacheTopic.setBootstrapCacheLoader(bcl);
		
		manager.addCache(cacheTopic);
	}
	
	private synchronized void initCacheStore(CacheManager manager, int sessionTimeout, int maxSize, String bootstrapAsynchronously) {
		if (logger.isDebug()) {
			logger.debug(logPrefix + " create cacheStore cache sessionTimeout="+sessionTimeout +", maxSize=" + maxSize);
		}
		cacheStore = new Cache(STORE_CAHCE_NAME, maxSize, false, false, sessionTimeout, sessionTimeout);

		CacheEventListenerFactory celf = new RMICacheReplicatorFactory();
		Properties celfProp = new Properties();
		celfProp.put("replicateAsynchronously", "false");
		celfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener cel = celf.createCacheEventListener(celfProp);
		cacheStore.getCacheEventNotificationService().registerListener(cel);

		// add Custom EventListener
		CacheEventListenerFactory ccelf = new CustomRMICacheReplicatorFactory();
		Properties ccelfProp = new Properties();
		ccelfProp.put("replicateAsynchronously", "false");
		ccelfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener ccel = ccelf.createCacheEventListener(ccelfProp);
		cacheStore.getCacheEventNotificationService().registerListener(ccel);
		
		@SuppressWarnings("rawtypes")
		BootstrapCacheLoaderFactory bclf = new RMIBootstrapCacheLoaderFactory();
		Properties bclfProp = new Properties();
		bclfProp.put("bootstrapAsynchronously", bootstrapAsynchronously);
		bclfProp.put("maximumChunkSizeBytes", "5000000");
		BootstrapCacheLoader bcl = bclf.createBootstrapCacheLoader(bclfProp);
		cacheStore.setBootstrapCacheLoader(bcl);

		manager.addCache(cacheStore);
	}
	
	private synchronized void initLogin(CacheManager manager, int sessionTimeout, int maxSize, String bootstrapAsynchronously) {
		if (logger.isDebug()) {
			logger.debug(logPrefix + " create loginSession cache  sessionTimeout="+sessionTimeout +", maxSize=" + maxSize);
		}

		cacheLogin = new Cache(LOGIN_CAHCE_NAME, 20000, false, false, sessionTimeout, sessionTimeout);

		String isAsync = PropManager.getInstance().getProperty("LOGIN", "IS_ASYNC", "false");

		CacheEventListenerFactory celf = new RMICacheReplicatorFactory();
		Properties celfProp = new Properties();
		celfProp.put("replicateAsynchronously", isAsync);
		// 비동기 방식을 사용할때 변경 내역을 다른 노드에 통지하는 주기 default 1000
		celfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener cel = celf.createCacheEventListener(celfProp);
		cacheLogin.getCacheEventNotificationService().registerListener(cel);

		// add Custom EventListener
		CacheEventListenerFactory ccelf = new CustomRMICacheReplicatorFactory();
		Properties ccelfProp = new Properties();
		ccelfProp.put("replicateAsynchronously", isAsync);
		ccelfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener ccel = ccelf.createCacheEventListener(ccelfProp);
		cacheLogin.getCacheEventNotificationService().registerListener(ccel);

		@SuppressWarnings("rawtypes")
		BootstrapCacheLoaderFactory bclf = new RMIBootstrapCacheLoaderFactory();
		Properties bclfProp = new Properties();
		bclfProp.put("bootstrapAsynchronously", bootstrapAsynchronously);
		bclfProp.put("maximumChunkSizeBytes", "5000000");
		BootstrapCacheLoader bcl = bclf.createBootstrapCacheLoader(bclfProp);

		cacheLogin.setBootstrapCacheLoader(bcl);
//		cacheLogin.getCacheConfiguration().setDiskExpiryThreadIntervalSeconds(10);
		manager.addCache(cacheLogin);
		logger.warn(logPrefix + " cacheLogin DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS = "
				+ Cache.DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS);
	}

	private synchronized void initHttpLogin(CacheManager manager, int sessionTimeout, int maxSize, String bootstrapAsynchronously) {
		if (logger.isDebug()) {
			logger.debug(logPrefix + " create httpLogin cache timeToIdleSeconds=" + sessionTimeout);
		}
		httpLogin = new Cache(HTTP_CAHCE_NAME, 20000, false, false, sessionTimeout, sessionTimeout);

		String isAsync = PropManager.getInstance().getProperty("LOGIN", "IS_ASYNC", "false");

		CacheEventListenerFactory celf = new RMICacheReplicatorFactory();
		Properties celfProp = new Properties();
		celfProp.put("replicateAsynchronously", isAsync);
		celfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener cel = celf.createCacheEventListener(celfProp);
		httpLogin.getCacheEventNotificationService().registerListener(cel);

		// add Custom EventListener
		CacheEventListenerFactory ccelf = new CustomRMICacheReplicatorFactory();
		Properties ccelfProp = new Properties();
		ccelfProp.put("replicateAsynchronously", isAsync);
		ccelfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener ccel = ccelf.createCacheEventListener(ccelfProp);
		httpLogin.getCacheEventNotificationService().registerListener(ccel);
		
		@SuppressWarnings("rawtypes")
		BootstrapCacheLoaderFactory bclf = new RMIBootstrapCacheLoaderFactory();
		Properties bclfProp = new Properties();
		bclfProp.put("bootstrapAsynchronously", bootstrapAsynchronously);
		bclfProp.put("maximumChunkSizeBytes", "5000000");
		BootstrapCacheLoader bcl = bclf.createBootstrapCacheLoader(bclfProp);
		httpLogin.setBootstrapCacheLoader(bcl);
		
		manager.addCache(httpLogin);
		if (logger.isWarn()) {
			logger.warn(logPrefix + " httpLogin DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS = "
					+ Cache.DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS);
		}
	}
	
	

	private synchronized void initConvertUserId(CacheManager manager, int sessionTimeout, int maxSize, String bootstrapAsynchronously) {
		if (logger.isDebug()) {
			logger.debug(logPrefix + " create convertUserIdSession cache sessionTimeout="+sessionTimeout +", maxSize=" + maxSize);
		}
		cacheConvertUserId = new Cache(USERID_CAHCE_NAME, maxSize, false, false, sessionTimeout, sessionTimeout);

		String isAsync = PropManager.getInstance().getProperty("LOGIN", "IS_ASYNC", "false");

		CacheEventListenerFactory celf = new RMICacheReplicatorFactory();
		Properties celfProp = new Properties();
		celfProp.put("replicateAsynchronously", isAsync);
		celfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener cel = celf.createCacheEventListener(celfProp);
		cacheConvertUserId.getCacheEventNotificationService().registerListener(cel);

		// add Custom EventListener
		CacheEventListenerFactory ccelf = new CustomRMICacheReplicatorFactory();
		Properties ccelfProp = new Properties();
		ccelfProp.put("replicateAsynchronously", isAsync);
		ccelfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener ccel = ccelf.createCacheEventListener(ccelfProp);
		cacheConvertUserId.getCacheEventNotificationService().registerListener(ccel);

		@SuppressWarnings("rawtypes")
		BootstrapCacheLoaderFactory bclf = new RMIBootstrapCacheLoaderFactory();
		Properties bclfProp = new Properties();
		bclfProp.put("bootstrapAsynchronously", bootstrapAsynchronously);
		bclfProp.put("maximumChunkSizeBytes", "5000000");
		BootstrapCacheLoader bcl = bclf.createBootstrapCacheLoader(bclfProp);
		cacheConvertUserId.setBootstrapCacheLoader(bcl);

		manager.addCache(cacheConvertUserId);
		if (logger.isWarn()) {
			logger.warn(logPrefix + " cacheConvertUserId DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS = "
					+ Cache.DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS);
		}
	}

	private synchronized void initWebSocketTimeout(CacheManager manager, int sessionTimeout, int maxSize, String bootstrapAsynchronously) {
		if (logger.isDebug()) {
			logger.debug(logPrefix + " create webSocketTimeout cache sessionTimeout="+sessionTimeout +", maxSize=" + maxSize);
		}

		cacheWebSocketTimeout = new Cache(WEBSOCKER_CAHCE_NAME, maxSize, false, false, sessionTimeout, sessionTimeout);

		String isAsync = PropManager.getInstance().getProperty("LOGIN", "IS_ASYNC", "false");

		CacheEventListenerFactory celf = new RMICacheReplicatorFactory();
		Properties celfProp = new Properties();
		celfProp.put("replicateAsynchronously", isAsync);
		celfProp.put("asynchronousReplicationIntervalMillis", "1000");// 비동기 방식을 사용할때 변경 내역을 다른 노드에 통지하는 주기 default 1000
		CacheEventListener cel = celf.createCacheEventListener(celfProp);
		cacheWebSocketTimeout.getCacheEventNotificationService().registerListener(cel);

		// add Custom EventListener
		CacheEventListenerFactory ccelf = new CustomRMICacheReplicatorFactory();
		Properties ccelfProp = new Properties();
		ccelfProp.put("replicateAsynchronously", isAsync);
		ccelfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener ccel = ccelf.createCacheEventListener(ccelfProp);
		cacheWebSocketTimeout.getCacheEventNotificationService().registerListener(ccel);

		@SuppressWarnings("rawtypes")
		BootstrapCacheLoaderFactory bclf = new RMIBootstrapCacheLoaderFactory();
		Properties bclfProp = new Properties();
		bclfProp.put("bootstrapAsynchronously", bootstrapAsynchronously);
		bclfProp.put("maximumChunkSizeBytes", "5000000");
		BootstrapCacheLoader bcl = bclf.createBootstrapCacheLoader(bclfProp);
		cacheWebSocketTimeout.setBootstrapCacheLoader(bcl);

		manager.addCache(cacheWebSocketTimeout);
		if (logger.isWarn()) {
			logger.warn(logPrefix + " cacheWebSocketTimeout DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS = "
					+ Cache.DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS);
		}
	}

	private synchronized void initTerminal(CacheManager manager, int sessionTimeout, int maxSize, String bootstrapAsynchronously) {
		if (logger.isDebug()) {
			logger.debug(logPrefix + " create TerminalSession cache  sessionTimeout="+sessionTimeout +", maxSize=" + maxSize);
		}
		cacheTerminal = new Cache(TERMINAL_CAHCE_NAME, maxSize, false, false, sessionTimeout, sessionTimeout);

		CacheEventListenerFactory celf = new RMICacheReplicatorFactory();
		Properties celfProp = new Properties();
		celfProp.put("replicateAsynchronously", "false");
		celfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener cel = celf.createCacheEventListener(celfProp);
		cacheTerminal.getCacheEventNotificationService().registerListener(cel);

		// add Custom EventListener
		CacheEventListenerFactory ccelf = new CustomRMICacheReplicatorFactory();
		Properties ccelfProp = new Properties();
		ccelfProp.put("replicateAsynchronously", "false");
		ccelfProp.put("asynchronousReplicationIntervalMillis", "1000");
		CacheEventListener ccel = ccelf.createCacheEventListener(ccelfProp);
		cacheTerminal.getCacheEventNotificationService().registerListener(ccel);
		
		@SuppressWarnings("rawtypes")
		BootstrapCacheLoaderFactory bclf = new RMIBootstrapCacheLoaderFactory();
		Properties bclfProp = new Properties();
		bclfProp.put("bootstrapAsynchronously", bootstrapAsynchronously);
		bclfProp.put("maximumChunkSizeBytes", "5000000");
		BootstrapCacheLoader bcl = bclf.createBootstrapCacheLoader(bclfProp);
		cacheTerminal.setBootstrapCacheLoader(bcl);

		manager.addCache(cacheTerminal);
		if (logger.isWarn()) {
			logger.warn(logPrefix + " cacheTerminal DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS = "
					+ Cache.DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS);
		}
	}
	
	// WebSocket Login 관련
	public SessionVO getLogin(String key) {
		try {
			Element element = cacheLogin.get(key);
			if (element != null) {
				return (SessionVO) element.getObjectValue();
			}
			return null;
		} catch (CacheException e) {
			logger.error(e.getMessage(), e);
			return null;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return null;
		}
	}

	public void putLogin(String key, SessionVO value) {
		putLogin(key, value, null);
	}

	public void putLogin(String key, SessionVO value, SessionInfoVO sessionInfoVO) {
		if (logger.isWarn()) {
			logger.warn("Put session key - " + key + ", Session - " + value.toString());
		}
		cacheLogin.put(new Element(key, value));
		// terminal to userID 매핑 생성
		if (value != null && value.getTerminalId() != null) {
			cacheConvertUserId.put(new Element(value.getTerminalId(), key));
		}

		if (sessionInfoVO != null) {
			SessionStatusManager.getInstance().manageStatus(sessionInfoVO);
		} else {
			SessionStatusManager.getInstance().manageLoginStatus(key);
		}
	}

	public void removeLogin(String key) {
		SessionVO vo = getLogin(key);
		if (logger.isWarn()) {
			logger.warn("Remove websocket login session key - " + key);
		}
		// terminal to userID 매핑 제거
		if (vo != null && vo.getTerminalId() != null) {
			cacheConvertUserId.remove(vo.getTerminalId());
		}
		cacheLogin.remove(key);
		SessionStatusManager.getInstance().manageLoginStatus(key);
	}

	// terminalId 2 userId convert
	public String getConvertUserId(String key) {
		try {
			Element element = cacheConvertUserId.get(key);
			if (element != null) {
				return (String) element.getObjectValue();
			}
			return null;
		} catch (CacheException e) {
			logger.error(e.getMessage(), e);
			return null;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return null;
		}
	}

	public SessionVO getHttpLogin(String key) {
		try {
			Element element = httpLogin.get(key);
			if (element != null) {
				return (SessionVO) element.getObjectValue();
			}
			return null;
		} catch (CacheException e) {
			logger.error(e.getMessage(), e);
			return null;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return null;
		}
	}

	public void putHttpLogin(String key, SessionVO value) {
		logger.warn("Put session key - " + key + ", Session - " + value.toString());
		httpLogin.put(new Element(key, value));
	}

	public void removeHTTPLogin(String key) {
		logger.warn("Remove http login session key - " + key);
		httpLogin.remove(key);
	}

	// Single Socket 관련
	public SessionVO getConnectionInfo(String key) {
		try {
			Element element = cacheSocket.get(key);
			if (element != null) {
				SessionVO sessionVo = (SessionVO) element.getObjectValue();
				return sessionVo;
			}
			return null;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return null;
		} 
	}

	public SessionVO getConnectionStatus(String adapterGroupName) {
		SessionVO sessionVo = null;
		try {
			sessionVo = getConnectionInfo(adapterGroupName);
			if (sessionVo != null) {
				String connectedInst = sessionVo.getLoginInstId();
				// 싱글어댑터 Start --------------------------->
//				EAIServerManager eaiServerManager = EAIServerManager.getInstance();
//				String localServerName = eaiServerManager.getLocalServerName();

				// 로컬서버가 연결시도 중인데 캐시에 자신의 인스턴스가 있을경우 삭제
				if (localServerName.equals(connectedInst)) {
					cacheSocket.remove(adapterGroupName);
					return null;
				}
				// 싱글어댑터 End <---------------------------

				// 케시에서 조회된 인스던스가 살았는지 확인 - 좀비캐시 삭제
				if (logger.isWarn()) {
					logger.warn("getConnectionInfo check primary server status - " + connectedInst);
				}
				boolean isAlive = HealthCheckManager.getInstance().getServerStatus(connectedInst);

				if (!isAlive) {
					if (logger.isWarn()) {
						logger.warn("getConnectionInfo check primary server is Not Alive, clear cache.");
					}
					cacheSocket.remove(adapterGroupName);
					return null;
				}
			}
			return sessionVo;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return null;
		}
	}

	public void putConnectionInfo(String key, SessionVO value) {
		if (logger.isWarn()) {
			logger.warn("Put socket session key - " + key + ", Session - " + value.toString());
		}
		cacheSocket.put(new Element(key, value));
	}

	public void removeConnectionInfo(String key) {
		if (logger.isWarn()) {
			logger.warn("Remove socket  session key - " + key);
		}
		cacheSocket.remove(key);
	}

	// Topic 관련
	public EAIMessage getTopic(String key) {
		try {
			Element element = cacheTopic.get(key);
			if (element != null) {
				return (EAIMessage) element.getObjectValue();
			}
			return null;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return null;
		} 
	}

	public void putTopic(String key, EAIMessage value) {
		if (logger.isWarn()) {
			logger.warn("Put Topic response - " + key + ", value - " + value.toString());
		}
		cacheTopic.put(new Element(key, value));
	}

	public void removeTopic(String key) {
		if (logger.isWarn()) {
			logger.warn("Remove Topic response key - " + key);
		}
		cacheTopic.remove(key);
	}
	
	@Override
	public String getCacheStore(String key) {
		try {
			Element element = cacheStore.get(key);
			if (element == null) {
				return null;
			} else {
//				String message = (String)element.getObjectValue();
				CacheStoreObject object = (CacheStoreObject) element.getObjectValue();
				if (object.isExpired()) {
					if (logger.isWarn())
						logger.warn("getCacheStore expired key [" + key + "]");
					cacheStore.remove(key);
					return null;
				} else {
					return object.getMessage();
				}
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return null;
		}
	}

	@Override
	public void putCacheStore(String key, String value) {
		putCacheStore(key, value, cacheStoreDurationSecs);
	}
	
	@Override
	public void putCacheStore(String key, String value, int ttlSecs) {
		if (logger.isWarn()) {
			logger.warn("Put cacheStore key[" + key + "], value - " + value);
		}
//		cacheStore.put(new Element(key, value));
		CacheStoreObject object = new CacheStoreObject(value, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(ttlSecs));
		cacheStore.put(new Element(key, object));
	}
	
	@Override
	public void removeCacheStore(String key) {
		if (logger.isWarn()) {
			logger.warn("Remove cacheStore key - " + key);
		}
		cacheStore.remove(key);
	}
	
	public TerminalVO getTerminal(String key) {
		try {
			Element element = cacheTerminal.get(key);
			if (element != null) {
				return (TerminalVO) element.getObjectValue();
			}
			return null;
		} catch (CacheException e) {
			logger.error(e.getMessage(), e);
			return null;
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return null;
		}
	}

	public void putTerminal(String key, TerminalVO value, TerminalInfoVO terminalInfoVO) {
		if (logger.isWarn()) {
			logger.warn("Put terminal key - " + key + ", Terminal - " + value.toString());
		}
		cacheTerminal.put(new Element(key, value));
		if (terminalInfoVO != null && EAIDBLogControl.isEnable()) {
			try {
				TerminalInfoDAO dao = ApplicationContextProvider.getContext().getBean(TerminalInfoDAO.class);
				dao.upsertTerminalInfo(terminalInfoVO);
			} catch (Exception e) {
				logger.error(" insert Terminal DB ERROR. - " + e.getMessage(), e);
			}
		} else if (terminalInfoVO != null) {
			logger.error("DB 장애로 업데이트 미수행 - " + value.toString());
		}
	}

	public void removeTerminal(String key) {
		if (logger.isWarn()) {
			logger.warn("Remove socket teminal session key - " + key);
		}
		cacheTerminal.remove(key);

		if (EAIDBLogControl.isEnable()) {
			try {
				TerminalInfoDAO dao = ApplicationContextProvider.getContext().getBean(TerminalInfoDAO.class);
				TerminalInfoVO terminalInfoVO = new TerminalInfoVO(key);
				terminalInfoVO.setSocSessionStatus(TerminalInfoVO.Status.inactive);
				dao.upsertTerminalInfo(terminalInfoVO);
			} catch (Exception e) {
				logger.error(" insert Terminal DB ERROR. - " + key, e);
			}
		} else {
			logger.error(" DB 장애로 인해 업데이트 미수행 - " + key);
		}
	}

	private CacheInfoVO convert(Cache cache) {
		CacheInfoVO v = new CacheInfoVO();
		v.setName(cache.getName());
		v.setSize(cache.getSize());
		v.setTimeToLive(cache.getCacheConfiguration().getTimeToLiveSeconds() * 1000L);
		v.setKeys(cache.getKeys());
		return v;
	}

	public CacheInfoVO getLoginCache() {
		return convert(cacheLogin);
	}

	public CacheInfoVO getHttpLoginCache() {
		return convert(httpLogin);
	}

	public CacheInfoVO getEvictMasterCache() {
		return convert(evictMasterCache);
	}

	public CacheInfoVO getTerminalCache() {
		return convert(cacheTerminal);
	}

	public CacheInfoVO getConvertUserIdCache() {
		return convert(cacheConvertUserId);
	}

	public CacheInfoVO getSocketCache() {
		return convert(cacheSocket);
	}

	@Override
	public CacheInfoVO getOutboundAccessTokenCache() {
		throw new UnsupportedOperationException();
	}

	@Override
	public void putWebSocketTimeout(String key, SessionVO value) {
		cacheWebSocketTimeout.put(new Element(key, value));
	}

	@Override
	public void removeWebSocketTimeout(String key) {
		cacheWebSocketTimeout.remove(key);
	}

	public String getMasterInstId() {
		Element element = evictMasterCache.get(MASTER_KEY);
		if (element == null) {
			return null;
		} else {
			return (String) element.getObjectValue();
		}
	}

	public void closeManager() {
		manager.shutdown();
	}	

	@Override
	public String getCacheStatus() {
		StringBuilder sb = new StringBuilder();
		String[] cacheNames = manager.getCacheNames();
		sb.append("<Caches type=\"Ehcache\">").append("\n");
        for(String cacheName : cacheNames) {
			Cache cache = manager.getCache(cacheName);
			// Get the cache size
	        long cacheSize = cache.getSize();
	        sb.append("<Cache name=\"" + cacheName + "\" Size=\"" + cacheSize).append("\"/>\n");
		}
        sb.append("</Caches>");
		return sb.toString();
	}

	@Override
	public BearerTokenInfo getCAToken(String key) {
		throw new UnsupportedOperationException();
	}

	@Override
	public void putCAToken(String key, BearerTokenInfo value) {
		throw new UnsupportedOperationException();
	}

	@Override
	public void removeCAToken(String key) {
		throw new UnsupportedOperationException();
	}

	@Override
	public AccessTokenVO getOutboundAccessToken(String key, Function<AccessTokenVO, AccessTokenVO> getNewToken) {
	    throw new UnsupportedOperationException();
	}

	@Override
	public void removeOutboundAccessToken(String key) {
	    throw new UnsupportedOperationException();
	}
	
	@Override
	public void clearOutboundAccessToken() {
	    throw new UnsupportedOperationException();
	}
} 
