package com.eactive.eai.common.session; 

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.cache.Cache;
import javax.cache.configuration.Factory;
import javax.cache.expiry.Duration;
import javax.cache.expiry.ModifiedExpiryPolicy;
import javax.cache.expiry.TouchedExpiryPolicy;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteEvents;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.Ignition;
import org.apache.ignite.cache.CacheAtomicityMode;
import org.apache.ignite.cache.CacheMetrics;
import org.apache.ignite.cache.CacheMode;
import org.apache.ignite.cache.CacheWriteSynchronizationMode;
import org.apache.ignite.cache.eviction.lru.LruEvictionPolicy;
import org.apache.ignite.cache.query.QueryCursor;
import org.apache.ignite.cache.query.ScanQuery;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.ClientConnectorConfiguration;
import org.apache.ignite.configuration.ConnectorConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.configuration.NearCacheConfiguration;
import org.apache.ignite.events.CacheEvent;
import org.apache.ignite.events.EventType;
import org.apache.ignite.kubernetes.configuration.KubernetesConnectionConfiguration;
import org.apache.ignite.lang.IgnitePredicate;
import org.apache.ignite.logger.slf4j.Slf4jLogger;
import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi;
import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
import org.apache.ignite.spi.discovery.tcp.ipfinder.kubernetes.TcpDiscoveryKubernetesIpFinder;
import org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder;
import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;

import com.eactive.eai.adapter.socket2.common.Env;
import com.eactive.eai.authserver.service.BearerTokenInfo;
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.server.EAIServerVO;
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.common.worker.Future;
import com.eactive.eai.common.worker.ResponseMap;
import com.eactive.eai.util.HealthCheckManager;
import com.openbanking.eai.common.token.AccessTokenVO;

import ch.qos.logback.classic.Level;

// use Ignite
//------------------------------------------------------------------------------------
// Important : 
// Persistence 를 사용할 경우, 일부서버 다운시 에러가 발생하므로 사용하지 않도록 한다. 
// 에러내용 : Failed to execute the cache operation 
// (all partition owners have left the grid, partition data has been lost) 
  //------------------------------------------------------------------------------------

public class SessionManagerForIgnite extends SessionManager {
    
    	private static final String DISTRIBUTED_TOKEN_LOCK = "DISTRIBUTED_TOKEN_LOCK";
	// evictMaster Instance - single socket 관련
	private static IgniteCache<String, String> evictMasterCache = null;
	// Socket Session cache 정보
	private static IgniteCache<String, SessionVO> cacheSocket = null;
	// SyncAsync Type 거래 매핑을 위한 Topic 용 cache 정보
	private static IgniteCache<String, EAIMessage> cacheTopic = null;
	// 비동기 거래 복원용
	private static IgniteCache<String, CacheStoreObject> cacheStore = null;
	// CA Gateway Token 관리용
	private static IgniteCache<String, BearerTokenInfo> cacheCAToken = null;
	// OutboundAccessToekn 관리용
	private static IgniteCache<String, AccessTokenVO> cacheOutBoundAccessToken = null;
	// webSocket Login cache 정보
	private static IgniteCache<String, SessionVO> cacheLogin = null; // key = loginId
	
	private static IgniteCache<String, String> cacheConvertUserId = null; // 단말ID 를 userID로 변경
	// HTTP Polling 에 대한 timeout 관리용
	private static IgniteCache<String, SessionVO> httpLogin = null; // key == uuid
	// Terminal용 cache 정보 ATM, 무인공과금
	private static IgniteCache<String, TerminalVO> cacheTerminal = null;
	// HTTP Login 후 WebSocket Connection 일정 시간동안 없을시 Expired되며 자동 로그아웃 발생
	private static IgniteCache<String, SessionVO> cacheWebSocketTimeout = null;

	private static Ignite manager = null;

	String localServerName = null;

	private final String logPrefix = this.getClass().getName();

	private boolean useMulticast = false;
	
	private IgnitePredicate<CacheEvent> localListener = null;
	
