package com.eactive.eai.authserver.custom;

import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Principal;
import java.security.SecureRandom;
import java.security.Signature;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.crypto.Cipher;
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.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
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.filter.JwtAuthException;
import com.eactive.eai.authserver.config.IssueLimitOAuth2Exception;
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 KjbMGOAuth2Controller { 
	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 = "/mapi/oauth2/token", method = RequestMethod.POST, consumes = "application/json", produces = "application/json; charset=UTF-8")
	@ResponseBody
	public ResponseEntity<String> tokenJson(@RequestBody KjbMGOAuth2AccessTokenRequest tokenRequest,
			HttpServletRequest request, HttpServletResponse response) {

		return issueToken(tokenRequest, request, response);
	}

	@RequestMapping(value = "/mapi/oauth2/token", method = RequestMethod.POST, consumes = "application/x-www-form-urlencoded", produces = "application/json; charset=UTF-8")
	@ResponseBody
	public ResponseEntity<String> tokenForm(@RequestParam Map<String, String> params, HttpServletRequest request,
			HttpServletResponse response) {

		KjbMGOAuth2AccessTokenRequest tokenRequest = new KjbMGOAuth2AccessTokenRequest();

		tokenRequest.setGrantType(params.get("grant_type"));
		tokenRequest.setClientId(params.get("client_id"));
		tokenRequest.setClientSecret(params.get("client_secret"));
		tokenRequest.setScope(params.get("scope"));
		tokenRequest.setResource(params.get("resource"));

		return issueToken(tokenRequest, request, response);
	}
	
	@GetMapping(value = "/mapi/oauth2/token", produces = "application/json; charset=UTF-8")
	@ResponseBody
	public ResponseEntity<String> tokenGet(@RequestParam Map<String, String> params, HttpServletRequest request,
			HttpServletResponse response) {

		KjbMGOAuth2AccessTokenRequest tokenRequest = new KjbMGOAuth2AccessTokenRequest();

		tokenRequest.setGrantType(params.get("grant_type"));
		tokenRequest.setClientId(params.get("client_id"));
		tokenRequest.setClientSecret(params.get("client_secret"));
		tokenRequest.setScope(params.get("scope"));
		tokenRequest.setResource(params.get("resource"));

		return issueToken(tokenRequest, request, response);
	}

	private ResponseEntity<String> issueToken(
	        KjbMGOAuth2AccessTokenRequest 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);
		

		KjbMGOAuth2AccessTokenResponse responseToken = new KjbMGOAuth2AccessTokenResponse();
		
		String grantType = tokenRequest.getGrantType();
		String clientId = tokenRequest.getClientId();
		String clientSecret = tokenRequest.getClientSecret();
		String scopes = tokenRequest.getScope();
		
		try {
			if (!StringUtils.equals(tokenRequest.getGrantType(), "client_credentials")) {
				throw new JwtAuthException(
						String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
						"Invalid Mandatory Field {grantType}");
			}

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

			String[] resourceArr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenRequest.getResource(), ",");
			Set<String> resourceSet = new HashSet<String>();
			for (String resource : resourceArr) {
				resourceSet.add(resource);
			}

			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<String, String>();
			authorizationParameters.put(OAuth2Utils.GRANT_TYPE, grantType);
			authorizationParameters.put(OAuth2Utils.CLIENT_ID, clientId);
			authorizationParameters.put("client_secret", clientSecret);

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

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

			Principal principal = new OAuth2Authentication(authorizationRequest, null);
			ResponseEntity<OAuth2AccessToken> result = tokenEndpoint().postAccessToken(principal,
					authorizationParameters);
			OAuth2AccessToken token = result.getBody();
			if(StringUtils.isNotBlank(token.getTokenType()) && StringUtils.equalsIgnoreCase(token.getTokenType(), "bearer")) {
				responseToken.setTokenType("Bearer");
			} else {	responseToken.setTokenType(token.getTokenType()); }
			responseToken.setAccessToken(token.getValue());
			responseToken.setExpiresIn(Long.valueOf(token.getExpiresIn()));
			responseToken.setrefreshToken("");
			responseToken.setExpiresOn(System.currentTimeMillis() + (token.getExpiresIn() * 1000));
			responseToken.setScope(tokenRequest.getScope());
			responseToken.setResource(tokenRequest.getResource());

			// 성공 시 JSON 문자열로 변환하여 반환
			ObjectMapper mapper = new ObjectMapper();
			String successJson = mapper.writeValueAsString(responseToken);

			return ResponseEntity.ok(successJson);

		} catch (JwtAuthException e) {
			logger.info("Token request[/mapi/oauth2/token] => clientId: {}, grantType: {}, scope: {}", 
		            clientId, grantType, scopes);
			logger.error(e.getMessage());
			String errorJson = MessageUtil.makeJsonErrorMessage(MessageUtil.ERROR_CODE_AUTH_FAIL, e.getMessage());
			int statusCode = NumberUtils.toInt(StringUtils.left(e.getCode(), 3), HttpStatus.UNAUTHORIZED.value());
			logTokenIssuance(false ,false, e.getMessage(), null);
			return ResponseEntity.status(statusCode).body(errorJson);

		} catch (Exception e) {
			logger.info("Token request[/mapi/oauth2/token] => clientId: {}, grantType: {}, scope: {}", 
		            clientId, grantType, scopes);
			logger.error(e.getMessage());
			String errorJson = MessageUtil.makeJsonErrorMessage(MessageUtil.ERROR_CODE_AUTH_FAIL, "Unauthorized. [Unknown]");
			logTokenIssuance(false ,false, e.getMessage(), null);
			return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorJson);
		}finally {
			RequestContextData.ThreadLocalRequestContext.clear();
		}
		
		
	}
	
	private String extractIpAddress(HttpServletRequest request) {
		String ipAddress = request.getHeader("X-Forwarded-For");
		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
			ipAddress = request.getHeader("Proxy-Client-IP");
		}
		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
			ipAddress = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
			ipAddress = request.getHeader("HTTP_CLIENT_IP");
		}
		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
			ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
		}
		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
			ipAddress = request.getRemoteAddr();
		}
		return ipAddress;
	}
	
	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 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();
			OAuth2Manager.getInstance();

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

			if(StringUtils.isNotBlank(clientId)) {
				ClientVO clientVO = OAuth2Manager.getInstance().getClientInfo(clientId);
				log.setAppName(clientVO.getClientName());
				log.setOrgId(clientVO.getOrgId());
				log.setOrgName(clientVO.getOrgName());
			}

			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); // Add access token value to the log

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

	public static void main(String[] args) throws Exception {
		// First generate a public/private key pair
		KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
		// generator.initialize(512, new SecureRandom());
		//generator.initialize(1024, new SecureRandom());
		generator.initialize(2048, new SecureRandom());
		KeyPair pair = generator.generateKeyPair();

		// The private key can be used to sign (not encrypt!) a message. The public key
		// holder can then verify the message.

		String message = "sSGJDwlJsel5WeXodzd3J3MQ20KYCZpL|2022-12-21T14:36:19+07:00";

		// Let's sign our message
		Signature privateSignature = Signature.getInstance("SHA256withRSA");
		privateSignature.initSign(pair.getPrivate());
		privateSignature.update(message.getBytes(StandardCharsets.UTF_8));
		System.out.println(
				"private key=" + Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded()));

		byte[] signature = privateSignature.sign();
		//System.out.println("signature=" + new String(signature, StandardCharsets.UTF_8));
		System.out.println("signature=" + Base64.getEncoder().encodeToString(signature));

		// Let's check the signature
		Signature publicSignature = Signature.getInstance("SHA256withRSA");
		publicSignature.initVerify(pair.getPublic());
		publicSignature.update(message.getBytes(StandardCharsets.UTF_8));
		boolean isCorrect = publicSignature.verify(signature);
		System.out.println("public key=" + Base64.getEncoder().encodeToString(pair.getPublic().getEncoded()));

		System.out.println("Signature correct: " + isCorrect);

		// The public key can be used to encrypt a message, the private key can be used
		// to decrypt it.
		// Encrypt the message
		Cipher encryptCipher = Cipher.getInstance("RSA");
		encryptCipher.init(Cipher.ENCRYPT_MODE, pair.getPublic());

		byte[] cipherText = encryptCipher.doFinal(message.getBytes(StandardCharsets.UTF_8));

		// Now decrypt it
		Cipher decriptCipher = Cipher.getInstance("RSA");
		decriptCipher.init(Cipher.DECRYPT_MODE, pair.getPrivate());

		String decipheredMessage = new String(decriptCipher.doFinal(cipherText), StandardCharsets.UTF_8);

		System.out.println(decipheredMessage);
	}
}
