package com.eactive.eai.adapter.http.dynamic.filter;

import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import com.eactive.eai.authserver.service.BearerTokenInfo;
//import com.eactive.eai.authserver.vo.BearerTokenInfo;
//import com.eactive.eai.authserver.custom.BearerTokenService;
import com.eactive.eai.authserver.service.OAuth2Manager;
import com.eactive.eai.authserver.vo.ClientVO;
import com.eactive.eai.common.message.EAIMessage;
import com.eactive.eai.common.message.EAIMessageManager;
import com.eactive.eai.common.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.session.SessionManager;
import com.eactive.eai.common.stdmessage.STDMessageManager;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.inbound.action.ActionFactory;
import com.eactive.eai.inbound.action.RequestAction;
import com.eactive.eai.inbound.processor.Processor;
import com.eactive.eai.message.StandardMessage;
import com.eactive.eai.message.StandardMessageUtil;
import com.eactive.eai.message.manager.StandardMessageManager;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.util.IOUtils;
import com.nimbusds.jwt.SignedJWT;

public class ApiAuthFilter implements HttpAdapterFilter {
	static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);

	public static final String PROP_GROUP_AUTH_SERVER = "OAuthServer";
	public static final String PROP_KEYSTORE_PATH = "certification.publicKeyPath";

	public static final String ERROR_AUTHENTICATION_FAIL = "E.AUTHENTICATION_FAIL";
	public static final String ERROR_AUTHORIZATION_FAIL = "E.AUTHORIZATION_FAIL";
	public static final String ERROR_TOKEN_EXPIRED = "E.TOKEN_EXPIRED";

	public static final String PAYLOAD_PARAM_NAME_SCOPE = "scope";
	public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id";

	private JWSVerifier jwsVerifier;

