
package com.eactive.eai.adapter.http.client;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.HashMap;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.SSLContext;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpVersion;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;

import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.Keys;
import com.eactive.eai.adapter.http.secure.CustomHttpsSocketFactory;
import com.eactive.eai.adapter.http.secure.CustomSSLProtocolSocketFactory;
import com.eactive.eai.adapter.http.secure.EasySSLProtocolSocketFactory;
import com.eactive.eai.adapter.http.secure.SSLContextFactory;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.httpouttlsinfo.HttpOutTlsInfoManager;
import com.eactive.eai.httpouttlsinfo.HttpOutTlsInfoVO;

public abstract class HttpClientAdapterServiceSupport implements HttpClientAdapterService, HttpClientAdapterServiceKey {
	protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
	protected HttpClient client;
	private MultiThreadedHttpConnectionManager connectionManager;

	public synchronized void close() {
		if (connectionManager != null) {
			connectionManager.shutdown();
		}
	}

	protected HttpClientAdapterVO setting(Properties prop, Properties tempProp) {
		String url = null;
		String parameterName = null;
		String method = null;
		String contentType = null;
		String encode = null;
		String testCallYn = null;
		String adapterGroupName = null;
		String adapterName = null;
		String responseType = null;
		String urlEncodeYn = null;
		String adapterHeader = null;

		boolean useMtls = false;
		String keyStorePath = null;
		String keyStorePassword = null;
		String trustStorePath = null;
		String trustStorePassword = null;

		int connectionTimeout = 0;
		int timeout = -1;
		int traceLevel = 0;
		long slowTranTime = 2000L;

		url = prop.getProperty(URL);

		String url2grp = tempProp.getProperty(URL);
		if (!StringUtils.isEmpty(url2grp)) {
			url = url2grp;
		}
		method = prop.getProperty(METHOD);
		parameterName = prop.getProperty(PARAMETER_NAME, PARAMETER_MESSAGE);
		contentType = prop.getProperty(CONTENT_TYPE, "");
		responseType = prop.getProperty(RESPONSE_TYPE, "SYNC");
		urlEncodeYn = prop.getProperty(URL_ENCODE_YN, "N");
		String connectionTimeoutTemp = prop.getProperty(CONNECTION_TIMEOUT, "0");

		String httpVersion = prop.getProperty(HTTP_VERSION);
		adapterHeader = prop.getProperty(ADAPTER_HEADER, "");

		String traceLevelTemp = prop.getProperty(TRACE_LEVEL, "0");
		String timeoutTemp = "10000";// 타임아웃 기준이 불명확해 운영 혼란 발생 → 인터페이스 TIMEOUT으로 단일화

		useMtls = StringUtils.equalsIgnoreCase(prop.getProperty(HttpClientAdapterServiceKey.USE_MTLS), "Y");
		keyStorePath = prop.getProperty(HttpClientAdapterServiceKey.KEYSTORE_PATH);
		keyStorePassword = prop.getProperty(HttpClientAdapterServiceKey.KEYSTORE_PWD);
		trustStorePath = prop.getProperty(HttpClientAdapterServiceKey.TRUSTSTORE_PATH);
		trustStorePassword = prop.getProperty(HttpClientAdapterServiceKey.TRUSTSTORE_PWD);

		encode = tempProp.getProperty(Keys.CHARSET_KEY_OUT, Charset.defaultCharset().name());
		testCallYn = tempProp.getProperty(TESTCALL_YN, "N");
		adapterGroupName = tempProp.getProperty(ADAPTER_GROUP_NAME);
		adapterName = tempProp.getProperty(ADAPTER_NAME);
		// 인터페이스 정보에 설정된 타임아웃
		String interfaceTimeout = tempProp.getProperty("INTERFACE_TIME_OUT");
		if (StringUtils.isNotBlank(interfaceTimeout) && NumberUtils.toInt(interfaceTimeout) > 0) {
			timeoutTemp = interfaceTimeout;
		}
		
		boolean isDrMode = Boolean.valueOf(PropManager.getInstance().getProperty(DR_PROP_GROUP, DR_PROP_KEY, "false"));

		if (isDrMode) {
			url = prop.getProperty(DR_URL);
		}

		url = StringUtils.trim(url);
		parameterName = StringUtils.trim(parameterName);
		method = StringUtils.trim(method);
		encode = StringUtils.trim(encode);
		contentType = StringUtils.trim(contentType);

		if (timeoutTemp == null) {
			timeoutTemp = "0";
		} else {
			timeoutTemp = timeoutTemp.trim();
		}

		try {
			connectionTimeout = Integer.parseInt(connectionTimeoutTemp);
		} catch (Exception e) {
			connectionTimeout = 3000;
			logger.debug("HttpClientAdapterSupport] connectionTimeoutTemp not int");
		}
		try {
			timeout = Integer.parseInt(timeoutTemp);
		} catch (NumberFormatException e) {
			logger.warn("HttpClientAdapterSupport] timeoutTemp not int", e);
		}

		try {
			traceLevel = Integer.parseInt(traceLevelTemp);
		} catch (Exception e) {
			logger.warn("HttpClientAdapterSupport] traceLevelTemp not int", e);
		}

		// client 중 BODY,POST,GET,NexCore 방식만 적용
		HashMap<String, String> adapterHeaders = new HashMap<>();
		try {
			Matcher matcher = Pattern.compile(
					"\\{[ ]*\"[ ]*([a-zA-Z0-9_-]+)[ ]*\"[ ]*,[ ]*\"[ ]*([a-zA-Z0-9/;,\\*=_ -]+)[ ]*\"[ ]*\\}[ ]*")
					.matcher(adapterHeader.trim());
			while (matcher.find()) {
				String key = matcher.group(1).trim();
				String value = matcher.group(2).trim();
				adapterHeaders.put(key, value);
			}
		} catch (Exception e) {
			if (logger.isDebug())
				logger.debug("HttpClientAdapterSupport] adapter header parsing error " + adapterHeader);
		}
		// GW 전달 로직 추가
		// tempProp로 넘겨서 하는 이유는 outboundProp가 clone 안하는 로직이라 outboundProp로 넘길시 문제의 소지가있음
		String transactionId = tempProp.getProperty(HttpClientAdapterServiceKey.TRANSACTION_ID);
		String instanceId = tempProp.getProperty(HttpClientAdapterServiceKey.INSTANCE_ID);
		if (StringUtils.isNotBlank(transactionId) && StringUtils.isNotBlank(instanceId)) {
			adapterHeaders.put(HttpClientAdapterServiceKey.TRANSACTION_ID, transactionId);
			adapterHeaders.put(HttpClientAdapterServiceKey.INSTANCE_ID, instanceId);
		}

		HttpClientAdapterVO vo = new HttpClientAdapterVO();
		vo.setUrl(url);
		vo.setMethod(method);
		vo.setParameterName(parameterName);
		vo.setEncode(encode);
		vo.setContentType(contentType);
		vo.setTestCallYn(testCallYn);
		vo.setResponseType(responseType);
		vo.setUrlEncodeYn(urlEncodeYn);
		vo.setAdapterGroupName(adapterGroupName);
		vo.setAdapterName(adapterName);

		vo.setMaxTotalConnections(getMaxTotalConnections());
		vo.setMaxHostConnections(getMaxConnectionsPerHost());
		vo.setConnectionTimeout(connectionTimeout);

		vo.setTimeout(timeout);
		vo.setTraceLevel(traceLevel);
		vo.setSlowTranTime(slowTranTime);
		vo.setAdapterHeaders(adapterHeaders);
		if (StringUtils.equalsAny(httpVersion, HTTP_VERSION_FULL_1_1, HTTP_VERSION_1_1)) {
			vo.setHttpVersion(HTTP_VERSION_1_1);
		} else {
			vo.setHttpVersion(HTTP_VERSION_1_0);
		}
		vo.setUseMtls(useMtls);
		vo.setKeyStorePath(keyStorePath);
		vo.setKeyStorePassword(keyStorePassword);
		vo.setTrustStorePath(trustStorePath);
		vo.setTrustStorePassword(trustStorePassword);

		if (logger.isDebug()) {
			logger.debug(vo.toString());
		}
		return vo;
	}

