
package com.eactive.eai.authoutbound.client.impl;
 
import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
import com.eactive.eai.adapter.http.secure.HttpClient5SSLContextFactory;
import com.eactive.eai.authoutbound.OutboundOAuthCredentialVo;
import com.eactive.eai.authoutbound.client.HttpClientAccessTokenServiceByDB;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.httpouttlsinfo.HttpOutTlsInfoManager;
import com.eactive.eai.httpouttlsinfo.HttpOutTlsInfoVO;
import com.eactive.eai.util.TestModeChecker;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.openbanking.eai.common.token.AccessTokenVO;
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.TrustAllStrategy;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.apache.hc.core5.util.Timeout;
import org.springframework.security.web.util.UrlUtils;
import tcseed.base64;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.*;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;

/**
 * 1. 기능 : HTTP 웹 컴포넌트를 POST 방식으로 호출할 수 있는 기능을 제공한다. 2. 처리 개요 : * - 2009.11.24
 * retry 로직 제거 : 요청 3. 주의사항
 *
 * @author :
 * @version : v 1.0.0
 * @see : WLIAdapterFactory.java, WLIAdapter.java, WLIDefaultAdapter.java
 * @since :
 * 
 */
public class HttpClientAccessTokenServiceWithBase64NiceOn implements HttpClientAccessTokenServiceByDB {