	private int cacheStoreDurationSecs = 60;
	
	public void init(int communicationPort, int discoveryPort) {
		int topicTimeout = 60;
		int topicMaxSize = 10000;
		int cacheStoreMaxSize = 10000;
		
		PropManager pManager = PropManager.getInstance();
		Properties prpty = pManager.getProperties(SessionManager.SESSION_MANAGER_GROUPNAME);
		// 224.0.1.0 ~ 239.255.255.255
		String mltIp = "230.0.0.1";
		
		String workingDirectory = null;
		
		int iPort = 4443;
		int sessionTimeout = 60;
		
		int webSocketTimeout = 60;

		EAIServerManager eaiServerManager = EAIServerManager.getInstance();
		localServerName = eaiServerManager.getLocalServerName();

		EAIServerVO eaiServerVo = eaiServerManager.getEAIServer(localServerName);

		if (prpty != null) {
			workingDirectory = prpty.getProperty(SessionManager.CACHE_WORKING_DIR);

			if (logger.isWarn()) {
				logger.warn(">> CACHE_WORKING_DIR = " + workingDirectory);
			}
			
			mltIp = prpty.getProperty(SessionManager.SESSION_MANAGER_MULTI_GROUP_IP);

			String sPort = prpty.getProperty(SessionManager.SESSION_MANAGER_MULTI_GROUP_PORT);
			if (sPort != null) {
				try {
					iPort = Integer.parseInt(sPort);
				} catch (NumberFormatException ex) {
					iPort = 4443;
				}
			}

			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 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;
			}
		}
		
		IgniteConfiguration config = new IgniteConfiguration();
		
		if(StringUtils.isNotEmpty(workingDirectory)) {
			config.setWorkDirectory(workingDirectory);
		}
		// System.setProperty("net.sf.ehcache.skipUpdateCheck", "true");
		// CommunicationSpi
		TcpCommunicationSpi communicationSpi = new TcpCommunicationSpi();
		communicationSpi.setLocalPort(communicationPort);
		communicationSpi.setIdleConnectionTimeout(15000);
		communicationSpi.setConnectTimeout(1000);
		communicationSpi.setReconnectCount(3);
		communicationSpi.setMaxConnectTimeout(30000);
		communicationSpi.setConnectionsPerNode(2);
		communicationSpi.setSlowClientQueueLimit(1000);

		String instanceLocalGroup = pManager.getProperty(SessionManager.SESSION_MANAGER_GROUPNAME, localServerName,
				"DEFAULT");

		// discoverySpi
		TcpDiscoverySpi discoverySpi = new TcpDiscoverySpi();
		discoverySpi.setLocalPort(discoveryPort);
		discoverySpi.setClientReconnectDisabled(false);
		discoverySpi.setSocketTimeout(5000);
		discoverySpi.setNetworkTimeout(5000);
		discoverySpi.setAckTimeout(5000);
		discoverySpi.setMaxAckTimeout(15000);
		discoverySpi.setReconnectCount(3);
		