	protected HttpClientAdapterServiceSupport() {

	}

	public synchronized void init(String adapterName, boolean useMtls, HttpOutTlsInfoVO mtlsInfo,
			int maxTotalConnections, int maxHostConnections) {
		if (connectionManager != null) {
			if ((connectionManager.getParams().getMaxTotalConnections() == maxTotalConnections)
					&& (connectionManager.getParams().getDefaultMaxConnectionsPerHost() == maxHostConnections)) {
				if (logger.isInfo()) {
					logger.info("ConnectionManager REUSE :  AdapterName=" + adapterName + " MaxTotalConnection ("
							+ connectionManager.getParams().getMaxTotalConnections() + ") MaxHostConnections ("
							+ connectionManager.getParams().getDefaultMaxConnectionsPerHost() + ")");
				}
				return;
			} else {
				if (logger.isWarn()) {
					logger.warn("ConnectionManager RELOAD :  AdapterName=" + adapterName + " MaxTotalConnection ("
							+ connectionManager.getParams().getMaxTotalConnections() + " -> " + maxTotalConnections
							+ ") MaxHostConnections (" + connectionManager.getParams().getDefaultMaxConnectionsPerHost()
							+ " -> " + maxHostConnections + ")");
				}
				connectionManager.shutdown();
				connectionManager = null;
				this.client = null;
			}
		}

		if (logger.isWarn()) {
			logger.warn("ConnectionManager INIT :  AdapterName=" + adapterName + " MaxTotalConnection ("
					+ maxTotalConnections + ") MaxHostConnections (" + maxHostConnections + ")");
		}
		connectionManager = new MultiThreadedHttpConnectionManager();
		connectionManager.getParams().setStaleCheckingEnabled(true);
		connectionManager.getParams().setMaxTotalConnections(maxTotalConnections);
		connectionManager.getParams().setDefaultMaxConnectionsPerHost(maxHostConnections);

		this.client = new HttpClient(connectionManager);
	}

