package com.eactive.eai.adapter.socket2.protocol;

import java.net.InetSocketAddress;
import java.util.concurrent.TimeoutException;

import javax.net.ssl.SSLContext;

import org.apache.mina.common.ConnectFuture;
import org.apache.mina.common.IoSession;
import org.apache.mina.common.ThreadModel;
import org.apache.mina.common.support.BaseIoConnector;
import org.apache.mina.common.support.BaseIoConnectorConfig;
import org.apache.mina.filter.SSLFilter;
import org.apache.mina.util.NewThreadExecutor;

import com.eactive.eai.adapter.EAIException;
import com.eactive.eai.adapter.socket2.EAIConnectException;
import com.eactive.eai.adapter.socket2.common.ExecutionContext;
import com.eactive.eai.adapter.socket2.common.SMSLogger;
import com.eactive.eai.adapter.socket2.common.SimpleFuture;
import com.eactive.eai.adapter.socket2.common.TimeUtil;
import com.eactive.eai.adapter.socket2.config.SocketAdapterContext;
import com.eactive.eai.adapter.socket2.filter.datacontrol.DataControlFilter;
import com.eactive.eai.adapter.socket2.filter.executor.BoundedExecutorFilter;
import com.eactive.eai.adapter.socket2.filter.initmsgshake.InitMsgFilter;
import com.eactive.eai.adapter.socket2.filter.polling.PollingFilter;
import com.eactive.eai.adapter.socket2.filter.ssl.SSLContextFactory;
import com.eactive.eai.adapter.socket2.filter.validation.ValidationFilter;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.session.SessionManager;
import com.eactive.eai.common.session.SessionVO;
import com.eactive.eai.util.HealthCheckManager;

import edu.emory.mathcs.backport.java.util.concurrent.Executor;
import edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor;


public abstract class SocketAdapterClient extends SocketAdapter {
	protected Executor ioProcessExecutor;
	protected BaseIoConnector connector;
	protected BaseIoConnectorConfig config;
	private Thread launcher = null;
	
	protected SocketAdapterClient(SocketAdapterContext context) throws Exception {
		super(context);
		
		if (context.getMaxConnection() == 0) {
			context.setMaxConnection(1);
		}

		/**
		 * Configure the service.
		 */
		config = createConnectorConfig();
	
		config.setConnectTimeout(context.getTimeout());
		config.setThreadModel(ThreadModel.MANUAL);
		
		/**
		 * filters
		 */		
		config.getFilterChain().addFirst("threadPool", new BoundedExecutorFilter(this, executor));
		//config.getFilterChain().addLast("logger", new LoggingFilter());		
		
		if ("Y".equals(context.getSslUse())) {
			SSLContext sslContext = SSLContextFactory.createSSLContext(context);
			SSLFilter sslFilter = new SSLFilter(sslContext);
			sslFilter.setUseClientMode(true);
			config.getFilterChain().addLast("ssl", sslFilter);
		}
		
		//KSH
		if (context.isInitMsgUse()) {
			config.getFilterChain().addLast("initMsg", new InitMsgFilter(this));
		}
	
		if (context.isPollingUse()) {
			config.getFilterChain().addLast("polling", new PollingFilter(this));
		}
		
		
		if (!"".equals(context.getValidationRule().trim())) {
			config.getFilterChain().addLast("validation", new ValidationFilter(this));
		}
		if (!"".equals(context.getDataControlRule().trim())) {
			config.getFilterChain().addLast("datacontrol", new DataControlFilter(this));
		}
	}

	public SimpleFuture start() throws Exception {
		if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_INFO) {
			logger.info(context.getAdapterName() + " | start client socket adapter.");
		}
		
		switch(getState()) {
		case STARTED:
			throw new Exception(context.getAdapterName() + " | socket adapter already started. - " + getState().name());
		case STARTING:
			throw new Exception(context.getAdapterName() + " | socket adapter already starting... - " + getState().name());
		case STOPPING:
			throw new Exception(context.getAdapterName() + " | socket adapter shutdown progressing. - " + getState().name());
		default:
			break;
		}
		
		/**
		 * start protocol handler
		 */
		handler.start();		
		
		/**
		 * io process executor
		 */
		ioProcessExecutor = new NewThreadExecutor();
		
		/**
		 * determine ioProcessor count
		 */
		int ioProcessorCount1 = 1 + Runtime.getRuntime().availableProcessors();
		int ioProcessorCount2 = 1 + (context.getMaxThread() / 10);
		int ioProcessorCount = ioProcessorCount1 < ioProcessorCount2 ? ioProcessorCount1 : ioProcessorCount2;
		
		/**
		 * create connector
		 */
		connector = createConnector(ioProcessorCount, ioProcessExecutor);
		