		if (!Env.isWindows() && BooleanUtils.toBoolean(System.getProperty("scalable", "false"))) {
			// https://kubernetes.default.svc.cluster.local:443/api/v1/namespaces/card/endpoints/card-channel-runtime
			String nameSpace    = pManager.getProperty(SessionManager.SESSION_MANAGER_GROUPNAME, SESSION_MANAGER_KUBERNETES_NAMESPACE, "");
			String serviceName  = pManager.getProperty(SessionManager.SESSION_MANAGER_GROUPNAME,	SESSION_MANAGER_KUBERNETES_SERVICENAME, "");
			String masterUrl    = pManager.getProperty(SessionManager.SESSION_MANAGER_GROUPNAME, SESSION_MANAGER_KUBERNETES_MASTERURL, "");
			String accountToken = pManager.getProperty(SessionManager.SESSION_MANAGER_GROUPNAME, SESSION_MANAGER_KUBERNETES_ACCOUNTTOKEN, "");
			if (logger.isDebug()) {
				logger.debug(logPrefix + " >>>>Ignite INIT<<< Multicast nameSpace = " + nameSpace);
			}
			KubernetesConnectionConfiguration kubernetesCfg = new KubernetesConnectionConfiguration();
			if (StringUtils.isNotBlank(nameSpace)) {
				kubernetesCfg.setNamespace(nameSpace.trim());
			}
			if (StringUtils.isNotBlank(serviceName)) {
				kubernetesCfg.setServiceName(serviceName.trim());
			}
			if (StringUtils.isNotBlank(masterUrl)) {
				kubernetesCfg.setMasterUrl(masterUrl.trim());
			}
			if (StringUtils.isNotBlank(accountToken)) {
				kubernetesCfg.setAccountToken(accountToken.trim());
			}
			TcpDiscoveryKubernetesIpFinder ipFinder = new TcpDiscoveryKubernetesIpFinder(kubernetesCfg);
			if (logger.isWarn()) {
				logger.warn(">> Use TcpDiscoveryKubernetesIpFinder setting nameSpace = " + nameSpace + ", serviceName = "
								+ serviceName + ", masterUrl = " + masterUrl + ", accountToken = " + accountToken);
				logger.warn(">> Use TcpDiscoveryKubernetesIpFinder config nameSpace = " + kubernetesCfg.getNamespace()
						+ ", servcieName = " + kubernetesCfg.getServiceName() + ", masterUrl = "
						+ kubernetesCfg.getMaster() + ", accountToken = " + kubernetesCfg.getAccountToken());
			}
			discoverySpi.setIpFinder(ipFinder);
		}else {
			// ipFinder
			List<String> lists = new ArrayList<>();
			for (String key : EAIServerManager.getInstance().getAllServer().keySet()) {
				EAIServerVO vo = EAIServerManager.getInstance().getEAIServer(key);
				if ("ALL".equals(key))
					continue;
				int discoverPort = vo.getCacheRemotePort();
				String instanceGroup = pManager.getProperty(SessionManager.SESSION_MANAGER_GROUPNAME, key, "DEFAULT");
	
				// 서버그룹별 Cluster 분리
				if (instanceLocalGroup.equals(instanceGroup)) {
					String server = null;
					// 서버정보의 IP가 시스템과 다른 ip로 설정되면 기동이 안되는 문제
		            if (localServerName.equals(key) && !"GW".equals(System.getProperty("spring.profiles.active", ""))) {
		               server = vo.getAddress() + ":" + discoverPort;
					} else {
						server = vo.getAddress() + ":" + discoverPort;
					}
					lists.add(server);
					if (logger.isWarn()) {
						logger.warn("ipFind key= " + key + ", lists = " + server + ", instanceLocalGroup ="
								+ instanceLocalGroup);
					}
				}
			}
	
			if (useMulticast) {
				if (logger.isDebug()) {
					logger.debug(logPrefix + " >>>>Ignite INIT<<< Multicast ip = " + mltIp);
				}
				TcpDiscoveryMulticastIpFinder ipFinder = new TcpDiscoveryMulticastIpFinder();
				ipFinder.setMulticastGroup(mltIp);
				ipFinder.setMulticastPort(iPort);
				if (logger.isWarn()) {
					logger.warn(">> Use TcpDiscoveryMulticastIpFinder mltIp = " + mltIp + ", port = " + iPort);
				}
				discoverySpi.setIpFinder(ipFinder);
			} else {
				if (lists.isEmpty()) {
					if (logger.isWarn()) {
						logger.warn(">> Single Server mode skip regist TcpDiscoveryVmIpFinder addresses");
					}
				} else {
					TcpDiscoveryVmIpFinder ipFinder = new TcpDiscoveryVmIpFinder();
					ipFinder.setAddresses(lists);
					discoverySpi.setIpFinder(ipFinder);
					if (logger.isWarn()) {
						logger.warn(">> Use TcpDiscoveryVmIpFinder addresses = " + lists);
					}
				}
			}
		}
		String bootstrapAsynchronously = "false";