	public int getConnectionsInPool() {
		if (this.client == null)
			return 0;
		MultiThreadedHttpConnectionManager mgr = (MultiThreadedHttpConnectionManager) this.client
				.getHttpConnectionManager();
		return mgr.getConnectionsInPool();
	}

	public int getMaxTotalConnections() {
		if (this.client == null)
			return 0;
		MultiThreadedHttpConnectionManager mgr = (MultiThreadedHttpConnectionManager) this.client
				.getHttpConnectionManager();
		return mgr.getParams().getMaxTotalConnections();
	}

	public int getMaxConnectionsPerHost() {
		if (this.client == null)
			return 0;
		MultiThreadedHttpConnectionManager mgr = (MultiThreadedHttpConnectionManager) this.client
				.getHttpConnectionManager();
		return mgr.getParams().getDefaultMaxConnectionsPerHost();
	}

	public String getResponseCharSet(String httpResponseCharset, String adapterGroupName) {
		if (StringUtils.isNotBlank(httpResponseCharset)) {
			return httpResponseCharset;
		} else {
			AdapterManager m = AdapterManager.getInstance();
			AdapterGroupVO agvo = m.getAdapterGroupVO(adapterGroupName);
			return agvo.getMessageEncode();
		}
	}

	/**
	 * encoding에 대한 정의 필요 고려할 문제 1. 어댑터그룹이 UJN 타입일경우 return 은 UTF-8 로 getBytes 된
	 * 데이터여야됨. 2. http response에 받은 encoding 타입은 고려대상임. 결론 : response Encoding 과
	 * 어댑터의 MESSAGE_TYPE 다르면 변환, 나머지면 return 어댑터의 ENCODE은 요청전문 encoding에서만 사용
	 * 
	 * 예) 어댑터의 ENCODE, 어댑터의 MESSAGE TYPE, RESPNSE ENCODING TYPE, 처리내용 예) ,ECU-KR
	 * ,JSN , EUC-KR , 응답받은 메시지를 return 예) ,ECU-KR , JSN , UTF-8 , 응답받은 메시지를 new
	 * String(xxx,"UTF-8") => getBytes("EUC-KR") 예) ,ECU-KR , UJN , EUC-KR , 응답받은
	 * 메시지를 new String(xxx,"EUC-KR") => getBytes("UTF-8") 예) ,ECU-KR , UJN , UTF-8 ,
	 * 응답받은 메시지를 return 예) ,UTF-8 , JSN , EUC-KR , 응답받은 메시지를 return 예) ,UTF-8 , JSN
	 * , UTF-8 , 응답받은 메시지를 new String(xxx,"UTF-8") => getBytes(EUC-KR") 예) ,UTF-8 ,
	 * UJN , EUC-KR , 응답받은 메시지를 new String(xxx,"EUC-KR") => getBytes("UTF-8") 예)
	 * ,UTF-8 , UJN , UTF-8 , 응답받은 메시지를 return
	 * 
	 * 
	 */
	public byte[] getEncodingConvert(String httpResponseCharset, String adapterGroupName, byte[] message) {
		byte[] responseMessage = null;
		if (StringUtils.isNotBlank(httpResponseCharset)) {
			AdapterManager m = AdapterManager.getInstance();
			AdapterGroupVO agvo = m.getAdapterGroupVO(adapterGroupName);
			String adapterTypeEncoding = StringUtils.defaultIfBlank(agvo.getMessageEncode(),
					Charset.defaultCharset().name());
			Charset responseCharset = null;
			Charset adapterCharset = null;
			try {
				responseCharset = Charset.forName(httpResponseCharset);
				adapterCharset = Charset.forName(adapterTypeEncoding);
				if (responseCharset.compareTo(adapterCharset) == 0) {
					responseMessage = message;
				} else {// http 응답 charset 과 어댑터의 charset 이 다를경우 charset 변환 필요
					String encodingString = new String(message, httpResponseCharset);
					responseMessage = encodingString.getBytes(adapterTypeEncoding);
				}
			} catch (UnsupportedCharsetException | UnsupportedEncodingException e) {
				responseMessage = message;
			}
		} else {
			responseMessage = message;
		}
		return responseMessage;
	}