		setState(State.STARTING);
		
		final SimpleFuture future = new SimpleFuture();
//		final ISocketAdapter adapter = this;
		
		if (!context.isPermanent()) {
			setAdapterStatus(true);
			setState(State.STARTED);
			future.set(Boolean.TRUE);
			return future;
		}
		
		Runnable runnable = new Runnable() {
			public void run() {
				boolean retry = true;
				while(true) {
					try {
						if(getState() == State.STARTED
							|| getState() == State.STOPPING
							|| getState() == State.STOPPED) {
							logger.warn("Exit Launcher - " + context.getAdapterName() + "-Terminator");
							return;
						}
						
						retry = true;
						if(context.getSingleSession().equals("Y")) {
							String adapterGroupoName = context.getAdapterGroupName();
							SessionVO sessionVo = SessionManager.getInstance().getConnectionStatus(adapterGroupoName);
							
							// 싱글어댑터 - Start ---------------------------------------------------------------------------------------------------------------------
							String primaryInst = context.getPrimaryInstId();
							EAIServerManager eaiServerManager = EAIServerManager.getInstance();
							String localServerName = eaiServerManager.getLocalServerName();
							
							if ( localServerName.equals(primaryInst)) { // CASE : Primary 서버
								SessionVO sessionVO = SessionManager.getInstance().getConnectionStatus(adapterGroupoName);
								
								if (sessionVO != null) {
									if(localServerName.equals(sessionVO.getLoginInstId())) {
										if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {
											logger.debug(context.getAdapterName() + "| SingleSession Validation, local server exist [" +sessionVO.getLoginInstId()+"], remove cache -> retry to connect after 10 secs");
										}
										SessionManager.getInstance().removeConnectionInfo(adapterGroupoName);										
									}
									else {// Secondary 서버가 연결하고 있음
										retry = false;
										if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {
											logger.debug(context.getAdapterName() + "| SingleSession Validation, Secondary server exist [" +sessionVO.getLoginInstId()+"]");
										}
									}
								} else {
									if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {
										logger.debug(context.getAdapterName() + "| SingleSession Validation, Connection empty -> retry to connect after 10 secs");
									}
									
								}
							} else { // CASE : Secondary 서버
								boolean isAlive = HealthCheckManager.getInstance().getServerStatus(primaryInst);
								
								if (isAlive) {  // Primary 서버 살아있으면 재연결 시도 안함
									retry = false;
									if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG)
										logger.debug(context.getAdapterName() + "| SingleSession Validation, Primary server exist [" +primaryInst+"]");
								} else {
									SessionManager.getInstance().removeConnectionInfo(adapterGroupoName);
								}
								
								SessionVO svo = SessionManager.getInstance().getConnectionInfo(adapterGroupoName);
								
								if (svo != null) {
									String connectedInst = svo.getLoginInstId();
									if(connectedInst.equals(localServerName))
										SessionManager.getInstance().removeConnectionInfo(adapterGroupoName);
								}
							}
							// 싱글어댑터 - End ---------------------------------------------------------------------------------------------------------------------
							
							
							if(sessionVo != null) {
								retry = false;
								if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {
									logger.debug(context.getAdapterName() + " | SingleSession Validation Exist ["+sessionVo.getLoginInstId() +"] -> retry to connect after 10 seconds...");
								}
							}
							else {
								if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {
									logger.debug(context.getAdapterName() + " | SingleSession Validation Connection Empty -> retry to connect after 10 seconds...");
								}
							}
						}
						
						if(retry) {
							boolean result = connect();
					
							if (result && getSessionPool().size() > 0) {
								setAdapterStatus(true);
								context.setStarted(true);
								setState(State.STARTED);
								future.set(Boolean.TRUE);
								break;
							} else {
								setAdapterStatus(false);
								context.setStarted(false);
								future.set(Boolean.FALSE);
								getSessionPool().shutdown();
							}
						}
					} catch(Throwable t) {
						setAdapterStatus(false);
						context.setStarted(false);
						future.set(t);
						getSessionPool().shutdown();
					}

					if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG
						&& retry && logger.isDebug()) {
						logger.debug(context.getAdapterName() + " | failed to connect -> retry to connect after 10 seconds...");
					}

					try {
						Thread.sleep(10000L);
					} catch(InterruptedException e) {
						if(logger.isDebug()) {
							logger.debug(context.getAdapterName() + " sleep error - " + e.getMessage());
						}
					}
				}
			}
		};
		//executor.execute(runnable);
		launcher = new Thread(runnable);
		launcher.setName(context.getAdapterName() + "-Launcher");
		launcher.start();
		
		return future;
	}

	public SimpleFuture stop() throws Exception {
		if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_INFO) {
			logger.info(context.getAdapterName() + " | stop client socket adapter.");
		}
		
		switch(getState()) {
		case STOPPING:
		case STOPPED:
			logger.warn(context.getAdapterName() + " | socket adapter already " + getState().name());
			final SimpleFuture future = new SimpleFuture();
			future.set(Boolean.TRUE);
			return future;
		default:
			break;
		}

		return doStop();
	}	
	
	public SimpleFuture stopForcely() throws Exception {
		if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_INFO) {
			logger.info(context.getAdapterName() + " | stop client socket adapter forcely.");
		}
		return doStop();		
	}
	
	private SimpleFuture doStop() throws Exception {
		setState(State.STOPPING);
		context.setStarted(false);

		final SimpleFuture future = new SimpleFuture();
		
		Runnable runnable = new Runnable() {
			public void run() {
				try {
					handler.stop();
					getSessionPool().shutdown();
					setAdapterStatus(false);
					setState(State.STOPPED);
//					List list = ioProcessExecutor.shutdownNow();
//					for (Iterator iter = list.iterator(); iter.hasNext(); ) {
//						((Thread) iter.next()).interrupt();
//					}					
					future.set(Boolean.TRUE);
					
					if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_INFO) {
						logger.info(context.getAdapterName() + " | socket adapter stopped.");
					}
				} catch(Exception e) {
					logger.error(context.getAdapterName() + " | socket adapter stop process failed.", e);
					addRecentError(context.getAdapterName() + " | socket adapter stop process failed.", e);
					future.set(e);
				}
			}
		};
		//executor.execute(runnable);
		Thread t = new Thread(runnable);
		t.setName(context.getAdapterName() + "-Terminator");
		t.start();

		return future;		
	}

	@Override
	public boolean connect() throws Exception {		
		for (int i = 0; i < context.getMaxConnection(); i++) {
			IoSession session = connectOne();
			if (session == null) {
		        setState(State.STOPPED);
				context.setStarted(false);
				throw new Exception(context.getAdapterName() + " | failed to establish connection ... ");
			}
		}
		return true;
	}
	
	@Override
	public IoSession connectOne() throws EAIConnectException {
		InetSocketAddress remoteAddress = new InetSocketAddress(context.getHostName(), context.getPortNumber());
		InetSocketAddress localAddress = null;
		
		if (context.getLocalPortNumber() > 0 || context.getLocalHostName().trim().length() > 0) {
			localAddress = new InetSocketAddress(context.getLocalHostName(), context.getLocalPortNumber());
		}
		
		IoSession session = null;
		
		try {
			ConnectFuture future = connector.connect(
					remoteAddress, 
					localAddress, 
					handler, 
					config);
			
			/**
			 * inbound connection 이 끊어졌을 경우, 
			 * outbound connection 시도 작업을 중단한다.
			 */
			ExecutionContext executionContext = ExecutionContext.getInstance();
			IoSession sess = (IoSession) executionContext.get("session");
			if (sess != null) {
				sess.setAttribute("connectFuture", future);
			} 
			
			boolean success = future.join(context.getTimeout() * 1000L);
			if (success) {
				session = future.getSession();	
				if(!session.isConnected()) {
					throw new EAIException("session closed by peer (" + session + ")");
				}				
				return session;
			} else {
				throw new TimeoutException("connect timeout '" + context.getTimeout() + "' sec");
			}
		} catch (Exception e) {
			if (session != null && session.isConnected()) {
				try {
					getSessionPool().remove(session);
					session.close().join();
				} catch(Exception ee) {
					logger.warn(context.getAdapterName() +" session close error");
				}
			}
			String errorMessage = context.getAdapterName() 
					+ " | cannot connect - " + context.getHostName() + "/" + context.getPortNumber();
			logger.error(errorMessage, e);
			
			if (getState() != State.STARTING) {
				addRecentError(errorMessage, e);
			} else {
				addRecentErrorOnly(errorMessage, e);
			}
			
			try {
				if (TimeUtil.isSMSRunningTime(context.getSmsRunningTime()))
					SMSLogger.loggingSMS4Soc(context.getAdapterName(), context.getDescription(), "RECEAINNA014");
			} catch(Throwable t) {
				logger.debug("[EAIDEBUG] SMS logging Error occured. but ignore this. - " + t.toString());
			}

			context.setStarted(false);
			
			throw new EAIConnectException("cannot connect - " + context.getHostName() + "/" + context.getPortNumber(), e);
		}
	}
	
	abstract protected BaseIoConnectorConfig createConnectorConfig();
	abstract protected BaseIoConnector createConnector(int ioProcessorCount, Executor ioProcessExecutor);
}