		ClientConnectorConfiguration cliConnCfg = new ClientConnectorConfiguration();
		cliConnCfg.setPort(Integer.parseInt(eaiServerVo.getPort()) + 6);
		config.setClientConnectorConfiguration(cliConnCfg);

		ConnectorConfiguration connectorCfg = new ConnectorConfiguration();
		connectorCfg.setPort(Integer.parseInt(eaiServerVo.getPort()) + 7);
		config.setConnectorConfiguration(connectorCfg);

		String serverGroupName = EAIServerManager.getInstance().getServerMappingType();
		String instName = EAIServerManager.getInstance().getLocalServerName();

		config.setGridName("elink_" + serverGroupName);// elink_mcc
		config.setIgniteInstanceName(instName);// elink_mcc
		config.setConsistentId(instName);

		ch.qos.logback.classic.Logger blogger = (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory
				.getLogger("org.apache.ignite");
		
		String loggerLevel = prpty.getProperty(SessionManager.LOGGER_LEVEL, "ERROR");
		
		switch(loggerLevel) {
		case "ERROR":
		    blogger.setLevel(Level.ERROR);
		    break;
		case "INFO":
		    blogger.setLevel(Level.INFO);
		    break;
		case "WARN":
		    blogger.setLevel(Level.WARN);
		    break;
		case "DEBUG":
		    blogger.setLevel(Level.DEBUG);
		    break;
		case "TRACE":
		    blogger.setLevel(Level.TRACE);
		    break;
		}
		
		if( logger.isDebug() ) {
		    
		    logger.debug("Ignite loggerLevel = "+loggerLevel);
		    
		}

		IgniteLogger log = new Slf4jLogger((org.slf4j.Logger) blogger);
		config.setGridLogger(log);
		config.setMetricsLogFrequency(10 * 60 * 1000); // 10분

		config.setCommunicationSpi(communicationSpi);
		config.setDiscoverySpi(discoverySpi);
		config.setClientMode(false);
		config.setIncludeEventTypes(EventType.EVT_CACHE_OBJECT_PUT, EventType.EVT_CACHE_OBJECT_REMOVED,
				EventType.EVT_CACHE_OBJECT_EXPIRED
		// , EventType.EVT_NODE_JOINED
		// , EventType.EVT_NODE_LEFT
		);

		List<CacheConfiguration> caches = new ArrayList<>();
		// Default
		caches.add(initCache(config, 0, bootstrapAsynchronously, ADAPTER_MASTER_CAHCE_NAME, 10, false));
		caches.add(initCache(config, 0, bootstrapAsynchronously, ADAPTER_CAHCE_NAME, 10000, false));
		
		caches.add(initCache(config, topicTimeout, bootstrapAsynchronously, TOPIC_CAHCE_NAME, topicMaxSize, false));
		
		if(logger.isWarn()) {
			logger.warn(STORE_CAHCE_NAME +" - cacheStoreDurationSecs = " + cacheStoreDurationSecs);
		}
		
		caches.add(initCache(config, cacheStoreDurationSecs, bootstrapAsynchronously, STORE_CAHCE_NAME, cacheStoreMaxSize, false));
		
		// for MCI 
		caches.add(initCache(config, sessionTimeout, bootstrapAsynchronously, LOGIN_CAHCE_NAME, 20000, false));
		caches.add(initCache(config, sessionTimeout, bootstrapAsynchronously, HTTP_CAHCE_NAME, 20000, false));
		caches.add(initCache(config, sessionTimeout, bootstrapAsynchronously, USERID_CAHCE_NAME, 20000, false));
		caches.add(initCache(config, webSocketTimeout, bootstrapAsynchronously, WEBSOCKER_CAHCE_NAME, 20000, false));
		caches.add(initCache(config, sessionTimeout, bootstrapAsynchronously, TERMINAL_CAHCE_NAME, 20000, false));
		caches.add(initCache(config, 0, bootstrapAsynchronously, OUTBOUND_ACCESS_TOKEN_CACHE_NAME, 1000, false, true)); // transactionMode true
		caches.add(initCache(config, 0, bootstrapAsynchronously, CAGATEWAY_TOKEN_CACHE_NAME, 1000, false));
		
		config.setCacheConfiguration(caches.stream().toArray(CacheConfiguration[]::new));

		// Start a node.
		if (logger.isWarn()) {
			logger.warn(">> Ignition.starting...");
		}
		manager = Ignition.start(config);
		manager.cluster().active(true);
		if (logger.isWarn()) {
			logger.warn(">> Ignition.started.");
		}

		IgniteEvents events = manager.events();
		// Local listener that listens to local events.
		localListener = new IgniteEvent();
		
		// Subscribe to the cache events that are triggered on the local node.
		events.localListen(localListener, 
				EventType.EVT_CACHE_OBJECT_PUT, 
				EventType.EVT_CACHE_OBJECT_REMOVED,
				EventType.EVT_CACHE_OBJECT_EXPIRED);
		
		evictMasterCache = manager.cache(ADAPTER_MASTER_CAHCE_NAME);
		cacheSocket = manager.cache(ADAPTER_CAHCE_NAME);
		cacheTopic = manager.cache(TOPIC_CAHCE_NAME);
		cacheStore = manager.cache(STORE_CAHCE_NAME);
		cacheCAToken = manager.cache(CAGATEWAY_TOKEN_CACHE_NAME);
		cacheOutBoundAccessToken = manager.cache(OUTBOUND_ACCESS_TOKEN_CACHE_NAME);
		
		cacheLogin = manager.cache(LOGIN_CAHCE_NAME);
		httpLogin = manager.cache(HTTP_CAHCE_NAME);
		cacheConvertUserId = manager.cache(USERID_CAHCE_NAME);
		cacheWebSocketTimeout = manager.cache(WEBSOCKER_CAHCE_NAME);
		cacheTerminal = manager.cache(TERMINAL_CAHCE_NAME);		
	}
	
