package com.eactive.eai.authserver.config;

import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.LocalDateTime;
import java.util.Arrays;

import javax.sql.DataSource;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import com.eactive.eai.agent.encryption.EncryptionManager;
import com.eactive.eai.authserver.dao.TokenIssuanceLogDAO;
import com.eactive.eai.authserver.jwt.PssJwtAccessTokenConverter;
import com.eactive.eai.authserver.service.OAuth2Manager;
import com.eactive.eai.authserver.vo.ClientVO;
import com.eactive.eai.common.dao.DAOException;
import com.eactive.eai.common.dao.Keys;
import com.eactive.eai.common.logger.EAIDBLogControl;
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.common.util.ServiceLocator;
import com.eactive.eai.data.entity.onl.authserver.TokenIssuanceLog;

@Configuration
@EnableAuthorizationServer
@Order(6)
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
	static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);

	public static final String PROP_GROUP_AUTH_SERVER = "OAuthServer";
	public static final String PROP_KEYSTORE_PATH = "certification.keystorePath";
	public static final String PROP_KEYSTORE_PW = "certification.keystorePassword";
	public static final String PROP_KEY_ALIAS = "certification.keyAlias";
	public static final String PROP_KEY_PW = "certification.keyPassword";
	public static final String PROP_TOKEN_TYPE = "oauth2.tokenType";

	@Autowired
	private AuthenticationManager authenticationManager;

	@Autowired
	private TokenIssuanceLogDAO tokenIssuanceLogDAO;

	@Autowired
	@Qualifier("eLinkClientDetailsService")
	private ClientDetailsService clientDetailsService;

	@Autowired
	@Qualifier("eLinkUserDetailsService")
	private UserDetailsService userDetailsService;
	
	

	@Override
	public void configure(final AuthorizationServerSecurityConfigurer security) throws Exception {
		security.tokenKeyAccess("permitAll()") // /oauth/token_key
				.checkTokenAccess("permitAll()") // /oauth/check_token
				.allowFormAuthenticationForClients();

		security.addTokenEndpointAuthenticationFilter(new OAuthRequestLoggingFilter());
	}

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.withClientDetails(clientDetailsService);
	}

	@Override
	public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		super.configure(endpoints);

		PropGroupVO vo = PropManager.getInstance().getPropGroupVO(PROP_GROUP_AUTH_SERVER);
		String tokenType = "JWT";
		if (vo != null) {
			tokenType = vo.getProperty(PROP_TOKEN_TYPE, "JWT");
		}

		endpoints.pathMapping("/oauth/token", "/oauth2/token") ;
		endpoints.authenticationManager(authenticationManager);
		endpoints.userDetailsService(userDetailsService);
		endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);

		// JWT 토큰을 사용하는 경우
		JwtAccessTokenConverter converter = jwtAccessTokenConverter();
		TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
		// JWT 변환 후에 로깅하도록 순서 변경
		tokenEnhancerChain.setTokenEnhancers(
				Arrays.asList(converter, new TokenIssuanceLogEnhancer())
		);

		endpoints
				.tokenStore(new JwtTokenStore(converter))
				.accessTokenConverter(converter)
				.tokenEnhancer(tokenEnhancerChain);
	
		endpoints.exceptionTranslator(new CustomWebResponseExceptionTranslator());
	}

	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter() {
		PropGroupVO vo = PropManager.getInstance().getPropGroupVO(PROP_GROUP_AUTH_SERVER);
		String keystorePath = "/certificate/elink-oauth-dev.jks";
		String keystorePassword = "elink1234";
		String keyAlias = "elink-oauth";
		String keyPassword = "elink1234";
		if (vo != null) {
			EncryptionManager encManager = EncryptionManager.getInstance();
			keystorePath = encManager.decryptDBData(vo.getProperty(PROP_KEYSTORE_PATH));
			keystorePassword = encManager.decryptDBData(vo.getProperty(PROP_KEYSTORE_PW));
			keyAlias = encManager.decryptDBData(vo.getProperty(PROP_KEY_ALIAS));
			keyPassword = encManager.decryptDBData(vo.getProperty(PROP_KEY_PW));
		} else {
			logger.warn("The properties has not been set.[" + PROP_GROUP_AUTH_SERVER + "]");
		}

		Resource keystore = new ClassPathResource(keystorePath);
		KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keystore, keystorePassword.toCharArray());
		KeyPair keyPair = keyStoreKeyFactory.getKeyPair(keyAlias, keyPassword.toCharArray());
		RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
		RSAPublicKey  publicKey  = (RSAPublicKey)  keyPair.getPublic();

		return new PssJwtAccessTokenConverter(privateKey, publicKey);
	}

	
	private DataSource getDataSource() {
		try {
			ServiceLocator sl = ServiceLocator.getInstance();
			DataSource dataSource = sl.getDataSource(Keys.EAI_DATASOURCE);
			return dataSource;
		} catch (Exception e) {
			logger.error("Failed get [" + Keys.EAI_DATASOURCE + "]");
			logger.error(e.getMessage());
			return null;
		}
	}

	private class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> {
		@Override
		public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
			OAuth2Exception oAuth2Exception;
			boolean isRestrictionExempt = false;
			if (e instanceof OAuth2Exception) {
				oAuth2Exception = (OAuth2Exception) e;
				if(e instanceof IssueLimitOAuth2Exception){
					isRestrictionExempt = true;
				}
			} else {
				oAuth2Exception = new OAuth2Exception("Server error", e);
			}

			// 토큰 발급 실패 로깅
			logTokenIssuance(false, isRestrictionExempt, oAuth2Exception.getMessage(), null);

			return ResponseEntity
					.status(oAuth2Exception.getHttpErrorCode())
					.body(oAuth2Exception);
		}
	}

	private class TokenIssuanceLogEnhancer implements TokenEnhancer {
		@Override
		public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
			if (!EAIDBLogControl.isEnable()) {
				logger.warn("DB logging is disabled. Skipping token issuance logging.");
				return accessToken;
			}

			String clientId = authentication.getOAuth2Request().getClientId();
			LocalDateTime startDateTime = LocalDateTime.now().minusHours(24);

			int recentTokenCount = tokenIssuanceLogDAO.findRecentLogsForRestrictionCount(clientId, startDateTime);

			try {
				ClientVO clientVO = OAuth2Manager.getInstance().getClientInfo(clientId);
				int dailyTokenLimit = clientVO.getDailyTokenLimit();
				if (dailyTokenLimit > 0 && recentTokenCount >= dailyTokenLimit) {
					throw new IssueLimitOAuth2Exception("Token issuance limit exceeded for this client");
				}

				// JWT 변환 이후의 최종 토큰 값 로깅
				logTokenIssuance(true, false, "Token issued successfully", accessToken.getValue());
			} catch (DAOException e) {
				logger.warn("cannot find clientVo - " + clientId);
			}
			return accessToken;
		}
	}

	private void logTokenIssuance(boolean isSuccess, boolean isRestrictionExempt, String resultMessage, String accessToken) {
		try {

			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
			
			if( !isSuccess ) {
				logger.error("Token request[/oauth/token or /oauth2/token] => clientId: {}, grantType: {}, scope: {}",
						clientId, data.getGrantType(), data.getScope());
			}
			
			if (!EAIDBLogControl.isEnable()) {
				logger.warn("DB logging is disabled. Skipping token issuance logging.");
				return;
			}

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