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.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import com.eactive.eai.authserver.service.OAuth2Manager;
import com.eactive.eai.common.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
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.StandardMessageUtil;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.util.IOUtils;
import com.nimbusds.jwt.SignedJWT;

public class JwtAuthFilter 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;

	public JwtAuthFilter() {
		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 = assignApiId(adptGrpName, adptName, message, prop, request);
		if (StringUtils.isBlank(apiId)) {
			throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Can not find apiId(apiCode) info");
		}

		try {
			String token = JwtAuthFilter.extractJWTToken(request);
			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");
			}

			if (!signedJWT.verify(jwsVerifier)) {
				throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Token invalid");
			}

			OAuth2Manager manager = OAuth2Manager.getInstance();
			HashSet<String> scopeSet = manager.getApiScopeMap().get(apiId);
			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 invalid");
		}

		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;
		}

		for (String scope : scopeArr) {
			if (scopeSet.contains(scope)) {
				return true;
			}
		}

		return false;
	}

	public static String extractJWTToken(HttpServletRequest request) throws JwtAuthException {
		String authorization = request.getHeader("Authorization");
		if (StringUtils.isBlank(authorization)) {
			throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "No have header info: Authorization");
		}

		String[] components = authorization.split("\\s");

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

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

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