	private CacheConfiguration initCache(IgniteConfiguration config, int expiryDurationSecs,
		String bootstrapAsynchronously, String name, int maxsize, boolean useCacheWriteModeAsync,
		boolean transactionMode) {
	    if (logger.isDebug()) {
		logger.debug(logPrefix + " create " + name + " cache expiryDurationSecs=" + expiryDurationSecs);
	    }
	    // near-cache
	    NearCacheConfiguration nearConfiguration = new NearCacheConfiguration();
	    LruEvictionPolicy nearEvictionPolicy = new LruEvictionPolicy();
	    nearEvictionPolicy.setMaxSize(maxsize);
	    nearConfiguration.setNearEvictionPolicy(nearEvictionPolicy);

	    CacheConfiguration cacheConfig = new CacheConfiguration(name);

	    cacheConfig.setCacheMode(CacheMode.PARTITIONED);

	    // ATOMIC,TRANSACTIONAL
	    if (transactionMode) {
		cacheConfig.setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL);
	    } else {
		cacheConfig.setAtomicityMode(CacheAtomicityMode.ATOMIC);
	    }

	    // Set the number of backups (replicas) for each partition : default(0)
	    cacheConfig.setBackups(1);
	    // Enable reading from backups to improve read consistency : default(true)
	    cacheConfig.setReadFromBackup(true);