	@SuppressWarnings("deprecation")
	public void configureHttpClient(HttpClient mclient, HttpMethodBase method, HttpClientAdapterVO vo, String url,
			String contentType) throws Exception {
		if (StringUtils.isNotBlank(vo.getEncode())) {
			mclient.getParams().setContentCharset(vo.getEncode());
		}

		if (vo.getConnectionTimeout() != 0) {
			mclient.getHttpConnectionManager().getParams().setConnectionTimeout(vo.getConnectionTimeout());
		}

		if (vo.getTimeout() > -1) {
			mclient.getHttpConnectionManager().getParams().setSoTimeout(vo.getTimeout());
		}

		if (HTTP_VERSION_1_1.equals(vo.getHttpVersion())) {
			method.setRequestHeader("Connection", "close");
			method.getParams().setParameter("http.protocol.version", HttpVersion.HTTP_1_1);
		} else {
			method.getParams().setParameter("http.protocol.version", HttpVersion.HTTP_1_0);
		}

		method.setFollowRedirects(false);

		if (StringUtils.startsWith(url, "https")) {
			if (vo.isUseMtls()) {
				HttpOutTlsInfoManager tlsManager = HttpOutTlsInfoManager.getInstance();
				String clientId = vo.getClientId();
				HttpOutTlsInfoVO mtlsInfo = tlsManager.getHttpOutTlsInfo(clientId);
				String storeType = mtlsInfo.getStoreType();
				String keyStoreInfo = mtlsInfo.getKeystoreInfo();
				String keyStorePassword = mtlsInfo.getKeystorePassword();
				String trustStoreInfo = mtlsInfo.getTruststoreInfo();
				String trustStorePassword = mtlsInfo.getTruststorePassword();
				
				// Server negotiation 에 의해 변경될 수 있음
				String[] tlsVersions = null;
//				{
//					"TLSv1.0"
//					, "TLSv1.1"
//					, "TLSv1.2"
//					, "TLSv1.3"
//				};
				String[] cipherSuites = null;
//				{
//					"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", 
//					"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", 
//					"TLS_RSA_WITH_AES_256_CBC_SHA256", 
//					"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", 
//					"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", 
//					"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", 
//					"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", 
//					"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", 
//					"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", 
//					"TLS_RSA_WITH_AES_256_CBC_SHA", 
//					"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", 
//					"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", 
//					"TLS_DHE_RSA_WITH_AES_256_CBC_SHA", 
//					"TLS_DHE_DSS_WITH_AES_256_CBC_SHA", 
//					"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", 
//					"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", 
//					"TLS_RSA_WITH_AES_128_CBC_SHA256", 
//					"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", 
//					"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", 
//					"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", 
//					"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", 
//					"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", 
//					"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 
//					"TLS_RSA_WITH_AES_128_CBC_SHA", 
//					"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", 
//					"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", 
//					"TLS_DHE_RSA_WITH_AES_128_CBC_SHA", 
//					"TLS_DHE_DSS_WITH_AES_128_CBC_SHA", 
//					"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 
//					"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 
//					"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 
//					"TLS_RSA_WITH_AES_256_GCM_SHA384", 
//					"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", 
//					"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", 
//					"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", 
//					"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", 
//					"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 
//					"TLS_RSA_WITH_AES_128_GCM_SHA256", 
//					"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", 
//					"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", 
//					"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", 
//					"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", 
//					"TLS_EMPTY_RENEGOTIATION_INFO_SCSV"
//			};
				
				if (StringUtils.isAnyEmpty(keyStoreInfo, keyStorePassword)) {
					throw new Exception("mTLS keyStore config error");
				}

				boolean skipTrust = false;
				// kbank에서는 trustStore를 사용하지 않음
				if (StringUtils.isAnyEmpty(trustStoreInfo, trustStorePassword)) {
//					throw new Exception("mTLS trustStore config error");
					if (logger.isWarn())
						logger.warn("Skip trustStore validation adapterName : " + vo.getAdapterName());
					skipTrust = true;
				}

				SSLContext sslContext = SSLContextFactory.createMTLSContextFromContent(storeType, keyStoreInfo,
						keyStorePassword, trustStoreInfo, trustStorePassword, skipTrust, tlsVersions, cipherSuites);
				// SSLProtocolSocketFactory 생성
				ProtocolSocketFactory socketFactory = new CustomSSLProtocolSocketFactory(sslContext);
				Protocol.registerProtocol("https", new Protocol("https", socketFactory, 443));
			} else {
				Protocol.registerProtocol("https",
						new Protocol("https", new CustomHttpsSocketFactory(new EasySSLProtocolSocketFactory()), 443));
			}
		}

		if (StringUtils.equalsIgnoreCase(method.getName(), "POST")
				|| StringUtils.equalsIgnoreCase(method.getName(), "PUT")) {
			if (StringUtils.isNotBlank(contentType)) {
				if (StringUtils.isBlank(vo.getEncode())) {
					method.setRequestHeader("Content-Type", contentType);
				} else {
					method.setRequestHeader("Content-Type",
							String.format("%s; charset=%s", contentType, vo.getEncode()));
				}
			}
		}
	}

	public abstract Object execute(Properties prop, Object message, Properties tempProp) throws Exception;
}
