package com.eactive.eai.authserver.custom;

import java.security.Principal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

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

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.eactive.eai.adapter.http.dynamic.UnknownMessageLogUtils;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.authserver.config.RequestContextData;
import com.eactive.eai.authserver.dao.TokenIssuanceLogDAO;
import com.eactive.eai.authserver.service.OAuth2Manager;
import com.eactive.eai.authserver.util.BeanUtils;
import com.eactive.eai.authserver.vo.ClientVO;
import com.eactive.eai.common.dao.DAOException;
import com.eactive.eai.common.logger.EAIDBLogControl;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.MessageUtil;
import com.eactive.eai.common.util.UUID;
import com.eactive.eai.data.entity.onl.authserver.TokenIssuanceLog;
import com.fasterxml.jackson.databind.ObjectMapper;

@Controller
public class DJBOAuth2Controller {
	public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);

	private static final String HEADER_TRACEID = "traceId";
	private static final String SERVICE_CODE_ACCESS_TOKEN = "00";

	@Autowired
	private TokenIssuanceLogDAO tokenIssuanceLogDAO;

	@RequestMapping(value = { "/*/oauth/token", "/*/oauth2/token" }, method = RequestMethod.POST,
			consumes = "application/json", produces = "application/json; charset=UTF-8")
	@ResponseBody
	public ResponseEntity<?> tokenJson(@RequestBody DJBOAuth2AccessTokenRequest tokenRequest,
			HttpServletRequest request, HttpServletResponse response) {
		return issueToken(tokenRequest, request, response);
	}

	@RequestMapping(value = { "/*/oauth/token", "/*/oauth2/token" }, method = RequestMethod.POST,
			consumes = "application/x-www-form-urlencoded", produces = "application/json; charset=UTF-8")
	@ResponseBody
	public ResponseEntity<?> tokenForm(@RequestParam Map<String, String> params,
			HttpServletRequest request, HttpServletResponse response) {
		DJBOAuth2AccessTokenRequest tokenRequest = new DJBOAuth2AccessTokenRequest();
		tokenRequest.setGrantType(params.get("grant_type"));
		tokenRequest.setClientId(params.get("client_id"));
		tokenRequest.setClientSecret(params.get("client_secret"));
		tokenRequest.setScope(params.get("scope"));
		return issueToken(tokenRequest, request, response);
	}

	@GetMapping(value = { "/*/oauth/token", "/*/oauth2/token" }, produces = "application/json; charset=UTF-8")
	@ResponseBody
	public ResponseEntity<?> tokenGet(@RequestParam Map<String, String> params,
			HttpServletRequest request, HttpServletResponse response) {
		DJBOAuth2AccessTokenRequest tokenRequest = new DJBOAuth2AccessTokenRequest();
		tokenRequest.setGrantType(params.get("grant_type"));
		tokenRequest.setClientId(params.get("client_id"));
		tokenRequest.setClientSecret(params.get("client_secret"));
		tokenRequest.setScope(params.get("scope"));
		return issueToken(tokenRequest, request, response);
	}

	private ResponseEntity<?> issueToken(DJBOAuth2AccessTokenRequest tokenRequest,
			HttpServletRequest request, HttpServletResponse response) {

		if (logger.isDebug()) {
			logger.debug(tokenRequest.toString());
		}

		RequestContextData data = new RequestContextData();
		data.setClientId(tokenRequest.getClientId());
		data.setIpAddress(extractIpAddress(request));
		data.setGrantType(tokenRequest.getGrantType());
		data.setScope(tokenRequest.getScope());
		data.setUsername("none");
		data.setResource("none");
		RequestContextData.ThreadLocalRequestContext.set(data);

		String grantType = tokenRequest.getGrantType();
		String clientId = tokenRequest.getClientId();
		String clientSecret = tokenRequest.getClientSecret();
		String scopes = tokenRequest.getScope();

		try {
			if (!StringUtils.equals(grantType, "client_credentials")) {
				throw new JwtAuthException(
						String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
						"Invalid Mandatory Field {grant_type}");
			}

			String[] scopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(scopes, " ");
			Set<String> scopeSet = new HashSet<>();
			for (String scope : scopeArr) {
				scopeSet.add(scope);
			}

			ClientDetails clientDetails = OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId);
			verifyClient(clientDetails, clientId, clientSecret, scopeSet);

			String traceId = UUID.randomUUID().toString().replace("-", "");
			response.setHeader(HEADER_TRACEID, traceId);

			HashMap<String, String> authorizationParameters = new HashMap<>();
			authorizationParameters.put(OAuth2Utils.GRANT_TYPE, grantType);
			authorizationParameters.put(OAuth2Utils.CLIENT_ID, clientId);
			authorizationParameters.put("client_secret", clientSecret);

			Set<String> responseType = new HashSet<>();
			responseType.add(grantType);

			OAuth2Request authorizationRequest = new OAuth2Request(authorizationParameters, clientId, null, true,
					scopeSet, null, "", responseType, null);

			Principal principal = new OAuth2Authentication(authorizationRequest, null);
			ResponseEntity<OAuth2AccessToken> result = tokenEndpoint().postAccessToken(principal, authorizationParameters);
			OAuth2AccessToken token = result.getBody();

			DJBOAuth2AccessTokenResponse responseToken = new DJBOAuth2AccessTokenResponse();
			responseToken.setAccessToken(token.getValue());
			responseToken.setTokenType(token.getTokenType());
			responseToken.setExpiresIn(Long.valueOf(token.getExpiresIn()));
			responseToken.setScope(StringUtils.join(token.getScope(), " "));
			responseToken.setJti((String) token.getAdditionalInformation().get("jti"));
			responseToken.setClientId((String) token.getAdditionalInformation().get("client_id"));

			//logTokenIssuance(true, false, "Token issued successfully", token.getValue());

			ObjectMapper mapper = new ObjectMapper();
			return ResponseEntity.ok(mapper.writeValueAsString(responseToken));

		} catch (JwtAuthException e) {
			logger.info("Token request[/dj/oauth2/token] => clientId: {}, grantType: {}, scope: {}",
					clientId, grantType, scopes);
			logger.error(e.getMessage());
			logTokenIssuance(false, false, e.getMessage(), null);
			
			Properties logProp = new Properties();
			logProp.put("clientId", clientId);
			UnknownMessageLogUtils.logUnkownMessage(this.getClass().getSimpleName(), this.getClass().getSimpleName(),
					tokenRequest.toString(), logProp, request, response, "RECEAIIRP301", e);
			
			String errorJson = MessageUtil.makeJsonErrorMessage(MessageUtil.ERROR_CODE_AUTH_FAIL, e.getMessage());
			int statusCode = NumberUtils.toInt(StringUtils.left(e.getCode(), 3), HttpStatus.UNAUTHORIZED.value());
			return ResponseEntity.status(statusCode).body(errorJson);

		} catch (Exception e) {
			logger.info("Token request[/dj/oauth2/token] => clientId: {}, grantType: {}, scope: {}",
					clientId, grantType, scopes);
			logger.error(e.getMessage());
			logTokenIssuance(false, false, e.getMessage(), null);
			
			Properties logProp = new Properties();
			logProp.put("clientId", clientId);
			UnknownMessageLogUtils.logUnkownMessage(this.getClass().getSimpleName(), this.getClass().getSimpleName(),
					tokenRequest.toString(), logProp, request, response, "RECEAIIRP301", e);
			
			String errorJson = MessageUtil.makeJsonErrorMessage(MessageUtil.ERROR_CODE_AUTH_FAIL, "Unauthorized. [Unknown]");
			return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorJson);

		} finally {
			RequestContextData.ThreadLocalRequestContext.clear();
		}
	}

	private void verifyClient(ClientDetails clientDetails, String clientId, String clientSecret, Set<String> scopeSet)
			throws JwtAuthException {

		if (StringUtils.isBlank(clientId)) {
			throw new JwtAuthException(
					String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
					"Invalid Mandatory Field {client_id}");
		}
		if (clientDetails == null) {
			throw new JwtAuthException(
					String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_ACCESS_TOKEN, "00"),
					"Unauthorized. [client not found]");
		}
		if (StringUtils.isBlank(clientSecret)) {
			throw new JwtAuthException(
					String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
					"Invalid Mandatory Field {client_secret}");
		}
		if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
			throw new JwtAuthException(
					String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_ACCESS_TOKEN, "00"),
					"Unauthorized. [Bad client credentials(Client Secret is not match)]");
		}
		if (scopeSet.isEmpty()) {
			throw new JwtAuthException(
					String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
					"Invalid Mandatory Field {scope}");
		}
		if (!clientDetails.getScope().containsAll(scopeSet)) {
			throw new JwtAuthException(
					String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_ACCESS_TOKEN, "00"),
					"Unauthorized. [Bad client credentials(Include unacceptable scope)]");
		}
	}

	private String extractIpAddress(HttpServletRequest request) {
		String ip = request.getHeader("X-Forwarded-For");
		if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) ip = request.getHeader("Proxy-Client-IP");
		if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) ip = request.getHeader("WL-Proxy-Client-IP");
		if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) ip = request.getHeader("HTTP_CLIENT_IP");
		if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) ip = request.getHeader("HTTP_X_FORWARDED_FOR");
		if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) ip = request.getRemoteAddr();
		return ip;
	}

	private TokenEndpoint tokenEndpoint() {
		return BeanUtils.getBean("tokenEndpoint", TokenEndpoint.class);
	}

	private void logTokenIssuance(boolean isSuccess, boolean isRestrictionExempt, String resultMessage, String accessToken) {
		try {
			if (!EAIDBLogControl.isEnable()) {
				logger.warn("DB logging is disabled. Skipping token issuance logging.");
				return;
			}
			RequestContextData data = RequestContextData.ThreadLocalRequestContext.get();

			TokenIssuanceLog log = new TokenIssuanceLog();
			String clientId = data.getClientId();
			log.setClientId(clientId);

			if (StringUtils.isNotBlank(clientId)) {
				try {
					ClientVO clientVO = OAuth2Manager.getInstance().getClientInfo(clientId);
					log.setAppName(clientVO.getClientName());
					log.setOrgId(clientVO.getOrgId());
					log.setOrgName(clientVO.getOrgName());
				} catch (DAOException e) {
					logger.warn("cannot find clientVO - " + clientId);
				}
			}

			log.setGrantType(data.getGrantType());
			log.setScope(data.getScope());
			log.setIpAddress(data.getIpAddress());
			log.setSuccessYn(isSuccess ? "Y" : "N");
			log.setIssuanceDateTime(LocalDateTime.now());
			log.setRestrictionExemptYn(isRestrictionExempt ? "Y" : "N");
			log.setResultMessage(resultMessage);
			log.setAccessToken(accessToken);

			tokenIssuanceLogDAO.saveTokenIssuanceLog(log);
		} catch (Throwable th) {
			logger.error("Error while logging token issuance: " + th.getMessage(), th);
		}
	}
}