	    // FULL_SYNC/FULL_ASYNC/PRIMARY_SYNC(default)
	    if (useCacheWriteModeAsync) {
		cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_ASYNC);
	    } else {
//		cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC);
		cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.PRIMARY_SYNC);
	    }

	    if (expiryDurationSecs > 0) {
		cacheConfig.setExpiryPolicyFactory(
			ModifiedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, expiryDurationSecs)));
	    } else {
		cacheConfig.setExpiryPolicyFactory(ModifiedExpiryPolicy.factoryOf(Duration.ETERNAL));
	    }
	    cacheConfig.setNearConfiguration(nearConfiguration);
	    return cacheConfig;
	}

	private CacheConfiguration initCache(IgniteConfiguration config, int expiryDurationSecs, String bootstrapAsynchronously,
			String name, int maxsize, boolean useCacheWriteModeAsync) {
	    return initCache(config, expiryDurationSecs, bootstrapAsynchronously, name, maxsize, useCacheWriteModeAsync, false);
	}

	// WebSocket Login 관련
	public SessionVO getLogin(String key) {
		try {
			return cacheLogin.get(key);
		} catch (Exception e) {
			logger.error("getLogin null key=" + key, e);
			return null;
		}
	}

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

	public void removeLogin(String key) {
		SessionVO vo = getLogin(key);
		// terminal to userID 매핑 제거
		if (vo != null && vo.getTerminalId() != null) {
			cacheConvertUserId.remove(vo.getTerminalId());
		}
		cacheLogin.remove(key);
	}

	// terminalId 2 userId convert
	public String getConvertUserId(String key) {
		try {
			return cacheConvertUserId.get(key);
		} catch (Exception e) {
			logger.error("getConvertUserId null key=" + key, e);
			return null;
		}
	}

	public SessionVO getHttpLogin(String key) {
		try {
			return httpLogin.get(key);
		} catch (Exception e) {
			logger.error("getHttpLogin null key=" + key, e);
			return null;
		}
	}

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

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

	// Single Socket 관련
	public SessionVO getConnectionInfo(String key) {
		try {
			return cacheSocket.get(key);
		} catch (Exception e) {
			logger.error("getConnectionInfo null key=" + key, 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());
		}
		try {
			cacheSocket.put(key, value);
		} catch (Exception e) {
			logger.error("Put socket session error key=" + key + " Session - " + value.toString(), e);
		}
	}

	public void removeConnectionInfo(String key) {
		if (logger.isWarn()) {
			logger.warn("Remove socket  session key - " + key);
		}
		try {
			cacheSocket.remove(key);
		} catch (Exception e) {
			logger.error("Remove socket session error key=" + key, e);
		}
	}

	// Topic 관련
	public EAIMessage getTopic(String key) {
		try {
			return cacheTopic.get(key);
		} catch (Exception e) {
			logger.error("getTopic null key=" + key, e);
			return null;
		}
	}

	public void putTopic(String key, EAIMessage value) {
		if (logger.isWarn()) {
			logger.warn("Put Topic response - " + key + ", value - " + value.toString());
		}
		
		// local event인 경우 ignite skip
		Future future = ResponseMap.getInstance().get(key);
		if(future != null) {
			if (logger.isDebug()) {
				logger.debug("PutTopic] local [" + key + "] put to Future");
			}
			future.getSlot().put(value);
			return;
		}
		// remote Event
		if (logger.isDebug()) {
			logger.debug("PutTopic] remote [" + key + "] put to Future");
		}
		int retry = 3;
		while(!cacheTopic.putIfAbsent(key, value) && retry > 0) {
			retry--;
			try {
				Thread.sleep(300);
			} catch (InterruptedException e) {
				if(logger.isDebug()) {
					logger.debug("PutTopic] sleep Fail");
				}
			}
			if (logger.isDebug()) {
				logger.debug("PutTopic] remote retry [" + key + "] put to Future");
			}
		};
		removeTopic(key);
	}

	public void removeTopic(String key) {
		if (logger.isWarn()) {
			logger.warn("Remove Topic response key - " + key);
		}
		cacheTopic.remove(key);
	}
	
	// cacheStore 관련
	public String getCacheStore(String key) {
		try {
			// Important : get 힌 후 바로 remove 하므로, 객체를 생성해서 return 해야 함.
//			String message = cacheStore.get(key);
			CacheStoreObject object = cacheStore.get(key);
			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("getTopic null key [" + key + "]", e);
			return null;
		}
	}
	@Override
	public void putCacheStore(String key, String value, int ttlSecs) {
		if (logger.isWarn()) {
			logger.warn("Put CacheStore key [" + key + "], value - " + value);
		}
//		cacheStore.put(key, value);
		CacheStoreObject object = new CacheStoreObject(value, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(ttlSecs));
		cacheStore.put(key, object);
	}
	
	@Override
	public void putCacheStore(String key, String value) {
		putCacheStore(key, value, cacheStoreDurationSecs);
	}
	
	public void removeCacheStore(String key) {
		if (logger.isWarn()) {
			logger.warn("Remove CacheStore key [" + key + "]");
		}
		cacheStore.remove(key);
	}
		
	// Terminal
	public TerminalVO getTerminal(String key) {
		try {
			return cacheTerminal.get(key);
		} catch (Exception e) {
			logger.error("getTerminal null key=" + key, 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(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);
		}
		try {
			// 서버다운시 org.apache.ignite.internal.processors.cache.CacheStoppedException 때문에
			// try catch 추가 : jun's 20220826
			cacheTerminal.remove(key);

		} catch (Exception e) {
			logger.error(" cacheTerminal remove error. - " + key, e);
		}
		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);
		}
	}

	@SuppressWarnings("rawtypes")
	private CacheInfoVO convert(IgniteCache cache) {
		CacheInfoVO v = new CacheInfoVO();
		long timeToLive = 0;
		CacheConfiguration cf = (CacheConfiguration) cache.getConfiguration(CacheConfiguration.class);
		Factory factory = cf.getExpiryPolicyFactory();
		Object obj = factory.create();
		if (obj instanceof TouchedExpiryPolicy) {
			TouchedExpiryPolicy cep = (TouchedExpiryPolicy) obj;
			Duration dur = cep.getExpiryForCreation();
			timeToLive = dur.getTimeUnit().toMillis(dur.getDurationAmount());
		}
		v.setTimeToLive(timeToLive);
		v.setName(cache.getName());

		List<String> keys = new ArrayList<>();
		QueryCursor<Cache.Entry<String, SessionVO>> cursor = cache.query(new ScanQuery<>());
		cursor.forEach(entry -> keys.add(entry.getKey()));
		v.setKeys(keys);
		v.setSize(keys.size());
		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() {
		return convert(cacheOutBoundAccessToken);
	}
	   
	@Override
	public void putWebSocketTimeout(String key, SessionVO value) {
		cacheWebSocketTimeout.put(key, value);
	}

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

	public String getMasterInstId() {
		try {
			return evictMasterCache.get(MASTER_KEY);
		} catch (Exception e) {
			logger.error("getMasterInstId null key=" + MASTER_KEY, e);
			return null;
		}
	}

	public void closeManager() {
		if(localListener != null) {
			((IgniteEvent) localListener).shutdown();
		}
		manager.close();
	}
	
	@Override
	public String getCacheStatus() {
		StringBuilder sb = new StringBuilder();
        Iterable<String> cacheNames = manager.cacheNames();
        sb.append("<Caches type=\"Ignite\">").append("\n");
        for (String cacheName : cacheNames) {
            IgniteCache<Object, Object> cache = manager.cache(cacheName);
            CacheMetrics metrics = cache.metrics();
            long cacheSize = metrics.getSize();
            sb.append("<Cache name=\"" + cacheName + "\" Size=\"" + cacheSize).append("\"/>\n");
        }
        sb.append("</Caches>");
		return sb.toString();
	}

	@Override
	public BearerTokenInfo getCAToken(String key) {
		try {
			return cacheCAToken.get(key);
		} catch (Exception e) {
			logger.error("getCAToken null key=" + key, e);
			return null;
		}
	}

	@Override
	public void putCAToken(String key, BearerTokenInfo value) {
		if (logger.isWarn()) {
			logger.warn("Put CAToken key - " + key + ", CAToken - " + value.toString());
		}
		cacheCAToken.put(key, value);
		
	}

	@Override
	public void removeCAToken(String key) {
		if (logger.isWarn()) {
			logger.warn("Remove CAToken key - " + key);
		}
		cacheCAToken.remove(key);
		
	}
	
	
	private void sleepQuietly(long millis) {
	    try {
	        Thread.sleep(millis);
	    } catch (InterruptedException e) {
	        // 인터럽트 상태 복구 (중요)
	        Thread.currentThread().interrupt();
	    }
	}
	
	public AccessTokenVO getOutboundAccessToken(String key, Function<AccessTokenVO, AccessTokenVO> tokenSupplier) {
	 // 1. 먼저 캐시에서 조회 (락 없이 빠르게)
	    AccessTokenVO token = cacheOutBoundAccessToken.get(key);

	    // 2. 만료되었거나 없다면 락 획득 시도
	    if (token == null || token.isExpired()) {
		 Lock lock = cacheOutBoundAccessToken.lock(DISTRIBUTED_TOKEN_LOCK); // Ignite 분산 락
		        
		 boolean acquired = false;
	        
	        try {
	            logger.debug("calling func getOutboundAccessToken = distributed ignite trylock");
	            acquired = lock.tryLock(60, TimeUnit.SECONDS);
	            
	            if ( acquired ) {
	        	logger.debug("calling func getOutboundAccessToken = distributed ignite trylock success=>"+acquired);
			// 3. 락 획득 후 다시 확인 (그 사이 다른 인스턴스가 갱신했을 수 있음)
			token = cacheOutBoundAccessToken.get(key);

			logger.debug(String.format("getting token in ignite=%s", token));

			if (token == null || token.isExpired()) {
			    // 4. 실제로 비어있을 때만 외부 API 호출 (I/O 발생)
			    token = tokenSupplier.apply(token);

			    logger.debug(String.format("getting token is null or expired borrow new token =%s", token));

			    cacheOutBoundAccessToken.put(key, token);
			}
	            }
	            else 
	            {
	            	logger.debug("calling func getOutboundAccessToken = distributed ignite trylock fail=>"+acquired);
	            }
	        }catch(Throwable e) {
	        	logger.error("occuring exception in getOutboundAccessToken", e);
	        }finally {
	            
	            try {
	        	if( acquired ) {
		        	lock.unlock();
		        	logger.debug("calling func getOutboundAccessToken = distributed ignite unlock");
		            }
	            }catch(Throwable e) {
	            	logger.error("occuring exception in getOutboundAccessToken unlock fail.", e);
	            }
	        }
	    }
	    return token;
	}


	@Override
	public void removeOutboundAccessToken(String key) {

	    Lock lock = cacheOutBoundAccessToken.lock(DISTRIBUTED_TOKEN_LOCK); // Ignite 분산 락

	    boolean acquired = false;
	    try {
		
		logger.debug("calling func removeOutboundAccessToken = distributed ignite trylock");
		acquired = lock.tryLock(60, TimeUnit.SECONDS);
		
		if( acquired ) {
		    if (logger.isDebug()) {

			logger.debug("calling func getOutboundAccessToken = distributed ignite trylock success=>"
				+ acquired);

			logger.debug("calling func removeOutboundAccessToken Remove OutBoundAccessToken key - " + key);
		    }
		    cacheOutBoundAccessToken.remove(key);
		}else {
		    logger.debug("calling func getOutboundAccessToken = distributed ignite trylock fail=>"+acquired);
		}
		    

	    } catch (Throwable e) {
	    	logger.error("occuring exception in removeOutboundAccessToken", e);
	    } finally {

		try {
		    if (acquired) {
			lock.unlock();
			logger.debug("calling func removeOutboundAccessToken = distributed ignite unlock");
		    }
		} catch (Throwable e) {
		    logger.error("occuring exception in removeOutboundAccessToken unlock fail.", e);
		}
	    }

	}
	
	@Override
	public void clearOutboundAccessToken() {
	    

	}

}