//    // CA 토큰 저장소
//    private final Map<String, BearerTokenInfo> CATokenStore = new ConcurrentHashMap<>();

    public ApiAuthFilter() {
		super();

		PropGroupVO vo = PropManager.getInstance().getPropGroupVO(PROP_GROUP_AUTH_SERVER);
		String publicKeyPath = "/certificate/elink-oauth-dev.pub";
		if (vo != null) {
			publicKeyPath = vo.getProperty(PROP_KEYSTORE_PATH);
		} else {
			logger.warn("The properties has not been set.[" + PROP_GROUP_AUTH_SERVER + "]");
		}

		Resource resource = new ClassPathResource(publicKeyPath);
		String publicKey = null;
		try {
			publicKey = IOUtils.readInputStreamToString(resource.getInputStream());
			publicKey = StringUtils.replace(publicKey, "-----BEGIN PUBLIC KEY-----", "");
			publicKey = StringUtils.replace(publicKey, "-----END PUBLIC KEY-----", "");
			publicKey = StringUtils.remove(publicKey, "\r");
			publicKey = StringUtils.remove(publicKey, "\n");

			X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey));
			KeyFactory keyFactory = KeyFactory.getInstance("RSA");
			jwsVerifier = new RSASSAVerifier((RSAPublicKey) keyFactory.generatePublic(keySpec));
		} catch (IOException | NoSuchAlgorithmException | RuntimeException | InvalidKeySpecException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
			HttpServletRequest request, HttpServletResponse response) throws Exception {
		String apiId = prop.getProperty("FINAL_STD_MESSAGE_KEY");
		String eaiSvcCd = prop.getProperty("API_SERVICE_CODE");
		if(StringUtils.isEmpty(eaiSvcCd)) {
			if(StringUtils.isEmpty(apiId))
				apiId = assignApiId(adptGrpName, adptName, message, prop, request);
			
			if (StringUtils.isBlank(apiId)) {
				throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Can not find apiId(apiCode) info");
			}
	
			/**
			 * TODO: 중복되는 표준메시지 조회로 리소드 낭비가 있을 수 있으므로 체크 자체를 추후 RequestProcessor혹은 RestProcessor로 이동 필요 해 보임
			 * ApiRequestProcessor을 만드는것도 괜찮을 수 있을 것 같으나 관리가 안될 것 같음
			 */
			StandardMessage standardMessage = STDMessageManager.getInstance().getSTDMessage(apiId);
			eaiSvcCd = StandardMessageManager.getInstance().getMapper().getEaiSvcCode(standardMessage);
		}
//		if (StringUtils.isNotEmpty(eaiSvcCd) && !StringUtils.equals(eaiSvcCd, ))
//			eaiSvcCd = prop.getProperty("API_SERVICE_CODE");
		EAIMessage eaiMessage = EAIMessageManager.getInstance().getEAIMessage(eaiSvcCd);

		if(StringUtils.isBlank(eaiMessage.getAuthType())){
			return message;
		}

		boolean isPassScope = false; 
		switch (eaiMessage.getAuthType()){
			case "oauth":
				try {
					String token = extractBearerToken(request, prop);
					SignedJWT signedJWT = SignedJWT.parse(token);

					if (signedJWT.getJWTClaimsSet().getExpirationTime() == null) {
						logger.debug("Static Token");
					} else if (new Date().after(signedJWT.getJWTClaimsSet().getExpirationTime())) {
						throw new JwtAuthException(ERROR_TOKEN_EXPIRED, "Access token expired");
					}
					try {
						if (!signedJWT.verify(jwsVerifier)) {
							throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Token invalid");
						}
					}catch (Exception e) {
						throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Token invalid", e);
					}
					

					OAuth2Manager manager = OAuth2Manager.getInstance();
					HashSet<String> scopeSet = manager.getApiScopeMap().get(apiId);
					
					// API에 scope 설정이 안되어 있는 경우, scope 체크를 pass하고 나중에 client API 맵을 체크한다.
					if(CollectionUtils.isEmpty(scopeSet))
						isPassScope = true;
					
					if(!isPassScope) {
						String[] scopeArr = signedJWT.getJWTClaimsSet().getStringArrayClaim(PAYLOAD_PARAM_NAME_SCOPE);
						if (scopeArr == null || scopeArr.length == 0) {
							throw new JwtAuthException(ERROR_AUTHORIZATION_FAIL,
									String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
						}
						
						for (String scope : scopeArr) {
							if (StringUtils.equals(scope, "oob") || StringUtils.equals(scope, "public")) {
								isPassScope = true;
							}
						}
						
						if (!isPassScope) {
							if (!verifyScope(scopeSet, signedJWT)) {
								throw new JwtAuthException(ERROR_AUTHORIZATION_FAIL,
										String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
							}
						}
					}
					
					// header 값으로 전송된 clientId와 토큰 소유 clientId check
					if (!verifyClientId(prop, signedJWT)) {
						throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "client_id not matched(header/token)");
					}
				} catch (JwtAuthException e) {
					logger.debug(e.getMessage());
					throw e;
				} catch (Exception e) {
					logger.error(e.getMessage());
					throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Token unknown Error", e);
				}
				break;
			case "api_key":
				// 기관별로 API Key 헤더명이 다를 수 있어, 어댑터별 설정(ADAPTER_TOKEN_HEADER_NAME)을 우선 사용하고
				// 없으면 ApiConfig 전역 설정(api.key.name)을 사용. 콤마로 다중 헤더명 지정 시 순서대로 값이 있는 헤더를 사용
				String apiKeyNameConf = prop.getProperty(HttpAdapterServiceKey.ADAPTER_APIKEY_HEADER_NAME);
				if (StringUtils.isBlank(apiKeyNameConf)) {
					apiKeyNameConf = PropManager.getInstance().getProperty("ApiConfig", "api.key.name", "x-api-key");
				}
				String[] apiKeyNames = org.springframework.util.StringUtils.tokenizeToStringArray(apiKeyNameConf, ",");
				String apiKeyName = null;
				String apiKey = null;
				for (String name : apiKeyNames) {
					String value = request.getHeader(name);
					if (StringUtils.isNotBlank(value)) {
						apiKeyName = name;
						apiKey = value.trim();
						break;
					}
				}
				if(apiKey == null){
					// QueryString으로 전달된 access_token 파라미터 확인
					String apiKeyParamName = PropManager.getInstance().getProperty("ApiConfig", "api.key.param.name", "x-api-key");
					String queryApiKey = request.getParameter(apiKeyParamName);
					if (StringUtils.isNotBlank(queryApiKey)) {
						apiKey = queryApiKey.trim();
					} else {
						throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Invalid or missing API key \""+apiKeyNameConf+"\" in Http Header");
					}
				}
				prop.setProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, apiKey);
				isPassScope = true;
				break;
			case "ca":
				String token = extractBearerToken(request, prop);
				BearerTokenInfo bearerTokenInfo = SessionManager.getInstance().getCAToken(token);
				if ( bearerTokenInfo != null ) {
					if( bearerTokenInfo.isExpired() ) {
						SessionManager.getInstance().removeCAToken(token);
						throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "expried API key \""+token+"\" in Http Authrozation Header");
					}
					prop.setProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, bearerTokenInfo.getClientId());
					isPassScope = true;
				}
				else {
					throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Invalid or missing API key \""+token+"\" in Http Authrozation Header");
				}
				break;
			case "none":
				return message;
		}

		if (isPassScope) {
			String clientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
			ClientVO clientVO = OAuth2Manager.getInstance().getClientVO(clientId);
			if(clientVO == null){
				throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Unregistered APP");
			}

			if(clientVO.getApiMap().containsKey(eaiSvcCd) == false){
				throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Unregistered API ["+eaiSvcCd+"] in ["+clientVO.getClientName()+"] APP");
			}
		}

		return message;
	}

	private boolean verifyClientId(Properties prop, SignedJWT signedJWT) throws JwtAuthException {
		try {
			// 이전 filter에서 셋팅한 clientId 확보
			String headerClientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
			String tokenClientId = (String) signedJWT.getJWTClaimsSet().getClaim(PAYLOAD_PARAM_NAME_CLIENT_ID);

			// 이전 filter에서 셋팅한 값이 없다면 token 정보에서 확보
			if (StringUtils.isBlank(headerClientId)) {
				if (StringUtils.isNotBlank(tokenClientId)) {
					prop.put(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, tokenClientId);
				}
			} else { // header로 전달된 clientId가 있을때만 비교
				if (!headerClientId.equals(tokenClientId)) {
					return false;
				}
			}
		} catch (Exception e) {
			logger.error(e.getMessage());
			throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Token invalid");
		}

		return true;
	}

	private String assignApiId(String adptGrpName, String adptName, Object message, Properties prop,
			HttpServletRequest request) {
		String apiId = null;
		try {
			String actionName = prop.getProperty(Processor.REQUEST_ACTION);
			RequestAction action = ActionFactory.createAction(actionName);
			action.setAdapterInfo(adptGrpName, adptName, prop);
			String[] keys = action.perform(message);
			apiId = keys[0];

			// PathVariable 지원 추가
			apiId = StandardMessageUtil.getMatchedKey(apiId, actionName);
		} catch (Exception e) {
			logger.error(e.getMessage());
		}

//		// header 에서 확보
//		String apiId = request.getHeader(HEADER_NAME_API_CODE);
//		if (StringUtils.isBlank(apiId)) {
//			// url에서 확보
//			// /ONLWeb/api/v1/public/getUserInfo.svc/
//			apiId = request.getRequestURI();
//			// /ONLWeb/api/v1/public/getUserInfo.svc
//			apiId = StringUtils.removeEnd(apiId, "/");
//			// getUserInfo.svc
//			apiId = StringUtils.substringAfterLast(apiId, "/");
//		}

		return apiId;
	}

	@Override
	public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
			HttpServletRequest request, HttpServletResponse response) throws Exception {
		return resultMessage;
	}

	private boolean verifyScope(HashSet<String> scopeSet, SignedJWT signedJWT) throws ParseException {
		if (scopeSet == null || scopeSet.isEmpty()) {
			return true; // If no scope is specified in the api, all are allowed
		}

		String[] scopeArr = signedJWT.getJWTClaimsSet().getStringArrayClaim(PAYLOAD_PARAM_NAME_SCOPE);
		if (scopeArr == null || scopeArr.length == 0) {
			return false;
		}
		
		String passScope = "oob,public";
		String[] passScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(passScope, ",");

		for (String scope : scopeArr) {
			for (String pScope : passScopeArr) {
				if ( StringUtils.equalsAny(scope, pScope)) {
					return true;
				}
			}
			if (scopeSet.contains(scope)) {
				return true;
			}
		}

		return false;
	}

	public static String extractBearerToken(HttpServletRequest request, Properties prop) throws JwtAuthException {
		// 기관별로 토큰 헤더명이 다를 수 있어, 어댑터별 설정(ADAPTER_APPKEY_HEADER_NAME)을 우선 사용하고
		// 없으면 ApiConfig 전역 설정(token.header.name)을 사용
		String tokenHeaderName = prop != null ? prop.getProperty(HttpAdapterServiceKey.ADAPTER_TOKEN_HEADER_NAME) : null;
		if (StringUtils.isBlank(tokenHeaderName)) {
			tokenHeaderName = PropManager.getInstance().getProperty("ApiConfig", "token.header.name", "Authorization");
		}
		String authorization = request.getHeader(tokenHeaderName);
		if (StringUtils.isBlank(authorization)) {
			// QueryString으로 전달된 access_token 파라미터 확인
			String tokenParamName = PropManager.getInstance().getProperty("ApiConfig", "token.param.name", "access_token");
			String queryToken = request.getParameter(tokenParamName);
			if (StringUtils.isNotBlank(queryToken)) {
				return queryToken.trim();
			}
			throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "No have header info: " + tokenHeaderName);
		}

		String[] components = authorization.trim().split("\\s+", 2);

		if (components.length != 2) {
			throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Malformat [" + tokenHeaderName + "] content");
		}

		if (!StringUtils.equalsIgnoreCase(components[0], "Bearer")) {
			throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "[Bearer] is needed");
		}

		return components[1].trim();
	}
}