	public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);

	private static boolean testMode = TestModeChecker.isTestMode();

	/**
	 * 1. 기능 : 법인검증 토큰 발급에 사용 2. 처리 개요 : - 속성 정보를 설정 하고 토큰 발급 URL 호출 한다. 3. 주의사항
	 *  - JSON 방식
	 *  - ( Header	Authorization	Basic <base64_Encode(client_id:client_secret)> ) 으로 구성
	 * @param adapterProp Http Adapter 속성 정보
	 * @return 반환 된 AccessTokenVO
	 * @exception Exception 수동 시스템 간 통신 중 발생
	 **/
	public AccessTokenVO execute(String name, Properties adapterProp, OutboundOAuthCredentialVo oAuthCredentialVo)
			throws Exception {
		AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(name);
		String adapterUrl = adapterProp.getProperty("URL");
		String encode = gvo.getMessageEncode();
		String timeoutTemp = adapterProp.getProperty("HTTP_TIME_OUT");
		if (StringUtils.isBlank(timeoutTemp)) {
			timeoutTemp = "30000";
		}
		String connectionTimeoutTemp = adapterProp.getProperty("CONNECTION_TIMEOUT");
		if (StringUtils.isBlank(connectionTimeoutTemp)) {
			connectionTimeoutTemp = "30000";
		}

		int timeout = Integer.parseInt(timeoutTemp);
		int connectionTimeout = Integer.parseInt(connectionTimeoutTemp);
		long currentTime = System.currentTimeMillis();

		String uri = oAuthCredentialVo.getUrl();

		if (!UrlUtils.isAbsoluteUrl(uri)) {
			uri = appendPath(adapterUrl, uri);
		}
		String contentType = "application/x-www-form-urlencoded";  // 광주은행 나이스지키미

		Charset charset;
		if (StringUtils.isNotBlank(encode)) {
			charset = Charset.forName(encode);
		} else {
			charset = Charset.defaultCharset();
			encode = charset.toString();
		}

		boolean useForwardProxy = StringUtils.equalsIgnoreCase(adapterProp.getProperty("FORWARD_PROXY_USE_YN"), "Y");
		String forwardProxyUrl = adapterProp.getProperty("FORWARD_PROXY_URL");

		logger.debug(
				"uri:{}, charset:{}, transactionTimeout:{}, connectionTimeout:{}, useForwardProxy:{}, forwardProxyUrl:{}",
				uri, encode, timeout, connectionTimeout, useForwardProxy, forwardProxyUrl);

		List<NameValuePair> formParams = new ArrayList<>();
		//formParams.add(new BasicNameValuePair("client_id", oAuthCredentialVo.getClientId()));
		//formParams.add(new BasicNameValuePair("client_secret", oAuthCredentialVo.getClientSecret()));
		formParams.add(new BasicNameValuePair("grant_type", oAuthCredentialVo.getGrantType()));
		formParams.add(new BasicNameValuePair("scope", oAuthCredentialVo.getScope()));
		//String postParameters = "grant_type=" + oAuthCredentialVo.getGrantType() + "&scope=" + oAuthCredentialVo.getScope();
		
		
		// mTLS config with default connection parameters
		boolean useMtls = StringUtils.equalsIgnoreCase(adapterProp.getProperty(HttpClientAdapterServiceKey.USE_MTLS),
				"Y");
		AdapterGroupVO adapterGroup = AdapterManager.getInstance().getAdapterGroup(name);
		String clientId = adapterGroup.getClientId();

		if (logger.isInfo()) {
			logger.info("MTLS adapterGroupName. : {}, useMtls. : {}, clientId : {}", name, useMtls, clientId);
		}

		int maxTotalConnections = HttpClientAdapterServiceKey.DEFAULT_MAX_TOTAL_CONNECTIONS;
		int maxHostConnections = HttpClientAdapterServiceKey.DEFAULT_MAX_CONNECTION_PER_HOST;

		HttpOutTlsInfoVO mtlsInfo = null;
		SSLContext sslContext = null;
		PoolingHttpClientConnectionManagerBuilder cmBuilder = PoolingHttpClientConnectionManagerBuilder.create();
		PoolingHttpClientConnectionManager connectionManager = null;

		try {
			if (useMtls) {
				HttpOutTlsInfoManager tlsManager = HttpOutTlsInfoManager.getInstance();
				if (StringUtils.isNotEmpty(clientId)) {
					mtlsInfo = tlsManager.getHttpOutTlsInfo(clientId);
				}
			}

			if (useMtls && mtlsInfo != null) {
				String storeType = mtlsInfo.getStoreType();
				String keyStoreInfo = mtlsInfo.getKeystoreInfo();
				String keyStorePassword = mtlsInfo.getKeystorePassword();
				String trustStoreInfo = mtlsInfo.getTruststoreInfo();
				String trustStorePassword = mtlsInfo.getTruststorePassword();

				String[] tlsVersions = null;
				String[] cipherSuites = null;

				if (StringUtils.isAnyEmpty(keyStoreInfo, keyStorePassword)) {
					throw new Exception("mTLS keyStore config error");
				}

				boolean skipTrust = false;
				if (StringUtils.isAnyEmpty(trustStoreInfo, trustStorePassword)) {
					if (logger.isWarn())
						logger.warn("Skip trustStore validation adapterGroupName : " + name);
					skipTrust = true;
				}

				sslContext = HttpClient5SSLContextFactory.createMTLSContextFromContent(storeType, keyStoreInfo,
						keyStorePassword, trustStoreInfo, trustStorePassword, skipTrust, tlsVersions, cipherSuites);

				SSLConnectionSocketFactory sslSocketFactory = null;
				if (testMode) {
					sslSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
				} else {
					sslSocketFactory = new SSLConnectionSocketFactory(sslContext);
				}
				cmBuilder.setSSLSocketFactory(sslSocketFactory);
			} else {
				sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
				SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
				cmBuilder.setSSLSocketFactory(csf);
			}
		} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
				| IOException | UnrecoverableKeyException e) {
			throw new RuntimeException(e);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

		connectionManager = cmBuilder.build();
		connectionManager.setMaxTotal(maxTotalConnections);
		connectionManager.setDefaultMaxPerRoute(maxHostConnections);

		try (CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();) {
			HttpPost httpPost = new HttpPost(uri);
			httpPost.setEntity(new UrlEncodedFormEntity(formParams, charset));
			//httpPost.setEntity(new StringEntity(postParameters, StandardCharsets.UTF_8));
			httpPost.setHeader("Content-Type", contentType+";charset=" + encode);  // 중간에 ; 주어야 함

			//HTTP HEADER에 client id, client secret를 base64Encode 한 후 추가
			String cId = oAuthCredentialVo.getClientId();
			String cSecret = oAuthCredentialVo.getClientSecret();
			String authValue = cId + ":" + cSecret;
			String encodedAuth = Base64.getEncoder().encodeToString(authValue.getBytes(StandardCharsets.UTF_8));
			httpPost.setHeader("Authorization","Basic "+ encodedAuth);


			RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();

			if (useForwardProxy) {
				java.net.URL url = new java.net.URL(forwardProxyUrl);

				// 프로토콜, 호스트, 포트 추출
				String protocol = url.getProtocol();
				String host = url.getHost();
				int port = url.getPort();
				HttpHost proxy = new HttpHost(protocol, host, port);
				requestConfigBuilder.setProxy(proxy);
			}

			RequestConfig requestConfig = requestConfigBuilder
					.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout))
					.setResponseTimeout(Timeout.ofMilliseconds(timeout)).build();
			httpPost.setConfig(requestConfig);

			logger.debug("uri                    = [" + uri + "]");
			logger.debug("oauthClientId          = [" + oAuthCredentialVo.getClientId() + "]");
			logger.debug("oauthClientSecret      = [" + oAuthCredentialVo.getClientSecret() + "]");
			logger.debug("oauthScope             = [" + oAuthCredentialVo.getScope() + "]");
			logger.debug("oauthGrantType         = [" + oAuthCredentialVo.getGrantType() + "]");
			logger.debug("oauthauthValue         = [" + authValue + "]");
			logger.debug("oauthauthEncodedAuth   = [" + encodedAuth + "]");
			logger.debug("contentType            = [" + contentType + "]");
			logger.debug("encode                 = [" + encode + "]");

			OAuth2AccessTokenVO accessToken = new OAuth2AccessTokenVO();
			try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
				if (response.getCode() != 200) {
					throw new Exception("NiceOn token receive status fail value== " + response.getCode());
				}
				
				logger.info("HttpClientAccessTokenServiceWithBase64NiceOn==>" + response.getCode());

				HttpEntity entity = response.getEntity();
				String responseString = EntityUtils.toString(entity, encode);
				logger.debug("Base64NiceOn oauthToken RECV == [" + responseString + "]");

				if (StringUtils.isNotBlank(responseString)) {

					ObjectMapper objectMapper = new ObjectMapper();
					JsonNode responseJSONRtn = objectMapper.readTree(responseString);
					JsonNode responseJSON = null;
					String gwRsltCd = responseJSONRtn.get("dataHeader").get("GW_RSLT_CD").asText();

					if ("1200".equals(gwRsltCd)) {
						responseJSON = responseJSONRtn.get("dataBody");
					}

					if (responseJSON.has("access_token") && responseJSON.has("token_type")
							&& responseJSON.has("expires_in") && responseJSON.has("scope")) {
						accessToken.setAccessToken(responseJSON.get("access_token").asText());
						accessToken.setTokenType(responseJSON.get("token_type").asText());

						long expiresIn = responseJSON.get("expires_in").asLong();
						accessToken.setExpiration(new Date(currentTime + expiresIn * 1000L));
						accessToken.setScope(responseJSON.get("scope").asText());
						accessToken.setClientId(cId);
					}

					if (responseJSON.has("client_use_code")) {
						accessToken.setClientUseCode(responseJSON.get("client_use_code").asText());
					}

					logger.debug("NiceOn oauthToken =" + accessToken.toString());

					return accessToken;
				} else {
					throw new Exception("oauth token return null");
				}
			} catch (Exception e) {
				e.printStackTrace();
		        logger.error("[HttpClientAccessTokenServiceWithBase64NiceOn] retrieve accessToken exception :" + e.getMessage());		                
		        return accessToken;
			}
		}
	}

	/**
	 * URL에 경로를 추가합니다. 중복되는 슬래시를 방지합니다.
	 *
	 * @param baseUrl   기본 URL
	 * @param pathToAdd 추가할 경로
	 * @return 완성된 URL 문자열
	 */
	public static String appendPath(String baseUrl, String pathToAdd) {
		if (!baseUrl.endsWith("/") && !pathToAdd.startsWith("/")) {
			return baseUrl + "/" + pathToAdd;
		} else if (baseUrl.endsWith("/") && pathToAdd.startsWith("/")) {
			return baseUrl + pathToAdd.substring(1);
		} else {
			return baseUrl + pathToAdd;
		}
	}
}
